WebSocket
背景介绍
WebSocket 是从 HTML5 开始⽀持的⼀种⽹⻚端和服务端保持⻓连接的 消息推送机制 .
理解消息推送:
传统的 web 程序, 都是属于 "⼀问⼀答" 的形式. 客⼾端给服务器发送了⼀个 HTTP 请求, 服务器给客⼾端返回⼀个 HTTP 响应.
这种情况下, 服务器是属于被动的⼀⽅. 如果客⼾端不主动发起请求, 服务器就⽆法主动给客⼾端响应.
像五⼦棋这样的程序, 或者聊天这样的程序, 都是⾮常依赖 "消息推送" 的. 如果只是使⽤原⽣的 HTTP协议, 要想实现消息推送⼀般需要通过 "轮询" 的⽅式.
轮询的成本⽐较⾼, ⽽且也不能及时的获取到消息的响应.
⽽ WebSocket 则是更接近于 TCP 这种级别的通信⽅式. ⼀旦连接建⽴完成, 客⼾端或者服务器都可以主动的向对⽅发送数据.
之前学习过的服务器开发,主要是这样的模型:
客户端,主动向服务器发起请求,服务器收到之后,返回一个响应。如果客户端不主动发起请求,服务器是不能主动联系客户端的
我们是否需要,服务器主动给客户端发消息这样的场景呢?
需要! "消息推送"
当前已有的知识,主要是HTTP。
HTTP自身是难以实现这种消息推送效果的~~ HTTP要想实现类似的效果,就需要基于"轮询"的机制
很明显,像这样的轮询操作,开销是比较大的,成本也是比较高的
如果轮询间隔时间长,玩家1落子之后,玩家2不能及时的拿到结果
如果轮询间隔时间短,虽然即时性得到改善,但是玩家2不得不浪费更多的机器资源(优其是带宽)
这就类似于去餐馆吃饭
1.每隔1分钟,就去前台看一眼,问问老板,我的饭好了没
2.我直接找个角落坐下来,玩手机 ,啥时候饭做好了,老板就端过来了
因此,websocket就是实现消息推送的一个主要的方式~~
原理解析
握⼿过程 (建立连接的过程)
WebSocket 协议本质上是⼀个基于 TCP 的协议。为了建⽴⼀个 WebSocket 连接,客⼾端浏览器⾸先要向服务器发起⼀个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了⼀些附加头信息,通过这个附加头信息完成握⼿过程.

使用网页端,尝试和服务器建立websocket连接
网页端会先给服务器发起一个HTTP请求,这个HTTP请求中会带有特殊的 header
Connection: upgrade
Upgrade: websocket
这两个header其实就是在告知服务器,我们要进行协议升级.
如果服务器支持websocket,就会返回一个特殊的HTTP响应,这个响应的状态码是101.(切换协议)
客户端和服务器之间就开始使用websocket来进行通信了
报⽂格式
也是一个应用层的协议。下层是基于 TCP 的~
opcode 描述了当前这个websocket报文是啥类型~
表示当前这是一个文本帧,还是一个二进制帧~
表示当前这是一个 ping帧,还是一个pong帧~
payload len 含义表示的是当前数据报携带的数据载荷的长度。这个字段本身就是一个变长的,一个websocket数据报能承载的载荷长度是非常非常长的!!
payload data 实际报文要传输的数据载荷~~

FIN : 为 1 表⽰要断开 websocket 连接.
• RSV1/RSV2/RSV3: 保留位, ⼀般为 0.
• opcode : 操作代码. 决定了如何理解后⾯的数据载荷.
◦ 0x0: 表⽰这是个延续帧. 当 opcode 为 0, 表⽰本次数据传输采⽤了数据分⽚, 当前收到的帧为其中⼀个分⽚.
◦ 0x1: 表⽰这是⽂本帧.
◦ 0x2: 表⽰这是⼆进制帧.
◦ 0x3-0x7: 保留, 暂未使⽤.
◦ 0x8: 表⽰连接断开.
◦ 0x9: 表⽰ ping 帧.
◦ 0xa: 表⽰ pong 帧.
◦ 0xb-0xf: 保留, 暂未使⽤.
• mask: 表⽰是否要对数据载荷进⾏掩码操作。从客⼾端向服务端发送数据时,需要对数据进⾏掩码操作;从服务端向客⼾端发送数据时,不需要对数据进⾏掩码操作。
• Payload length:数据载荷的⻓度,单位是字节。为7位,或7+16位,或1+64位。
假设数Payload length === x,如果
• x为0~126:数据的⻓度为x字节。
• x为126:后续2个字节代表⼀个16位的⽆符号整数,该⽆符号整数的值为数据的⻓度。
• x为127:后续8个字节代表⼀个64位的⽆符号整数(最⾼位为0),该⽆符号整数的值为数据的⻓度。
• Masking-key:0或4字节(32位)所有从客⼾端传送到服务端的数据帧,数据载荷都进⾏了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key
为啥要使⽤掩码算法?
主要是从安全⻆度考虑, 避免⼀些缓冲区溢出攻击.
• payload data: 报⽂携带的载荷数据.
代码⽰例
Spring 内置了 websocket . 可以直接进⾏使⽤.
服务器代码
创建 api.TestAPI 类.
这个类⽤来处理 websocket 请求, 并返回响应.
每个⽅法中都带有⼀个 session 对象, 这个 session 和 Servlet 的 session 并不相同, ⽽是 WebSocket 内部搞的另外⼀组 Session.
通过这个 Session 可以给客⼾端返回数据, 或者主动断开连接.
java
@Component
public class TestAPI extends TextWebSocketHandler {
public TestAPI() {
System.out.println("TestAPI load!");
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
System.out.println("连接成功");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
System.out.println("收到消息: " + message.toString()); //也可以用System.out.println("收到消息: " + message.geyPayload()); 用TextMessage自带的方法geyPayload()也可以获取消息内容
session.sendMessage(message); //让服务器收到数据之后,把数据原封不动的返回回去~
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.out.println("连接异常");
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("连接关闭");
}
}
创建 config.WebSocketConfig 类
这个类⽤于配置 请求路径和 TextWebSocketHandler 之间的对应关系.
java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private TestAPI testAPI;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(testAPI, "/test");
}
}
写到这里,我们的web socket的服务器这边的辑就已经编写完毕。总结一下:
我们在写这代码中,主要是涉及到两个类。
一个类是Test API。通过这个类,这里面我去重写了几个方法。主要是四个方法。这四个方法分别对到我们WebSocket它一个四个不同阶段。一个是连接建立完成,一个是收到消息,还有一个是连接出现异常,还有一个是连接正常关闭。那光有这个TestAPI还不够,还需要把这关联上一个具体的路径。
此处我们就通过这WebSocket的confifigure这个类来去实现这样的一个注册效果。把我们这个类和这边的这个路径能够关联起来。这个路径就会在我们后面写这个前端代码时,跟这个URL关联在一起。那么再进一步的我们当前这个类要想被Spring正确识别,还得加上上述的两个注解,尤其是第二个注解@EnableWebSocket,这是开启WebSocket的关键所在。完成了这些之后,我们的后段逻辑就已经编写完毕。
那么下一步我们动手来去实现前端这一块的相关处理。
客⼾端代码
创建 test.html
html
<input type="text" id="message">
<button id="sendButton">发送</button>
<script>
//创建WebSocket实例
let websocket = new WebSocket("ws://127.0.0.1:8080/test"); //ws=>websocket,不是"猥琐"的缩写~
//需要给实例挂载一些回调函数
websocket.onopen = function() {
console.log("open!");
}
websocket.onmessage = function(e) {
console.log("recv: " + e.data);
}
websocket.onclose = function() {
console.log("close!");
}
websocket.onerror = function() {
console.log("error!");
}
//实现点击按钮后,通过websocket发送请求
let messageInput = document.querySelector("#message");
let sendButton = document.querySelector("#sendButton");
sendButton.onclick = function() {
console.log("send: " + messageInput.value);
websocket.send(messageInput.value);
}
</script>
启动服务器, 通过浏览器访问⻚⾯, 观察效果.
WebSocket参考资料
https://geek-docs.com/spring/spring-tutorials/websocket.html
https://www.sohu.com/a/227600866_472869

