HTTP 协议在设计上就是一个单向的网络协议,服务器只能被动的接收请求,然后返回相应的数据。对于需要双向通信的场景,虽然可以通过轮询,Comet 等方式实现,但每次链接都要三次握手,效率低下。
与http比较:
1.都基于 TCP 的、应用层的可靠性传输协议
2.WebSocket 在握手时的数据是通过 HTTP 传输的,一旦连接建立后就不再依赖 HTTP 了
社区开源方案:
socket.io, ws 等
webSocket的应用场景
- 通知: 由业务服务端发起,由客户端接收的场景,这类场景下业务通常会有兜底逻辑
- 聊天:服务端和客户端发双向消息进行交互,用在聊天场景
- 游戏:服务端和客户端做高频消息交互
- 语音:从客户端持续不断产生语音包,语音包由大语音包切分而来,需要在服务端重新做组合,要求大包传输 + 顺序性保证
- 直播:大量用户加入同一个直播间,同一直播间内的用户可发弹幕,礼物
- ioT:边缘节点设备,如单车,共享充电宝等
- 数据上报:从客户端持续上报数据到服务端
通信建立:
- cloent向服务端发出一个
Upgrade: WebSocket
的协议升级 HTTP 请求,该请求附带了一个标识Sec-WebSocket-Key
; - 服务端接收到协议升级请求后返回,其状态码为
101
,表明服务端已经成功升级为 WebSocket 协议了。该信息中同样也包含了一个标识Sec-WebSocket-Accept
,该标识符是服务端根据客户端发请求中的Sec-WebSocket-Key
值计算出来的; - 客户端接受到服务器的返回后,会判断服务端返回的
Sec-WebSocket-Accept
标识是否和发出Sec-WebSocket-Key
对应,如果不是就会抛出一个 “Error during WebSocket handshake” 的错误并关闭连接。
Sec-WebSocket-Accept
值的计算:
- 将
Sec-WebSocket-Key
和258EAFA5-E914-47DA-95CA-C5AB0DC85B11
做字符串拼接; - 通过 SHA1 计算出摘要,并转成 base64 字符串。
上代码
client 建立socket链接
... <script> const host = '127.0.0.1'; const port = 8001; const ws = new WebSocket(`ws://${host}:${port}`); </script>
node端监听 WebSocket 升级请求,返回升级成功的数据:
server.on('upgrade', (req, socket) => { if (req.headers['upgrade'] !== 'websocket') { res.end('HTTP/1.1 400 Bad Request'); return; } const secWsKey = req.headers['sec-websocket-key']; const secWsAccept = generateSecWsAccept(secWsKey); const responseHeaders = [ 'HTTP/1.1 101 Web Socket Protocol Handshake', 'Upgrade: WebSocket', 'Connection: Upgrade', 'Sec-WebSocket-Accept: ' + secWsAccept ]; socket.write(responseHeaders.join(' ') + ' '); }
Sec-WebSocket-Accept
值生成函数
function generateSecWsAccept (secWsKey) { return crypto .createHash('SHA1') .update(secWsKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11') .digest('base64'); }
开浏览器调试工具,可以看到 WebSocket 通信确实建立起来了。
前端发送消息
<script> ... ws.addEventListener('open', () => { ws.send('I am client.'); }); </script>
服务端监听消息
server.on('upgrade', (req, socket) => { ... socket.on('data', (data) => { console.log(data.toString()); }) });
数据帧解析
此时发现服务端监听打印的信息是乱码,因为这里接收的 data 并不完全等同于消息的信息,拿到的是 WebSocket 的数据帧。
WebSocket 通信的最小信息单位就是帧,一个或多个帧构成一条完成的消息。
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+
假定数据的长度一定是小于 126 Byte 的,忽略 Payload len 长度的各种判断,解析成消息数据
socket.on('data', (data) => { // Receive message const payloadLen = data[1] & parseInt(1111111, 2); // 假设发送的数据长度小于 125 const maskingKey = data.slice(2, 6); const payloadData = new Buffer(payloadLen); for (let i = 0; i < payloadLen; i++) { let j = i % 4; payloadData[i] = data[6 + i] ^ maskingKey[j]; } console.log(payloadData.toString()); });
向客户端发送消息
同上,在向客户端发送消息前,需要把要发送的消息数据 包装成一个帧信息。这里为了方便我们设置 MASK 字段为 0,表示发出的数据没有进行加密。
const dataBuffer = new Buffer('I am Server.'); const payloadLen = dataBuffer.length; const assistData = []; assistData.push(129); // 129: 1000 0001 assistData.push(payloadLen); let assistBuffer = new Buffer(assistData); let message = Buffer.concat([assistBuffer, dataBuffer]); socket.write(message);
客户端监听
<script> ... ws.addEventListener('message', (event) => { console.log('message:' + event.data); // 打印 message 信息 }); </script>