1.websocket介绍 #

2. websocket实战 #

2.1 server.js #

server.js

const { Server } = require('ws');
const wss = new Server({ port: 8888 });
wss.on('connection', (socket) => {
    socket.on('message', (message) => {
        socket.send(message);
    });
});

2.2 client.js #

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>websocket</title>
</head>
<body>
    <input type="text" id="text">
    <button onclick="send()">发送</button>
    <script type="text/javascript">
        let text = document.getElementById('text');
        var ws = new WebSocket("ws://localhost:8888");
        ws.onopen = function () {
            ws.send("server");
        };
        ws.onmessage = function (event) {
            console.log('onmessage', event.data);
        };
        function send() {
            ws.send(text.value);
            text.value = '';
        }
    </script>
</body>
</html>

3. websocket连接 #

3.1 客户端:申请协议升级 #

GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: IHfMdf8a0aQXbwQO1pkGdA==
字段 含义
Connection: Upgrade 表示要升级协议
Upgrade: websocket 表示要升级到websocket协议
Sec-WebSocket-Version: 13 表示websocket的版本
Sec-WebSocket-Key 与后面服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,比如恶意的连接,或者无意义的连接

3.2 服务端:响应协议升级 #

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aWAY+V/uyz5ILZEoWuWdxjnlb7E=
字段 含义
Connection: Upgrade 升级协议
Upgrade: websocket 升级到websocket协议
Sec-WebSocket-Accept Accept字符串

3.3 Sec-WebSocket-Accept的计算 #

const crypto = require('crypto');
const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
function toAcceptKey(wsKey) {
    return crypto.createHash('sha1').update(wsKey + CODE).digest('base64');;
}
const webSocketKey = 'IHfMdf8a0aQXbwQO1pkGdA==';
console.log(toAcceptKey(webSocketKey));//aWAY+V/uyz5ILZEoWuWdxjnlb7E=

4. 数据帧格式 #

4.1 bit和byte #

4.2 位运算符 #

4.2.1 按位与(&) #

4.2.2 按位或(|) #

4.2.3 按位异或(^) #

4.3 数据帧格式 #

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-------+-+-------------+-------------------------------+
 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 | |1|2|3|       |K|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Extended payload length continued, if payload len == 127  |
 + - - - - - - - - - - - - - - - +-------------------------------+
 |                               |Masking-key, if MASK set to 1  |
 +-------------------------------+-------------------------------+
 | Masking-key (continued)       |          Payload Data         |
 +-------------------------------- - - - - - - - - - - - - - - - +
 :                     Payload Data continued ...                :
 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |                     Payload Data continued ...                |
 +---------------------------------------------------------------+
字段 含义
FIN 1个比特 如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)
RSV1, RSV2, RSV3 各占1个比特。一般情况下全为0
Opcode 4个比特,操作代码
Mask 1个比特。表示是否要对数据载荷进行掩码操作,从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作,如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1
Payload length 数据载荷的长度
Masking-key 0或4字节(32位) 所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。载荷数据的长度,不包括mask key的长度
Payload data 载荷数据

Opcode

字段 含义
%x0 表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片
%x1 表示这是一个文本帧
%x2 表示这是一个二进制帧
%x3-7 保留的操作代码
%x8 表示连接断开
%x9 表示这是一个ping操作
%xA 表示这是一个pong操作
%xB-F 保留的操作代码

4.4 Payload length #

let buffer = Buffer.from([0b00000001, 0b00000000]);
console.log(Math.pow(2, 8));
console.log(buffer.readUInt16BE(0));// 00000001 00000000
console.log(buffer.readUInt16LE(0));// 00000000 00000001
function getLength(buffer) {
    const byte = buffer.readUInt8(1);
    let length = parseInt(byte.toString(2).substring(1), 2);
    if (length === 126) {
        length = buffer.readUInt16BE(2);
    } else if (length === 127) {
        length = buffer.readBigUInt64BE(2);
    }
    return length;
}
console.log(126..toString(2));
console.log(127..toString(2));
console.log(getLength(Buffer.from([0b10000001, 0b10000001])));
console.log(getLength(Buffer.from([0b10000001, 0b11111110, 0b00000000, 0b00000001])));
console.log(getLength(Buffer.from([0b10000001, 0b11111111, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000000, 0b00000001])));

4.5 掩码算法 #

function unmask(buffer, mask) {
    const length = buffer.length;
    for (let i = 0; i < length; i++) {
        buffer[i] ^= mask[i % 4];
    }
}

let mask = Buffer.from([1, 0, 1, 0]);
let buffer = Buffer.from([0, 1, 0, 1, 0, 1, 0, 1]);
unmask(buffer, mask);
console.log(buffer);

5. 实现websocket服务器 #

const net = require('net');
const { EventEmitter } = require('events');
const crypto = require('crypto');
const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const OP_CODES = {
    TEXT: 1,
    BINARY: 2
};
class Server extends EventEmitter {
    constructor(options) {
        super(options);
        this.options = options;
        this.server = net.createServer(this.listener);
        this.server.listen(options.port);
    }
    listener = (socket) => {
        socket.setKeepAlive(true);
        socket.send = function (payload) {
            let _opcode;
            if (Buffer.isBuffer(payload)) {
                _opcode = OP_CODES.BINARY;
            } else {
                _opcode = OP_CODES.TEXT;
                payload = Buffer.from(payload);
            }
            let length = payload.length;
            let buffer = Buffer.alloc(2 + length);
            buffer[0] = 0b10000000 | _opcode;
            buffer[1] = length;
            payload.copy(buffer, 2);
            socket.write(buffer);
        }
        socket.on('data', (chunk) => {
            if (chunk.toString().match(/Upgrade: websocket/)) {
                this.upgrade(socket, chunk.toString());
            } else {
                this.onmessage(socket, chunk);
            }
        });
        this.emit('connection', socket);
    }
    onmessage = (socket, chunk) => {
        let FIN = (chunk[0] & 0b10000000) === 0b10000000;//判断是否是结束位,第一个bit是不是1
        let opcode = chunk[0] & 0b00001111;//取一个字节的后四位,得到的一个是十进制数
        let masked = (chunk[1] & 0b10000000) === 0b10000000;//第一位是否是1
        let payloadLength = chunk[1] & 0b01111111;//取得负载数据的长度
        let payload;
        if (masked) {
            let masteringKey = chunk.slice(2, 6);//掩码
            payload = chunk.slice(6);//负载数据
            unmask(payload, masteringKey);//对数据进行解码处理
        }
        if (FIN) {
            switch (opcode) {
                case OP_CODES.TEXT:
                    socket.emit('message', payload.toString());
                    break;
                case OP_CODES.BINARY:
                    socket.emit('message', payload);
                    break;
                default:
                    break;
            }
        }
    }
    upgrade = (socket, chunk) => {
        let rows = chunk.split('\r\n');//按分割符分开
        let headers = toHeaders(rows.slice(1, -2));//去掉请求行和尾部的二个分隔符
        let wsKey = headers['Sec-WebSocket-Key'];
        let acceptKey = toAcceptKey(wsKey);
        let response = [
            'HTTP/1.1 101 Switching Protocols',
            'Upgrade: websocket',
            `Sec-WebSocket-Accept: ${acceptKey}`,
            'Connection: Upgrade',
            '\r\n'
        ].join('\r\n');
        socket.write(response);
    }
}
function toAcceptKey(wsKey) {
    return crypto.createHash('sha1').update(wsKey + CODE).digest('base64');;
}
function toHeaders(rows) {
    const headers = {};
    rows.forEach(row => {
        let [key, value] = row.split(': ');
        headers[key] = value;
    });
    return headers;
}
function unmask(buffer, mask) {
    const length = buffer.length;
    for (let i = 0; i < length; i++) {
        buffer[i] ^= mask[i & 3];
    }
}

exports.Server = Server;