JavaScript内核与高级编程之:`JavaScript`的`WebSocket`握手:其从 `HTTP` 升级到 `WebSocket` 协议的细节。

各位观众老爷,早上好!我是你们的导游,不对,是你们的 JavaScript 语言探险家。今天我们要一起深入丛林,探索一下 WebSocket 握手这个神秘的环节,看看它是如何从平平无奇的 HTTP 变成高大上的 WebSocket 的。

准备好了吗?系好安全带,我们要发车了!

第一站:HTTP 的日常和 WebSocket 的野心

首先,我们得了解一下 HTTP 和 WebSocket 的区别。HTTP 就像是快递小哥,每次你发一个请求,他就送一次包裹,送完就走,下次再来。而 WebSocket 就像是电话,一旦接通,就可以一直聊天,不用每次都拨号。

HTTP 是单向的,request-response 的模式,每次都需要客户端发起请求。而 WebSocket 是双向的,服务器和客户端都可以主动发送消息。

这就引出了 WebSocket 的野心:它想要建立一个持久的连接,让客户端和服务器可以实时通信,就像两个人面对面聊天一样。

第二站:握手协议:一场精心策划的升级

要从 HTTP 升级到 WebSocket,需要进行一次握手。这个握手过程就像是一场精心策划的舞会,双方需要按照特定的步骤和暗号来确认身份,才能最终跳起双人舞。

这个握手过程其实就是一个 HTTP 请求和一个 HTTP 响应。但是,这两个 HTTP 消息中包含了一些特殊的头部信息,这些头部信息是握手成功的关键。

我们先来看一下客户端发送的握手请求:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13

我们来逐行解读一下:

  • GET /chat HTTP/1.1: 这是一个普通的 HTTP GET 请求,/chat 表示你想要连接的 WebSocket 服务的路径。
  • Host: example.com: 指定服务器的域名。
  • Upgrade: websocket: 告诉服务器,我想要升级到 WebSocket 协议。
  • Connection: Upgrade: 告诉服务器,我想要保持连接,以便进行协议升级。
  • Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==: 这是一个随机生成的 Base64 编码的字符串。服务器会用它来生成一个响应 key,用来验证客户端的身份。这个 key 的作用就像是舞会的邀请函上的暗号。
  • Origin: http://example.com: 指定请求的来源。这是一个安全措施,防止跨站点的 WebSocket 连接。
  • Sec-WebSocket-Version: 13: 指定 WebSocket 协议的版本。13 是目前最常用的版本。

客户端发送了这个请求之后,服务器如果支持 WebSocket 协议,并且验证通过,就会返回一个握手成功的响应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiHLiIEN/6/epBrzuOgz8=

我们再来解读一下服务器的响应:

  • HTTP/1.1 101 Switching Protocols: 表示服务器同意协议升级,状态码 101 表示 "Switching Protocols"。
  • Upgrade: websocket: 确认服务器已经升级到 WebSocket 协议。
  • Connection: Upgrade: 确认服务器保持连接,以便进行协议升级。
  • Sec-WebSocket-Accept: s3pPLMBiHLiIEN/6/epBrzuOgz8=: 这是服务器根据客户端发送的 Sec-WebSocket-Key 生成的响应 key。它的生成规则是:

    1. 将客户端发送的 Sec-WebSocket-Key 加上一个固定的字符串:"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"。
    2. 对结果进行 SHA-1 哈希计算。
    3. 将哈希结果进行 Base64 编码。

    这个 key 的作用是验证服务器是否真的收到了客户端的握手请求,并且按照 WebSocket 协议进行了处理。

第三站:代码实战:用 Node.js 模拟握手过程

理论讲完了,我们来点实际的。我们用 Node.js 来模拟一下 WebSocket 的握手过程。

首先,我们需要一个 HTTP 服务器来监听客户端的请求:

const http = require('http');
const crypto = require('crypto');

const server = http.createServer((req, res) => {
  if (req.headers['upgrade'] === 'websocket') {
    // 处理 WebSocket 握手
    handleWebSocketHandshake(req, res);
  } else {
    // 处理普通的 HTTP 请求
    res.writeHead(200, {'Content-Type': 'text/plain'});
    res.end('Hello, World!');
  }
});

function handleWebSocketHandshake(req, res) {
  const secWebSocketKey = req.headers['sec-websocket-key'];
  const magicString = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  const hash = crypto.createHash('sha1');
  hash.update(secWebSocketKey + magicString);
  const secWebSocketAccept = hash.digest('base64');

  const headers = {
    'Upgrade': 'websocket',
    'Connection': 'Upgrade',
    'Sec-WebSocket-Accept': secWebSocketAccept
  };

  res.writeHead(101, headers);
  res.end();

  console.log('WebSocket handshake successful!');
}

server.listen(3000, () => {
  console.log('Server listening on port 3000');
});

这段代码做了以下几件事:

  1. 创建了一个 HTTP 服务器,监听 3000 端口。
  2. createServer 的回调函数中,判断请求头中是否包含 Upgrade: websocket。如果包含,就认为是 WebSocket 握手请求,调用 handleWebSocketHandshake 函数进行处理。
  3. handleWebSocketHandshake 函数首先从请求头中获取 Sec-WebSocket-Key
  4. 然后,将 Sec-WebSocket-Key 加上固定的字符串 magicString,进行 SHA-1 哈希计算,并进行 Base64 编码,得到 Sec-WebSocket-Accept
  5. 最后,构造一个 HTTP 响应,设置状态码为 101,设置 UpgradeConnectionSec-WebSocket-Accept 头部,并发送响应。

接下来,我们需要一个客户端来发送握手请求。我们可以使用 wscat 这个命令行工具,或者自己编写一个 Node.js 客户端。这里我们使用 wscat

wscat -c ws://localhost:3000

如果一切顺利,你应该能在服务器的控制台中看到 "WebSocket handshake successful!" 的消息,并且 wscat 会提示你已经连接成功。

第四站:安全考量:Origin 和 Sec-WebSocket-Protocol

在 WebSocket 握手中,有两个重要的头部信息,它们和安全性密切相关:OriginSec-WebSocket-Protocol

  • Origin: 指定请求的来源。服务器可以使用 Origin 头部来验证客户端的来源是否合法,防止跨站点的 WebSocket 连接。例如,服务器可以只允许来自 http://example.com 的客户端连接。

  • Sec-WebSocket-Protocol: 指定客户端希望使用的子协议。WebSocket 协议本身只是一个传输层协议,它不关心传输的数据的具体格式。子协议可以用来定义数据的格式和语义,例如,可以使用 JSON 作为子协议,或者使用自定义的协议。客户端可以在握手请求中指定多个子协议,服务器可以选择其中一个进行使用。

下面是一个包含 OriginSec-WebSocket-Protocol 的握手请求示例:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat

在这个示例中,客户端指定了两个子协议:chatsuperchat。服务器可以选择其中一个进行使用,或者拒绝连接。

如果服务器选择了 chat 子协议,它会在握手响应中包含 Sec-WebSocket-Protocol 头部:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiHLiIEN/6/epBrzuOgz8=
Sec-WebSocket-Protocol: chat

第五站:错误处理:握手失败的场景

握手过程可能会失败,原因有很多。例如:

  • 服务器不支持 WebSocket 协议。
  • 客户端发送的握手请求格式不正确。
  • 客户端指定的 Origin 不合法。
  • 服务器拒绝了客户端指定的子协议。

如果握手失败,服务器会返回一个错误响应。例如:

HTTP/1.1 400 Bad Request

或者:

HTTP/1.1 403 Forbidden

客户端需要根据错误响应来判断握手失败的原因,并进行相应的处理。

第六站:握手后的数据传输

握手成功之后,客户端和服务器就可以开始进行数据传输了。WebSocket 的数据传输是基于帧的。每一帧都包含一个头部和数据。头部包含了帧的类型、数据长度等信息。

WebSocket 帧的格式比较复杂,这里就不详细介绍了。如果你感兴趣,可以参考 WebSocket 协议的 RFC 文档。

总结:WebSocket 握手流程

为了方便大家理解,我们用一个表格来总结一下 WebSocket 的握手流程:

步骤 客户端 服务器
1 发送握手请求 (HTTP GET 请求,包含 Upgrade、Connection、Sec-WebSocket-Key 等头部) 接收握手请求,检查 Upgrade 头部是否为 websocket
2 如果支持 WebSocket,则从 Sec-WebSocket-Key 生成 Sec-WebSocket-Accept
3 发送握手响应 (HTTP 101 Switching Protocols 响应,包含 Upgrade、Connection、Sec-WebSocket-Accept 等头部)
4 接收握手响应,验证 Sec-WebSocket-Accept 是否正确
5 握手成功,建立 WebSocket 连接 握手成功,建立 WebSocket 连接

最后:WebSocket 的应用场景

WebSocket 是一种非常强大的协议,它可以用于很多场景,例如:

  • 实时聊天应用
  • 在线游戏
  • 股票行情
  • 实时监控
  • 协同编辑

总而言之,只要你需要客户端和服务器之间进行实时通信,WebSocket 都是一个不错的选择。

好了,今天的 WebSocket 握手之旅就到此结束了。希望大家有所收获!如果有什么疑问,欢迎在评论区留言。我们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注