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 Map 和 List 的简单实现代码
Jul 08 Javascript
JQuery1.8 判断元素是否绑定事件的方法
Jul 10 Javascript
用JS实现轮播图效果(二)
Jun 26 Javascript
jQuery简单动画变换效果实例分析
Jul 04 Javascript
使用jQuery.Qrcode插件在客户端动态生成二维码并添加自定义Logo
Sep 01 Javascript
利用jQuery对无序列表排序的简单方法
Oct 16 Javascript
jquery实现input框获取焦点的简单实例
Jan 26 Javascript
浅谈Node.js ORM框架Sequlize之表间关系
Jul 24 Javascript
vue todo-list组件发布到npm上的方法
Apr 04 Javascript
JSON数据中存在单个转义字符“\”的处理方法
Jul 11 Javascript
vue + typescript + video.js实现 流媒体播放 视频监控功能
Jul 07 Javascript
jquery实现广告上下滚动效果
Mar 04 jQuery
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
phpmyadmin中配置文件现在需要绝密的短语密码的解决方法
2007/02/11 PHP
去掉destoon资讯内容页keywords关键字自带的文章标题的方法
2014/08/21 PHP
PHP中UNIX时间戳和日期间的转换与计算实例
2014/11/19 PHP
详解php比较操作符的安全问题
2015/12/03 PHP
PHP中使用foreach()遍历二维数组的简单实例
2016/06/13 PHP
作为PHP程序员你要知道的另外一种日志
2018/07/30 PHP
PHP cookie,session的使用与用户自动登录功能实现方法分析
2019/06/05 PHP
php创建类并调用的实例方法
2019/09/25 PHP
JavaScript 组件之旅(三):用 Ant 构建组件
2009/10/28 Javascript
一个基于jquery的文本框记数器
2012/09/19 Javascript
详解JavaScript语言的基本语法要求
2015/11/20 Javascript
jquery插件ContextMenu设置右键菜单
2017/03/13 Javascript
jquery ztree实现右键收藏功能
2017/11/20 jQuery
20行JS代码实现粘贴板复制功能
2018/02/06 Javascript
centos 上快速搭建ghost博客方法分享
2018/05/23 Javascript
[02:41]《西雅图我们来了》2015国际邀请赛出征全记录
2015/07/23 DOTA
Python中使用第三方库xlrd来写入Excel文件示例
2015/04/05 Python
用生成器来改写直接返回列表的函数方法
2017/05/25 Python
Python从零开始创建区块链
2018/03/06 Python
使用Django和Python创建Json response的方法
2018/03/26 Python
使用Python3内置文档高效学习以及官方中文文档
2019/05/19 Python
如何利用Pyecharts可视化微信好友
2019/07/04 Python
python re.sub()替换正则的匹配内容方法
2019/07/22 Python
CSS3 linear-gradient线性渐变生成加号和减号的方法
2017/11/21 HTML / CSS
Kiehl’s科颜氏西班牙官方网站:源自美国的植物护肤品牌
2020/02/22 全球购物
用你熟悉的语言写一个连接ORACLE数据库的程序,能够完成修改和查询工作
2012/06/11 面试题
如何填写个人简历自我评价
2013/12/10 职场文书
高中物理教学反思
2014/02/08 职场文书
学校节能减排倡议书
2014/05/16 职场文书
师范生免费教育协议书范本
2014/10/09 职场文书
2014年接待工作总结
2014/11/26 职场文书
入队仪式主持词
2015/07/04 职场文书
Python中使用subprocess库创建附加进程
2021/05/11 Python
python爬取网页版QQ空间,生成各类图表
2021/06/02 Python
Win11运行育碧游戏总是崩溃怎么办 win11玩育碧游戏出现性能崩溃的解决办法
2022/04/06 数码科技
一文搞懂PHP中的抽象类和接口
2022/05/25 PHP