博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
双向通信之websocket
阅读量:6394 次
发布时间:2019-06-23

本文共 6819 字,大约阅读时间需要 22 分钟。

简介

定义一个API,用以在网页浏览器和服务器之间建立socket连接,这个连接是持久的,两边可以在任意时间开始发送数据

  • HTML5开始提供的一种浏览器和服务器之间进行全双工通讯的网络技术
  • 属于应用层协议,基于TCP,并复用http的握手通信
  • 优点

    支持双向通信,实时性强

    更好的二进制支持

    没有同源限制,客户端可以与任意服务器通信

    较少的控制开销(创建后,ws客户端\服务端进行数据交换时,协议控制的数据包头部较小)

实战

  • 客户端
let socket = new WebSocket('ws://localhost:9999');socket.onopen = () => { // 连接成功后的回调    socket.send('hello')}socket.onmessage = (event) => { };// 接收到服务器数据时的回调  socket.onclose = function(event) { };// 连接关闭时的回调socket.onerror = function(event) { };// 报错时的回调复制代码
  • 服务器端
let webSocketServer = require('ws').Server;let server = new webSocketServer({port: 8888}); // 支持跨域  端口号不能冲突server.on('connection', (socket) => { // 连接成功回调    socket.on('message', (msg) => { // 监听客户端发送的消息        socket.send(msg); // 向客户端返回消息    });});复制代码

websocket如何建立连接

客户端通过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵照WebSocket的协议。

  • 客户端申请协议升级(标准的http报文的格式 只支持get方法)
GET ws://localhost:8888/ HTTP/1.1Host: localhost:8888Connection: Upgrade  表示要升级协议Upgrade: websocket  要升级的协议Sec-WebSocket-Version: 13  协议版本Sec-WebSocket-Key: IHfMdf8a0aQXbwQO1pkGdA== 与服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防护,确保服务端理解websocket连接, 避免恶意\无意等非法连接。复制代码
  • 服务器端响应协议升级
HTTP/1.1 101 Switching Protocols  // 101标识协议转换Upgrade: websocketConnection: UpgradeSec-WebSocket-Accept: aWAY+V/uyz5ILZEoWuWdxjnlb7E= 到此完成协议升级,后续的数据交互都按照新的协议来。复制代码

Sec-WebSocket-Accept计算公式

const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // 常量const webSocketKey = 'IHfMdf8a0aQXbwQO1pkGdA==';let websocketAccept = require('crypto').createHash('sha1').update(webSocketKey + CODE).digest('base64'); // crypto提供通用的加密和哈希算法复制代码

数据帧格式

客户端和服务器端通信的最小单位是帧, 由1或多个帧组成一条完整的消息 客户端: 将消息切割为多个帧发送服务器端 服务器端:接收消息帧, 并将关联的帧重新组装成完整的消息

单位是比特  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个比特 表示是否是消息(message)的最后一个分片(fragment),1代表是,0代表不是

  • RSV1, RSV2, RSV3:各占1个比特。一般情况下全为0。只有当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义,否则连接出错。

  • Opcode: 4个比特。操作代码,即如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)

    %x0:延续帧。表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。

    %x1:文本帧(frame)

    %x2:二进制帧(frame)

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

    %x8:连接断开。

    %x9:一个ping操作。

    %xA:一个pong操作。

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

  • Mask: 1个比特。表示是否要对数据载荷进行掩码操作

    从客户端向服务端发送数据时,Mask都是1,即需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作

    如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。

    如果Mask是1,那么在Masking-key中会定义一个掩码键(masking-key),并用这个掩码键来对数据载荷进行反掩码。

  • Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或7+64位。

    x为0~125:数据的长度为x字节。

    x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。

    x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。

    如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)

  • Masking-key:0或4字节(32位),所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。载荷数据的长度,不包括mask key的长度

  • Payload data:(x+y) 字节 载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。

    扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。

    应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。

实例

let net = require('net'); // 实现tcp协议const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';const crypto = require('crypto');let server = net.createServer((socket) => {    socket.once('data', (data) => { // once只会执行一次回调        data = data.toString(); // data为请求流        if (data.match(/Connection: Upgrade/)) { // 说明需要请求升级协议            let rows = data.split('\r\n'); //按分割符分开            rows = rows.slice(1, -2); //去掉请求行和尾部的二个分隔符            // 获取请求头            let headers = {};            rows.reduce((memo, item) => {                 let [key, value] = item.split(': ');                memo[key] = value;                return memo;            }, headers);            // console.log(headers, 'headers');            if(headers['Sec-WebSocket-Version'] === '13'){ // 需要升级为13版本                let SecWebSocketKey = headers['Sec-WebSocket-Key'];                let SecWebSocketAccept = crypto.createHash('sha1').update(SecWebSocketKey + CODE).digest('base64');                let response = [                    'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${SecWebSocketAccept}`,                    '\r\n', // 响应头和响应体之间有两个\r\n                ].join('\r\n');                socket.write(response); // 返回响应头给客户端 表明握手成功                // 后面格式基于websocket协议                socket.on('data', (buffers) => { // data默认为buffer类型                                        let fin = buffers[0]&0b10000000 == 0b10000000; // 获取第一个字节的第一位  即结束位的值                    let opcode = buffers[0]&0b00001111; // 获取第一个字节的后四位 即操作码                     let ismask = buffers[1]&0b10000000; // 是否进行掩码                    let payloadLength = buffers[1]&0b01111111;                     let payload;                    if (payloadLength<=125) {                        if (ismask) {                            let mask = buffers.slice(2,6); // 掩码键                            payload = buffers.slice(6); // 携带的真实数据                             // console.log(payload, 'before unmask');                            unmask(payload, mask); // 对数据进行反掩码                            // console.log(payload.toString(), 'unmask');                        } else {                            payload = buffers.slice(2);                        }                    } else if (payloadLength<=126) {                        // ....                    }                                                           // 拼接响应帧                    let res = Buffer.alloc(2+payload.length);                     res[0] = 0b10000000|opcode;                     res[1] = payloadLength;                    payload.copy(res, 2);                    socket.write(res);                });            }        }    });});function unmask(payload,mask){ // mask为4个字节长度    const length = payload.length;    for (let i=0;i

转载于:https://juejin.im/post/5cd12f85f265da038f7757a5

你可能感兴趣的文章
一些iOS高效开源类库(转)
查看>>
JAVA编程心得-JAVA实现CRC-CCITT(XMODEM)算法
查看>>
C# DES加密
查看>>
C# GDI+绘制一维条码打印模糊的解决办法
查看>>
STL系列 map
查看>>
UVA 825 Walking on the Safe Side(记忆化搜索)
查看>>
Java面试题之一
查看>>
HDOJ 3359 Kind of a Blur
查看>>
Mybatis 示例之 SelectKey
查看>>
jquery批量设置属性readonly和disabled
查看>>
20-ab压力测试及nginx性能统计模块
查看>>
几种常见模式识别算法整理和总结
查看>>
在_Layout模版中使用@Styles.Render()没有效果
查看>>
他叫索隆
查看>>
使用checkstyle来规范你的项目
查看>>
wireshark 实用过滤表达式(针对ip、协议、端口、长度和内容) 实例介绍(转)...
查看>>
Loadrunner中影响"响应时间"的设置
查看>>
进入css3动画世界(二)
查看>>
2017 部门文化宣贯会议内容
查看>>
南方的养老金拿去支援东北,解燃眉之急!这位专家的建议,让网友议论开了...
查看>>