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 []:[email protected]
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