2、WebSocket协议基础
好,咱们开始聊WebSocket协议。说实话,我刚接触这个协议时,第一反应是——这不就是HTTP加了个长连接吗?后来踩了坑才发现,事情没那么简单。
2.1 WebSocket协议概述
WebSocket,说白了就是一个全双工通信协议。什么意思?就是客户端和服务端可以同时说话,不用等对方说完。
我记得第一次用WebSocket做实时推送时,心里还挺忐忑的。毕竟之前一直用轮询,每隔几秒发个HTTP请求,服务器都快被问烦了。WebSocket一上来,服务器终于能主动找客户端聊天了。
它的核心特点就三个:
- 全双工:两边都能随时发消息
- 低延迟:没有HTTP请求头那些开销
- 持久连接:一次握手,长期通话
你想想看,如果做股票行情推送,用HTTP轮询,每秒请求一次,服务器压力多大?换成WebSocket,数据一有变化,服务器直接推过去,体验完全不一样。
核心要点:WebSocket不是HTTP的升级版,而是独立的协议。它只是借用了HTTP的握手通道,之后就跑自己的协议了。
2.2 WebSocket与HTTP的关系
很多人问我:WebSocket和HTTP到底啥关系?我一般这么解释——它们是兄弟,但不是父子。
WebSocket的握手阶段用的是HTTP协议,但握手成功后,就切换到自己的协议格式了。我刚开始做项目时,以为WebSocket全程走HTTP,结果抓包一看,握手之后的数据包格式完全变了。
这里有个对比表,一目了然:
| 特性 | HTTP | WebSocket |
|---|---|---|
| 通信模式 | 请求-响应 | 全双工 |
| 连接方式 | 短连接(每次请求新建) | 长连接(一次握手持续通信) |
| 协议开销 | 请求头大(几百字节) | 数据帧小(2-14字节) |
| 服务器推送 | 不支持(需轮询) | 原生支持 |
| 端口 | 80/443 | 80/443(握手后同端口) |
嗯,这里要注意:WebSocket握手成功后,虽然还是用80或443端口,但协议已经变了。我曾经在防火墙配置上吃过亏,以为放行HTTP就够了,结果WebSocket连接死活不通。
个人经验:如果WebSocket连不上,先检查代理和防火墙。很多代理只认HTTP,不认WebSocket的升级请求。
2.3 WebSocket握手过程
握手过程,我习惯把它分成三步:
- 客户端发起升级请求
- 服务端确认升级
- 协议切换完成
咱们来看一个实际的握手请求。客户端发出去的大概长这样:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
这里有个关键字段——Sec-WebSocket-Key。它是一个随机生成的Base64编码字符串。服务端收到后,会拼接一个固定GUID,然后做SHA-1哈希,再Base64编码,返回给客户端。
服务端的响应是这样的:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
为什么要有这个验证过程?说白了就是为了防止非WebSocket客户端误连。我刚开始写代码时,自己写了个测试客户端,忘了处理握手响应,结果一直连不上,查了半天才发现是握手没通过。
避坑指南:我曾经在生产环境遇到过一个问题——Nginx没配置WebSocket支持,导致握手请求被拦截。如果你用反向代理,记得加上 Upgrade $http_upgrade 和 Connection "upgrade"。
2.4 WebSocket数据帧格式
握手完成后,数据传输就走WebSocket自己的帧格式了。这个格式,我刚开始看RFC文档时觉得挺复杂,其实拆开来看就几个关键部分。
一个数据帧的结构如下:
| 位 | 0-1 | 2-3 | 4-7 | 8-15 | 16-... |
|---|---|---|---|---|---|
| 含义 | FIN + Opcode | Mask + Payload Len | 扩展长度(可选) | Masking Key(可选) | Payload Data |
咱们重点看几个关键字段:
- FIN:1位,表示这是不是最后一帧。如果数据太大,会被拆成多帧发送。
- Opcode:4位,表示帧类型。0x1是文本,0x2是二进制,0x8是关闭连接,0x9是Ping,0xA是Pong。
- Mask:1位,客户端发数据时必须为1,服务端发数据时为0。这是协议强制要求。
- Payload Len:7位或更多,表示数据长度。
我个人觉得,最坑的就是Mask位。客户端发数据必须掩码,服务端不用。为什么?协议设计者说是为了防止缓存污染攻击。我当年写WebSocket客户端时,忘了做掩码处理,结果服务端一直报错,查了半天RFC才发现这个细节。
来看一个实际的文本帧例子:
// 客户端发送 "Hello" 的帧结构
// 二进制表示(16进制):
// 81 85 37 fa 21 3d 7f 9f 4d 51 58
// 解析:
// 0x81 = 10000001
// FIN=1, Opcode=0x1 (文本帧)
// 0x85 = 10000101
// Mask=1, Payload Len=5
// 0x37fa213d = Masking Key
// 0x7f9f4d5158 = 掩码后的数据
嗯,这里要注意:掩码后的数据需要和Masking Key做异或运算才能还原。我刚开始调试时,直接打印掩码后的字节,看到一堆乱码,还以为协议出问题了。
关键点:WebSocket帧的最小开销只有2字节(FIN+Opcode+Mask+Len),比HTTP请求头小得多。这就是为什么WebSocket适合高频实时通信。
最后说一句,WebSocket还支持Ping/Pong帧,用来检测连接是否存活。我习惯每隔30秒发一个Ping,如果连续3次没收到Pong,就认为连接断了,主动重连。这个机制在移动端特别重要,因为网络环境不稳定。
我的习惯:生产环境中,一定要实现心跳机制。我曾经遇到过服务端进程挂了,但TCP连接还活着,客户端傻等数据的情况。加上心跳检测,能及时发现并恢复连接。
好了,WebSocket协议基础就聊到这儿。下一章咱们动手写代码,把握手和帧解析实现一遍。到时候你会发现,理解了协议细节,写代码就顺多了。