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判断用户浏览器是否是XP SP2的IE6
Mar 08 Javascript
node.js中的socket.io入门实例
Apr 26 Javascript
javascript自动切换焦点控制效果完整实例
Feb 02 Javascript
javascript每日必学之基础入门
Feb 16 Javascript
微信小程序 教程之注册程序
Oct 17 Javascript
angular分页指令操作
Jan 09 Javascript
详解Jquery 遍历数组之$().each方法与$.each()方法介绍
Jan 09 Javascript
VueJs组件之父子通讯的方式
May 06 Javascript
javascript实现简易的计算器
Jan 17 Javascript
Vue2.4+新增属性.sync、$attrs、$listeners的具体使用
Mar 08 Javascript
JavaScript实现烟花绽放动画效果
Aug 04 Javascript
Ajax实现异步加载数据
Nov 17 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
多人战的战术与战略
2020/03/04 星际争霸
无线电广播的开始
2002/01/30 无线电
PHP 反射机制实现动态代理的代码
2008/10/22 PHP
Thinkphp框架中D方法与M方法的区别
2016/12/23 PHP
php使用gd2绘制基本图形示例(直线、圆、正方形)
2017/02/15 PHP
PHP html_entity_decode()函数讲解
2019/02/25 PHP
深入理解Javascript中的自执行匿名函数
2016/06/03 Javascript
浅析Javascript的自动分号插入(ASI)机制
2016/09/29 Javascript
jquery对象与DOM对象转化
2017/02/08 Javascript
[js高手之路]单例模式实现模态框的示例
2017/09/01 Javascript
如何为你的JS项目添加智能提示与类型检查详解
2019/03/12 Javascript
整理 node-sass 安装失败的原因及解决办法(小结)
2020/02/19 Javascript
Vue项目配置跨域访问和代理proxy设置方式
2020/09/08 Javascript
[01:32]TI珍贵瞬间系列(一)
2020/08/26 DOTA
python中urllib模块用法实例详解
2014/11/19 Python
Python中MySQLdb和torndb模块对MySQL的断连问题处理
2015/11/09 Python
解决python文件字符串转列表时遇到空行的问题
2017/07/09 Python
python学习教程之Numpy和Pandas的使用
2017/09/11 Python
Python工程师面试必备25条知识点
2018/01/17 Python
python取数作为临时极大值(极小值)的方法
2018/10/15 Python
如何使用pyinstaller打包32位的exe程序
2019/05/26 Python
python如何以表格形式打印输出的方法示例
2019/06/21 Python
PyQt5+Caffe+Opencv搭建人脸识别登录界面
2019/08/28 Python
使用OpenCV获取图片连通域数量,并用不同颜色标记函
2020/06/04 Python
Python如何急速下载第三方库详解
2020/11/02 Python
商务英语求职自荐信范文
2013/12/24 职场文书
小学英语教学反思案例
2014/02/04 职场文书
大学活动总结格式
2014/04/29 职场文书
大学生志愿者活动总结
2014/06/27 职场文书
小学安全教育月活动总结
2014/07/07 职场文书
2014学校领导四风对照检查材料思想汇报
2014/09/23 职场文书
思想作风建设心得体会
2014/10/22 职场文书
党校毕业个人总结
2015/02/28 职场文书
小学生安全教育心得体会
2016/01/15 职场文书
《槐乡的孩子》教学反思
2016/02/20 职场文书
仅仅使用 HTML/CSS 实现各类进度条的方式汇总
2021/11/11 HTML / CSS