Websocket协议详解及简单实例代码


Posted in Javascript onDecember 12, 2016

Websocket协议详解

关于websocket的协议是用来干嘛的,请参考其他文章。

WebSocket关键词

HTML5协议,实时,全双工通信,长连接

WebSocket比传统Http的好处

  1. 客户端与服务端只建立一个TCP连接,可以使用更少的连接
  2. WebSocket的服务端可以将数据推送到客户端,如实时将证券信息反馈到客户端(这个很关键),实时天气数据,比http请求响应模式更灵活
  3. 更轻量的协议头,减少数据传送量

数据帧格式

下图为手工打造的数据帧格式

/**
 * fin  |masked    |      |
 * srv1 |  length   |      |
 * srv2 |  (7bit   |mask数据   |payload
 * srv3 |   7+2字节  | 4字节    |真实数据
 opcode |   7+64字节 |      |
 *(4bit)
 */

作以下说明:

1.前8个bit(一个字节)
—fin: 是否数据发送完成,为1发送完成为0发送未完。
—srv1,srv2,srv3:留作后用
—opcode:数据类型操作码,4bit表示,其中
TEXT: 1, text类型的字符串
BINARY: 2,二进制数据,通常用来保存图片
CLOSE: 8,关闭连接的数据帧。
PING: 9, 心跳检测。ping
PONG: 10,心跳检测。pong

var events = require('events');
var http = require('http');
var crypto = require('crypto');
var util = require('util');

/**
 * 数据类型操作码 TEXT 字符串
 * BINARY 二进制数据 常用来保存照片
 * PING,PONG 用作心跳检测
 * CLOSE 关闭连接的数据帧 (有很多关闭连接的代码 1001,1009,1007,1002)
 */
var opcodes = {
  TEXT: 1,
  BINARY: 2,
  CLOSE: 8,
  PING: 9,
  PONG: 10
};
var WebSocketConnection = function (req, socket, upgradeHead) {
  "use strict";
  var self = this;

  var key = hashWebSocketKey(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: " + key + '\r\n\r\n');

  /**
   * 接收数据
   */
  socket.on('data', function (buf) {
    self.buffer = Buffer.concat([self.buffer, buf]);
    while (self._processBuffer()) {

    }
  });
  socket.on('close', function (had_error) {
    if (!self.closed) {
      self.emit("close", 1006);
      self.closed = true;
    }
  });
  this.socket = socket;
  this.buffer = new Buffer(0);
  this.closed = false;

};

//websocket连接继承事件
util.inherits(WebSocketConnection, events.EventEmitter);

/*
 发送数据函数
 * */
WebSocketConnection.prototype.send = function (obj) {
  "use strict";
  var opcode;
  var payload;
  if (Buffer.isBuffer(obj)) {
    opcode = opcodes.BINARY;
    payload = obj;
  } else if (typeof obj) {
    opcode = opcodes.TEXT;
    //创造一个utf8的编码 可以被编码为字符串
    payload = new Buffer(obj, 'utf8');
  } else {
    throw new Error('cannot send object.Must be string of Buffer');
  }

  this._doSend(opcode, payload);
};
/*
 关闭连接函数
 * */
WebSocketConnection.prototype.close = function (code, reason) {
  "use strict";
  var opcode = opcodes.CLOSE;
  var buffer;
  if (code) {
    buffer = new Buffer(Buffer.byteLength(reason) + 2);
    buffer.writeUInt16BE(code, 0);
    buffer.write(reason, 2);
  } else {
    buffer = new Buffer(0);
  }
  this._doSend(opcode, buffer);
  this.closed = true;
};

WebSocketConnection.prototype._processBuffer = function () {
  "use strict";
  var buf = this.buffer;
  if (buf.length < 2) {
    return;
  }
  var idx = 2;
  var b1 = buf.readUInt8(0);  //读取数据帧的前8bit
  var fin = b1 & 0x80; //如果为0x80,则标志传输结束
  var opcode = b1 & 0x0f;//截取第一个字节的后四位
  var b2 = buf.readUInt8(1);//读取数据帧第二个字节
  var mask = b2 & 0x80;//判断是否有掩码,客户端必须要有
  var length = b2 | 0x7f;//获取length属性 也是小于126数据长度的数据真实值
  if (length > 125) {
    if (buf.length < 8) {
      return;//如果大于125,而字节数小于8,则显然不合规范要求
    }
  }
  if (length === 126) {//获取的值为126 ,表示后两个字节用于表示数据长度
    length = buf.readUInt16BE(2);//读取16bit的值
    idx += 2;//+2
  } else if (length === 127) {//获取的值为126 ,表示后8个字节用于表示数据长度
    var highBits = buf.readUInt32BE(2);//(1/0)1111111
    if (highBits != 0) {
      this.close(1009, "");//1009关闭代码,说明数据太大
    }
    length = buf.readUInt32BE(6);//从第六到第十个字节为真实存放的数据长度
    idx += 8;
  }

  if (buf.length < idx + 4 + length) {//不够长 4为掩码字节数
    return;
  }

  var maskBytes = buf.slice(idx, idx + 4);//获取掩码数据
  idx += 4;//指针前移到真实数据段
  var payload = buf.slice(idx, idx + length);
  payload = unmask(maskBytes, payload);//解码真实数据
  this._handleFrame(opcode, payload);//处理操作码
  this.buffer = buf.slice(idx + length);//缓存buffer
  return true;
};

/**
 * 针对不同操作码进行不同处理
 * @param 操作码
 * @param 数据
 */
WebSocketConnection.prototype._handleFrame = function (opcode, buffer) {
  "use strict";
  var payload;
  switch (opcode) {
    case opcodes.TEXT:
      payload = buffer.toString('utf8');//如果是文本需要转化为utf8的编码
      this.emit('data', opcode, payload);//Buffer.toString()默认utf8 这里是故意指示的
      break;
    case opcodes.BINARY: //二进制文件直接交付
      payload = buffer;
      this.emit('data', opcode, payload);
      break;
    case opcodes.PING://发送ping做响应
      this._doSend(opcodes.PING, buffer);
      break;
    case opcodes.PONG: //不做处理
      break;
    case opcodes.CLOSE://close有很多关闭码
      let code, reason;//用于获取关闭码和关闭原因
      if (buffer.length >= 2) {
        code = buffer.readUInt16BE(0);
        reason = buffer.toString('utf8', 2);
      }
      this.close(code, reason);
      this.emit('close', code, reason);
      break;
    default:
      this.close(1002, 'unknown opcode');
  }
};

/**
 * 实际发送数据的函数
 * @param opcode 操作码
 * @param payload 数据
 * @private
 */
WebSocketConnection.prototype._doSend = function (opcode, payload) {
  "use strict";
  this.socket.write(encodeMessage(opcode, payload));//编码后直接通过socket发送
};

/**
 * 编码数据
 * @param opcode 操作码
 * @param payload  数据
 * @returns {*}
 */
var encodeMessage = function (opcode, payload) {
  "use strict";
  var buf;
  var b1 = 0x80 | opcode;
  var b2;
  var length = payload.length;
  if (length < 126) {
    buf = new Buffer(payload.length + 2 + 0);
    b2 |= length;
    //buffer ,offset
    buf.writeUInt8(b1, 0);//读前8bit
    buf.writeUInt8(b2, 1);//读8?15bit
    //Buffer.prototype.copy = function(targetBuffer, targetStart, sourceStart, sourceEnd) {
    payload.copy(buf, 2)//复制数据,从2(第三)字节开始

  } else if (length < (1 << 16)) {
    buf = new Buffer(payload.length + 2 + 2);
    b2 |= 126;
    buf.writeUInt8(b1, 0);
    buf.writeUInt8(b2, 1);
    buf.writeUInt16BE(length, 2)
    payload.copy(buf, 4);
  } else {
    buf = new Buffer(payload.length + 2 + 8);
    b2 |= 127;
    buf.writeUInt8(b1, 0);
    buf.writeUInt8(b2, 1);
    buf.writeUInt32BE(0, 2)
    buf.writeUInt32BE(length, 6)
    payload.copy(buf, 10);
  }

  return buf;
};

/**
 * 解掩码
 * @param maskBytes 掩码数据
 * @param data payload
 * @returns {Buffer}
 */
var unmask = function (maskBytes, data) {
  var payload = new Buffer(data.length);
  for (var i = 0; i < data.length; i++) {
    payload[i] = maskBytes[i % 4] ^ data[i];
  }
  return payload;
};
var KEY_SUFFIX = '258EAFA5-E914-47DA-95CA-C5ABoDC85B11';

/*equals to crypto.createHash('sha1').update(key+'KEY_SUFFIX').digest('base64')
 * */
var hashWebSocketKey = function (key) {
  "use strict";
  var sha1 = crypto.createHash('sha1');
  sha1.update(key + KEY_SUFFIX, 'ascii');
  return sha1.digest('base64');
};

exports.listen = function (port, host, connectionHandler) {
  "use strict";
  var srv = http.createServer(function (req, res) {
  });

  srv.on('upgrade', function (req, socket, upgradeHead) {
    "use strict";
    var ws = new WebSocketConnection(req, socket, upgradeHead);
    connectionHandler(ws);
  });
  srv.listen(port, host);
};

 感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

Javascript 相关文章推荐
js树形控件脚本代码
Jul 24 Javascript
javascript之bind使用介绍
Oct 09 Javascript
用JS提交参数创建form表单在FireFox中遇到的问题
Jan 16 Javascript
基于JavaScript 下namespace 功能的简单分析
Jul 05 Javascript
JavaScript获取XML数据附示例截图
Mar 05 Javascript
jQuery中 prop() attr()使用详解
May 19 Javascript
JS运动特效之同时运动实现方法分析
Jan 24 Javascript
jquery.picsign图片标注组件实例详解
Feb 02 jQuery
利用JS代码自动删除稿件的普通弹幕功能
Sep 20 Javascript
node创建Vue项目步骤详解
Mar 06 Javascript
详解js中的几种常用设计模式
Jul 16 Javascript
axios解决高并发的方法:axios.all()与axios.spread()的操作
Nov 09 Javascript
jQuery中值得注意的trigger方法浅析
Dec 12 #Javascript
jQuery实现字符串全部替换的方法
Dec 12 #Javascript
Radio 单选JS动态添加的选项onchange事件无效的解决方法
Dec 12 #Javascript
简单实现jquery焦点图
Dec 12 #Javascript
javascript中setAttribute兼容性用法分析
Dec 12 #Javascript
jQuery焦点图左右转换效果
Dec 12 #Javascript
js实现刷新页面后回到记录时滚动条的位置【两种方案可选】
Dec 12 #Javascript
You might like
用php获取本周,上周,本月,上月,本季度日期的代码
2009/08/05 PHP
PHP CURL模拟GET及POST函数代码
2010/04/25 PHP
Discuz Uchome ajaxpost小技巧
2011/01/04 PHP
php合并数组中相同元素的方法
2014/11/13 PHP
php使用cookie保存用户登录的用户名实例
2015/01/26 PHP
PHP7数组的底层实现示例
2019/08/25 PHP
Javascript 变量作用域 两个可能会被忽略的小特性
2010/03/23 Javascript
理解Javascript_05_原型继承原理
2010/10/13 Javascript
JS操作Cookies的小例子
2013/10/15 Javascript
老生常谈原生JS执行环境与作用域
2016/11/22 Javascript
BootStrap实现轮播图效果(收藏)
2016/12/30 Javascript
vue.js实现用户评论、登录、注册、及修改信息功能
2020/05/30 Javascript
微信小程序实现皮肤功能(夜间模式)
2017/06/18 Javascript
详解微信小程序 template添加绑定事件
2017/06/23 Javascript
JS实现定时任务每隔N秒请求后台setInterval定时和ajax请求问题
2017/10/15 Javascript
微信小程序实现传参数的几种方法示例
2018/01/10 Javascript
vue v-model动态生成详解
2018/06/30 Javascript
layui中table表头样式修改方法
2018/08/15 Javascript
Node.js 如何利用异步提升任务处理速度
2019/01/07 Javascript
JavaScript继承与聚合实例详解
2019/01/22 Javascript
JS 自执行函数原理及用法
2019/08/05 Javascript
vue treeselect获取当前选中项的label实例
2020/08/31 Javascript
Python中实现对list做减法操作介绍
2015/01/09 Python
python将MongoDB里的ObjectId转换为时间戳的方法
2015/03/13 Python
python tkinter实现界面切换的示例代码
2019/06/14 Python
python发送多人邮件没有展示收件人问题的解决方法
2019/06/21 Python
Python3读取和写入excel表格数据的示例代码
2020/06/09 Python
python爬虫中采集中遇到的问题整理
2020/11/27 Python
Selenium执行完毕未关闭chromedriver/geckodriver进程的解决办法(java版+python版)
2020/12/07 Python
Shopee印度尼西亚:东南亚与台湾市场最大电商平台
2018/06/17 全球购物
蔻驰法国官网:COACH法国
2018/11/14 全球购物
泰国健康和美容服务预订网站:GoWabi
2019/06/03 全球购物
通信工程专业女生个人求职信
2013/09/21 职场文书
天网工程实施方案
2014/03/26 职场文书
2015年医院科室工作总结范文
2015/05/26 职场文书
关于HTML编码导致的乱码问题
2021/09/04 HTML / CSS