背景
WebSocket 消息到底需不需要应用层定义方案解决粘包/半包问题?网上也没有找到对这个问题比较清晰的解释,所以简单研究了一下 《RFC 6455 - The WebSocket Protocol》 ,本文是一些总结。
结论
WebSocket 设计本身是不需要处理粘包的,但是要看具体实现,尽管没有找到相关文档说明,但是可以基本相信各大现代浏览器关于 WebSocket 相关的实现是完善的。对于其他场景,例如移动 APP 或者游戏客户端,可能使用了某些 WebSocket 的客户端库(例如在 JVM 栈中使用 Netty)来实现通信,对于这些库的实现完善程度就要具体情况具体分析了。
稍微深入一下
为什么说 WebSocket 协议本身是不需要处理粘包的
WebSocket 协议中,一条应用消息是基于帧 (frame) 来传递的,所谓帧,就是组成一条应用消息的基本单位,两个端之间源源不断地传递帧数据,一帧的二进制数据结构如下:
可以看到第 1 个 bit 的数据是FIN
位,表示是否有后续帧,其实这个标记就可以解释原因了,如果FIN
位是 0,则表示还有后续帧数据[1],如果FIN
位是 1,则表示当前帧就是本条消息的最后一帧,也就是说客户端/服务端收到FIN=1
的帧,就表示应用消息的结束位置;就好像在一些编程语言中的迭代器都会定义hasNext()
函数一样。最常见的情况是,因为每帧的消息承载量是足够大的,所以每条消息只有一帧数据,它也是最后一帧的数据 (FIN=1
)。
第一个字节的后续几位数据与本问题基本无关,先略过,再后面的Payload len
表示当前帧承载的应用数据的长度,这里的单位是字节。仔细看看上面的图,可以发现Extended payload length
,这表示每帧所能承载的应用数据是弹性的,当基础长度Payload len
不够用时,可以扩展它的长度。具体来说,如果Payload len
在 0~125 这个范围内,就不需要Extended payload length
,如果Payload len=126
,则Extended payload length
就延伸出 2 个字节,如果Payload len=127
,则Extended payload length
就延伸出 8 个字节,这就是通常情况下,一帧足够用来承载应用数据的原因。
客户端是如何处理多帧数据的?
这里仅仅是指浏览器客户端。在浏览器客户端中,多帧的处理是对开发人员透明的,客户端自动将分散的帧汇总到一起,形成一个完整的应用消息,然后才会触发 WebSocket 的 onmessage 回调函数,作为开发人员无法单独处理某一帧。
所以,不能利用这个机制设计某些特定场合的 API,例如流媒体数据的传输。
注意,这里仅仅说的是浏览器客户端,如果使用某些 WebSocket 库,可能会有其独有的 API 或者机制来单独处理每一帧数据。
使用 Netty 向客户端发送多帧数据
在网上能找到的基于 Netty 的 WebSocket 服务器的配置一般在 handler 的 pipeline 中配置WebSocketServerProtocolHandler
,并且在自定义的 handler 中使用:
channel.writeAndFlush(new TextWebSocketFrame("data")); // 或者 BinaryWebSocketFrame
向客户端发送数据,打开TextWebSocketFrame
的代码,可以发现它的其他构造函数:
public TextWebSocketFrame(boolean finalFragment, int rsv, String text) {
super(finalFragment, rsv, fromText(text));
}
参数boolean finalFragment
就表示这个帧是否是最后一个帧,而int rsv
则对应RSV1-3
(见上面“帧的二进制示意图”),这几个字段没有什么特殊含义,只是用于拓展协议的预留位,传递 0 就行。
作为第一个帧,可以向客户端写入 new TextWebSocketFrame(false, 0, "data-1")
,后续数据需要使用 ContinuationWebSocketFrame 表示,ContinuationWebSocketFrame 和 TextWebSocketFrame 一样也是 WebSocketFrame 的子类;例如,如果希望向客户端发送三个帧:
channel.writeAndFlush(new TextWebSocketFrame(false, 0, "data-1"));
channel.writeAndFlush(new ContinuationWebSocketFrame(false, 0, "data-2"));
channel.writeAndFlush(new ContinuationWebSocketFrame(true, 0, "data-3"));
这里只是为了演示如何使用 Netty 利用 WebSocket 协议的定义,发送多个帧的消息,我想了半天没有想到使用场景。
-
这里的一条消息拆分成多个帧进行传递也就是有些资料中说的消息分片 ↩