基于node实现websocket协议


Posted in Javascript onApril 25, 2016

一、协议
WebSocket是一种基于TCP之上的客户端与服务器全双工通讯的协议,它在HTML5中被定义,也是新一代webapp的基础规范之一。

它突破了早先的AJAX的限制,关键在于实时性,服务器可以主动推送内容 到客户端!可能的应用有:多人在线游戏,即时聊天,实时监控,远程桌面,新闻服务器等等。

对于我自己,当前最想尝试的是canvas+websocket组合起来能做什么。

二、实现
由于握手的过程是一个标准的HTTP请求,因此 websocket 的实现有两种选择:1)TCP上实现; 2) 现有HTTP软件上实现。后者的优势在于可以共用现有的HTTP服务器端口,并且不用重新实现认证功能和解析HTTP请求的功能。

这个示例中使用的 node 的HTTP模块。(TCP版及所有文件见 附件)

1、node服务器端代码:

var http = require('http');
var url = require('url');
// var mime = require('mime');
var crypto = require('crypto');

var port = 4400;
var server = http.createServer();
  server.listen(port,function() {
    console.log('server is running on localhost:',port);
    server
    .on('connection',function(s) {
      console.log('on connection ',s);
    })
    .on('request',onrequest)
    .on('upgrade',onupgrade);
  });

var onrequest = function(req,res) {
  console.log( Object.keys(req) ,req.url,req['upgrade']);
  if( !req.upgrade ){
    // 非upgrade请求选择:中断或提供普通网页
    res.writeHead(200, { 'content-type': 'text/plain' });
    res.write( 'WebSocket server works!' );
    
  }
  res.end();
  return;
};

var onupgrade = function (req,sock,head) {
  // console.log('方法:',Object.keys(sock));
  if(req.headers.upgrade !== 'WebSocket'){
    console.warn('非法连接');
    sock.end();
    return;
  }
  
  bind_sock_event(sock);

  try{
    handshake(req,sock,head);
  }catch(e){
    console.error(e);
    sock.end();
  }
};

// 包装将要发送的帧
var wrap = function(data) {
  var fa = 0x00, fe = 0xff, data = data.toString()
    len = 2+Buffer.byteLength(data),
    buff = new Buffer(len);

  buff[0] = fa;
  buff.write(data,1);
  buff[len-1] = fe;
  return buff;
}
// 解开接收到的帧
var unwrap = function(data) {
  return data.slice(1,data.length-1);
}

var bind_sock_event = function(sock) {
  sock
  .on('data',function(buffer) {
    var data = unwrap(buffer);
    console.log('socket receive data : ',buffer,data,'\n>>> '+data);
    // send('hello html5,'+Date.now())
    sock.emit('send',data);
  })
  .on('close',function() {
    console.log('socket close');
  })
  .on('end',function() {
    console.log('socket end');
  })
  .on('send',function(data) { //自定义事件
    sock.write(wrap(data),'binary');
  })
};

var get_part = function(key) {
  var empty  = '',
    spaces = key.replace(/\S/g,empty).length,
    part  = key.replace(/\D/g,empty);
  if(!spaces) throw {message:'Wrong key: '+key,name:'HandshakeError'}
  return get_big_endian(part / spaces);
}

var get_big_endian = function(n) { 
  return String.fromCharCode.apply(null,[3,2,1,0].map(function(i) { return n >> 8*i & 0xff }))
}

var challenge = function(key1,key2,head) {
  var sum = get_part(key1) + get_part(key2) + head.toString('binary');
  return crypto.createHash('md5').update(sum).digest('binary');
}

var handshake = function(req,sock,head) {
  var output = [],h = req.headers, br = '\r\n';

  // header
  output.push(
    'HTTP/1.1 101 WebSocket Protocol Handshake','Upgrade: WebSocket','Connection: Upgrade',
    'Sec-WebSocket-Origin: ' + h.origin,
    'Sec-WebSocket-Location: ws://' + h.host + req.url,
    'Sec-WebSocket-Protocol: my-custom-chat-protocol'+br
  );
  // body
  var c = challenge(h['sec-websocket-key1'],h['sec-websocket-key2'],head);
  output.push(c);

  sock.write(output.join(br),'binary');
}

2、浏览器客户端代码:

<html>
<head>
  <title>WebSocket Demo</title>
</head>
<style type="text/css">
 textarea{width:400px;height:150px;display:block;overflow-y:scroll;}
 #output{width:600px;height:400px;background:whiteSmoke;padding:1em .5em;color:#000;border:none;}
 button{padding:.2em 1em;}
</style>
<link href="layout.css" rel="stylesheet" type="text/css" /> 
<body>

<textarea id="output" readonly="readonly"></textarea>
<br>
<textarea id="input"></textarea>
<button id="send">send</button>

<script type="text/javascript">
// localhost
var socket = new WebSocket('ws://192.168.144.131:4400/')
socket.onopen = function(e) {
  log(e.type);
  socket.send('hello node');
}
socket.onclose = function(e) {
  log(e.type);
}
socket.onmessage = function(e) {
  log('receive @ '+ new Date().toLocaleTimeString() +'\n'+e.data);
 output.scrollTop = output.scrollHeight
}
socket.onclose = function(e) {
  log(e.type);
}
socket.addEventListener('close',function() {
  log('a another close event handler..');
},false);

// dom
var id = function(id) {
  return document.getElementById(id);
}
var output = id('output'), input = id('input'), send = id('send');
var log = function(msg) {
  output.textContent += '> '+msg + '\n'
}
send.addEventListener('click',function() {
  socket.send(input.value);
},false);

</script>
</body>
</html>

三、细节
在 http 协议之上的 websocket 协议实现只有两步:握手,发送数据。

1、握手
握手的过程被称为 challenge-response。首先客户端发起一个名为Upgrade的HTTP GET请求,服务器验证此请求,给出101响应以表示接受此次协议升级,握手即完成了。

chrome inspector美化过的握手信息:

Request URL:ws://192.168.144.131:4400/pub/chat?q=me
Request Method:GET
Status Code:101 WebSocket Protocol Handshake

Request Headers
Connection:Upgrade
Host:192.168.144.131:4400
Origin:http://localhost:800
Sec-WebSocket-Key1:p2    G 947T 80  661 jAf2
Sec-WebSocket-Key2:z Z Q ^326 5 9= 7s1  1 7H4
Sec-WebSocket-Protocol::my-custom-chat-protocol
Upgrade:WebSocket
(Key3):7C:44:56:CA:1F:19:D2:0A

Response Headers
Connection:Upgrade
Sec-WebSocket-Location:ws://192.168.144.131:4400/pub/chat?q=me
Sec-WebSocket-Origin:http://localhost:800
Sec-WebSocket-Protocol:my-custom-chat-protocol
Upgrade:WebSocket
(Challenge Response):52:DF:2C:F4:50:C2:8E:98:14:B7:7D:09:CF:C8:33:40

请求头部分

Host: websocket服务器主机
Connection: 连接类型
Upgrade: 协议升级类型
Origin: 访问来源
Sec-WebSocket-Protocol: 可选,子协议名称,由应用自己定义,多个协议用空格分割。(*另外一个仅剩的可选项是cookie)
Sec-WebSocket-Key1: 安全认证key,xhr请求不能伪造'sec-'开头的请求头。
Sec-WebSocket-Key2: 同上
Key3: 响应体内容,8字节随机。
响应头部分

Sec-WebSocket-Protocol: 必须包含请求的子协议名
Sec-WebSocket-Origin: 必须等于请求的来源
Sec-WebSocket-Location: 必须等于请求的地址
Challenge Response: 响应体内容,根据请求中三个key计算得来,16字节。
应答字符串计算过程伪代码:

part_1 = key1中所有数字 / key1中空格数量
part_2 同上
sum = big_endian(part_1)+big_endian(part_2)+key3
challenge_response = md5_digest(sum);

32位整数的big_endian计算策略:

# 很类似于rgba颜色计算,从下面的函数可以看出计算过程
var big_endian = function(n) {
  return [3,2,1,0].map(function(i) { return n >> 8*i & 0xff });
}
big_endian(0xcc77aaff);
// -> [204, 119, 170, 255]

2、发送数据
WebSocket API的被设计成用事件处理数据,客户端只要得到事件通知就可以获取到完整的数据,而不需要手动处理缓冲器。

这种情况下,每一笔数据被称为一帧。在规范的定义中,它的头部必须以0x00开始,尾部属性以0xff结束,这样每一次数据发送至少有两个字节。

服务器实现中,收到数据时要截掉头尾;而发送数据是要包装头尾。格式如下:

# '你好'的原始二进制表示,请求头和这里都是utf8编码
<Buffer e4 bd a0 e5 a5 bd>
# 包装后的二进制表示。
<Buffer 00 e4 bd a0 e5 a5 bd ff>

以上就是本文的全部内容,希望对大家的学习有所帮助。

Javascript 相关文章推荐
解析arp病毒背后利用的Javascript技术附解密方法
Aug 06 Javascript
利用google提供的API(JavaScript接口)获取网站访问者IP地理位置的代码详解
Jul 24 Javascript
JavaScript实现模仿桌面窗口的方法
Jul 18 Javascript
jQuery获取DOM节点实例分析(2种方式)
Dec 15 Javascript
jquery分隔Url的param方法(推荐)
May 25 Javascript
JS监控关闭浏览器操作的实例详解
Sep 12 Javascript
JavaScript数组排序reverse()和sort()方法详解
Dec 24 Javascript
Vue 实现树形视图数据功能
May 07 Javascript
vscode中vue-cli项目es-lint的配置方法
Jul 30 Javascript
JavaScript中this用法学习笔记
Mar 17 Javascript
Vant 中的Toast设置全局的延迟时间操作
Nov 04 Javascript
JavaScript严格模式不支持八进制的问题讲解
Nov 07 Javascript
Bootstrap每天必学之导航组件
Apr 25 #Javascript
JavaScript常用字符串与数组扩展函数小结
Apr 24 #Javascript
javascript断点调试心得分享
Apr 23 #Javascript
基于 Node.js 实现前后端分离
Apr 23 #Javascript
javascript学习指南之回调问题
Apr 23 #Javascript
探寻JavaScript中this指针指向
Apr 23 #Javascript
javascript中this指向详解
Apr 23 #Javascript
You might like
咖啡的传说和历史
2021/03/03 新手入门
模拟flock实现文件锁定
2007/02/14 PHP
PHP实现防盗链的方法分析
2017/07/25 PHP
laravel框架的安装与路由实例分析
2019/10/11 PHP
PHP实现基本留言板功能原理与步骤详解
2020/03/26 PHP
21个值得收藏的Javascript技巧
2014/02/04 Javascript
当达到输入长度时表单自动切换焦点
2014/04/06 Javascript
js仿土豆网带缩略图的焦点图片切换效果实现方法
2015/02/23 Javascript
jfinal与bootstrap的登录跳转实战演习
2015/09/22 Javascript
Vue.js系列之项目搭建(1)
2017/01/03 Javascript
Angular.js与node.js项目里用cookie校验账户登录详解
2017/02/22 Javascript
jquery表单验证实例仿Toast提示效果
2017/03/03 Javascript
vue+iview 兼容IE11浏览器的实现方法
2019/01/07 Javascript
vue插件mescroll.js实现移动端上拉加载和下拉刷新
2019/03/07 Javascript
JS实现音乐钢琴特效
2020/01/06 Javascript
JavaScript Blob对象原理及用法详解
2020/10/14 Javascript
[09:34]2018DOTA2国际邀请赛寻真——永不放弃的iG
2018/08/14 DOTA
[43:43]完美世界DOTA2联赛PWL S2 LBZS vs Forest 第三场 11.29
2020/12/02 DOTA
python实现html转ubb代码(html2ubb)
2014/07/03 Python
python实现每次处理一个字符的三种方法
2014/10/09 Python
深入理解Python中的元类(metaclass)
2015/02/14 Python
Python实现文件复制删除
2016/04/19 Python
使用Python3制作TCP端口扫描器
2017/04/17 Python
Python的语言类型(详解)
2017/06/24 Python
Python selenium根据class定位页面元素的方法
2019/02/26 Python
pytorch 固定部分参数训练的方法
2019/08/17 Python
Python使用for生成列表实现过程解析
2020/09/22 Python
PyQt实现计数器的方法示例
2021/01/18 Python
CSS3图片旋转特效(360/60/-360度)
2013/10/10 HTML / CSS
卡骆驰新加坡官网:Crocs新加坡
2018/06/12 全球购物
年终自我鉴定
2013/10/09 职场文书
个人公开承诺书
2014/03/28 职场文书
社区四风存在问题及整改措施
2014/10/26 职场文书
营销经理工作检讨书
2014/11/03 职场文书
春节慰问信范文
2015/02/15 职场文书
2016入党积极分子心得体会
2016/01/06 职场文书