Websocket 简介
WebSocket 是一种基于 TCP 连接的全双工通信的协议,其工作在应用层,建立连接的时候通过复用 Http 握手通道,完成 Http 协议的切换升级,即切换到 WebSocket 协议,协议切换成功后,将不再需要客户端发起请求,服务端就可以直接主动向客户端发送数据,实现双向通信。
和 Http 相比,WebSocket有以下优点:
- WebSocket 是双向通信协议,可以双向发送或接受信息。HTTP是单向的,只能由客户端发起请求时,服务器才能响应,服务器不能主动向客户端发送数据。
- WebSocket 可以和 HTTP Server 共享相同端口。
- WebSocket 协议可以更好的支持二进制,可以直接传送二进制数据。
- 同时WebSocket协议的头部非常小,服务器发到客户端的数据包的包头,只有2~10个字节(取决于数据包的长度),客户端发送服务端的包头稍微大一点,因为其要进行掩码加密,所以还要加上4个字节的掩码。总得来说,头部不超过14个字节。
- 支持扩展,用户可以扩展协议实现自己的子协议。
Websocket 建立过程
客户端: 申请协议升级
首先由客户端发起协议升级请求, 根据WebSocket协议规范, 请求头必须包含如下的内容:
GET / HTTP/1.1 Host: localhost:8080 Origin: http://127.0.0.1:3000 Connection: Upgrade Upgrade: websocket Sec-WebSocket-Version: 13 Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
- 请求行: 请求方法必须是GET, HTTP版本至少是1.1。
- 请求必须含有Host。
- 如果请求来自浏览器客户端, 必须包含Origin。
- 请求必须含有 Connection, 其值必须含有 "Upgrade" 记号。
- 请求必须含有 Upgrade, 其值必须含有 "websocket" 关键字。
- 请求必须含有 Sec-Websocket-Version, 其值必须是 13。
- 请求必须含有 Sec-Websocket-Key, 用于提供基本的防护, 比如无意的连接。
服务器: 响应协议升级
服务器返回的响应头必须包含如下的内容:
HTTP/1.1 101 Switching Protocols Connection:Upgrade Upgrade: websocket Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
- 响应行: HTTP/1.1 101 Switching Protocols。
- 响应必须含有 Upgrade, 其值为 "weboscket"。
- 响应必须含有 Connection, 其值为 "Upgrade"。
- 响应必须含有 Sec-Websocket-Accept, 根据请求首部的 Sec-Websocket-key计算出来。。
Sec-WebSocket-Key/Accept的计算
Sec-WebSocket-Key 值由一个随机生成的16字节的随机数通过 base64(见 RFC4648 的第四章)编码得到的。例如, 随机选择的16个字节为:
// 十六进制 数字1~16 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10
测试代码如下:
const list = Array.from({ length: 16 }, (v, index) => ++index) const key = Buffer.from(list) console.log(key.toString('base64')) // AQIDBAUGBwgJCgsMDQ4PEA==
而 Sec-WebSocket-Accept 值的计算方式为:
将 Sec-Websocket-Key 的值和 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接 通过 SHA1 计算出摘要, 并转成 base64 字符串。
此处不需要纠结神奇字符串 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, 它就是一个 GUID, 没准儿是写 RFC 的时候随机生成的。
测试代码如下:
const crypto = require('crypto') function hashWebSocketKey (key) { const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' return crypto.createHash('sha1') .update(key + GUID) .digest('base64') } console.log(hashWebSocketKey('w4v7O6xFTi36lq3RNcgctw==')) // Oy4NRAQ13jhfONC7bP8dTKb4PTU=
Sec-WebSocket-Key的作用
前面简单提到他的作用为: 提供基础的防护, 减少恶意连接, 进一步阐述如下:
- Key 可以避免服务器收到非法的 WebSocket 连接, 比如 Http 请求连接到 Websocket, 此时服务端可以直接拒绝。
- Key 可以用来初步确保服务器认识 ws 协议, 但也不能排除有的 Http服务器只处理 Sec-WebSocket-Key, 并不实现ws协议。
- Key可以避免反向代理缓存。
- 在浏览器中发起 ajax 请求, Sec-Websocket-Key 以及相关 header 是被禁止的, 这样可以避免客户端发送 ajax 请求时, 意外请求协议升级。
- 最终需要强调的是: Sec-WebSocket-Key/Accept 并不是用来保证数据的安全性, 因为其计算/转换公式都是公开的, 而且非常简单, 最主要的作用是预防一些意外的情况。
后端服务
安装 node.js
wget https://nodejs.org/dist/v14.16.0/node-v14.16.0-linux-x64.tar.xz tar -xJvf node-v14.16.0-linux-x64.tar.xz ln -s /root/node-v14.16.0-linux-x64/bin/node /usr/local/bin/node ln -s /root/node-v14.16.0-linux-x64/bin/npm /usr/local/bin/npm
安装依赖
npm install ws
后端 websocket 服务部署
本次实验后端服务 Http 和 Websocket 使用相同的 80 和 443 端口。在实际应用中有个好处,如果原先是提供的是 Http 服务,后来新增了 Websocket 服务,不需要暴露新的端口,也不需要修改防火墙规则。
把 WebSocketServer 和 Http 绑定到同一个端口的关键代码是先获取创建的 http.Server 的引用,再根据 http.Server 创建 WebSocketServer。
不管是 Http 还是 Websocket,客户端发送的都是标准的 Http 请求,都会先将请求交给 http.Server 处理。WebSocketServer 会首先判断请求是不是 Websocket 请求,如果是,它将处理该请求,如果不是,该请求仍由 http.Server 处理。服务器代码:
// app.js 文件 // 导入相关模块 const WebSocket = require('ws'); const http = require('http'); // 使用 http 模块创建的 http.Server httpserver = http.createServer(function (request, response) { // 发送 HTTP 头部 // HTTP 状态值: 200 : OK // 内容类型: text/plain response.writeHead(200, {'Content-Type': 'text/plain'}); // 发送响应数据 "Hello World" response.end('Http Message: Hello World\n'); }).listen(80); // 监听 80 端口, 根据 http.Server 创建 WebSocketServer //创建 WebSocketServer const WebSocketServer = WebSocket.Server; const wss = new WebSocketServer({ server: httpserver //根据 http.Server 创建 WebSocketServer }); wss.on('connection', function (ws) { ws.send("Websocket Send: Hello World") //客户端连接成功后立即向客户端发送一条消息 console.log(`WebSocket connection()`); ws.on('message', function (message) { //收到客户端的消息 console.log(`Websocket Received: ${message}`); }) }); console.log('WebSocket and Http Server started at port 80...');
启动后端服务
[root@ws1 ws-http-server]# node app.js WebSocket and Http Server started at port 80...
验证
分别使用客户端验证 Http 和 Websocket 服务,后端服务器的地址为 192.168.1.141:
- 当客户端未发起协议升级请求时,使用 Http 服务响应客户端。
- 当客户端发起协议升级请求时,Websocket 会复用 Http 的握手通道,升级完成后,后续数据交换使用 Websocket。
测试 Http 连接
# curl -i http://192.168.1.141 HTTP/1.1 200 OK Content-Type: text/plain Date: Thu, 25 Mar 2021 08:00:40 GMT Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked Http Message: Hello World
测试 Websocket 连接
# 方式一:使用 wscat(客户端 npm install wscat 安装) # wscat --connect ws://192.168.1.141 Connected (press CTRL+C to quit) < Websocket Send: Hello World #接收到服务器的消息 > send hello #向服务器发送消息 # 方式二:使用 curl curl -i \ --header "Upgrade: websocket" \ --header "Sec-WebSocket-Key: AQIDBAUGBwgJCgsMDQ4PEA==" \ --header "Sec-WebSocket-Version: 13" \ --header "Connection: upgrade" \ #直接访问后端服务的 Websocket 需要带上该头部 http://192.168.1.141
抓包查看交互报文,可以看到 Websocket 复用了 HTTP 的握手通道, 客户端通过 HTTP 请求与 WebSocket 服务器协商升级协议, 协议升级完成后, 后续的数据交换则遵照 WebSocket协议。
在后端服务器上抓包:
tcpdump -i any host 192.168.1.141 and port 80 -nn -w ws.pcap
通过 Wireshark 软件打开查看:
Nginx 配置
生成自签名证书
https 证书我们都在 CA 站点申请,并由 CA 机构颁发,本次实验使用 openssl 生成自签名 https 证书。
[root@nginx-plus1 certs]# openssl req -newkey rsa:2048 -nodes -keyout server.key -x509 -days 365 -out server.crt # 以下信息自行添加,可以随意 Generating a 2048 bit RSA private key ....................+++ ......+++ writing new private key to 'server.key' ----- You are about to be asked to enter information that will be incorporated into your certificate request. What you are about to enter is what is called a Distinguished Name or a DN. There are quite a few fields but you can leave some blank For some fields there will be a default value, If you enter '.', the field will be left blank. ----- Country Name (2 letter code) [XX]:CN State or Province Name (full name) []:Shanghai Locality Name (eg, city) [Default City]:Shanghai Organization Name (eg, company) [Default Company Ltd]:Ect Organizational Unit Name (eg, section) []:Ect Common Name (eg, your name or your server's hostname) []:chengzw Email Address []:chengzw258@163.com
Nginx 配置文件
Nginx 监听 80 端口用于 Http 和 ws 服务,监听 443 端口用于 Https 和 wss 服务。wss 就是加密的 ws 服务。
events{} http { upstream websocket { server 192.168.1.141:80; #后端服务器地址 } server { listen 443 ssl; # ssl 相关配置 ssl_protocols TLSv1 TLSv1.1 TLSv1.2 SSLv3 SSlv2; ssl_certificate_key /usr/local/nginx/certs/server.key; ssl_certificate /usr/local/nginx/certs/server.crt; location / { proxy_pass http://websocket; # 添加 WebSocket 协议升级 Http 头部 proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } server { listen 80; location / { proxy_pass http://websocket; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } } }
启动 Nginx 服务
/usr/local/nginx/sbin/nginx #根据自己安装 nginx 的路径
验证
测试 Http & Https 连接
# Http 连接 # curl -i http://192.168.1.134 HTTP/1.1 200 OK Server: nginx/1.14.2 Date: Thu, 25 Mar 2021 08:16:59 GMT Content-Type: text/plain Transfer-Encoding: chunked Connection: keep-alive Http Message: Hello World # Https 连接 # curl -i https://192.168.1.134 -k HTTP/1.1 200 OK Server: nginx/1.14.2 Date: Thu, 25 Mar 2021 08:17:07 GMT Content-Type: text/plain Transfer-Encoding: chunked Connection: keep-alive Http Message: Hello World
测试 ws & wss 连接
# 方式一:使用wscat # ws 连接 # wscat --connect ws://192.168.1.134 Connected (press CTRL+C to quit) < Websocket Send: Hello World > send hello # wss 连接,由于是自签名证书需要 -n 参数,表示不检验证书 # wscat --connect wss://192.168.1.134 -n Connected (press CTRL+C to quit) < Websocket Send: Hello World > send hello # 方式二:使用curl # ws 连接 curl -i \ --header "Upgrade: websocket" \ --header "Sec-WebSocket-Key: MlRAR6bQZi07587UD4H8oA==" \ --header "Sec-WebSocket-Version: 13" \ http://192.168.1.134 HTTP/1.1 101 Switching Protocols Server: nginx/1.14.2 Date: Thu, 25 Mar 2021 08:18:48 GMT Connection: upgrade Upgrade: websocket Sec-WebSocket-Accept: iURIl3uIT+tsPMmZ0x1IVH7EL98= # wss 连接,由于是自签名证书需要 -k 参数,表示不检验证书 curl -i \ --header "Upgrade: websocket" \ --header "Sec-WebSocket-Key: MlRAR6bQZi07587UD4H8oA==" \ --header "Sec-WebSocket-Version: 13" \ https://192.168.1.134 -k HTTP/1.1 101 Switching Protocols Server: nginx/1.14.2 Date: Thu, 25 Mar 2021 08:20:20 GMT Connection: upgrade Upgrade: websocket Sec-WebSocket-Accept: iURIl3uIT+tsPMmZ0x1IVH7EL98=
参考链接
- https://juejin.cn/post/6844903850667671560
- https://www.liaoxuefeng.com/wiki/1022910821149312/1103327377678688
- https://www.nginx.com/blog/websocket-nginx/
- https://segmentfault.com/a/1190000022075295