什么是 WebSocket
WebSocket 是一种网络通信协议,是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通信的协议。WebSocket 属于应用层协议,它基于 TCP 传输协议,并复用 HTTP 的握手通道。
为什么出现 WebSocket
我们已经拥有了 HTTP 协议,为什么还要搞出一套 WebSocket 协议呢?
因为 HTTP 协议有一个缺陷:通信只能由客户端发起,服务端被动响应。要想实现实时通信,必须要客户端实现轮询或者长轮询机制,每次请求都需要完整 HTTP 头部,通信开销较大;每次 TCP 连接重建可能引入延迟问题。
有些场景比如实时聊天、在线协作、实时监控大屏等高频双向通信的场景不再适合使用 HTTP 请求,这时候 WebSocket 协议就应运而生了。
WebSocket 的优点
WebSocket 基于全双工通信,建立连接后客户端和服务器可以双向实时通信,它的特点如下:
- 持久连接:一次握手后保持长连接,避免反复建立连接的开销。
- 主动推送:建立连接后,服务器可以主动向客户端发送数据,无需等待请求。
- 低开销:数据帧轻量(仅需 2~10 字节头部,而 HTTP 请求头通常数百字节)。
- 低延迟:连接建立后,数据可以直接传输(无需 HTTP 头部冗余信息)。
如何实现 WebSocket 通信
WebSocket 复用了 HTTP 的握手通道,客户端通过 HTTP 请求与 WebSocket 服务端协商升级协议。协议升级完成后,后续的数据交换则遵照 WebSocket 的协议。
1. 客户端:申请协议升级
客户端发起协议升级请求,采用的是标准的 HTTP 报文格式,且只支持 GET
方法。
// 部分请求头
GET ws://localhost:8080/ HTTP/1.1
connection:Upgrade
host:localhost:8080
origin:http://192.168.10.6:8000
pragma:no-cache
sec-websocket-extensions:permessage-deflate; client_max_window_bits
sec-websocket-key:oBBcvePJE1NgPiGrza6JnQ==
sec-websocket-version:13
upgrade:websocket
在 URL ws://localhost:8080/ 中,ws
是 WebSocket 协议的标识符,它表示非加密的 WebSocket 协议,直接通过明文传输数据,类似于 HTTP 中的 http。WebSocket 协议的标识符还有 wss
,它表示加密的 WebSocket 协议,基于 TLS/SSL 加密传输,用于保护敏感数据,类似于 HTTP 中的 https。
升级协议请求需要重点关注下面几个请求头字段:
Connection: Upgrade
:表示连接要升级协议Upgrade: websocket
:表示要升级到 websocket 协议。Sec-WebSocket-Version: 13
:表示 websocket 的版本。如果服务端不支持该版本,需要返回一个 Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。Sec-WebSocket-Key:oBBcvePJE1NgPiGrza6JnQ==
:与后面服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。
2. 服务端:响应协议升级
服务端返回内容如下,状态代码 101
表示协议切换。到此完成协议升级,后续的数据交互都按照新的协议来。
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: py6RSjDCGHR78M/JEOSfd3DrLQk=
响应头部字段 Sec-WebSocket-Accept 是根据客户端请求首部的 Sec-WebSocket-Key 计算出来的。服务端根据请求头字段 Sec-WebSocket-Key 的值跟字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
拼接,然后对拼接后的值进行 SHA-1 哈希运算,最后将得到的哈希值进行 base64 编码最终得到 Sec-WebSocket-Accept 的值。
const crypto = require('crypto')
const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
const secWebSocketKey = 'oBBcvePJE1NgPiGrza6JnQ=='let secWebSocketAccept = crypto.createHash('sha1').update(secWebSocketKey + magic).digest('base64')console.log(secWebSocketAccept) // py6RSjDCGHR78M/JEOSfd3DrLQk=
WebSocket 客户端、服务端通信的最小单位是帧(frame),由1个或多个帧组成一条完整的消息(message)。客户端将消息切割成多个帧,并发送给服务端;服务端接收消息帧,并将关联的帧重新组装成完整的消息。数据帧格式不再讲解,感兴趣的同学请参考 RFC6455 5.2节。
WebSocket 简单示例
以下是一个简单的 WebSocket 编程示例,客户端通过 WebSocket 向服务器(node实现)发送数据,并接收服务器返回的数据。
客户端部分代码如下:
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>WebSocket 示例</title>
</head>
<body><textarea></textarea><button onclick="sendConnection()">建立websocket连接</button><button onclick="sendMsgToServer()">发送信息</button><script>// 建立websocket连接function sendConnection() {ws = new WebSocket('ws://localhost:8080')ws.onopen = function () {console.log('ws onopen')clientLog('websocket连接成功')}ws.onmessage = function (e) {console.log('ws onmessage')console.log('来自服务端:', e.data)}ws.onerror = function (e) {console.log('ws onerror: s%', e)isOpenInterval = '0'clientLog('websocket连接出错')}ws.onclose = function (e) {console.log('ws onclose')isOpenInterval = '0'clientLog('关闭websocket连接')}}function sendMsgToServer() {if (!ws || ws.readyState !== 1) {clientLog('请先建立websocket连接')return}const textarea = document.querySelector('textarea')if (!textarea.value) {clientLog('请输入发送内容')return}ws.send(textarea.value)}</script>
</body>
</html>
服务端部分代码如下:
// 这里服务端用了ws这个库。
// 引入依赖
const app = require('express')()
const Websocket = require('ws')const wss = new Websocket.Server({port: 8080, // websocket监听端口
})// 监听websocket连接
wss.on('connection', ws => {console.log('服务端接受连接')ws.send('服务端:websocket连接已建立')// 监听客户端连接ws.on('message', message => {// 监听客户端消息console.log('服务端接受数据: %s', message)ws.send(`服务端:client端数据${message}已接收`)})// 监听客户端断开连接ws.on('close', () => {timer && clearInterval(timer)timer = nullconsole.log('服务端断开连接')ws.send('服务端:websocket连接已断开')ws.terminate()})
})// 监听端口
app.listen(3000, () => {console.log('服务端口:3000')
})
演示代码已放入 gitHub,请下载演示。