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 处理。

Nginx Websocket 配置-LMLPHP

服务器代码:

// 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 Websocket 配置-LMLPHP

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=

参考链接

欢迎关注

Nginx Websocket 配置-LMLPHP

03-28 20:48