各位观众老爷,早上好!我是你们的导游,不对,是你们的 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。它的生成规则是:- 将客户端发送的
Sec-WebSocket-Key
加上一个固定的字符串:"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"。 - 对结果进行 SHA-1 哈希计算。
- 将哈希结果进行 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');
});
这段代码做了以下几件事:
- 创建了一个 HTTP 服务器,监听 3000 端口。
- 在
createServer
的回调函数中,判断请求头中是否包含Upgrade: websocket
。如果包含,就认为是 WebSocket 握手请求,调用handleWebSocketHandshake
函数进行处理。 handleWebSocketHandshake
函数首先从请求头中获取Sec-WebSocket-Key
。- 然后,将
Sec-WebSocket-Key
加上固定的字符串magicString
,进行 SHA-1 哈希计算,并进行 Base64 编码,得到Sec-WebSocket-Accept
。 - 最后,构造一个 HTTP 响应,设置状态码为 101,设置
Upgrade
、Connection
和Sec-WebSocket-Accept
头部,并发送响应。
接下来,我们需要一个客户端来发送握手请求。我们可以使用 wscat
这个命令行工具,或者自己编写一个 Node.js 客户端。这里我们使用 wscat
:
wscat -c ws://localhost:3000
如果一切顺利,你应该能在服务器的控制台中看到 "WebSocket handshake successful!" 的消息,并且 wscat
会提示你已经连接成功。
第四站:安全考量:Origin 和 Sec-WebSocket-Protocol
在 WebSocket 握手中,有两个重要的头部信息,它们和安全性密切相关:Origin
和 Sec-WebSocket-Protocol
。
-
Origin: 指定请求的来源。服务器可以使用
Origin
头部来验证客户端的来源是否合法,防止跨站点的 WebSocket 连接。例如,服务器可以只允许来自http://example.com
的客户端连接。 -
Sec-WebSocket-Protocol: 指定客户端希望使用的子协议。WebSocket 协议本身只是一个传输层协议,它不关心传输的数据的具体格式。子协议可以用来定义数据的格式和语义,例如,可以使用 JSON 作为子协议,或者使用自定义的协议。客户端可以在握手请求中指定多个子协议,服务器可以选择其中一个进行使用。
下面是一个包含 Origin
和 Sec-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
在这个示例中,客户端指定了两个子协议:chat
和 superchat
。服务器可以选择其中一个进行使用,或者拒绝连接。
如果服务器选择了 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 握手之旅就到此结束了。希望大家有所收获!如果有什么疑问,欢迎在评论区留言。我们下次再见!