初识 WebSocket 协议

什么是 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 = null
    console.log('服务端断开连接')
    ws.send('服务端:websocket连接已断开')
    ws.terminate()
  })
})

// 监听端口
app.listen(3000, () => {
  console.log('服务端口:3000')
})

演示代码已放入 gitHub,请下载演示。

posted @ 2025-03-20 10:53  老甄Home  阅读(135)  评论(0)    收藏  举报