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握手过程

握手过程,我习惯把它分成三步:

  1. 客户端发起升级请求
  2. 服务端确认升级
  3. 协议切换完成

咱们来看一个实际的握手请求。客户端发出去的大概长这样:

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_upgradeConnection "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协议基础就聊到这儿。下一章咱们动手写代码,把握手和帧解析实现一遍。到时候你会发现,理解了协议细节,写代码就顺多了。