WebSocket(叁) 生成数据帧

昨天的文章中介绍了WebSocket数据帧的结构和解析。其实对从服务器发送往客户端的数据也是同样的数据帧。但因此觉得这看似和解析数据帧一样简单那就错了。我们需要自己去生成数据帧。而且会遇上和解析时候不同的问题,比如数据帧分片传输的情况。
从服务器发送到客户端的数组帧不需要掩码,这是非常值得庆幸的地方。于是要写出一个生成数据帧的函数并不难

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//NodeJS
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,0,0, //8字节数据,前4字节一般没用留空
    (l&0xFF000000)>>24,(l&0xFF0000)>>16,(l&0xFF00)>>8,l&0xFF
  );
  //返回头部分和数据部分的合并缓冲区
  return Buffer.concat([new Buffer(s),o]);
};

可以把它用于一个实例中

1
2
3
4
5
//客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000/");
ws.onmessage=function(e){
  console.log(e);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//服务器程序
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];
      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');
      o.write('Sec-WebSocket-Accept: '+key+'\r\n');
      o.write('\r\n');
      //握手成功后给客户端发送个数据
      o.write(encodeDataFrame({
        FIN:1,Opcode:1,PayloadData:"次碳酸钴"
      }));
    };
  });
}).listen(8000);

上面是最基本的用法。但是有时候数据需要分成多个数据包来发送,这就需要用到分片,也就是使用多个数据帧来传输一个数据。分片传输分为三个部分:
开始帧:FIN=0,Opcode>0;一个
传输帧:FIN=0,Opcode=0;零个或多个
终止帧:FIN=1,Opcode=0;一个
FIN是FINAL的缩写,它为1时表示一个数据传输结束,而开始和传输帧的时候数据都没结束,所以是0,之后最后的结束帧FIN是1。同一个数据即使分片传输,它的每个数据帧的Opcode也应该相同,为了避免冲突,只对分片传输的开始帧设置Opcode,传输帧和结束帧的Opcode留0。因此把上面实例的部分代码改成

1
2
3
4
5
6
7
8
9
10
 //握手成功后给客户端发送个数据
      o.write(encodeDataFrame({
        FIN:0,Opcode:1,PayloadData:"ABC"
      }));
      o.write(encodeDataFrame({
        FIN:0,Opcode:0,PayloadData:"-DEF-"
      }));
      o.write(encodeDataFrame({
        FIN:1,Opcode:0,PayloadData:"GHI"
      }));

就可以在客户端得到

这就是分片传输的关键所在。

转自: https://www.web-tinker.com/article/20307.html

WebSocket(贰) 解析数据帧

知道了怎么握手只是让客户端和服务器建立连接而已,WebSocket真正麻烦的地方是在数据的传输上!为了环保,它使用了特定格式的数据帧,这个数据帧需要自己去解析(当然也有别人编写好的库可以用)。虽然官方文档描述的很详细,但是看起来还是蛋疼。
当客户端向服务器发送一个数据时服务器收到一个数据帧,比如下面的程序

1
2
3
4
5
//客户端程序
var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onopen=function(e){
  ws.send("次碳酸钴"); //发送数据
};
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=e.toString().match(/Sec-WebSocket-Key: (.+)/)[1];
      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');
      o.write('Sec-WebSocket-Accept: '+key+'\r\n');
      o.write('\r\n');
    }else onmessage(e); //接收并交给处理函数
  });
}).listen(8000);
 
function onmessage(e){
  console.log(e); //把数据输出到控制台
};

这里是直接把接收到的数据输出了,得到这样一个东西

这就是一个完整的数据帧,直接的16进制数据我们当然无法直接阅读,需要按照数据帧的格式把它里面的数据取出来才行。对于这个数据帧,官方文档提供了一个结构图

 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 ...                |
 +---------------------------------------------------------------+

光拿出这个实在很难看懂,顶部数字用十进制而不是八进制太让人蛋疼了。当然官方文档在后面的描述中也有详细介绍,看完后再回头来看图表才能看明白。其实WebSocket目前还不太完善,很多实验性的东西,所以完全按照官方文档来理解是蛋疼的。这里就说我自己的理解。
现在再看左上角上面的图标,左上角的四个小列,也就是4位,第一位是FIN,后面三位是RSV1到3。官方文档上说RSV是预留的空间,正常为0,这就意味着,正常情况下他们可以当做0填充,那么前4位只有第一位的FIN需要设置,FIN表示帧结束,由于这篇中它不重要就不特别介绍了。接着后面的四位是储存opcode的值,这个opcode是标识数据类型的。这样数据的第一个字节我们就能理解它的含义了,看上面16进制的数据的第一个字节81换成二进制是1000001,第一个1是FIN的值,最后一个1是opcode的值。
接着是第二个字节的数据,它由1位的MASK和7位的PayloadLen组成,MASK标识这个数据帧的数据是否使用掩码,PayloadLen表示数据部分的长度。但是PayloadLen只有7位,换成无符号整型的话只有0到127的取值,这么小的数值当然无法描述较大的数据,因此规定当数据长度小于或等于125时候它才作为数据长度的描述,如果这个值为126,则时候后面的两个字节来储存储存数据长度,如果为127则用后面八个字节来储存数据长度。所以上面的图片第一行的最右侧那块和第二行看起来有些颓然。从我们的示例数据来看,第二个字节的8C中80是最高位为1,这意味着MASK为1,后面的C表示这个数据部分有12个字节。
再接着是上面图表中的MaskingKey,它占四个字节,储存掩码的实体部分。但是只有在前面的MASK被设置为1时候才存在这个数据,否则不使用掩码也就没有这个数据了。看我们的示例数据,由于前面的MASK为1,所以3到6字节的“79 77 3d 41”是数据的掩码实体。
最后是数据部分,如果掩码存在,那么所有数据都需要与掩码做一次异或运算,四个字节的掩码与所有数据字节轮流发生性关系。如果不存在掩码,那么后面的数据就可以直接使用。
这样数据帧就解析完了。下面是我写的数据帧解析的程序,请不要吐槽代码没优化

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
function decodeDataFrame(e){
  var i=0,j,s,frame={
    //解析前两个字节的基本数据
    FIN:e[i]&gt;&gt;7,Opcode:e[i++]&amp;15,Mask:e[i]&gt;&gt;7,
    PayloadLength:e[i++]&amp;0x7F
  };
  //处理特殊长度126和127
  if(frame.PayloadLength==126)
    frame.PayloadLength=(e[i++]&lt;&lt;8)+e[i++];
  if(frame.PayloadLength==127)
    i+=4, //长度一般用四字节的整型,前四个字节通常为长整形留空的
    frame.PayloadLength=(e[i++]&lt;&lt;24)+(e[i++]&lt;&lt;16)+(e[i++]&lt;&lt;8)+e[i++];
  //判断是否使用掩码
  if(frame.Mask){
    //获取掩码实体
    frame.MaskingKey=[e[i++],e[i++],e[i++],e[i++]];
    //对数据和掩码做异或运算
    for(j=0,s=[];j&lt;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;
};

既然有了解析程序,那么我们就可以把上面实例服务器端的onmessage方法修改一下

1
2
3
4
function onmessage(e){
  e=decodeDataFrame(e); //解析数据帧
  console.log(e); //把数据帧输出到控制台
};

这样服务器接收客户端穿过了的数据就没问题了。嘛,这篇文章就只说接收,至于从服务器发送到客户的情况会有更复杂的情况出现,咱下一篇再说。

转自: https://www.web-tinker.com/article/20306.html

WebSocket(壹) 握手连接

  WebSocket虽然很先进,很好用,但却是个很麻烦的东西。与普通的Web通信机制不同,它本身可以算是一个协议。要使用WebSocket首先得让客户端和服务器建立连接,而且这个连接蛋疼的比TCP那样的传输层协议还复杂,需要通过验证KEY来做握手工作。
这个握手协议使用的是HTTP格式的请求,并再头部分带上一个Sec-WebSocket-Key字段,服务器对这个字段加上一个特定的字符串后做一次sha1运算,然后把结果用Base64的形式以同样的方式发送回去就可以完成握手的工作了。当然,WebSocket这个协议还在不断完善更新,目前这么用是没问题的,历史版本中还出现过其它方式,这里就无视他们了。
下面是足以完成握手的服务器端程序,使用NodeJS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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');
}else{
//数据处理
};
});
}).listen(8000);

客户端一段简单的代码即可

1
2
3
4
5
6
7
var ws=new WebSocket("ws://127.0.0.1:8000");
ws.onerror=function(e){
  console.log(e);
};
ws.onopen=function(){
  console.log("握手成功");
};

一旦握手成功就会触发open事件,否则触发error事件。在open之后就可以进行数据传输了,至于数据要如何传输这比这个握手连接要复杂的多,咱下一篇再说。

转自: https://www.web-tinker.com/article/20305.html