Websocket技术接入整体方案架构

又又又好久没更新,在新公司实在是比较忙+太懒+没有什么有价值的内容,最近负责自研工单项目,实践了整套websocket,把方案搬到博客记录下

因为这篇文章当时写的时候主要是面向组内分享,因为是做SaaS的企业,所以同事之间针对分布式和高可用这些技术的知识背景各不相同,所以整个思路偏讲解和启发

WebSocket生态介绍

Sock.js(接入层)

sock.js是连接层方案,主要解决的浏览器之间对websocket通信协议的兼容问题。

  • 当浏览器不支持WebSocket或网络环境阻止WebSocket连接时,SockJS会自动降级到其他传输方式
  • 降级顺序:WebSocket → EventSource → Long Polling → XHR Polling等

类似的接入层方案:Socket.io。类比java中概念:Netty,HttpClient。

最终我们选sock.js原因是因为他比较轻量化,Socket.io是node.js用的,非node使用前后端都需要三方包,引入更高的复杂度和接入成本

Stomp(协议层)

Stomp是协议层方案,实现的是STOMP协议(Simple Text Oriented Messaging Protocol)

  • 支持主题订阅(Topic)
  • 支持点对点消息传递(Queue)
  • 支持消息确认机制
  • 消息头管理
  • 事务支持
  • 心跳机制
  • 消息确认和重试

为websocket提供一套发布订阅的通用消息模型

类似的协议层方案:MQTT.js(IoT)。类比java中概念:Grpc、Dubbo协议

最终我们选Stomp的原因是因为Spring Boot的消息对他支持的最好,并且他的订阅风格也天然支持我们这次的会话模型。WebSocket细节方案

负载均衡

Websocket是长连接,为了符合分布式架构并且让Websocket也具备可扩展性高可用性,我们就需要考虑负载均衡方案

在选择和确认负载均衡方案之前,我们需要先了解下会话模型以及连接模型

会话模型

STOMP提供了2种发送消息的api,分别对应2种模型:

私聊(通知)模型

messagingTemplate.convertAndSendToUser("userId",destination, message);

这里的userId是基于框架提供了连接各个生命周期时的拦截方法,我们实现方法在建立连接时解析token塞进去的

image-20250928111831070

订阅:客户和工程师(会话的双方)对服务器建立长连接,并且对于某一个topic端点进行订阅

发送:客户和工程师通过接口,调用服务端的api,向某个topic端点进行发送消息

聊天室模型

messagingTemplate.convertAndSend(topic, message);

image-20250928111919866

订阅:客户和工程师(会话的双方)对服务器建立长连接,并且对于某一个topic端点进行订阅

发送:客户和工程师通过接口,调用服务端的api,向某个topic端点进行发送消息

工单这次就包含以下端点:

  1. /ticket/topic/ticket/工单id,作为聊天室模型
  2. /innerMessage。作为点对点站内信通知

综上两种方式,可以发现通过这两种api,我们可以通过让用户订阅到不同的topic+使用不同的api,来完成各种消息通信效果。

比如:

聊天室:

所有人订阅到/topic/聊天室id,所有人的消息发送到/topic/聊天室id 中的所有人。

私聊:

A和B都订阅到一个私有的聊天室topic:/topic/A+B

通知:

A(系统)通知到所有在线的某些人时。

所有在线者订阅到一个通用topic:/topic/InnerMessage,A(系统)向这个topic,根据需要通知到的userUuid,挨个进行通知

这里建议大家可以回去思考一下,通知方式也可以实现聊天室+私聊的效果,聊天室+私聊的方式也可以实现通知的效果,为什么最终这两种模型会区分开?

连接模型

我们需要先了解一下Spring Boot中STOMP协议对消息实际的处理逻辑,不过不管是不是用STOMP,本质都是一样的,因为STOMP只是协议层。

当A和B同时建立连接时,会发生什么?

image-20250928111955574

当A和B都到服务器建立连接,此时的连接是全双工的,既可以通过这个连接发送消息到服务端,也可以接受服务端的消息。

在服务端,框架会维护一个能力类似简易内存消息队列的结构,用于消息的路由分发和暂存。但是需要注意的是,这个消息队列并没有ack、重投,并且也不会将消息保存过长时间,你可以认为它基本就是负责消息的路由而已。

同时,虽然连接是全双工的,但是我们完全可以使用websocket连接仅作为订阅的通道,发送消息使用http接口,这样我们发送消息时的复杂逻辑处理起来会更简单。

粘性会话

基于连接模型,在通过网关负载均衡后,就会有一个很致命的问题(Spring Cloud的Ribbon默认是轮询式的):

用户A连接到了机器1,用户B连接到了机器2,此时他们实际是割裂的,不能收发到彼此的任何消息

image-20250928112018842

因为连接是通过网关负载均衡,所以我们可以在网关修改负载均衡策略,采用哈希等逻辑,让同一个sessionId一定路由到同一台机器上,来解决这个问题,也就是“粘性”会话。并且如果客户断线重连,因为我们的负载均衡策略,他还是会连回原机器,保障消息接受。

todo:思考下这个策略的缺点。

消息桥接

既然维护的是一个内存的类消息队列的场景,那么我们就可以用更强的消息队列进行桥接

image-20250928112042220

本质上,就是每台机器在发到本地的消息队列后,再发到中间件,然后其他机器通过消费中间件的消息,达成广播,将这条消息也写进本地的机器中

todo:思考下这个策略的缺点。

结合改造网关有风险+我们的消息量并不多。我们目前只用了消息桥接,没有结合粘性会话+本地保存连接信息。

权限校验

前面也说到过,选stomp的原因其一就是他有独立的协议,可以在独立的协议中进行鉴权。

鉴权本身最大的问题就在于:WebSocket是不能携带Http header的。

所以我们的做法是,在STMOP握手时传递header,然后在服务端STOMP握手阶段进行鉴权,如果鉴权失败,拒绝握手中断连接。

也就是实际连接过程是:websocket连接建立->stomp握手

当然也存在一定风险,就是通过无限建立websocket连接但是延缓握手来攻击(类似DDOS,连接攻击就是这么难防,安全和性能不能两得),但是这个真发生了可以通过SLB和应用层gateway来限流。

实际消息拉取逻辑

我们的消息是有messageId的。

我们考虑两种场景:

  1. 客户进到页面
  2. 客户在页面待着,但是websocket重连

如何保证消息的正确性?

这里只描述下我们的方案,这个建议大家自己也去思考。没有唯一解。我们也是考虑过很多种方式然后经过取舍,最终想到了一个比较好的方案。

  1. 前端每次打开某个会话页面时,生成一个唯一的UUID,作为这次websocket的唯一UUID。
  2. 前端进入后,调用getHistoryMessage接口(入参为前端当前时间戳),拉取历史消息
  3. 前端建立连接时,上送webSocket的唯一UUID+前端当前获取到的最后一条消息的时间戳+sessionId
  4. 这里最后一条消息,如果是getHistoryMessage,那么就是getHistoryMessage的最后一条
  5. 如果是重连,就是之前的最后一条
  6. 后端建立websocket连接时,根据前端上送的时间戳,触发一个逻辑(事件、任务),将所有大于该时间戳的消息查出来,使用发送逻辑,将这些消息全部再发一次
  7. 后端在发送消息时做一个统一幂等,根据websocket的唯一UUID+messageId,使用redis缓存做幂等。即同一个websocket中,某条消息只发一次
  8. 到websocket的唯一UUID级别的幂等,是为了解决页面多开问题,此情况下用sessionId做幂等有问题
  9. 该方案仅能保证消息不丢失不重发,无法保证重连前后的消息可能会乱序的问题

技术方案架构设计思维过程图

image-20250928112115429

发现缺陷、解决缺陷的方案、整合方案、重构这几个过程的重复后。最终能将方案做到怎样的上限,就依赖于设计人的经验、技术功底、技术眼界了。

版权声明:除特殊说明,博客文章均为intotw原创,依据CC BY-SA 4.0许可证进行授权,转载请附上出处链接及本声明。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇