websocket 是一种让浏览器与服务器之间建立持久的连接,并能进行双向数据传输的一种协议。

websocket 属性应用层协议,基于tcp传输协议,并复用http的握手通道。

 

一、如何进行websocket连接。

websocket复用了http的握手通道,客户端通过http请求与服务端进行协商,升级协议。协议升级完后,后面的数据交换则遵照websocket协议。

1、客户端申请协议升级

Request URL: ws://localhost:8888/
Request Method: GET
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: uR5YP/BMO6M24tAFcmHeXw==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

Connection: Upgrade 表示要升级协议

Upgrade: websocket 表示升级到websocket协议

Sec-WebSocket-Version: 13 表示websocket的版本

Sec-WebSocket-Key 表示websocket的验证,防止恶意的连接,与服务端响应的Sec-WebSocket-Accept是配套。

 

2、服务端响应协议升级

Status Code: 101 Switching Protocols
Connection: Upgrade
Sec-WebSocket-Accept: eS92kXpBNI6fWsCkj6WxH6QeoHs=
Upgrade: websocket

Status Code:101 表示状态码,协议切换。

Sec-WebSocket-Accept 表示服务端响应的校验,与客户端的Sec-WebSocket-Key是配套的。

 

3、Sec-WebSocket-Accept是如何计算的

将 Sec-WebSocket-Key 的值与 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。

然后通过sha1计算,再转成base64。

const crypto = require('crypto');

function getSecWebSocketAccept(key) {
    return crypto.createHash('sha1')
        .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
        .digest('base64');
}

console.log(getSecWebSocketAccept('uR5YP/BMO6M24tAFcmHeXw=='));

  

4、协议升级完后,后续的数据传输就需要按websocket协议来走。

websocket客户端与服务端通信的最小单位是 帧,由1个或多个帧组成完整的消息。

客户端:将消息切割成多个帧,发送给服务端。

服务端:接收到消息帧,将帧重新组装成完整的消息。

 

5、数据帧的格式

单位是1个比特位,FIN,PSV1,PSV2,PSV3 占1个比特位,opcode占4个比特位。

 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|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表示是,0表示不是。

RSV1,RSV2,RSV3  分别占1位,一般情况下全为0,扩展使用,值的含义由扩展进行定义。

opcode 占4位,表示如何解析后面的数据载荷(Payload Data)。

  %x0 表示一个延续帧,opcode为0时,表示数据传输采用了数据分片,当前的数据帧只是其中一个数据分片。

  %x1 表示这是一个文本帧

  %x2 表示这是一个二进制帧

  %x3-7 保留的操作代码,用于定义后续的非控制帧。

  %x8 表示连接断开

  %x9 表示这是一个ping操作

  %xA 表示这是一个pong操作

  %xB-F 保留的操作代码,用于定义后续的控制帧。

MASK 占1位,表示是否要对数据载荷进行掩码操作。

  客户端向服务端发数据,需要对数据进行掩码操作,服务端向客户端发数据,不需要对数据进行掩码操作。

  如果Mask为1,则Masking-key中会定义一个掩码键,通过该掩码键对数据载荷进行反掩码。客户端发送给服务端的数据帧,MASK都是1。

Payload len 为7位,或7+16位,或7+64位,表示数据载荷的长度,单位字节。

  如果Payload len=0~125,表示,数据的长度为0~125字节。

  如果Payload len=126,表示,后续的2个字节代表一个16位的无符号整数,该整数表示数据的长度。

  如果Payload len=127,表示,后续的8个字节代表一个64位的无符号整数,该整数表示数据的长度。

  如果Payload len占用多个字节,Payload len的二进制表达采用Big-endian。

Masking-key 占0或32位,客户端向服务端发送数据帧,数据载荷都进行了掩码操作,Mask为1,且带了4字节的Masking-key。如果Mask为0,则没有Masking-key。

注意数据载荷的长度,不包括Masking-key的长度。

 

6、掩码的算法

Masking-key掩码键是由客户端生成的32位随机数,掩码操作不会影响数据载荷的长度。

function unmask(buffer, mask) {
    const length = buffer.length;
    for (var i = 0; i < length; i++) {
        buffer[i] ^= mask[i & 3];
    }
}

  

7、实现websocket的握手

const crypto = require('crypto');
const net = require('net');

//计算websocket校验
function getSecWebSocketAccept(key) {
    return crypto.createHash('sha1')
        .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
        .digest('base64');
}

//掩码操作
function unmask(buffer, mask) {
    const length = buffer.length;
    for (var i = 0; i < length; i++) {
        buffer[i] ^= mask[i & 3];
    }
}

//创建一个tcp服务器
let server = net.createServer(function (socket) {

    socket.once('data', function (data) {
        data = data.toString();

        //查看请求头中是否有升级websocket协议的头信息
        if (data.match(/Upgrade: websocket/)) {
            let rows = data.split('\r\n');
            //去掉第一行的请求行
            //去掉请求头的尾部两个空行
            rows = rows.slice(1, -2);
            let headers = {};
            rows.forEach(function (value) {
                let [k, v] = value.split(': ');
                headers[k] = v;
            });
            //判断websocket的版本
            if (headers['Sec-WebSocket-Version'] == 13) {
                let secWebSocketKey = headers['Sec-WebSocket-Key'];
                //计算websocket校验
                let secWebSocketAccept = getSecWebSocketAccept(secWebSocketKey);
                //服务端响应的内容
                let res = [
                    'HTTP/1.1 101 Switching Protocols',
                    'Upgrade: websocket',
                    `Sec-WebSocket-Accept: ${secWebSocketAccept}`,
                    'Connection: Upgrade',
                    '\r\n'
                ].join('\r\n');
                //给客户端发送响应内容
                socket.write(res);

                //注意这里不要断开连接,继续监听'data'事件
                socket.on('data', function (buffer) {
                    //注意buffer的最小单位是一个字节
                    //取第一个字节的第一位,判断是否是结束位
                    let fin = (buffer[0] & 0b10000000) === 0b10000000;
                    //取第一个字节的后四位,得到的一个是十进制数
                    let opcode = buffer[0] & 0b00001111;
                    //取第二个字节的第一位是否是1,判断是否掩码操作
                    let mask = buffer[1] & 0b100000000 === 0b100000000;
                    //载荷数据的长度
                    let payloadLength = buffer[1] & 0b01111111;
                    //掩码键,占4个字节
                    let maskingKey = buffer.slice(2, 6);
                    //载荷数据,就是客户端发送的实际数据
                    let payloadData = buffer.slice(6);

                    //对数据进行解码处理
                    unmask(payloadData, maskingKey);

                    //向客户端响应数据
                    let send = Buffer.alloc(2 + payloadData.length);
                    //0b10000000表示发送结束
                    send[0] = opcode | 0b10000000;
                    //载荷数据的长度
                    send[1] = payloadData.length;
                    payloadData.copy(send, 2);
                    socket.write(send);
                });
            }
        }
    });

    socket.on('error', function (err) {
        console.log(err);
    });

    socket.on('end', function () {
        console.log('连接结束');
    });

    socket.on('close', function () {
        console.log('连接关闭');
    });
});

//监听8888端口
server.listen(8888);

index.html的代码:

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<script>
    var ws = new WebSocket('ws://localhost:8888');
    ws.onopen = function () {
        console.log('连接成功');
        ws.send('你好服务端');
    };
    ws.onmessage = function (ev) {
        console.log('接收数据', ev.data);
    };
    ws.onclose = function () {
        console.log('连接断开');
    };
</script>
</body>
</html>

  

 

posted on 2019-01-18 16:54  怀素真  阅读(1459)  评论(1编辑  收藏  举报