说到websocket想比大家不会陌生,如果陌生的话也没关系,一句话概括
“WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信”
WebSocket相比较传统那些服务器推技术简直好了太多,我们可以挥手向comet和长轮询这些技术说拜拜啦,庆幸我们生活在拥有HTML5的时代~
这篇文章我们将分三部分探索websocket
首先是websocket的常见使用,其次是完全自己打造服务器端websocket,最终是重点介绍利用websocket制作的两个demo,传输图片和在线语音聊天室,let’s go
一、websocket常见用法
这里介绍三种我认为常见的websocket实现……(注意:本文建立在node上下文环境)
1、socket.io
先给demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var http = require('http'); var io = require('socket.io'); var server = http.createServer(function(req, res) { res.writeHeader(200, {'content-type': 'text/html;charset="utf-8"'}); res.end(); }).listen(8888); var socket =.io.listen(server); socket.sockets.on('connection', function(socket) { socket.emit('xxx', {options}); socket.on('xxx', function(data) { // do someting }); }); |
相信知道websocket的同学不可能不知道socket.io,因为socket.io太出名了,也很棒,它本身对超时、握手等都做了处理。我猜测这也是实现websocket使用最多的方式。socket.io最最最优秀的一点就是优雅降级,当浏览器不支持websocket时,它会在内部优雅降级为长轮询等,用户和开发者是不需要关心具体实现的,很方便。
不过事情是有两面性的,socket.io因为它的全面也带来了坑的地方,最重要的就是臃肿,它的封装也给数据带来了较多的通讯冗余,而且优雅降级这一优点,也伴随浏览器标准化的进行慢慢失去了光辉
Chrome | Supported in version 4+ |
Firefox | Supported in version 4+ |
Internet Explorer | Supported in version 10+ |
Opera | Supported in version 10+ |
Safari | Supported in version 5+ |
在这里不是指责说socket.io不好,已经被淘汰了,而是有时候我们也可以考虑一些其他的实现~
2、http模块
刚刚说了socket.io臃肿,那现在就来说说便捷的,首先demo
1 2 3 4 5 6 |
var http = require(‘http’); var server = http.createServer(); server.on(‘upgrade’, function(req) { console.log(req.headers); }); server.listen(8888); |
很简单的实现,其实socket.io内部对websocket也是这样实现的,不过后面帮我们封装了一些handle处理,这里我们也可以自己去加上,给出两张socket.io中的源码图
3、ws模块
后面有个例子会用到,这里就提一下,后面具体看~
二、自己实现一套server端websocket
刚刚说了三种常见的websocket实现方式,现在我们想想,对于开发者来说
websocket相对于传统http数据交互模式来说,增加了服务器推送的事件,客户端接收到事件再进行相应处理,开发起来区别并不是太大啊
那是因为那些模块已经帮我们将数据帧解析这里的坑都填好了,第二部分我们将尝试自己打造一套简便的服务器端websocket模块
感谢次碳酸钴的研究帮助,我在这里这部分只是简单说下,如果对此有兴趣好奇的请百度【web技术研究所】
自己完成服务器端websocket主要有两点,一个是使用net模块接受数据流,还有一个是对照官方的帧结构图解析数据,完成这两部分就已经完成了全部的底层工作
首先给一个客户端发送websocket握手报文的抓包内容
客户端代码很简单
1 |
ws = new WebSocket("ws://127.0.0.1:8888"); |
服务器端要针对这个key验证,就是讲key加上一个特定的字符串后做一次sha1运算,将其结果转换为base64送回去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var crypto = require('crypto'); var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; require('net').createServer(function(o) { var key; o.on('data',function(e) { if(!key) { // 获取发送过来的KEY key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; // 连接上WS这个字符串,并做一次sha1运算,最后转换成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); // 输出返回给客户端的数据,这些字段都是必须的 o.write('HTTP/1.1 101 Switching Protocols\r\n'); o.write('Upgrade: websocket\r\n'); o.write('Connection: Upgrade\r\n'); // 这个字段带上服务器处理后的KEY o.write('Sec-WebSocket-Accept: '+key+'\r\n'); // 输出空行,使HTTP头结束 o.write('\r\n'); } }); }).listen(8888); |
这样握手部分就已经完成了,后面就是数据帧解析与生成的活了
先看下官方提供的帧结构示意图
简单介绍下
FIN为是否结束的标示
RSV为预留空间,0
opcode标识数据类型,是否分片,是否二进制解析,心跳包等等
给出一张opcode对应图
MASK是否使用掩码
Payload len和后面extend payload length表示数据长度,这个是最麻烦的
PayloadLen只有7位,换成无符号整型的话只有0到127的取值,这么小的数值当然无法描述较大的数据,因此规定当数据长度小于或等于125时候它才作为数据长度的描述,如果这个值为126,则时候后面的两个字节来储存数据长度,如果为127则用后面八个字节来储存数据长度
Masking-key掩码
下面贴出解析数据帧的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
function decodeDataFrame(e) { var i = 0, j,s, frame = { FIN: e[i] >> 7, Opcode: e[i++] & 15, Mask: e[i] >> 7, PayloadLength: e[i++] & 0x7F }; if(frame.PayloadLength === 126) { frame.PayloadLength = (e[i++] << 8) + e[i++]; } if(frame.PayloadLength === 127) { i += 4; frame.PayloadLength = (e[i++] << 24) + (e[i++] << 16) + (e[i++] << 8) + e[i++]; } if(frame.Mask) { frame.MaskingKey = [e[i++], e[i++], e[i++], e[i++]]; for(j = 0, s = []; j < frame.PayloadLength; j++) { s.push(e[i+j] ^ frame.MaskingKey[j%4]); } } else { s = e.slice(i, i+frame.PayloadLength); } s = new Buffer(s); if(frame.Opcode === 1) { s = s.toString(); } frame.PayloadData = s; return frame; } |
然后是生成数据帧的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function encodeDataFrame(e) { var s = [], o = new Buffer(e.PayloadData), l = o.length; s.push((e.FIN << 7) + e.Opcode); if(l < 126) { s.push(l); } else if(l < 0x10000) { s.push(126, (l&0xFF00) >> 8, l&0xFF); } else { s.push(127, 0, 0 |