qq_46628314 2022-03-09 19:04 采纳率: 50%
浏览 20

websocket客户端和服务端使用node.js通信

这是websocket请求地址,

img

开了12222端口,把server给websocke了

img

这是websocket

img

之前用端口得可以通信,这种地址不知道怎么处理了ws://localhost:8080/api/v2/ioteventext?session=c8d39c92-d789-4e36-bfcc-ae6c4133eeaf

  • 写回答

1条回答 默认 最新

  • 沐卿゚ 2022-03-10 09:49
    关注
    
    // nodejs在http模块实现websocket的例子
    const http = require('http');
    
    // Create an HTTP server
    const server = http.createServer((req, res) => {
      res.writeHead(200, { 'Content-Type': 'text/plain' });
      res.end('okay');
    });
    server.on('upgrade', (req, socket, head) => {
      socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
                   'Upgrade: WebSocket\r\n' +
                   'Connection: Upgrade\r\n' +
                   '\r\n');
    
      socket.pipe(socket); // echo back
    });
    第一眼以为通过upgrade拿到socket套接字,然后就可以直接用socket.write和socket.on(‘data’)的方法来发送和获取数据。但事实并不是这样。
    
    第一坑:Sec-WebSocket-Accept
    我在浏览器中写好websocket的例子:
    
        var ws = new WebSocket(`ws://${window.location.host}/`);
        ws.onopen = function()
        {
            console.log("握手成功");
            ws.send("发送数据测试");
        };      
        ws.onmessage = function (e) 
        { 
            console.log(e.data);
        };
    结果一连接就断开,说我没有Sec-WebSocket-Accept这个http头,网上一查一点结果都没有,看来实现这个的确实不多。 找来找去终于找到了websocket的协议文档(https://tools.ietf.org/html/rfc6455)。
    
    发现Sec-WebSocket-Accept这个返回头是根据客户端的请求头sec-websocket-key,加上全局唯一ID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)后使用sha1摘要后,再以base64格式输出。
    
    const crypto=require('crypto')
    function getSecWebSocketAccept (secWebsocketKey){
        return crypto.createHash('sha1')
        .update(`${secWebsocketKey}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
        .digest('base64');
    }
    
    const secWebSocketAccept = getSecWebSocketAccept(req.headers['sec-websocket-key'])
    socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
                'Upgrade: WebSocket\r\n' +
                'Connection: Upgrade\r\n' +
                'Sec-WebSocket-Accept: '+ secWebSocketAccept +'\r\n' +
                '\r\n');
    再刷新下浏览器,发现握手成功了。
    
    第二坑:接收到的客户端数据是乱码
    握手成功后,肯定是要看客户端给我发了什么数据,原来是个buffer,但toString后居然是乱码。
    
    socket.on('data', (data) => {
        console.log(data.toString())
    });
    当时就在想里面是不是有猫腻,一看果然websocket还有frame的概念,接收到data就是一个frame,在这个框架里面有一定的结构。
    
    在文档中叫Base Framing Protocol(https://tools.ietf.org/html/rfc6455#section-5.2),大概的结构如下:
    
    /**
        我在第二三行重新加了个按字节和比特来计算的比例尺
         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
                       1               2               3               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 ...                |
        +---------------------------------------------------------------+
        */
    什么意思呢?那么按照我标记的字节来算吧
    
    ---
    第1个字节的第1个比特是FIN的值,用来标识这个frame信息传递是否结束,1是结束
    第1个字节的第2-3个比特是RSV的值,用来标识这个frame信息传递是否结束
    第1个字节的第4-8个比特是opcode的值,来标记状态1是文本数据2是二进制数据8是请求关闭链接
    ---
    第2个字节的第1个比特是Mask的值,用来标识数据是否使用Masking-key来做异或解码
    第2个字节的第2-8个比特PayloadLen,
    代表数据长度,如果为126,则使用16位的扩展数据长
    代表数据长度,如果为127,则使用8位的扩展数据长度
    扩展长度使用大字端读取就好
    那知道这些就可以通过代码来实现解码,代码实现如下
    
    function decodeSocketFrame (bufData){
        let bufIndex = 0
        const byte1 = bufData.readUInt8(bufIndex++).toString(2)
        const byte2 = bufData.readUInt8(bufIndex++).toString(2)
        const frame =  {
            fin:parseInt(byte1.substring(0,1),2),
            // RSV是保留字段,暂时不计算
            opcode:parseInt(byte1.substring(4,8),2),
            mask:parseInt(byte2.substring(0,1),2),
            payloadLen:parseInt(byte2.substring(1,8),2),
        }
        // 如果frame.payloadLen为126或127说明这个长度不够了,要使用扩展长度了
        // 如果frame.payloadLen为126,则使用Extended payload length同时为16/8字节数
        // 如果frame.payloadLen为127,则使用Extended payload length同时为64/8字节数
        // 注意payloadLen得长度单位是字节(bytes)而不是比特(bit)
        if(frame.payloadLen==126) {
            frame.payloadLen = bufData.readUIntBE(bufIndex,2);
            bufIndex+=2;
        } else if(frame.payloadLen==127) {
            // 虽然是8字节,但是前四字节目前留空,因为int型是4字节不留空int会溢出
            bufIndex+=4;
            frame.payloadLen = bufData.readUIntBE(bufIndex,4);
            bufIndex+=4;
        }
        if(frame.mask){
            const payloadBufList = []
            // maskingKey为4字节数据
            frame.maskingKey=[bufData[bufIndex++],bufData[bufIndex++],bufData[bufIndex++],bufData[bufIndex++]];
            for(let i=0;i<frame.payloadLen;i++) {
                payloadBufList.push(bufData[bufIndex+i]^frame.maskingKey[i%4]);
            }
            frame.payloadBuf = Buffer.from(payloadBufList)
        } else {
            frame.payloadBuf = bufData.slice(bufIndex,bufIndex+frame.payloadLen)
        }
        return frame
    }
    那如果你解码数据,那么发送的时候其实也是要遵循这种基本框架的,所以还要进行加码成frame框架后再发送,同理根据协议,可以实现如下代码:
    
    function encodeSocketFrame (frame){
        const frameBufList = [];
        // 对fin位移七位则为10000000加opcode为10000001
        const header = (frame.fin<<7)+frame.opcode;
        console.log(header)
        frameBufList.push(header)
        const bufBits = Buffer.byteLength(frame.payloadBuf);
        let payloadLen = bufBits;
        let extBuf;
        if(bufBits>=126) {
            //65536是2**16即两字节数字极限
            if(bufBits>=65536) {
                extBuf = Buffer.allocUnsafe(8);
                buf.writeUInt32BE(bufBits, 4);
                payloadLen = 127;
            } else {
                extBuf = Buffer.allocUnsafe(2);
                buf.writeUInt16BE(bufBits, 0);
                payloadLen = 126;
            }
        }
        let payloadLenBinStr = payloadLen.toString(2);
        while(payloadLenBinStr.length<8){payloadLenBinStr='0'+payloadLenBinStr;}
        frameBufList.push(parseInt(payloadLenBinStr,2));
        if(bufBits>=126) {
            frameBufList.push(extBuf);
        }
        frameBufList.push(...frame.payloadBuf)
        return Buffer.from(frameBufList)
    }
    那么我们发送和接受就简单了,直接通过socket再发送就好了,如下
    
    socket.on('data', (data) => {
        console.log(decodeSocketFrame(data).payloadBuf.toString())
        socket.write(encodeSocketFrame({
            fin:1,
            opcode:1,
            payloadBuf:Buffer.from('你好')
        }))
    });
    总结
    其实websocket和http对于socket来说都是在上面加了一层协议,通过不同方法来实现其功能,随着技术的发展,协议也确实往复杂的方向发展。 在工作种如果自己实现协议可能就太费时间了,但是如果是非工作,实现一遍也还是能收获良多的
    
    评论

报告相同问题?

问题事件

  • 修改了问题 3月9日
  • 创建了问题 3月9日

悬赏问题

  • ¥15 无线电能传输系统MATLAB仿真问题
  • ¥50 如何用脚本实现输入法的热键设置
  • ¥20 我想使用一些网络协议或者部分协议也行,主要想实现类似于traceroute的一定步长内的路由拓扑功能
  • ¥30 深度学习,前后端连接
  • ¥15 孟德尔随机化结果不一致
  • ¥15 apm2.8飞控罗盘bad health,加速度计校准失败
  • ¥15 求解O-S方程的特征值问题给出边界层布拉休斯平行流的中性曲线
  • ¥15 谁有desed数据集呀
  • ¥20 手写数字识别运行c仿真时,程序报错错误代码sim211-100
  • ¥15 关于#hadoop#的问题