WebSocket的原理与基于Spring Boot的实现

引言

记得几年前微博刚兴起,很多人都会下载来玩。微博上的内容相较于纸质媒体与电视媒体,从信息渠道上较为多元,也比一般的传统媒体更加实时。
当时刚上大学,有了手机,2G网络也已经普及,但资费很贵,没记错的话,一个月30M流量网络要10块钱,超出部分按1元/M来计算。流量很少,但新鲜事物很多,特别是微博上更多(当时小破单机手游别说多火)。一回宿舍就在电脑前开始刷,当“有一条新消息”出现时就要点。包括工作之后,都觉得这个新消息推送很神奇。
前两年查资料是说用长轮询或周期任务来实现,当时还找了个叫Pushlets的框架做了实验,但自从H5被各大浏览器厂商兼容之后,越来越多人开始用Websocket。那Websocket是什么呢?
在这里插入图片描述

所有代码都在github:https://github.com/cyancy911/spring-websocket-demo.git ,相应的实现逻辑与类、方法注释都在代码中,有兴趣的可以clone下来。
WebSocket测试插件为Chrome插件:Simple WebSocket Client,链接这里就不贴了。

如果有什么错误可以在评论或私信反馈。

目录

本篇将分6部分讲解WebSocket,主要涉及原理、关系、使用实现等几个部分。
目录如下:

  1. WebSocket是什么
  2. 它与http的区别
  3. 兼容性如何
  4. 基于Spring WebSocket的简单实现
  5. 更高级使用sockjs
  6. 基于Spring的stomp实现

1.WebSocket是什么

这里主要摘录百度百科wiki两部分内容给大家参考。
在这里插入图片描述

百度百科

WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

wiki

WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection.
翻译:
WebSocket是一种在单TCP连接下,提供全双工通讯通道的计算机通讯协议

ps:好像百度说的大白话些。。。详细了解可看附录链接[wiki]WebSocket

建立连接过程

整个过程分两个阶段
第一个阶段:HTTP请求,握手阶段,变更协议
第二个阶段:成功转换为WebSocket协议进行通讯传输。
在这里插入图片描述

第一阶段
HTTP请求变更协议为WebSocket(请求报文)-> 服务端处理Sec-WebSocket-Key加上盐值(默认为:258EAFA5-E914-47DA-95CA-C5AB0DC85B11)通过SHA-1散列后得到Sec-WebSocket-Accept -> Sec-WebSocket-Accept 经过BASE64编码后回调(响应报文)-> 客户端验证响应报文完整性与正确性

请求报文:
GET /chat HTTP/1.1 - 必须是GET,大于1.1版本
Host: server.example.com - 必须带着请求域名
Upgrade: websocket - 转换头,必须是websocket
Connection: Upgrade - 连接方式,必须是Upgrade
Origin: http://example.com - 跨域,必填
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== - 必填,随机16字节经过base64编码的头
Sec-WebSocket-Protocol: chat, superchat - 可协商子协议,选填,有点像https对称加密协商时的手法
Sec-WebSocket-Version: 13 - 版本,必填,并且必须是13
Sec-WebSocket-Extensions: x-webkit-deflate-frame - 协商层扩展,可选,b端或者s端一方携带一方不携带都会导致连接失败
响应报文,准许:
HTTP/1.1 101 Switching Protocols - 必须是101,否则不能转换
Connection:Upgrade - 必须是Upgrade
Server:beetle websocket server - 服务器类型
Upgrade:WebSocket - 必须是websocket
Date:Mon, 26 Nov 2012 23:42:44 GMT
Access-Control-Allow-Credentials:true - 跨域校验成功
Access-Control-Allow-Headers:content-type - 跨域头
Sec-WebSocket-Accept:FCKgUr8c7OsDsLFeJTWrJw6WO8Q= - GUID,丢失、校验不一致、状态不为101都不能转换
Sec-WebSocket-Protocol: chat - 报文子协议,可选

响应报文,错误:
HTTP/1.1 400 Bad Request - 400参数错误
...
Sec-WebSocket-Version: 13, 8, 7 - 指明服务端支持使用版本

第二阶段
HTTP协议转换为WebSocket协议 -> 所有报文必须通过帧序列封装(mask a sequence frame 这里的帧格式就不说了) -> 否则响应错误状态码(status code)1002并关闭WebSocket连接 -> 正常就通过控制帧和数据帧(UTF-8编码)交替通讯

下边有几个概念比较重要,碎片化,控制帧,数据帧,基本看后边的解释就能懂。

碎片化(Fragmentation):碎片即在传输时把数据内容分块,类似HTTP里的chunk
主要是避免内存缓冲区占满,以及兼容通道复用

例如传输一个经过分块的数据:
数据=123456,碎片化成 块1=123 块2=456
1、CF opcode=1
2、DF 块1
3、CF opcode=0
4、DF 块2
控制帧(Control Frame):主要为了协商协议状态,比如下一帧是做什么的,
通过opcode(operation code 操作码)控制,有下列值:
= 0 - 持续帧(碎片帧,分块传输) Continuation Frame
= 1 - 文本帧 Text Frame
= 2 - 二进制帧 Binary Frame
= 8 - 关闭连接帧 Connection Close Frame
= 9 - 心跳帧 Ping Frame
= A - 响应心跳帧 Pong Frame
数据帧(Data Frame):就是你实际传输内容的帧,都携带一个叫负载数据(Payload Data)的东西
主要分两部分两种:
1、应用数据(Application Data):你实际传输的数据,必填
2、额外数据(Extension Data):在应用数据之前,选填
主要为了添加额外功能,比如后边说到的sockjs和STOMP协议,可以在额外数据中添加其他未定义的控制帧

ps:如果还有继续研究的兴趣,可查看附录:[rfc]The WebSocket Protocol,但内容都是英文的~~~

2.它与http的区别

http历史
讲到区别就要讲到历史。HTTP是超文本传输协议(hyper text transfer protocol)的简写,如果从网络比较普及的协议版本1.0开始算,96年到现在,已经有22年的历史,时隔一年,1997年协议又更新发布了1.1版本,经过较长一段时间后,15年又发布了HTTP/2版本。

http特性
HTTP不管是1.0还是1.1,都只在有请求的情况下做出响应,不管请求多频繁,一个Request对应一个Response,按需分配,前置服务器只能给你限制最大单IP并发请求数。这种效率相对低下的请求方式,比较适合于请求较为集中、交互较少的接口或者静态文件如:html、css、img、json等。共同点是阻塞,请求只能同步。而它们比较重要的不同点是通道复用,即1.1相较于1.0多加了默认响应头维持连接Connection: keep-alive,如果当次请求与上一次请求响应的是同一域名地址,tcp连接在b端(browser)或s端(server)没被关闭,那么浏览器还是会一直用上一次的连接进行请求。

新消息提醒实现手段
如果我们想实现像引言所说的微博/twitter新消息提醒功能,可以根据上述HTTP协议,进行频繁轮训(frequent polling)或长轮询(long polling),二者主要的区别在于,前者是快速返回(即无消息立刻返回状态信息),后者是一直连接等待,具体可附录中的:[知乎]WebSocket 是什么原理?为什么可以实现持久连接?一文,相当形象。

但如果是b端频繁请求或者长时间阻塞连接,假设很长一段时间都没有新消息,那必定是对服务器端资源的巨大浪费,s端将在永恒的tcp握手与挥手和数量逐渐增多的阻塞响应中慢慢逼近503。

所以适用于web的长连接Websocket在H5推广之后很快被大家上线使用,对客户端与服务端需要频繁异步请求响应的场景是一个巨大变革。

3.兼容性如何

基本只要浏览器支持H5,一般都能正常连接使用。服务端限制比较少,只要能用socket建立连接,遵循协议内容,你自己都能写一个。

4.基于Spring Websocket的简单实现

可查看github项目中的websocket-demo模块。

启动应用之后,访问域名:ws://localhost:8080/web-socket 正常响应如下截图:
在这里插入图片描述

5. 更高级使用sockjs

与websocket不同的是,sockjs不是一个协议,只是一套js库,它的出现旨在让浏览器更加“兼容”WebSocket协议规范。

sockjs

这里我就不列概念了,具体可看参考中的:[github]sockjssockjs官网Sockjs简单介绍,官网上翻译的内容基本跟最后一个网址的内容一致,不再赘述。

它主要做了一层协调,如果浏览器不兼容WebSocket,就通过兼容的协议,使用其他长轮询或流传输方式。

stomp

STOMP虽说是个协议,但其实更像个规范,内容不会像WebSocket一样复杂,它类似于HTTP协议,传输于TCP之上,使用帧传输,效果跟WebSocket很像,但你可以不用WebSocket,只要能实现规范内容就行。

wiki上的概念如下:
Simple (or Streaming) Text Oriented Message Protocol (STOMP), formerly known as TTMP, is a simple text-based protocol, designed for working with message-oriented middleware (MOM).
翻译:
面向简单(或流)文本的消息协议,之前叫TTMP,是一个简单的基于文本的协议,设计用来与面向消息的中间件协同工作。

STOMP的数据帧报文很像HTTP报文,如下:

报文1:
["MESSAGE - 命令
destination:/topic/ws/xuidkl/action - 传输目的地
content-type:application/json;charset=UTF-8
subscription:sub-1
message-id:3hyzmuf2-26402
content-length:315

{
	"changeType": "create",
	"lastModified": 1538211260000
}

\u0000"] - \u0000 报文结尾

报文2:
["CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000

\u0000"]

它主要做了一些命令处理,命令处理如开启一个事务、开始传送数据、提交回滚等:

CONNECT - 建立连接方式
SEND - 发送消息
SUBSCRIBE - 订阅主题
UNSUBSCRIBE - 取消订阅主题
BEGIN - 开始一个事务
COMMIT - 提交事务
ABORT - 回滚事务
ACK - 确认成功消费
NACK - 消费失败
DISCONNECT - 断开连接

6. 基于Spring的stomp实现

还是跟上边的4.基于Spring Websocket的简单实现一样,代码都在github仓库,有兴趣的可以上去看。这里列下源码结构和调用逻辑。

代码结构

java:
- config/ - WebSocketConfig - websocket、sockjs、stomp配置
- message/ - TestMessageController - 消息接收接口,订阅接口
- SockjsApplication - 启动类

html:
stomp.html

调用逻辑

SockjsApplication启动 
	-> 扫描WebSocketConfig
	-> 生效STOMP注解@EnableWebSocketMessageBroker,创建经纪人Broker
	-> 配置握手地址:/ws,接收消息接口前缀:/app,订阅接口前缀:/topic,用户Session绑定接口前缀:/user
	-> 扫描TestMessageController
	-> 注册接收消息接口:@MessageMapping,注册订阅返回信息接口:@SubscribeMapping
	-> 界面stomp.html调用

附录-参考

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实现用户实时聊天可以使用 WebSocket 技术,Spring Boot 提供了对 WebSocket 的支持。下面是基于 Spring Boot 实现用户实时聊天的简单步骤。 1. 引入相关依赖 在 pom.xml 中引入 Spring Boot WebSocket 依赖: ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 编写 WebSocket 配置类 创建一个 WebSocket 配置类,使用 `@EnableWebSocket` 注解启用 WebSocket: ```java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired private ChatWebSocketHandler chatWebSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, "/chat").setAllowedOrigins("*"); } } ``` 上面的代码中,`ChatWebSocketHandler` 是自定义的 WebSocket 处理器,用于处理 WebSocket 连接、消息发送等操作。`/chat` 是 WebSocket 的端点,客户端通过这个端点连接 WebSocket。 3. 编写 WebSocket 处理器 创建一个 WebSocket 处理器,实现 `WebSocketHandler` 接口: ```java @Component public class ChatWebSocketHandler implements WebSocketHandler { private static final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { sessions.put(session.getId(), session); } @Override public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception { // 处理客户端发送的消息 String payload = message.getPayload().toString(); // ... } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { sessions.remove(session.getId()); } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { sessions.remove(session.getId()); } public void sendMessage(String sessionId, String message) { WebSocketSession session = sessions.get(sessionId); if (session != null && session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { e.printStackTrace(); } } } } ``` 上面的代码中,使用 `ConcurrentHashMap` 存储所有连接的 WebSocketSession,`afterConnectionEstablished` 方法在客户端连接 WebSocket 时被调用,保存新的 WebSocketSession。`handleMessage` 方法在客户端发送消息时被调用,处理客户端发送的消息。`afterConnectionClosed` 方法在 WebSocket 连接关闭时被调用,从 `sessions` 中移除对应的 WebSocketSession。`handleTransportError` 方法在 WebSocket 出现异常时被调用,从 `sessions` 中移除对应的 WebSocketSession。 4. 发送消息 在需要发送消息的地方,注入 `ChatWebSocketHandler`,调用 `sendMessage` 方法发送消息: ```java @Autowired private ChatWebSocketHandler chatWebSocketHandler; public void sendMessage(String sessionId, String message) { chatWebSocketHandler.sendMessage(sessionId, message); } ``` 上面的代码中,`sessionId` 是 WebSocketSession 的 ID,可以通过 `WebSocketSession.getId()` 获取。`message` 是要发送的消息内容。 5. 前端实现 在前端页面中使用 JavaScript 连接 WebSocket,发送消息和接收消息: ```javascript var socket = new WebSocket("ws://" + window.location.host + "/chat"); socket.onopen = function(event) { console.log("WebSocket opened."); }; socket.onmessage = function(event) { console.log("WebSocket message received:", event.data); }; socket.onclose = function(event) { console.log("WebSocket closed."); }; function sendMessage() { var message = $("#message").val(); socket.send(message); } ``` 客户端通过 `new WebSocket()` 创建 WebSocket 连接,`onopen`、`onmessage`、`onclose` 分别是连接成功、接收到消息、连接关闭时的回调函数。`sendMessage` 方法用于发送消息。 以上是基于 Spring Boot 实现用户实时聊天的简单步骤,具体实现还需要根据具体需求进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值