WebSocket 底层原理
1. WebSocket 协议的基本原理
WebSocket 是一个在客户端和服务器之间建立持久、全双工的连接的协议。与传统的 HTTP 请求/响应模型不同,WebSocket 允许客户端和服务器双方通过一个持久的连接进行双向通信。
1.1 WebSocket 握手过程
WebSocket 握手是一个基于 HTTP 的协议升级过程。以下是详细步骤:
-
客户端发起请求:
客户端向服务器发送一个 HTTP 请求,包含一个Upgrade
头部,表明想要将连接从 HTTP 协议切换到 WebSocket 协议。示例请求:
GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
关键字段:
Upgrade
:请求将协议切换为 WebSocket。Sec-WebSocket-Key
:客户端生成的一个随机 Base64 编码值,用于验证服务器响应。Sec-WebSocket-Version
:WebSocket 协议的版本号。
-
服务器回应:
服务器收到请求后,如果支持 WebSocket 协议并同意建立连接,会返回一个 HTTP 101 状态码,表示协议升级成功。示例响应:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
关键字段:
Sec-WebSocket-Accept
:通过将客户端发送的Sec-WebSocket-Key
与特定的 GUID 进行处理,计算出的一个值,用于确认客户端的请求是否合法。
-
连接建立后,数据交换:
一旦握手完成,客户端和服务器之间就建立了一个持久的 WebSocket 连接,双方可以在该连接上任意时刻发送和接收消息。这个过程基于 WebSocket 数据帧(Frame)进行。WebSocket 数据帧:
- 每个数据帧包含一个固定的头部,之后是数据内容(例如文本消息、二进制数据)。
- 数据帧的头部包含了帧的类型、数据长度、是否加密等信息。
1.2 WebSocket 数据传输
WebSocket 使用帧(Frame)来传输数据。每个数据帧的格式如下:
字段 | 长度 | 描述 |
---|---|---|
FIN, RSV1-3 | 1 Byte | 控制位,表示数据帧是否结束以及是否有扩展数据 |
Opcode | 1 Byte | 操作码,标识帧的类型(如文本帧、二进制帧等) |
Mask | 1 Byte | 是否启用了掩码(客户端必须启用掩码) |
Payload Length | 1-8 Bytes | 数据帧的长度(实际有效数据长度) |
Masking-Key | 4 Bytes | 客户端发送数据时的掩码密钥(如果 Mask 为 1) |
Payload Data | N Bytes | 数据帧的实际数据 |
- 文本数据:对于文本数据,WebSocket 使用 UTF-8 编码进行传输。
- 二进制数据:WebSocket 也支持二进制数据传输,例如图像文件、音频流等。
1.3 客户端和服务器的双向通信
- 客户端到服务器:客户端可以通过
send()
方法将数据发送到服务器。 - 服务器到客户端:服务器通过 WebSocket 会话(Session)发送消息。
这种全双工通信模型保证了客户端和服务器之间能够即时、连续地交换数据,消除了 HTTP 的请求/响应延迟。
Java 使用 WebSocket 示例
2.1 WebSocket 服务端代码
我们可以使用 Java 进行 WebSocket 服务端开发。Java EE 提供了 @ServerEndpoint
注解来标识 WebSocket 端点,接收和发送消息。
以下是一个简单的 WebSocket 服务端示例:
2.1.1 服务端代码(ChatServer.java)
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;@ServerEndpoint("/chat")
public class ChatServer {// 当客户端连接时调用@OnOpenpublic void onOpen(Session session) {System.out.println("Client connected: " + session.getId());}// 当接收到客户端的消息时调用@OnMessagepublic void onMessage(String message, Session session) {System.out.println("Message received: " + message);try {// 向客户端发送消息session.getBasicRemote().sendText("Echo: " + message);} catch (IOException e) {e.printStackTrace();}}// 当客户端断开连接时调用@OnClosepublic void onClose(Session session) {System.out.println("Client disconnected: " + session.getId());}// 当发生错误时调用@OnErrorpublic void onError(Session session, Throwable throwable) {System.out.println("Error occurred: " + throwable.getMessage());throwable.printStackTrace();}
}
2.1.2 配置与部署
要使 WebSocket 服务端正常运行,需要在 web.xml
中配置 WebSocket 端点。
<servlet><servlet-name>WebSocketServlet</servlet-name><servlet-class>org.glassfish.tyrus.servlet.TyrusServlet</servlet-class>
</servlet><servlet-mapping><servlet-name>WebSocketServlet</servlet-name><url-pattern>/chat/*</url-pattern>
</servlet-mapping>
这将使得客户端可以通过 /chat
路径与服务器建立连接。
2.2 WebSocket 客户端代码
Java WebSocket 客户端可以通过 WebSocketContainer
创建与服务器的连接,发送和接收消息。
2.2.1 客户端代码(ChatClient.java)
import javax.websocket.*;
import java.net.URI;@ClientEndpoint
public class ChatClient {private Session session;public void connectToServer() {try {WebSocketContainer container = ContainerProvider.getWebSocketContainer();container.connectToServer(this, new URI("ws://localhost:8080/chat"));} catch (Exception e) {e.printStackTrace();}}@OnOpenpublic void onOpen(Session session) {System.out.println("Connected to server: " + session.getId());this.session = session;}@OnMessagepublic void onMessage(String message) {System.out.println("Received message: " + message);}@OnClosepublic void onClose() {System.out.println("Disconnected from server");}@OnErrorpublic void onError(Throwable error) {error.printStackTrace();}public void sendMessage(String message) {try {session.getBasicRemote().sendText(message);} catch (Exception e) {e.printStackTrace();}}public static void main(String[] args) {ChatClient client = new ChatClient();client.connectToServer();client.sendMessage("Hello, WebSocket server!");}
}
2.3 运行 WebSocket 服务
- 启动服务器并运行
ChatServer.java
。 - 启动客户端
ChatClient.java
,客户端将连接到服务端并发送消息。 - 服务端将接收到的消息原样返回。
前后端基于 WebSocket 的答题游戏开发示例
我们可以用 WebSocket 实现一个简单的答题游戏,其中客户端向服务器发送答案,服务器根据答案判断是否正确,并实时更新得分。
3.1 游戏流程
- 客户端:显示问题并接收用户输入的答案。
- 服务器端:接收答案并返回是否正确,更新得分。
- 实时反馈:每个用户的得分通过 WebSocket 即时反馈给客户端。
3.2 游戏前后端实现
3.2.1 服务端实现(GameServer.java)
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArrayList;@ServerEndpoint("/game")
public class GameServer {// 存储所有连接的玩家会话private static final CopyOnWriteArrayList<Session> players = new CopyOnWriteArrayList<>();@OnOpenpublic void onOpen(Session session) {players.add(session);System.out.println("New player connected: " + session.getId());}@OnMessagepublic void onMessage(String message, Session session) {System.out.println("Received answer: " + message);// 假设问题的正确答案是 "42"String response = message.equals("42") ? "Correct!" : "Wrong!";try {session.getBasicRemote().sendText(response);} catch (IOException e) {e.printStackTrace();}}@OnClosepublic void onClose(Session session) {players.remove(session);System.out.println("Player disconnected: " + session.getId());}@OnErrorpublic void onError(Throwable error) {error.printStackTrace();}
}
3.2.2 客户端实现(GameClient.html)
前端使用简单的 HTML 和 JavaScript 通过 WebSocket 与后端进行通信。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>答题游戏</title><script>var socket;function connect() {socket = new WebSocket("ws://localhost:8080/game");socket.onopen = function () {console.log("Connected to server");};socket.onmessage = function (event) {document.getElementById("response").innerText = event.data;};socket.onclose = function () {console.log("Disconnected from server");};}function sendAnswer() {var answer = document.getElementById("answer").value;socket.send(answer);}</script>
</head>
<body onload="connect()"><h1>答题游戏</h1><p>问题:是什么是答案?</p><input type="text" id="answer" placeholder="输入你的答案"><button onclick="sendAnswer()">提交</button><p id="response"></p>
</body>
</html>
3.3 游戏流程
- 客户端:加载 HTML 页面,连接到 WebSocket 服务器。
- 客户端输入答案并点击提交。
- 服务器:判断答案是否正确并返回反馈。
- 客户端:接收反馈并显示给玩家。
总结
通过 WebSocket,Java 可以高效地实现实时通信。在实际应用中,WebSocket 适用于那些需要双向、低延迟通信的场景,比如实时游戏、聊天应用、实时数据监控等。通过结合前后端的 WebSocket 使用,我们可以快速开发出高互动、低延迟的应用。
这个简单的答题游戏示例展示了如何使用 WebSocket 实现前后端实时数据交换。在实际项目中,你可以扩展更多功能,例如计时器、多人游戏、动态问题和答案等。