WebSocket握手过程解析
WebSocket简介
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它使得客户端和服务器之间可以进行实时、双向的数据传输,打破了传统 HTTP 协议只能由客户端发起请求,服务器被动响应的限制。在 WebSocket 出现之前,要实现实时通信,往往需要使用轮询(Polling)或者长轮询(Long Polling)等方式。轮询是指客户端定时向服务器发送请求,询问是否有新数据,这种方式效率较低,会浪费大量的带宽和服务器资源。长轮询则是客户端发送一个请求后,服务器如果没有新数据,不会立即响应,而是保持连接,直到有新数据或者超时才响应,然后客户端再重新发起请求。虽然长轮询比轮询有了很大改进,但仍然不是真正意义上的实时双向通信。
WebSocket 协议在 2011 年被 IETF 定为标准 RFC 6455,并被所有主流浏览器所支持。它基于 TCP 协议,使用类似于 HTTP 的握手过程来建立连接,但又和 HTTP 有本质的区别。WebSocket 连接一旦建立,就可以在客户端和服务器之间进行任意数据的传输,并且可以随时主动向对方发送数据,非常适合实时应用场景,如在线聊天、实时游戏、股票行情推送等。
WebSocket握手过程概述
WebSocket 连接的建立是通过一个 HTTP 握手过程来完成的。之所以使用 HTTP 握手,是为了能复用现有的网络基础设施和 HTTP 协议的优势,比如 HTTP 的代理、缓存等机制。当客户端想要与服务器建立 WebSocket 连接时,它会向服务器发送一个 HTTP 请求,这个请求包含了一些特殊的头部信息,用于表明这是一个 WebSocket 连接请求。服务器收到请求后,如果支持 WebSocket 协议,会返回一个 HTTP 响应,同样带有特殊的头部信息来确认接受 WebSocket 连接。一旦握手成功,后续的数据传输就不再使用 HTTP 协议,而是使用 WebSocket 协议在已建立的 TCP 连接上进行全双工通信。
客户端握手请求
客户端发起的握手请求本质上是一个 HTTP 请求,但是包含了特定的头部字段来标识这是一个 WebSocket 连接请求。以下是一个典型的客户端握手请求示例:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec - WebSocket - Version: 13
GET 请求方法
WebSocket 握手请求使用 HTTP 的 GET 方法,这是因为 GET 方法语义上更符合请求建立连接的操作。在这个请求中,路径(这里是 /chat
)用于标识服务器上的特定资源或者服务,服务器可以根据这个路径来决定是否接受这个 WebSocket 连接请求,比如不同的路径可能对应不同的业务逻辑或者权限要求。
Host 头部字段
Host
字段指定了请求的目标服务器的主机名和端口号(如果是非标准端口)。在上述例子中,Host: server.example.com
表明请求的目标服务器是 server.example.com
。这个字段对于服务器在处理多个虚拟主机或者多站点部署时非常重要,它能帮助服务器确定客户端请求的具体目标。
Upgrade 和 Connection 头部字段
Upgrade
字段和 Connection
字段一起告诉服务器,客户端希望将当前的 HTTP 连接升级为 WebSocket 连接。Upgrade: websocket
明确表示要升级到 WebSocket 协议,而 Connection: Upgrade
则表明这是一个升级连接的请求。这两个字段的配合使用是 HTTP 协议中用于协议升级的标准方式。
Sec - WebSocket - Key 头部字段
Sec - WebSocket - Key
是一个非常关键的字段,它是由客户端随机生成的一个 Base64 编码的字符串。这个密钥的作用是防止未经授权的连接。服务器在接收到这个密钥后,会使用它来生成一个响应密钥。具体来说,服务器会将接收到的 Sec - WebSocket - Key
与一个固定字符串 258EAFA5 - E914 - 47DA - 95CA - C5AB0DC85B11
拼接在一起,然后对拼接后的字符串进行 SHA - 1 哈希计算,最后将计算结果进行 Base64 编码,得到的就是响应密钥。这个过程确保了服务器能够验证客户端请求的合法性,因为只有真正的客户端才能生成与服务器计算结果匹配的响应。
Origin 头部字段
Origin
字段用于标识请求的来源,即客户端所在的域。在上述例子中,Origin: http://example.com
表明请求来自 http://example.com
这个域。服务器可以根据这个字段来进行跨域访问控制(CORS)。如果服务器允许来自该 Origin
的连接,就会继续进行握手过程;否则,服务器可能会拒绝连接。这对于保护服务器免受恶意跨域请求非常重要。
Sec - WebSocket - Version 头部字段
Sec - WebSocket - Version
字段指定了客户端支持的 WebSocket 协议版本。目前最新的标准版本是 13,大多数现代浏览器和服务器都支持这个版本。服务器会检查这个版本号,如果它支持客户端请求的版本,就会继续握手过程;如果不支持,服务器会返回一个包含 Sec - WebSocket - Version
字段的错误响应,告知客户端它支持的版本列表。
服务器握手响应
当服务器接收到客户端的握手请求,并确认请求合法且支持客户端要求的 WebSocket 协议版本后,会返回一个 HTTP 响应作为握手响应。以下是一个典型的服务器握手响应示例:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec - WebSocket - Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
HTTP 101 Switching Protocols 状态码
服务器返回的状态码 101 Switching Protocols
表示服务器同意将当前的 HTTP 连接升级到 WebSocket 协议。这个状态码是专门用于协议升级的,它告诉客户端握手成功,即将切换到新的协议进行通信。
Upgrade 和 Connection 头部字段
与客户端请求中的 Upgrade
和 Connection
字段呼应,服务器在响应中也包含这两个字段,再次确认要将连接升级为 WebSocket 连接。Upgrade: websocket
和 Connection: Upgrade
表明服务器同意按照客户端的请求进行协议升级。
Sec - WebSocket - Accept 头部字段
Sec - WebSocket - Accept
字段是服务器对客户端 Sec - WebSocket - Key
的响应。正如前面提到的,服务器会将接收到的 Sec - WebSocket - Key
与固定字符串 258EAFA5 - E914 - 47DA - 95CA - C5AB0DC85B11
拼接后进行 SHA - 1 哈希计算并 Base64 编码,得到的值就是 Sec - WebSocket - Accept
字段的值。客户端在收到响应后,会验证这个值是否正确。如果验证通过,就表明服务器是合法的,并且握手成功。
代码示例 - 基于 Node.js 的 WebSocket 握手实现
在 Node.js 中,我们可以使用 ws
库来实现 WebSocket 功能,这个库已经帮我们处理好了复杂的握手过程,但了解其底层原理有助于我们更好地理解 WebSocket。以下是一个简单的基于 ws
库的 WebSocket 服务器示例,展示了基本的握手和消息处理:
首先,确保你已经安装了 ws
库:
npm install ws
然后,创建一个 server.js
文件,内容如下:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', function connection(ws) {
ws.on('message', function incoming(message) {
console.log('Received message =>', message);
ws.send('You sent: ' + message);
});
});
console.log('WebSocket server is running on ws://localhost:8080');
在上述代码中,我们创建了一个 WebSocket 服务器,监听在本地的 8080 端口。当有客户端连接时(也就是握手成功后),connection
事件会被触发。在连接建立后,服务器会监听客户端发送的消息(message
事件),并将接收到的消息回显给客户端,同时在控制台打印接收到的消息。
对于客户端,我们可以使用浏览器自带的 WebSocket 对象来进行连接,以下是一个简单的 HTML 页面示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF - 8">
<meta name="viewport" content="width=device - width, initial - scale=1.0">
<title>WebSocket Client</title>
</head>
<body>
<input type="text" id="input" placeholder="Type a message">
<button onclick="sendMessage()">Send</button>
<div id="output"></div>
<script>
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = function () {
console.log('Connected to WebSocket server');
};
socket.onmessage = function (event) {
document.getElementById('output').innerHTML += '<p>' + event.data + '</p>';
};
function sendMessage() {
const input = document.getElementById('input').value;
socket.send(input);
}
</script>
</body>
</html>
在这个 HTML 页面中,我们创建了一个输入框和一个按钮,当用户点击按钮时,会将输入框中的内容发送到 WebSocket 服务器。服务器接收到消息后回显,客户端在 onmessage
事件中处理并显示回显的消息。
深入理解握手过程中的安全性
在 WebSocket 握手过程中,Sec - WebSocket - Key
和 Sec - WebSocket - Accept
机制提供了一定程度的安全性。通过客户端生成随机密钥并由服务器进行验证,防止了中间人伪造客户端请求。然而,这并不意味着 WebSocket 连接就是绝对安全的。
例如,虽然握手过程能防止未经授权的连接,但如果传输的数据没有加密,仍然可能被中间人窃听和篡改。为了确保数据的保密性和完整性,WebSocket 支持使用 SSL/TLS 协议进行加密传输。当使用 wss://
协议(类似于 https://
对于 HTTP)时,WebSocket 连接会通过 SSL/TLS 进行加密,这样在传输过程中的数据就会被加密,即使被中间人截取,也无法轻易获取其内容。
此外,对于跨域访问,虽然 Origin
字段能帮助服务器进行基本的跨域控制,但恶意攻击者可能会尝试绕过这个机制。服务器需要谨慎处理 Origin
字段,比如只允许特定的、可信的域进行连接,并且在必要时结合其他安全机制,如身份验证和授权,来确保只有合法的客户端能够建立 WebSocket 连接并进行数据传输。
处理握手失败的情况
在实际应用中,握手过程可能会失败。服务器可能因为不支持客户端请求的 WebSocket 版本、不允许来自特定 Origin
的请求、或者验证 Sec - WebSocket - Key
失败等原因拒绝连接。当握手失败时,服务器会返回一个 HTTP 错误响应,而不是 101 Switching Protocols
响应。
客户端需要能够处理这些错误情况。在 JavaScript 中,当使用 WebSocket
对象连接服务器时,如果握手失败,onerror
事件会被触发。例如:
const socket = new WebSocket('ws://localhost:8080');
socket.onerror = function (error) {
console.log('WebSocket connection error:', error);
};
在上述代码中,当握手失败时,onerror
事件处理函数会被调用,我们可以在这个函数中记录错误信息或者向用户显示友好的错误提示。
对于服务器端,处理握手失败通常意味着记录错误日志,以便排查问题。例如,在基于 ws
库的 Node.js 服务器中,如果因为版本不支持而拒绝连接,可以这样处理:
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('clientError', function (error, socket) {
console.error('Client error:', error);
socket.close(1008, 'Unsupported WebSocket version');
});
在上述代码中,clientError
事件会在客户端握手请求出现错误时触发。我们记录错误信息,并通过 socket.close
方法关闭连接,同时传入一个状态码(1008 表示策略违规,这里表示不支持的 WebSocket 版本)和一个简短的关闭原因。
总结握手过程的关键要点
WebSocket 的握手过程是建立可靠、安全的全双工通信连接的重要基础。通过 HTTP 风格的请求和响应,客户端和服务器能够协商并建立 WebSocket 连接。在这个过程中,Sec - WebSocket - Key
和 Sec - WebSocket - Accept
用于验证连接的合法性,Origin
字段用于跨域访问控制,Upgrade
和 Connection
字段用于协议升级的标识,Sec - WebSocket - Version
字段用于协商协议版本。
理解握手过程对于开发健壮、安全的 WebSocket 应用至关重要。无论是处理握手失败、优化安全性,还是深入理解底层机制以解决潜在的性能问题,都需要对握手过程有清晰的认识。通过实际的代码示例,我们可以更直观地看到握手过程在实际应用中的体现,帮助我们更好地掌握 WebSocket 技术。同时,随着 Web 应用对实时性要求的不断提高,WebSocket 的应用场景也越来越广泛,深入理解其握手过程能为我们开发出更优秀的实时应用提供有力支持。