WebSocket协议及其在实时通信中的重要性

本文深入介绍了WebSocket协议及其在实时通信中的重要性。从HTTP的限制到WebSocket的优势,再到连接建立和实际应用,全面阐述了WebSocket的工作原理及其在实际业务中的应用场景。

一、引言

在生产中,有时需要服务端向客户端发送消息,但是在传统的 HTTP 协议中,是请求-响应模式,也就是说每个请求都是独立的,是由客户端向服务器发送请求,服务器处理请求并返回响应,然后连接就会关闭。

这种请求-响应模式并不能支持后端发起请求,为了解决传统 HTTP 协议在实时通信中的限制,WebSocket协议被引入。

二、WebSocket 介绍

WebSocket 是一种网络传输协议。它允许客户端和服务器之间进行双向通信,而不需要每次请求都重新建立连接。可在单个 TCP 连接上进行全双工通信,位于 OSI 模型的应用层。

WebSocket 通信始于 HTTP 握手,之后升级到WebSocket协议。具有以下优势:

  1. 双向通信:WebSocket支持全双工通信,允许服务器主动向客户端推送数据,而不需要客户端发送请求。
  2. 较低的延迟:WebSocket 通过在建立连接后保持持久连接的方式,避免了重复建立和关闭连接的开销。可以减少延迟,实现更快的数据传输和实时更新。
  3. 减少网络流量:与轮询方式相比,WebSocket 采用事件驱动的方式,只在有新数据时才发送更新,避免了不必要的网络流量和服务器负载。
  4. 兼容性:WebSocket 协议已经得到了广泛的支持。

三、其他主动推送

短轮询长轮询iframe流SSEWebSocketSocket.IO
实时性较差较差较好较好较好较好
网络开销较大较大较小较小较小较小
协议支持HTTP 协议HTTP 协议HTTP 协议HTTP 协议WebSocket 协议自适应
跨域支持较差较差较差支持支持支持
兼容性较好较好较好较好较好较好
双向通信单向通信单向通信单向通信单向通信双向通信双向通信
实现复杂度相对简单相对简单相对简单相对复杂相对复杂相对复杂
延迟较高相对较低相对较低较低最低较低

1.短轮询

优势:

  • 实现简单。简单场景的快速的解决方案。
    劣势:
  • 请求频繁。频繁的请求和响应会导致较高的网络开销和延迟。
  • 资源占用高。对服务器资源的占用较高,每次请求需要建立连接和断开连接。

2.长轮询

优势:

  • 相比短轮询,能够减少一部分请求的频繁性。客户端在收到响应后会立即发起新的请求。
  • 相比短轮询速度相较快。服务器在有数据时才会响应,能够更快地将数据推送给客户端。
    劣势:
  • 延迟高。存在一定程度的延迟,客户端需要等待服务器有数据时才能收到响应。
  • 资源占用高。服务器需要维护大量的长连接,可能会影响服务器的性能。

3.iframe流

优势:

  • 通过在 iframe 中加载长连接资源来实现数据推送,相对于传统的轮询方式,可以降低一定程度的延迟。
    劣势:
  • 受到浏览器同源策略的限制。
  • 复杂应用场景难以管理和维护。

4.SSE(Server-Sent Events)

优势:

  • 基于 HTTP,在不需要额外的握手过程的情况下实现服务器向客户端的数据推送,降低了通信的开销。
  • 相对于传统的轮询方式,能够实现较低的延迟。
    劣势:
  • 仅支持单向通信,无法实现客户端到服务器的双向通信。

5.Socket.IO

优势:

  • 提供了跨浏览器的双向通信能力,可以自动选择最佳的通信方式,包括 WebSocket、轮询等,从而实现较低的延迟。
  • 支持实时双向通信。
    劣势:
  • 需要额外的库支持,增加代码的复杂性。

6.WebSocket

优势:

  • 提供了实时的双向通信能力,实现最低的延迟。
  • 与 HTTP 不同,WebSocket 在建立连接后能够直接实现服务器和客户端之间的双向通信,而不需要频繁地发起新的连接。
    劣势:
  • 部署和维护复杂。

7.如何选择

双向通讯:WebSocket、Socket.IO
实时性:WebSocket >= Socket.IO > SSE ≈ iframe流 ≈ 长轮询 > 短轮询
仅单向通讯:SSE
场景简单且不复杂:长轮询、短轮询

四、WebSocket 应用

1.传统 HTTP 的限制

HTTP 是请求-响应模式,也就是说每个请求都是独立的。
WebSocket 是全双工通信。全双工(Full Duplex)是指在发送数据的同时也能够接收数据,两者同步进行。

用一个例子来解释一下两个的区别:

WebSocket(ws)就像你在餐厅里用餐,旁边有一位专门跟着你的服务员。你点菜,服务员会立刻把你的要求传达给厨房,菜做好后立刻送到你桌上,甚至如果厨房需要你的建议,服务员也能立即传达给你。

而 HTTP 就像整个餐厅共用一批服务员。可能 A 服务员会给你送第一道菜,但送第二道菜的时候会换成 B 服务员。而且服务员之间不共享信息。也就是说,如果你向 A 服务员点了菜,当你问 B 服务员的时候,B 会回答你“很快就上了”,但实际上 B 并不知道这个事情,只是在安慰你的情绪。

从资源使用上讲,相比于 HTTP,WebSocket 更具优势。相比于每次通讯需要建立连接,WebSocket只需要维持 TCP 即可。
![[Pasted image 20231209204843.png]]

2.WebSocket 连接的建立

WebSocket 连接流程
  1. 建立 TCP 连接:WebSocket 连接首先需要建立一个基础的 TCP 连接,这是因为WebSocket 是基于 TCP 的。
  2. 发送特殊的 HTTP 请求(WebSocket 握手):客户端会发送一个特殊的 HTTP 请求,这个请求被称为 WebSocket 握手请求。这个请求包含一些特殊的头部信息,其中包括 Upgrade 和 Connection 字段,告诉服务器希望升级到 WebSocket 协议。服务器收到这个请求后,如果支持 WebSocket,就会进行协议升级。
  3. 服务器确认协议升级:如果服务器支持 WebSocket,它会发送一个类似的响应,这个响应也包含特殊的头部信息,告诉客户端协议已经升级到 WebSocket。这样,从此之后,客户端和服务器之间的通信就不再遵循传统的 HTTP 协议,而是遵循 WebSocket 协议。
  4. 使用 WebSocket 协议进行通讯:一旦协议升级完成,客户端和服务器就可以使用 WebSocket 协议进行实时的双向通信,发送和接收数据帧而无需频繁地建立和断开连接。
    ![[Pasted image 20231209204917.png]]
WebSocket 握手

创建HTTP请求,对连接进行升级参数如下:

# 请求头
Request Headers
# websocket版本
Sec-WebSocket-Version: 13
# 唯一口令,base64编码
Sec-WebSocket-Key: 2icxlyJOYxKXJpCXa8T14Q==
# 连接升级为websocket协议
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Host: 127.0.0.1:9095


# 返回值
Response Headers
# 升级为websocket协议
Upgrade: websocket
Connection: upgrade
# 口令
Sec-WebSocket-Accept: stCq5oQ38vohTpeBYUTMb0IU8Fo=
Sec-WebSocket-Extensions: permessage-deflate;client_max_window_bits=15

![[Pasted image 20231209202336.png]]

![[Pasted image 20231209202607.png]]

3.分布式环境中的 Session 共享

分布式问题

在生产中,程序往往是以分布式进行部署的,即启动多份程序,通过代理提高并发能力。

如下图,用户A 通过负载在节点1上建立了 WebSocket 连接,连接的 Session 也存储在节点1。当 用户A 的数据通过其他方式到达系统时,通过负载代理,最终到达节点4,当节点4对数据进行发送时,发现此节点没有用户A的Session,这样就导致了分布式情况下出现发送不了的情况。

WebSocket 的 Session 是继承自 Closeable,不能像 HttpSession 一样,把内容序列化到Redis中,每个客户端都会与服务器之间建立独立的 WebSocket 连接,Session 存储在建立连接的服务中,那么就引申出 WebSocket 的分布式问题。

![[Pasted image 20231129215436.png]]

解决方法

解决方法大概有三种,分别为中间件广播处理、服务间建立WebSocket连接、请求代理哈希转发。

  1. 中间件广播处理:利用中间件广播的能力,当出现需要发送的消息时,广播所有节点,至少有一个节点能够处理此消息,发送给客户端。
  2. 服务间建立WebSocket连接:所有服务间建立WebSocket连接,分内外两部分连接,外部为用户发起的连接,内部为节点间的连接。通过记录用户连接关系,即可知消息需发送的客户端节点。
  3. 请求代理哈希转发:通过重写代理的方式,获取请求头中用户信息,哈希分配到指定节点处理操作。

下面是不同方法间的对比:

中间件广播处理服务间建立WebSocket连接请求代理哈希转发
优势简单易实现点对点通讯,灵活性较高定向请求,保证数据一致性
劣势依赖于中间件,广播效率低系统复杂,需维护大量连接可能负载不均衡,需自定义负载策略
扩展性扩展性好随节点增加复杂节点增加减少需重新计算哈希值
分析

第二种、第三种方法是可以解决分布式的问题,但在实现难易程度、扩展性方面相比第一种不具有优势。

  • 第二种,需要维护众多WebSocket连接,需要节点间长连接,自动重连等功能,维护调试成本高。
  • 第三种,需要对集群节点可用数量精确把控,若节点已经down掉,需要重新计算哈希值,以免代理到此节点上,维护成本高。

综上所述,选择第一种方案相比来讲,在开发、维护方面更加具有优势。

![[Pasted image 20231129215455.png]]

五、WebSocket案例

1.业务场景和需求

消息数据通过三方接口接入到系统中,服务端主动发送消息到小程序,小程序进行页面跳转,展示消息(由于小程序不支持SSE,选择WebSocket解决主动推送问题)。

要求有较高实时性。由于消息不知什么时候发送到系统中,所以连接在跳转前一直维持。

2.分析

由于消息会不定时接入到系统中,请求通过负载到不同节点,需要保持连接,且解决分布式问题。分析结果如下:

  1. 分布式问题。消息接收节点 与 用户建立 WebSocket 连接节点,两节点不一定一致。
  2. 连接空挡。连接到达最大时间后自动断开,与下次连接间存在时间差。所以需要重复连接,这样会导致多个连接存在,消息多次发送问题。
  3. 恶意连接。恶意使用不存在的 key 进行恶意连接。

针对上述问题方案如下:

  1. 使用MQ广播解决分布式问题。接收消息广播的方式分发。
  2. 在 WebSocket 连接成功后也进行广播,目的是断开此人其他的连接,保证连接有且仅有一个。
  3. 在连接建立前重写钩子函数,对用户进行校验。

![[Pasted image 20231129220451.png]]

3.关键代码

对Session进行封装,扩展Session数据,添加Session管理类,封装相应的方法

public class SessionExt {  
    private WebSocketSession session;  
    private String uniqueId;
    ... 省略 get set 方法
}
@Component  
public class SessionContainer {  
    private static final ConcurrentHashMap<String, SessionExt> SESSION_MAP =  
            new ConcurrentHashMap<>(WebSocketConstants.INT_16);  
  
    /**  
     * 获取 session 数据  
     *  
     * @param key key  
     * @return session  
     */    
     public SessionExt getSessionExt(String key) {  
        return SESSION_MAP.getOrDefault(key, null);  
    }  
  
    /**  
     * 添加 session 数据  
     *  
     * @param key        key  
     * @param sessionExt sessionExt  
     */    
     public void addSessionExt(String key, SessionExt sessionExt) {  
        SESSION_MAP.put(key, sessionExt);  
    }  
  
    /**  
     * 添加 session 数据  
     *  
     * @param key        key  
     * @param sessionExt sessionExt  
     */    
     public void addSessionExtAndClose(String key, SessionExt sessionExt) {  
        SessionExt sessionExtOld = SESSION_MAP.get(key);  
        if (sessionExtOld != null) {  
            sessionExtOld.closeSession();  
        }  
        SESSION_MAP.put(key, sessionExt);  
    }  
  
    /**  
     * 删除 session 数据  
     *  
     * @param key key  
     */    
     public void delSessionExt(String key) {  
        SESSION_MAP.remove(key);  
    }  
}

连接创建、关闭后的广播回调,关闭多余连接

 
public void handleOpenProcessing(String key, String uniqueId) {  
    SessionExt sessionExt = SESSION_CONTAINER.getSessionExt(key);  
    if (sessionExt != null) {  
        String uniqueIdOld = sessionExt.getUniqueId();  
        if (!uniqueIdOld.equals(uniqueId)) {  
            log.debug("[websocket]广播,连接后置处理,关闭用户之前连接");  
            sessionExt.closeSession();  
            SESSION_CONTAINER.delSessionExt(key);  
        }  
    }  
    log.info("[websocket]广播,连接后置处理,完成");  
}  
  

public void handleCloseProcessing(String key, String uniqueId) {  
    log.debug("[websocket]广播处理 关闭连接,key:" + key);  
    SessionExt sessionExt = SESSION_CONTAINER.getSessionExt(key);  
    if (sessionExt != null) {  
        String uniqueIdOld = sessionExt.getUniqueId();  
        if (uniqueIdOld.equals(uniqueId)) {  
            sessionExt.closeSession();  
            SESSION_CONTAINER.delSessionExt(key);  
        }  
    }  
    log.info("[websocket]广播,关闭后置处理,完成");  
}

六、总结

本文介绍了 WebSocket 协议及其在实时通信中的重要性。因为传统 HTTP 协议的限制,引出了 WebSocket 协议,展现了 WebSocket 相对于传统HTTP的优越性。

对WebSocket连接的建立过程进行了详细解析,包括TCP连接的建立、特殊HTTP请求的发送以及协议升级的过程。

最后通过具体案例 WebSocket 在分布式环境中的 Session 共享问题,并提出了解决方案。

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值