消息推送机制——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

相关推荐
xiaoye-duck30 分钟前
《Linux 网络编程》深入理解 IO 多路复用:select 函数详解与 Echo 服务实战
linux·网络
xfan_me1 小时前
SSL 证书落地应用与网站安全加固
网络协议·安全·ssl
SDWAN_Cheap1 小时前
TCP滑动窗口与拥塞控制是什么?从流量控制到网络稳定性的完整解析
网络·tcp/ip·php
广东融科数据服务有限公司1 小时前
无网络仓储库房安防监控改造落地案例|离线闭环存储 + 4G 低功耗远程适配实战优化
网络·仓储改造
程序员清风1 小时前
Java 智能体开发:从对话接口到任务执行
java·人工智能·python
YumiProxy1 小时前
动态IP冲突排查实战:从ARP检测到DHCP地址池的完整链路
服务器·网络·网络协议·tcp/ip·ip
Dynadot_tech1 小时前
域名投资中的域名管理策略
网络·域名·域名注册·dynadot·域名管理
一 乐1 小时前
博客管理系统|基于springboot + vue博客管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕设
QuZhengRong2 小时前
【Spring】后端接收的请求参数多了一个逗号的处理办法
java·后端·spring