网络协议(四):Socket 和 Websocket

155 阅读7分钟

Socket

基本概念

Socket 是一种用于网络通信的编程接口(API),它提供了一种机制,使不同主机之间可以通过网络进行数据传输和通信。

Socket(套接字)本身确实不是一个协议,而是一种编程接口(API),它提供了网络通信的能力,使得开发者可以在不同的进程之间,或者在不同的计算机之间进行数据传输。这些数据传输可以是基于 TCP(传输控制协议)的,也可以是基于 UDP(用户数据报协议)的,或者在某些情况下,还可能基于其他类型的协议(尽管 TCP 和 UDP 是最常见的)。

在 OSI 模型中,网络通信被划分为七层,其中应用层是最高层,直接面向用户的应用程序;传输层则负责端到端的数据传输,确保数据的完整性和顺序。Socket API 允许应用程序(位于应用层)通过标准的、统一的方式来访问传输层服务,而无需关心底层的实现细节。

具体来说,当开发者使用 Socket 进行编程时,他们通常会在应用层编写代码,通过 Socket API 来发送和接收数据。这些 API 调用会被操作系统转换成适当的 TCP 或 UDP 数据包,然后通过网络发送到目标地址,使得网络通信变得简单和可靠。

因此,Socket 是网络通信中非常关键的一个组成部分,它提供了一种高效、灵活的方式来在应用程序之间交换数据。通过 Socket API,开发者可以构建出各种复杂的网络应用程序,如聊天应用、在线游戏等。

通信流程

Socket 服务端

  1. 创建 ServerSocket 实例:通过调用 ServerSocket 的构造器,并指定一个端口号(可选的,也可以指定一个 IP 地址,但通常指定为 null,表示监听所有可用的网络接口)。

  2. 等待连接:通过调用 ServerSocket 的 accept() 方法等待客户端的连接。这个方法会阻塞,直到一个连接被建立。

  3. 通信:一旦 accept() 方法返回了一个 Socket 实例,就可以通过这个实例的输入流和输出流与客户端进行通信了。

  4. 关闭连接:通信结束后,需要关闭 Socket 和 ServerSocket 实例以释放资源。

import java.io.*;  
import java.net.*;  
  
public class Server {  
    public static void main(String[] args) {  
        int port = 12345; // 设定端口号  
        try (ServerSocket serverSocket = new ServerSocket(port)) {  
            System.out.println("Server is running on port " + port);  
  
            while (true) {  
                // 等待客户端连接  
                Socket clientSocket = serverSocket.accept();  
                System.out.println("New client connected: " + clientSocket.getInetAddress().getHostAddress());  
  
                // 处理客户端请求  
                handleClient(clientSocket);  
  
                // 关闭客户端连接  
                clientSocket.close();  
            }  
        } catch (IOException e) {  
            System.err.println("Server exception: " + e.getMessage());  
            e.printStackTrace();  
        }  
    }  
  
    private static void handleClient(Socket clientSocket) {  
        try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));  
             PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {  
  
            String inputLine;  
            while ((inputLine = in.readLine()) != null) {  
                System.out.println("Received from client: " + inputLine);  
                // 回显消息给客户端  
                out.println(inputLine);  
            }  
  
        } catch (IOException e) {  
            System.err.println("Error handling client: " + e.getMessage());  
            e.printStackTrace();  
        }  
    }  
}

Socket 客户端

  1. 创建 Socket 实例:通过调用 Socket 的构造器,并指定服务器的 IP 地址和端口号。

  2. 通信:一旦 Socket 实例被创建,就可以通过它的输入流和输出流与服务器进行通信了。

  3. 关闭连接:通信结束后,需要关闭 Socket 实例以释放资源。

import java.io.*;  
import java.net.*;  
  
public class Client {  
    public static void main(String[] args) {  
        String hostname = "localhost"; // 服务端地址  
        int port = 12345; // 服务端端口  
  
        try (Socket socket = new Socket(hostname, port);  
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);  
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {  
  
            BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));  
            String userInput;  
  
            System.out.println("Connected to server. Enter messages (type 'exit' to quit):");  
  
            while ((userInput = stdIn.readLine()) != null) {  
                out.println(userInput);  
  
                // 输入exit并回车以退出客户端
                if ("exit".equalsIgnoreCase(userInput)) {  
                    break;  
                }  
  
                // 读取并显示服务端响应  
                System.out.println("Server response: " + in.readLine());  
            }  
  
        } catch (UnknownHostException e) {  
            System.err.println("Don't know about host " + hostname);  
            e.printStackTrace();  
        } catch (IOException e) {  
            System.err.println("Couldn't get I/O for the connection to " + hostname);  
            e.printStackTrace();  
        }  
    }  
}

长连接和短连接

长连接:

  1. 客户端和服务器之间只建立一次连接,该连接在数据传输期间保持开启状态

  2. 连接建立后,双方可以在该连接上多次发送和接收数据

  3. 为了保持连接活跃,防止因网络问题或超时等导致的连接中断,长连接通常会实现心跳机制来检测对方是否仍然在线,从而保持连接的活性,心跳包的发送和接收通常由专门的线程或定时器来处理。

  4. 长连接的关闭通常在会话结束时或发生错误时进行。

短连接:

  1. 在每次需要进行数据传输时,客户端都会建立一个新的连接

  2. 连接建立后,立即进行数据传输

  3. 数据传输完成后,客户端会主动关闭连接,不再保持。

Websocket

基本概念

WebSocket 和 HTTP 相同,都是应用层协议,它俩的传输层都是基于 TCP 的。那么就有一个问题了:已经有了 HTTP 协议,为什么还需要 WebSocket ?

因为 HTTP 协议有一个缺陷:通信只能由客户端发起,做不到服务器主动向客户端推送信息。HTTP 协议的这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。只能使用定时任务来不断地向服务器发起请求,非常浪费资源。

所以 WebSocket 的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,即 WebSocket 允许服务器端与客户端进行全双工通信

WebSocket 没有同源限制,客户端可以与任意服务器通信。

WebSocket 协议标识符是ws(如果加密,则为wss)。

WebSocket 和 HTTP

区别:

  1. 通信模式

    • HTTP:HTTP 是一种单向的通信协议,即客户端主动发起请求(request),服务器被动响应(response)。服务器不能主动向客户端推送数据,除非客户端发起新的请求。

    • WebSocket:WebSocket 是一种双向通信协议,它建立了一个持久化的连接,允许服务器和客户端之间进行全双工通信。即服务器和客户端都可以随时主动发送数据给对方。

  2. 连接类型

    • HTTP:HTTP 连接通常是短连接(非持久化),即每次请求都会建立一个新的连接,并在响应后关闭连接。虽然 HTTP/1.1 引入了 keep-alive 机制来保持连接在一定时间内不关闭,但仍然需要客户端主动发起请求来维持连接。

    • WebSocket:WebSocket 建立的是长连接,一旦连接建立,就可以保持这个连接状态直到明确关闭为止。这使得 WebSocket 能够更高效地处理实时通信需求。

  3. 数据格式

    • HTTP:HTTP 通信的数据主要是基于文本格式的,二进制数据(如图片等)需要转化为 base64 编码文本后才能传输。

    • WebSocket:WebSocket 支持发送文本和二进制数据,不需要进行额外的编码转换。

联系:

WebSocket 协议的建立过程中,客户端会向服务器发送一个标准的 HTTP 请求来发起连接。这个请求中包含了特定的头部字段(如Upgrade: websocketConnection: Upgrade),用于告诉服务器客户端希望将连接升级为 WebSocket 协议。服务器在接收到这个请求后,如果同意升级,就会返回一个状态码为 101 Switching Protocols [ˈproʊtəˌkɔlz] 的响应,并包含相应的 WebSocket 协议头部字段。这个过程实际上是在利用 HTTP 协议来完成 WebSocket 连接的握手阶段。

image.png

image.png

客户端示例代码

<template>
    <div>
        <button @click="send('你要发送的数据')">发消息</button>
    </div>
</template>
 
<script>
export default {
    data () {
        return {
            path:"ws://192.168.0.200:8005/qrCodePage/ID=1/refreshTime=5",
            socket:""
        }
    },
    mounted () {
        // 初始化
        this.init()
    },
    methods: {
        init: function () {
            if(typeof(WebSocket) === "undefined"){
                alert("您的浏览器不支持socket")
            }else{
                // 实例化socket
                this.socket = new WebSocket(this.path)
                // 监听socket连接
                this.socket.onopen = this.open
                // 监听socket错误信息
                this.socket.onerror = this.error
                // 监听socket消息
                this.socket.onmessage = this.getMessage
            }
        },
        // 发送消息给被连接的服务端
        send: function (params) {
            this.socket.send(params)
        },
        open: function () {
            console.log("socket连接成功")
        },
        error: function () {
            console.log("连接错误")
        },
        getMessage: function (msg) {
            console.log(msg.data)
        },
        close: function () {
            console.log("socket已经关闭")
        }
    },
    destroyed () {
        // 销毁监听
        this.socket.onclose = this.close
    }
}
</script>