这篇文章主要介绍了基于 node 实现 websocket 协议的相关资料, 需要的朋友可以参考下
Node.js 是一个基于 Chrome JavaScript 运行时建立的一个平台, 用来方便地搭建快速的 易于扩展的网络应用 · Node.js 借助事件驱动, 非阻塞 I/O 模型变得轻量和高效, 非常适合 运行在分布式设备 的 数据密集型 的实时应用
一、协议
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{aa0aa}
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 编码
# 包装后的二进制表示。
来源: http://www.phperz.com/article/17/0225/266191.html