八股——WebSocket
文章目录
- 1、 什么是 WebSocket?与 Http 协议的区别是什么?
- 2、 Http 是如何升级为 WebSocket 的?
- 3、 为什么 WebSocket 可以进行全双工模式的消息传输,而 Http 不可以?
- 4、 什么是 TCP 的沾包和拆包?
- 5、 WebSocket 与 Socket 套接字的区别是什么?
- 6、 keep-alive 是什么?
- 7、 网线被一脚踢断了,TCP 连接会发生什么事情?
- 8、SSE协议了解吗?为什么用websocket而不用SSE?
- 9、 Websocket如何保证消息可靠性?(重点强调不是安全性)
1、 什么是 WebSocket?与 Http 协议的区别是什么?
WebSocket 是一种应用层协议,用于在 Web 应用程序中创建实时、双向的通信通道。传统的 HTTP 请求通常是一次请求一次响应的,而 WebSocket 则可以建立一个持久连接,允许服务器即时向客户端推送数据,同时也可以接收客户端发送的数据。WebSocket 相比于传统的轮询或长轮询方式,能够显著减少网络流量和延迟,提高数据传输的效率和速度。
WebSocket 可以在浏览器和服务器之间建立一条双向通信的通道,实现服务器主动向浏览器推送消息,而无需浏览器向服务器不断发送请求。其原理是在浏览器和服务器之间建立一个“套接字”,通过“握手”的方式进行数据传输。由于该协议需要浏览器和服务器都支持,因此需要在应用程序中对其进行判断和处理。
WebSocket 建立在 HTTP 协议之上
,所有的 WebSocket 请求都会通过普通的 HTTP 协议发送出去,然后在服务器端根据 HTTP 协议识别特定的头信息 Upgrade,服务端也会判断请求信息中 Upgrade 是否存在。 这里面 HTTP 是必不可少的,不然 WebSocket 根本无法建立。特别的,WebSocket 在握手时采用了 Sec-WebSocket-Key 加密处理,并采用 SHA-1 (hash)签名。
一旦建立了 WebSocket 连接,客户端和服务器端就可以互相发送二进制流或 Unicode 字符串。所有的数据都是经过 mask 处理过的,mask 的值是由服务器端随机生成的。在数据进行发送之前,必须先进行 mask 处理,这样可以有效防止数据被第三方恶意篡改。
最后需要说明一下的是,WebSocket 的通信协议是基于帧(数据包)的。在数据发送时,一个完整的数据包可以分为多个帧进行发送(拆包/沾包),而每一个帧都包含了数据的一部分,同时还包含了帧头信息。
WebSocket 与 Http 协议的区别
- HTTP (Hypertext Transfer Protocol) 是一个基于请求和响应模式的协议,最早用于 Web 应用。
- WebSocket 是一种双向通信协议,可以在客户端和服务器之间建立持久的连接,以实现实时通信。
- WebSocket 协议在建立连接时需要使用 HTTP 协议。 具体来说,当客户端想要建立 WebSocket 连接时,它们需要通过 HTTP 请求发送一个握手请求。如果服务器同意握手,它将发送一个握手响应,HTTP 协议随后会升级到 WebSocket 协议。
- HTTP 和 WebSocket 协议在用途上也有所不同。 HTTP 协议主要用于客户端和服务器之间的请求和响应通信,而 WebSocket 协议主要用于实现实时通信和服务器推送。通俗的说,HTTP 协议是“一问一答”,WebSocket 协议是“对话”。
2、 Http 是如何升级为 WebSocket 的?
3、 为什么 WebSocket 可以进行全双工模式的消息传输,而 Http 不可以?
你可能会问,既然HTTP 与WebSocket 底层都是通过 TCP 进行消息传输的,为什么 Http 协议却不能服务端主动给客户端进行主动进行消息推送?其实这并不是 TCP 不支持全双工的工作模式,HTTP的设计初衷是文档传输,不是实时通信,所以没有考虑持续的双向通信需求。因此这是由于应用层协议设计的问题导致的 Http 协议不支持全双工的工作模式。
4、 什么是 TCP 的沾包和拆包?
在回答这个问题之前我们需要明确沾包和拆包的定义是什么,如下
- 粘包(Sticky Packets)
发送方连续发送多个数据包时,接收方可能一次性收到多个包"粘"在一起的数据(如:发送 A|B,接收AB)。 - 拆包(Unpacking)
发送方的一个数据包可能被拆分成多次接收(如:发送 C,接收为 C1 和 C2)
为什么会出现沾包和拆包这个问题呢?其根本原因是 TCP 面向的是字节流(字节流是你可以认为是一个一直在流水的水管)的协议,TCP 压根就不知道你想发送的每个数据包之间的边界是什么,那么如果你发送的一段信息过长,超过了 TCP 的缓冲区,那就会导致你的一个大的数据包被拆分为若干小的数据包进行发送,这就是拆包;当你的一个数据包过小,那么 TCP 会认为频繁发送小数据包会频繁进行网络传输,因此会使用 Nagle 算法合并小数据包,这就是沾包。
解决方案
(1) 固定消息长度 - 每个数据包长度固定(如1024字节),不足部分用空位填充。
- 缺点:浪费带宽,不灵活。
(2) 分隔符协议 - 用特殊字符(如\n、\r\n)标记消息边界。
- 示例:Redis协议使用\r\n分割命令。
(3) 消息头声明长度 - 在消息头部添加长度字段(如4字节int),明确后续数据的长度。
- 示例:HTTP协议通过Content-Length头指定正文长度。
在本项目中 netty 框架已经帮我们在应用层方面解决了这个问题,Netty框架:内置解码器如LengthFieldBasedFrameDecoder 自动处理粘包/拆包。
5、 WebSocket 与 Socket 套接字的区别是什么?
首先,Socket套接字,属于传输层的API,像TCP和UDP都是基于Socket实现的。它允许应用程序通过网络进行通信,是操作系统提供的接口。而WebSocket是一种应用层协议,建立在HTTP之上,提供全双工通信,通常用于浏览器和服务器之间的实时交互。换句话说二者不属于同一层面的东西。
6、 keep-alive 是什么?
TCP Keepalive机制 是一种用于检测空闲连接是否存活的传输层保活策略。它通过定期发送探测报文,确认对端是否仍在线,避免因网络中断或对端崩溃导致连接长期占用资源。
- Keepalive 的工作原理
(1) 触发条件
- 空闲超时:当连接在 tcp_keepalive_time 时间内无数据交互,触发Keepalive探测。
- 探测过程:发送空数据包(ACK标志,序列号为当前期望值减1),强制对端响应。
(2) 探测流程 - 发送探测包:每隔 tcp_keepalive_intvl 秒发送一次探测包。
- 等待响应:若收到有效ACK,重置计时器,连接保持。
- 失败判定:连续 tcp_keepalive_probes 次无响应,判定连接死亡,发送RST断开。
# 临时生效(重启后失效)
sysctl -w net.ipv4.tcp_keepalive_time=600 # 空闲10分钟后探测
sysctl -w net.ipv4.tcp_keepalive_intvl=30 # 每隔30秒发送探测
sysctl -w net.ipv4.tcp_keepalive_probes=3 # 最多探测3次# 永久生效:将配置写入 /etc/sysctl.conf
echo "net.ipv4.tcp_keepalive_time = 600" >> /etc/sysctl.conf
sysctl -p
7、 网线被一脚踢断了,TCP 连接会发生什么事情?
首先,我们要回忆一下 TCP 的基础知识,TCP 在会在什么情况下断开一个链接呢?
- 连接双方的一端主动进行 TCP 连接的关闭,就是我们常说的发送 FIN 包,进行四次挥手;
- 重传次数达到阈值,TCP有重传机制,如果多次重传失败,导致 TCP 连接断开;
- Keepalive 机制如果启用,在探测无响应后也会断开连接;
- 接收到 RST 终止报文
…
既然我们已经知道了 TCP 链接可能被关闭的情况,那么我们就可以对这个问题进行回答。首先分情况进行讨论,如果是网线被踢开了,那么我们软件系统其实是无法马上感知物理链路已经断开了,TCP 连接状态仍然ESTABLISHED状态,此时如果你马上把断掉的网线插回去,那么其实 TCP 会当作什么都没发生,因为他根本没有感知到刚才的那次网线的断开。如果你在网线断开的时候,进行了消息的发送,那么由于物理网络的断开,那么你这条消息一定是发送不出去的,会触发 TCP 的重传机制,进行重传,如果你在重传时,将网线插回,那么这个 TCP 连接仍然不会断开,因为重传成功了,否则 TCP 连接将会被断开。那么如果网线一直没有被插回去,并且客户端与服务端一直没有通过这个 TCP 进行通信,那么这个 TCP 连接将会一直存在,那有没有一个好的方法解决这个问题呢?答案是开启Keepalive机制,Keepalive 机制简单的说,就是定期的向 TCP 连接中发送探测数据包,来确保这条 TCP 连接是正常存活的,就像我们在 IM 项目的长链接模块中,加入了心跳机制一样。如果 Keepalive 探测数据包没有得到响应,会帮我们自动关闭这条连接。
8、SSE协议了解吗?为什么用websocket而不用SSE?
SSE是一种基于HTTP协议的服务器推送技术,允许服务器单向地向客户端推送事件流数据。它的主要特点是:
- 单向通信: 数据只能从服务器流向客户端。
- 基于HTTP: 无需新的协议,复用HTTP连接。
- 文本协议: 主要传输UTF-8编码的文本数据。
- 自动重连: 浏览器标准中定义了断线重连机制。
- 实现相对简单: 相比WebSocket,服务器端实现可能更简单一些。
我们项目选择WebSocket而不是SSE,主要是基于以下考虑:
- 核心需求是双向通信: 我们的项目是一个即时通讯(IM)系统。用户不仅需要接收来自服务器的消息推送(如新消息、在线状态更新),还需要能够实时地向服务器发送消息、发送已读回执、发送正在输入状态等操作。WebSocket提供了全双工通信能力,允许客户端和服务器在建立连接后,随时互相发送数据。而SSE是单向的,客户端无法通过同一个SSE连接向服务器发送数据,如果需要客户端发送,就必须发起新的HTTP请求,这违背了IM系统低延迟、高实时性的要求。
- 协议开销和效率: 虽然WebSocket的握手阶段比SSE复杂,但在连接建立后,其数据帧的协议开销相对较小,尤其适合频繁、小数据包的传输场景,这与IM的消息传输特性相符。如果使用SSE推送、HTTP发送,会增加额外的连接建立和请求开销。
- 功能丰富性: WebSocket不仅支持文本数据,还原生支持二进制数据传输,这为未来可能扩展的功能(如传输图片、文件、音视频信令等)提供了更好的基础。
总结来说, 对于需要客户端和服务端进行高频实时双向交互的IM应用场景,WebSocket的全双工通信能力是其相比SSE最核心的优势,也是我们选择它的根本原因。SSE更适用于只需要服务器向客户端单向推送信息的场景,例如股票行情更新、新闻推送等。
9、 Websocket如何保证消息可靠性?(重点强调不是安全性)
WebSocket协议本身是建立在TCP之上的,TCP协议提供了传输层的可靠性,包括数据包的顺序保证和丢包重传机制。但这并不等同于应用层消息的可靠性。比如,TCP连接可能意外断开,或者服务器/客户端在处理消息时崩溃,或者网络长时间抖动导致TCP认为连接超时。因此,为了确保WebSocket上的应用层消息传递真正可靠(即消息不丢失、不重复、基本有序),我们在应用层(也就是在 Real-TimeCommunicationService 和客户端之间)需要实现额外的机制:
1.消息确认机制 (ACK): 这是最核心的机制。
- 发送方: 每发送一条需要保证可靠性的消息时,会附带一个唯一的、单调递增的消息ID(或序列号)。同时启动一个定时器,等待接收方的确认应答(ACK)。
- 接收方: 收到消息后,进行处理。处理成功后,向发送方回复一个ACK消息,其中包含收到的消息ID。
- 超时与重传: 如果发送方在定时器超时前没有收到对应的ACK,就认为消息可能丢失,会进行重传(可能需要设定最大重传次数和退避策略)。
- 消息序列号 (Sequence Number): 为了保证消息的基本有序性(尤其是在重传发生时),发送方可以为每个会话(或用户连接)维护一个递增的序列号。接收方可以根据序列号来判断消息是否重复或失序。对于失序的消息,可以先缓存,等待前面的消息到达后再处理,或者直接丢弃(取决于业务要求)。
3.** 幂等性保证**: 由于存在重传机制,接收方可能会收到重复的消息。接收方需要具备幂等处理能力。通常可以通过检查收到的消息ID是否已经被处理过来实现。例如,在Redis中记录最近处理过的一批消息ID,或者在数据库存储消息时利用唯一ID约束。 - 心跳机制 (Heartbeat/Ping-Pong): 定期由客户端或服务器(或双方)发送心跳消息(Ping),对方收到后回复Pong。如果在一定时间内没有收到对方的心跳响应,就认为连接已经失效。这有助于及早发现“假死”连接,触发重连和后续的消息同步/重发逻辑。Netty本身提供了 IdleStateHandler 来方便地实现心跳检测。
- 重连后的消息同步: 当连接断开并重新建立后,客户端需要与服务器同步状态,拉取在离线期间可能错过的消息。在我们的设计中,这部分主要是通过登录时(包括重连后的自动登录)与 OfflineDataStore 通过拉取离线消息来实现。对于重连前的“飞行中”消息(已发出但未确认ACK的),可以通过上述的ACK和序列号机制来决定是否需要重发。
通过这些应用层的机制组合,我们在TCP提供的基础传输可靠性之上,构建了更强的应用层消息可靠性保障,确保IM消息能够尽可能地准确、完整、有序地送达。