前言
WebSocket 是从 HTML5 开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的 消息推送机制.
理解消息推送:
传统的 web 程序, 都是属于 “⼀问⼀答” 的形式. 客⼾端给服务器发送了⼀个 HTTP 请求, 服务器给客
⼾端返回⼀个 HTTP 响应.这种情况下, 服务器是属于被动的⼀⽅. 如果客⼾端不主动发起请求, 服务器就⽆法主动给客⼾端响应.
像聊天这样的程序, 是⾮常依赖 “消息推送” 的. 如果只是使⽤原⽣的 HTTP 协议, 要想实现消息推送⼀般需要通过 “轮询” 的⽅式.轮询的成本⽐较⾼, ⽽且也不能及时的获取到消息的响应.⽽ WebSocket 则是更接近于 TCP 这种级别的通信⽅式. ⼀旦连接建⽴完成, 客⼾端或者服务器都可以
主动的向对⽅发送数据.
一.WebScoket是什么?
1.1 webscoket的概念
WebSocket 是基于 TCP 的一种新的应用层网络协议。它实现了浏览器与服务器全双工通信,即允许服务器主动发送信息给客户端。因此,在 WebSocket 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输,客户端和服务器之间的数据交换变得更加简单。
1.2 webscoket 特点
- 建立在 TCP 协议之上;
- 与 HTTP 协议有着良好的兼容性:默认端口也是 80(ws) 和 443(wss,运行在 TLS 之上),并且握手阶段采用 HTTP 协议;
- 较少的控制开销:连接创建后,ws 客户端、服务端进行数据交换时,协议控制的数据包头部较小,而 HTTP 协议每次通信都需要携带完整的头部;
- 可以发送文本,也可以发送二进制数据;
- 没有同源限制,客户端可以与任意服务器通信;
- 协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL;
- 支持扩展:ws 协议定义了扩展,用户可以扩展协议,或者实现自定义的子协议(比如支持自定义压缩算法等);
1.3 Webscoket 协议帧
-
FIN, 长度为 1 比特, 该标志位用于指示当前的 frame 是消息的最后一个分段, 因为 WebSocket 支持将长消息切分为若干个 frame 发送, 切分以后, 除了最后一个 frame, 前面的 frame 的 FIN 字段都为 0, 最后一个 frame 的 FIN 字段为 1, 当然, 若消息没有分段, 那么一个 frame 便包含了完成的消息, 此时其 FIN 字段值为 1
-
RSV 1 ~ 3, 这三个字段为保留字段, 只有在 WebSocket 扩展时用, 若不启用扩展, 则该三个字段应置为 1, 若接收方收到 RSV 1 ~ 3 不全为 0 的 frame, 并且双方没有协商使用 WebSocket 协议扩展, 则接收方应立即终止 WebSocket 连接
-
Opcode, 长度为 4 比特, 该字段将指示 frame 的类型, RFC 6455 定义的 Opcode 共有如下几种:
0x0, 代表当前是一个 continuation frame
0x1, 代表当前是一个 text frame
0x2, 代表当前是一个 binary frame
0x3 ~ 7, 目前保留, 以后将用作更多的非控制类 frame
0x8, 代表当前是一个 connection close, 用于关闭 WebSocket 连接
0x9, 代表当前是一个 ping frame (将在下面讨论)
0xA, 代表当前是一个 pong frame (将在下面讨论)
0xB ~ F, 目前保留, 以后将用作更多的控制类 frame
-
Mask, 长度为 1 比特, 该字段是一个标志位, 用于指示 frame 的数据 (Payload) 是否使用掩码掩盖, RFC 6455 规定当且仅当由客户端向服务端发送的 frame, 需要使用掩码覆盖, 掩码覆盖主要为了解决代理缓存污染攻击 (更多细节见 RFC 6455 Section 10.3)
-
Payload Len, 以字节为单位指示 frame Payload 的长度, 该字段的长度可变, 可能为 7 比特, 也可能为 7 + 16 比特, 也可能为 7 + 64 比特. 具体来说, 当 Payload 的实际长度在 [0, 125] 时, 则 Payload Len 字段的长度为 7 比特, 它的值直接代表了 Payload 的实际长度; 当 Payload 的实际长度为 126 时, 则 Payload Len 后跟随的 16 位将被解释为 16-bit 的无符号整数, 该整数的值指示 Payload 的实际长度; 当 Payload 的实际长度为 127 时, 其后的 64 比特将被解释为 64-bit 的无符号整数, 该整数的值指示 Payload 的实际长度
-
Masking-key, 该字段为可选字段, 当 Mask 标志位为 1 时, 代表这是一个掩码覆盖的 frame, 此时 Masking-key 字段存在, 其长度为 32 位, RFC 6455 规定所有由客户端发往服务端的 frame 都必须使用掩码覆盖, 即对于所有由客户端发往服务端的 frame, 该字段都必须存在, 该字段的值是由客户端使用熵值足够大的随机数发生器生成, 关于掩码覆盖, 将下面讨论, 若 Mask 标识位 0, 则 frame 中将设置该字段 (注意是不设置该字段, 而不仅仅是不给该字段赋值)
-
Payload, 该字段的长度是任意的, 该字段即为 frame 的数据部分, 若通信双方协商使用了 WebSocket 扩展, 则该扩展数据 (Extension data) 也将存放在此处, 扩展数据 + 应用数据, 它们的长度和便为 Payload Len 字段指示的值
1.5 WebSocket 与 HTTP、TCP
WebSocket、HTTP 和 TCP 都是用于网络通信的技术,但它们之间存在一些关键的区别:
- 层级:
TCP: 属于传输层协议,是面向连接的、可靠的、点对点的传输协议。
HTTP: 属于应用层协议,是无状态的、非可靠的、面向请求-响应的协议。
WebSocket: 属于应用层协议,构建在 TCP 协议之上,是一种全双工、低延迟、低开销的协议。
- 连接类型:
TCP: 通常是短连接,即连接建立后,数据传输完成后立即断开连接。
HTTP: 通常是短连接,但也可以使用持久连接。
WebSocket: 是长连接,即连接建立后,可以在连接保持期间传输多个数据包。
- 协议:
TCP: 使用 TCP/IP 协议族进行通信。
HTTP: 使用文本格式的协议进行通信。
WebSocket: 使用 HTTP 协议进行握手建立连接,然后使用基于帧的二进制协议进行数据传输。
用途:
TCP: 广泛应用于各种网络应用,例如 Web 服务器、FTP 客户端/服务器、电子邮件等。
HTTP: 主要用于 Web 服务器和客户端之间的通信,用于传输 Web 页面、图片、视频等数据。
WebSocket: 常用于实时通信应用,例如 Web 聊天、游戏、协作编辑等。
1.6 Websocket 握手原理
1.7 基于WebSoket编程
java中有两种方式
- 使用Tomcat提供原生的WebScoket api .
- 使用spring体哦那个的WebScoket api.
编写服务器代码
- 创建一个一个类,作为WebSoketHandler
@Component
public class TestWebSocketController extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 这个方法会在 websocket 连接建立成功后, 被自动调用.
System.out.println("TestAPI 连接成功!");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
// 这个方法是在 websocket 收到消息的时候, 被自动调用的.
System.out.println("TestAPI 收到消息!" + message.toString());
// session 是个会话, 里面就记录了通信双方是谁. (session 中就持有了 websocket 的通信连接)
session.sendMessage(message);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 这个方法是在连接出现异常的时候, 被自动调用的.
System.out.println("TestAPI 连接异常!");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
// 这个方法是在连接正常关闭后, 被自动调用的
System.out.println("TestAPI 连接关闭!");
}
}
- 把上面的类注册到spring里面,配置路由。
创建一个类,实现一个webscoket配置的interface
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestWebSocketController testWebSocketAPI;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//通过这个方法,把创建好的Handler注册到具体的路径上
//此时当前浏览器,websocket的请求路径是"/test的时候,就会调用TestWebSocketController的处理方法"
registry.addHandler(testWebSocketAPI, "/test");
}
}
编写客户端代码
实现js的编写
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试 websocket 的使用</title>
</head>
<body>
<input type="text" id="message">
<button id="send-button">发送</button>
<script>
// 编写 js 使用 websocket 的代码.
// 创建一个 websocket 实例
var websocket = new WebSocket("ws://localhost:8080/test");
// 给这个 websocket 注册上一些回调函数.
websocket.onopen = function() {
// 连接建立完成后, 就会自动执行到.
console.log("websocket 连接成功!");
}
websocket.onclose = function() {
// 连接断开后, 自动执行到.
console.log("websocket 连接断开!");
}
websocket.onerror = function() {
// 连接异常时, 自动执行到
console.log("websocket 连接异常!");
}
websocket.onmessage = function(e) {
// 收到消息时, 自动执行到
console.log("websocket 收到消息! " + e.data);
}
let messageInput = document.querySelector('#message');
let sendButton = document.querySelector('#send-button');
sendButton.onclick = function() {
console.log("websocket 发送消息: " + messageInput.value);
websocket.send(messageInput.value);
}
</script>
</body>
</html>