(一) 基本原理1. 了解现有的HTTP的架构模式: 1-1. HTTP的特点是: 当初这么设计也是有原因的,假如服务器能主动推送数据给浏览器的话,那么浏览器很容易受到攻击,比如一些广告商会主动把一些广告信息强行的传输给客户端。 2. 了解HTTP轮询,长轮询和流化。 轮询: 轮询是通过浏览器定时的向web服务器发送http的Get请求,服务器收到请求后,就把最新的数据发回给客户端,客户端得到数据后,将其显示出来,然后再定期的重复这一过程,虽然可以满足需求,但是存在一些缺点,比如某一段时间内web服务器没有更新的数据,但是浏览器仍然需要定时的发送Get请求过来询问,那么即浪费了带宽,又浪费了cpu的利用率。 长轮询: 客户端向服务器请求信息,并在设定的时间段内打开一个连接,服务器如果没有任何信息,会保持请求打开,直到有客户端可用的信息,或者直到 缺点是: 如下图: 流化: 在流化技术中,客户端发送一个请求,服务器发送并维护一个持续更新和保持打开的开放响应。每当服务器有需要交付给客户端信息时,它就更新响应 3. 了解WebSocket WebSocket减少了延迟,因为一旦建立起Websocket连接,服务器可以在消息可用时发送他们。和轮询不同的是:WebSocket只发出一个请求,服务器 优点有如下: WebSocket的应用场景? (二) WebSocket协议WebSocket协议是为了解决web即时应用中服务器与客户端浏览器全双工通信问题而设计的。协议定义ws和wss协议,分别为普通请求和基于SSL的安全传输, ws端口是80,wss的端口为443. WebSocket协议由两部分组成,握手和数据传输。 2-1 握手 <!DOCTYPE html><html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>websocket</title> <meta name="viewport" content="width=device-width, initial-scale=1"/> </head> <body> <script type="text/javascript"> var wsUrl = "wss://echo.websocket.org"; var ws = new WebSocket(wsUrl); ws.onopen = function() { console.log('open'); }; ws.onmessage = function(msg) { console.log(msg.data); } ws.onclose = function() { console.log('已经被关闭了'); } </script> </body></html> 页面运行后,我们可以看到链接到 wss://echo.websocket.org 期间记录的一个握手协议。先来看看客户端发送http的请求头: GET /chat HTTP/1.1Host:echo.websocket.org Upgrade:websocket Connection:Upgrade Sec-WebSocket-Key:ALS2AoBJtUup67heKDgzFg==Origin:file://Sec-WebSocket-Version:13 服务器响应的头字段 Connection:Upgrade Sec-WebSocket-Accept:qyzx/EgbRK15QNmr5PhpMQrPZMM=Server: Kaazing Gateway Upgrade:websocket 下面是请求和响应头字段的含义: Sec-WebSocket-Key: ALS2AoBJtUup67heKDgzFg== Sec-WebSocket-Key 的值是一串长度为24的字符串是客户端随机生成的base64编码的字符串,它发送给服务器,服务器需要使用它经过一定的运算规则生成服务器的key,然后把服务器的key发到客户端去,客户端验证正确后,握手成功。 握手的具体原理:当我们客户端执行 new WebSocket(''wss://echo.websocket.org')的时候,客户端就会发起请求报文进行握手申请,报文中有一个key就是 下面是实现一个简单的握手协议的demo,代码如下: ### 目录结构如下: demo hands.html 代码如下: <html><head> <title>WebSocket Demo</title></head><body> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:8000"); ws.onerror = function(e) { console.log(e); }; ws.onopen = function() { console.log('握手成功'); } </script></body></html> hands.js 代码如下: 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) { console.log(e); key = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; console.log(key); // WS的字符串 加上 key, 变成新的字符串后做一次sha1运算,最后转换成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); console.log(key); // 输出字段数据,返回到客户端, o.write('HTTP/1.1 101 Switching Protocol\r\n'); o.write('Upgrade: websocket\r\n'); o.write('Connection: Upgrade\r\n'); o.write('Sec-WebSocket-Accept:' +key+'\r\n'); // 输出空行,使HTTP头结束 o.write('\r\n'); } else { // 数据处理 } }) }).listen(8000); 首先在命令行中 进入相对应项目目录后,运行 node hands.js, 然后打开 hands.html 运行一下即可看到 命令行中打印出来如下信息: $ node hands.js<Buffer 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a 48 6f 73 74 3a 20 31 32 37 2e 30 2e 30 2e 31 3a 38 30 30 30 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 ... > +iHlfGTolBaWYpnyTIw22g==W7IEsdQtwv8EP2204kssK/6pg+c= 然后在浏览器中查看请求头如下信息: Request Headers: Connection:Upgrade Host:127.0.0.1:8000Origin:file://Sec-WebSocket-Extensions:permessage-deflate; client_max_window_bits Sec-WebSocket-Key:+iHlfGTolBaWYpnyTIw22g==Sec-WebSocket-Version:13Upgrade:websocket 响应头如下信息: Response Headers: Connection:Upgrade Sec-WebSocket-Accept:W7IEsdQtwv8EP2204kssK/6pg+c= Upgrade:websocket 如上信息可以看到,获取报文中的key代码: 和 Request Headers:中的 Sec-WebSocket-Key 值是一样的,该值是浏览器自动生成的,然后获取该值后,与
'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',相连,对新的字符串通过sha1安全散列算法计算出结果后,再进行Base64编码, (三) 解析数据帧1-1 理解数据帧的含义: 基本帧协议如下: 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ 如上是基本帧协议,它带有操作码(opcode)的帧类型,负载长度,和用于 "扩展数据" 与 "应用数据" 及 它们一起定义的 "负载数据"的指定位置, FIN(1位): 是否为消息的最后一个数据帧。 0x0 表示附加数据帧0x1 表示文本数据帧0x2 表示二进制数据帧0x3-7 暂时无定义,为以后的非控制帧保留0x8 表示连接关闭0x9 表示ping0xA 表示pong0xB-F 暂时无定义,为以后的控制帧保留 Mask(占1位): 表示是否经过掩码处理, 1 是经过掩码的,0是没有经过掩码的。 payload length (7位+16位,或者 7位+64位),定义负载数据的长度。 Masking-key(0或者4个字节),该区块用于存储掩码密钥,只有在第二个子节中的mask为1,也就是消息进行了掩码处理时才有,否则没有, Payload data 扩展数据,是0字节,除非已经协商了一个扩展。 1-2 客户端到服务器掩码 二进制位运算符知识扩展: >> 含义是右移运算符, << 含义是左移运算符 注意1: 在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1(一般情况下). 注意2:负数的二进制位如何计算? 再来看一个列子: 数据帧解析的程序如下代码:(decodeDataFrame.js 代码如下:) 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 = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; // WS的字符串 加上 key, 变成新的字符串后做一次sha1运算,最后转换成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); // 输出字段数据,返回到客户端, o.write('HTTP/1.1 101 Switching Protocol\r\n'); o.write('Upgrade: websocket\r\n'); o.write('Connection: Upgrade\r\n'); o.write('Sec-WebSocket-Accept:' +key+'\r\n'); // 输出空行,使HTTP头结束 o.write('\r\n'); } else { // 数据处理 onmessage(e); } }) }).listen(8000);/* >> 含义是右移运算符, 右移运算符是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0. 比如 11 >> 2, 意思是说将数字11右移2位。 首先将11转换为二进制数为 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2个数字移出,因为该数字是正数, 所以在高位补零,则得到的最终结果为:0000 0000 0000 0000 0000 0000 0000 0010,转换为10进制是2. << 含义是左移运算符 左移运算符是将一个二进制位的操作数按指定移动的位数向左移位,移出位被丢弃,右边的空位一律补0. 比如 3 << 2, 意思是说将数字3左移2位, 首先将3转换为二进制数为 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把该数字高位(左侧)的两个零移出,其他的数字都朝左平移2位, 最后在右侧的两个空位补0,因此最后的结果是 0000 0000 0000 0000 0000 0000 0000 1100,则转换为十进制是12(1100 = 1*2的3次方 + 1*2的2字方) 注意1: 在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1(一般情况下). 比如:十进制数13在计算机中表示为00001101,其中第一位0表示的是符号 注意2:负数的二进制位如何计算? 比如二进制的原码为 10010101,它的补码怎么计算呢? 首先计算它的反码是 01101010; 那么补码 = 反码 + 1 = 01101011 再来看一个列子: -7 >> 2 意思是将数字 -7 右移2位。 负数先用它的绝对值正数取它的二进制代码,7的二进制位为: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二进制位就是 取反, 取反后再加1,就变成补码。 因此-7的二进制位: 1111 1111 1111 1111 1111 1111 1111 1001, 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此转换成十进制的话 -7 >> 2 ,值就变成 -2了。*/function decodeDataFrame(e) { var i = 0, j, s, arrs = [], frame = { // 解析前两个字节的基本数据 FIN: e[i] >> 7, Opcode: e[i++] & 15, Mask: e[i] >> 7, PayloadLength: e[i++] & 0x7F }; // 处理特殊长度126和127 if (frame.PayloadLength === 126) { frame.PayloadLength = (e[i++] << 8) + e[i++]; } if (frame.PayloadLength === 127) { i += 4; // 长度一般用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, arrs = []; j < frame.PayloadLength; j++) { arrs.push(e[i+j] ^ frame.MaskingKey[j%4]); } } else { // 否则的话 直接使用数据 arrs = e.slice(i, i + frame.PayloadLength); } // 数组转换成缓冲区来使用 arrs = new Buffer(arrs); // 如果有必要则把缓冲区转换成字符串来使用 if (frame.Opcode === 1) { arrs = arrs.toString(); } // 设置上数据部分 frame.PayloadLength = arrs; // 返回数据帧 return frame; }function onmessage(e) { console.log(e) e = decodeDataFrame(e); // 解析数据帧 console.log(e); // 把数据帧输出到控制台} index.html代码如下: <html><head> <title>WebSocket Demo</title></head><body> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:8000"); ws.onerror = function(e) { console.log(e); }; ws.onopen = function(e) { console.log('握手成功'); ws.send('次碳酸钴'); } </script></body></html> demo还是一样,decodeDataFrame.js 和 index.html, 先进入项目中对应的目录后,使用node decodeDataFrame.js, 然后打开index.html后查看效果 如下: 这样服务器接收客户端穿过了的数据就没问题了。 (四) 生成数据帧从服务器发往客户端的数据也是同样的数据帧,但是从服务器发送到客户端的数据帧不需要掩码的。我们自己需要去生成数据帧,解析数据帧的时候我们需要分片。 消息分片: 如果大数据不能被碎片化,那么一端就必须将消息整个载入内存缓冲之中,然后需要计算长度等操作并发送,但是有了碎片化机制,服务器端或者中间件就可以选取适用的内存缓冲长度,然后当缓冲满了之后就发送一个消息碎片。 分片规则: 注意: 下面我们来理解下上面分片规则2中的话的含义: 还是看基本帧协议如下: 1 2 3 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ demo解析: <Buffer 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89>{ FIN: 1, Opcode: 1, Mask: 1, PayloadLength: '123456789', MaskingKey: [ 176, 35, 82, 90 ] } 上面返回的数据部分是16进制,因此我们需要他们转换成二进制,有关16进制,10进制,2进制的转换表如下: 我们现在需要把 81 89 b0 23 52 5a 81 11 61 6e 85 15 65 62 89 这些16进制先转换成10进制,然后转换成二进制,分析代码如下: 16进制 10进制 2进制81 8*16的1次方 + 1*16的0次方 = 129 10000001 89 8*16的1次方 + 9*16的0次方 = 137 10001001b0 11*16的1次方 + 0*16的0次方 = 176 10110000 23 2*16的1次方 + 3*16的0次方 = 35 00100011 52 5*16的1次方 + 2*16的0次方 = 82 010100105a 5*16的1次方 + 10*16的0次方 = 90 01011010 81 8*16的1次方 + 1*16的0次方 = 129 10000001 11 1*16的1次方 + 1*16的0次方 = 17 00010001 61 6*16的1次方 + 1*16的0次方 = 97 001111016e 6*16的1次方 + 14*16的0次方 = 110 01101110 85 8*16的1次方 + 5*16的0次方 = 133 10000101 15 1*16的1次方 + 5*16的0次方 = 21 00010101 65 6*16的1次方 + 5*16的0次方 = 101 01100101 62 6*16的1次方 + 2*16的0次方 = 98 01100010 89 8*16的1次方 + 9*16的0次方 = 137 10001001 我们把上面的转换后的二进制 对照上面的 基本帧协议表看下: 0x0 表示附加数据帧 注意:其中8进制是以0开头的,16进制是以0x开头的。 0001,是文本数据帧了。 3. 第九位是1,那么对应的帧协议表就是MASK部分了,Mask(占1位): 表示是否经过掩码处理, 1 是经过掩码的,0是没有经过掩码的。说明是经过掩码处理的, 4. 第10~16位是 0001001 = 9 < 125, 对应帧协议中的 payload length的部分了,数据长度为9,因此小于125位,因此使用7位来表示实际数据长度。 5. b0, 23, 52, 5a 对应的部分是 属于Masking-key(0或者4个字节),该区块用于存储掩码密钥,只有在第二个子节中的mask为1,也就是消息进行了掩码处理时才有。 6. 81 11 61 6e 85 15 65 62 89 这些就是对应表中的数据部分了。 下面我们再来理解下 消息 123456789 怎么通过掩码加密成 81 11 61 6e 85 15 65 62 89 这些数据了。 数字字符1的ASCLL码的16进制为31,转换成10进制就是49了。其他的数字依次类推+1; 数字 10进制 二进制1 49 00110001 2 50 00110010 3 51 00110011 4 52 00110100 5 53 00110101 6 54 00110110 7 55 00110111 8 56 00111000 9 57 00111001 6-1: 其中字符1的二进制位 00110001,掩码b0的二进制位 10110000, 因此: 00110001 进行交配的话,二进制就变成:10000001,转换成10进制为 129了,那么转换成16进制就是 81了。 6-2:字符2的二进制位 00110010,掩码23的二进制位 00100011,因此: 00110010 进行交配的话,二进制就变成 00010001,转换10进制为17,那么转换成16进制就是 11了。 6-3: 字符3的二进制位 00110011,掩码52的二进制位 01010010,因此: 00110011 进行交配的话,二进制就变成:01100001,转换成10进制为 97,那么转换成16进制就是 61了。 6-4: 字符4的二进制位 00110100,掩码 5a 的二进制位 01011010,因此: 00110100 进行交配的话,二进制就变成 01101110,转换成10进制为 110,那么转换成16进制为 6e. 6-5: 字符5的二进制位 00110101,掩码b0的二进制位 10110000, 因此: 00110101 进行交配的话,二进制就变成:10000101,转换成10进制为 133,那么转换成16进制就是 85了。 6-6: 字符6的二进制位 00110110,掩码23的二进制位 00100011,因此: 00110110 进行交配的话,二进制就变成:00010101,转换成10进制为 21,那么转换成16进制就是 15了。 6-7: 字符7的二进制位 00110111,掩码52的二进制位 01010010,因此: 00110111 进行交配的话,二进制就变成:01100101,转换成10进制为 101,那么转换成16进制就是 65了。 6-8: 字符8的二进制位 00111000,掩码 5a 的二进制位 01011010,因此: 00111000 进行交配的话,二进制就变成:01100010,转换成10进制为 98,那么转换成16进制就是 62了。 6-9: 字符9的二进制位 00111001,掩码b0的二进制位 10110000, 因此: 00111001 进行交配的话,二进制就变成:10001001,转换成10进制为 137,那么转换成16进制就是 89了。 字符123456789与掩码加密的整个过程如上面分析,可以看到,字符分别依次与掩码交配,如果掩码不够的话,依次从头循环即可。 因此我们可以编写如下encodeDataFrame.js代码: 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 = e.toString().match(/Sec-WebSocket-Key: (.+)/)[1]; // WS的字符串 加上 key, 变成新的字符串后做一次sha1运算,最后转换成Base64 key = crypto.createHash('sha1').update(key+WS).digest('base64'); // 输出字段数据,返回到客户端, o.write('HTTP/1.1 101 Switching Protocol\r\n'); o.write('Upgrade: websocket\r\n'); o.write('Connection: Upgrade\r\n'); o.write('Sec-WebSocket-Accept:' +key+'\r\n'); // 输出空行,使HTTP头结束 o.write('\r\n'); // 握手成功后给客户端发送数据 o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789" })) } else { } }) }).listen(8001);/* >> 含义是右移运算符, 右移运算符是将一个二进制位的操作数按指定移动的位数向右移动,移出位被丢弃,左边移出的空位一律补0. 比如 11 >> 2, 意思是说将数字11右移2位。 首先将11转换为二进制数为 0000 0000 0000 0000 0000 0000 0000 1011 , 然后把低位的最后2个数字移出,因为该数字是正数, 所以在高位补零,则得到的最终结果为:0000 0000 0000 0000 0000 0000 0000 0010,转换为10进制是2. << 含义是左移运算符 左移运算符是将一个二进制位的操作数按指定移动的位数向左移位,移出位被丢弃,右边的空位一律补0. 比如 3 << 2, 意思是说将数字3左移2位, 首先将3转换为二进制数为 0000 0000 0000 0000 0000 0000 0000 0011 , 然后把该数字高位(左侧)的两个零移出,其他的数字都朝左平移2位, 最后在右侧的两个空位补0,因此最后的结果是 0000 0000 0000 0000 0000 0000 0000 1100,则转换为十进制是12(1100 = 1*2的3次方 + 1*2的2字方) 注意1: 在使用补码作为机器数的机器中,正数的符号位为0,负数的符号位为1(一般情况下). 比如:十进制数13在计算机中表示为00001101,其中第一位0表示的是符号 注意2:负数的二进制位如何计算? 比如二进制的原码为 10010101,它的补码怎么计算呢? 首先计算它的反码是 01101010; 那么补码 = 反码 + 1 = 01101011 再来看一个列子: -7 >> 2 意思是将数字 -7 右移2位。 负数先用它的绝对值正数取它的二进制代码,7的二进制位为: 0000 0000 0000 0000 0000 0000 0000 0111 ,那么 -7的二进制位就是 取反, 取反后再加1,就变成补码。 因此-7的二进制位: 1111 1111 1111 1111 1111 1111 1111 1001, 因此 -7右移2位就成 1111 1111 1111 1111 1111 1111 1111 1110 因此转换成十进制的话 -7 >> 2 ,值就变成 -2了。*/function decodeDataFrame(e) { var i = 0, j, s, arrs = [], frame = { // 解析前两个字节的基本数据 FIN: e[i] >> 7, Opcode: e[i++] & 15, Mask: e[i] >> 7, PayloadLength: e[i++] & 0x7F }; // 处理特殊长度126和127 if (frame.PayloadLength === 126) { frame.PayloadLength = (e[i++] << 8) + e[i++]; } if (frame.PayloadLength === 127) { i += 4; // 长度一般用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, arrs = []; j < frame.PayloadLength; j++) { arrs.push(e[i+j] ^ frame.MaskingKey[j%4]); } } else { // 否则的话 直接使用数据 arrs = e.slice(i, i + frame.PayloadLength); } // 数组转换成缓冲区来使用 arrs = new Buffer(arrs); // 如果有必要则把缓冲区转换成字符串来使用 if (frame.Opcode === 1) { arrs = arrs.toString(); } // 设置上数据部分 frame.PayloadLength = arrs; // 返回数据帧 return frame; }function encodeDataFrame(e) { var arrs = [], o = new Buffer(e.PayloadData), l = o.length; // 处理第一个字节 arrs.push((e.FIN << 7)+e.Opcode); // 处理第二个字节,判断它的长度并放入相应的后溪长度 if (l < 126) { arrs.push(l); } else if(l < 0x0000) { arrs.push(126, (1&0xFF00) >> 8, 1&0xFF); } else { arrs.push(127, 0, 0, 0, 0, (l&0xFF000000)>>24,(l&0xFF0000)>>16,(l&0xFF00)>>8,l&0xFF ); } // 返回头部分和数据部分的合并缓冲区 return Buffer.concat([new Buffer(arrs), o]); } 然后index.html代码如下: <html><head> <title>WebSocket Demo</title></head><body> <script type="text/javascript"> var ws = new WebSocket("ws://127.0.0.1:8001"); ws.onerror = function(e) { console.log(e); }; ws.onopen = function(e) { console.log('握手成功'); ws.send('123456789'); } ws.onmessage = function(e) { console.log(e); } </script></body></html> 进入目录后,运行node encodeDataFrame.js后,打开index.html页面,在控制台看待效果图如下: 使用分片的方式重新修改代码: 上面是基本的使用方法,但是有时候我们需要将一个大的数据包需要分成多个数据帧来传输,因此分片它分为3个部分: 1个开始帧:FIN=0, Opcode > 0; 因此之前的握手成功后发送的数据代码: o.write(encodeDataFrame({ FIN: 1, Opcode: 1, PayloadData: "123456789"})) 需要分成三部分来发送了; 改成如下代码: // 握手成功后给客户端发送数据o.write(encodeDataFrame({ FIN: 0, Opcode: 1, PayloadData: "123"})); o.write(encodeDataFrame({ FIN: 0, Opcode: 0, PayloadData: "456"})); o.write(encodeDataFrame({ FIN: 1, Opcode: 0, PayloadData: "789"})); (五) 心跳及重连机制在使用websocket的过程中,有时候会遇到网络断开的情况,但是在网络断开的时候服务器端并没有触发onclose的事件。这样会有:服务器会继续向客户端发送多余的链接,并且这些数据还会丢失。所以就需要一种机制来检测客户端和服务端是否处于正常的链接状态。因此就有了websocket的心跳了。还有心跳,说明还活着,没有心跳说明已经挂掉了。 1. 为什么叫心跳包呢? 2. 心跳机制是? 那么需要怎么去实现它呢?如下所有代码: <html> <head> <meta charset="utf-8"> <title>WebSocket Demo</title> </head> <body> <script type="text/javascript"> // var ws = new WebSocket("wss://echo.websocket.org"); /* ws.onerror = function(e) { console.log('已关闭'); }; ws.onopen = function(e) { console.log('握手成功'); ws.send('123456789'); } ws.onclose = function() { console.log('已关闭'); } ws.onmessage = function(e) { console.log('收到消息'); console.log(e); } */ var lockReconnect = false;//避免重复连接 var wsUrl = "wss://echo.websocket.org"; var ws; var tt; function createWebSocket() { try { ws = new WebSocket(wsUrl); init(); } catch(e) { console.log('catch'); reconnect(wsUrl); } } function init() { ws.onclose = function () { console.log('链接关闭'); reconnect(wsUrl); }; ws.onerror = function() { console.log('发生异常了'); reconnect(wsUrl); }; ws.onopen = function () { //心跳检测重置 heartCheck.start(); }; ws.onmessage = function (event) { //拿到任何消息都说明当前连接是正常的 console.log('接收到消息'); heartCheck.start(); } } function reconnect(url) { if(lockReconnect) { return; }; lockReconnect = true; //没连接上会一直重连,设置延迟避免请求过多 tt && clearTimeout(tt); tt = setTimeout(function () { createWebSocket(url); lockReconnect = false; }, 4000); } //心跳检测 var heartCheck = { timeout: 3000, timeoutObj: null, serverTimeoutObj: null, start: function(){ console.log('start'); var self = this; this.timeoutObj && clearTimeout(this.timeoutObj); this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj); this.timeoutObj = setTimeout(function(){ //这里发送一个心跳,后端收到后,返回一个心跳消息, console.log('55555'); ws.send("123456789"); self.serverTimeoutObj = setTimeout(function() { console.log(111); console.log(ws); ws.close(); // createWebSocket(); }, self.timeout); }, this.timeout) } } createWebSocket(wsUrl); </script> </body> </html> 具体的思路如下: function createWebSocket() { try { ws = new WebSocket(wsUrl); init(); } catch(e) { console.log('catch'); reconnect(wsUrl); } } 2. 第二步调用init方法,该方法内把一些监听事件封装如下: function init() { ws.onclose = function () { console.log('链接关闭'); reconnect(wsUrl); }; ws.onerror = function() { console.log('发生异常了'); reconnect(wsUrl); }; ws.onopen = function () { //心跳检测重置 heartCheck.start(); }; ws.onmessage = function (event) { //拿到任何消息都说明当前连接是正常的 console.log('接收到消息'); heartCheck.start(); } } 3. 如上第二步,当网络断开的时候,会先调用onerror,onclose事件可以监听到,会调用reconnect方法进行重连操作。正常的情况下,是先调用 4. 重连操作 reconnect代码如下: var lockReconnect = false;//避免重复连接function reconnect(url) { if(lockReconnect) { return; }; lockReconnect = true; //没连接上会一直重连,设置延迟避免请求过多 tt && clearTimeout(tt); tt = setTimeout(function () { createWebSocket(url); lockReconnect = false; }, 4000); } 如上代码,如果网络断开的话,会执行reconnect方法,使用了一个定时器,4秒后会重新创建一个新的websocket链接,重新调用createWebSocket函数, 5. 最后一步就是实现心跳检测的代码:如下: //心跳检测var heartCheck = { timeout: 3000, timeoutObj: null, serverTimeoutObj: null, start: function(){ console.log('start'); var self = this; this.timeoutObj && clearTimeout(this.timeoutObj); this.serverTimeoutObj && clearTimeout(this.serverTimeoutObj); this.timeoutObj = setTimeout(function(){ //这里发送一个心跳,后端收到后,返回一个心跳消息, //onmessage拿到返回的心跳就说明连接正常 console.log('55555'); ws.send("123456789"); self.serverTimeoutObj = setTimeout(function() { console.log(111); console.log(ws); ws.close(); // createWebSocket(); }, self.timeout); }, this.timeout) } } 实现心跳检测的思路是:每隔一段固定的时间,向服务器端发送一个ping数据,如果在正常的情况下,服务器会返回一个pong给客户端,如果客户端通过 |
|