消息推送机制——WebSocket

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

相关推荐
镜花水月linyi27 分钟前
ConcurrentHashMap 深入解析:从0到1彻底掌握(1.3万字)
java·后端
极客Bob28 分钟前
Java 集合操作完整清单(Java 8+ Stream API)
java
雨中飘荡的记忆28 分钟前
Javassist实战指南
java
Knight_AL36 分钟前
JWT 无状态认证深度解析:原理、优势
java·jwt
寒山李白1 小时前
IDEA中如何配置Java类注释(Java类注释信息配置,如作者、备注、时间等)
java
我要添砖java1 小时前
<JAVAEE> 多线程4-wait和notify方法
android·java·java-ee
Rysxt_1 小时前
Spring Boot SPI 教程
java·数据库·sql
海边夕阳20061 小时前
主流定时任务框架对比:Spring Task/Quartz/XXL-Job怎么选?
java·后端·spring·xxl-job·定时任务·job
q***98521 小时前
VS Code 中如何运行Java SpringBoot的项目
java·开发语言·spring boot