参见协议
WebSocket数据帧结构如下图所示:
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 ... |
+---------------------------------------------------------------+
FIN:1位
表示这是消息的最后一帧(结束帧),一个消息由一个或多个数据帧构成。若消息由一帧构成,起始帧即结束帧。
RSV1,RSV2,RSV3:各1位
这里我翻译不好,大致意思是如果未定义扩展,各位是0;如果定义了扩展,即为非0值。如果接收的帧此处非0,扩展中却没有该值的定义,那么关闭连接。
OPCODE:4位
解释PayloadData,如果接收到未知的opcode,接收端必须关闭连接。
0x0表示附加数据帧
0x1表示文本数据帧
0x2表示二进制数据帧
0x3-7暂时无定义,为以后的非控制帧保留
0x8表示连接关闭
0x9表示ping
0xA表示pong
0xB-F暂时无定义,为以后的控制帧保留
MASK:1位
用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。
Payload length:7位,7+16位,7+64位
PayloadData的长度(以字节为单位)。
如果其值在0-125,则是payload的真实长度。
如果值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
如果值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
长度表示遵循一个原则,用最少的字节表示长度(我理解是尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不允许长度1是126或127,然后长度2是124,这样违反原则。
以上说明很重要.下面的代码就是按此原则设计
Payload长度是ExtensionData长度与ApplicationData长度之和。ExtensionData长度可能是0,这种情况下,Payload长度即是ApplicationData长度。
WebSocket协议规定数据通过帧序列传输。
客户端必须对其发送到服务器的所有帧进行掩码处理。
服务器一旦收到无掩码帧,将关闭连接。服务器可能发送一个状态码是1002(表示协议错误)的Close帧。
而服务器发送客户端的数据帧不做掩码处理,一旦客户端发现经过掩码处理的帧,将关闭连接。客户端可能使用状态码1002。
消息分片
分片目的是发送长度未知的消息。如果不分片发送,即一帧,就需要缓存整个消息,计算其长度,构建frame并发送;使用分片的话,可使用一个大小合适的buffer,用消息内容填充buffer,填满即发送出去。
分片规则:
1.一个未分片的消息只有一帧(FIN为1,opcode非0)
2.一个分片的消息由起始帧(FIN为0,opcode非0),若干(0个或多个)帧(FIN为0,opcode为0),结束帧(FIN为1,opcode为0)。
3.控制帧可以出现在分片消息中间,但控制帧本身不允许分片。
4.分片消息必须按次序逐帧发送。
5.如果未协商扩展的情况下,两个分片消息的帧之间不允许交错。
6.能够处理存在于分片消息帧之间的控制帧
7.发送端为非控制消息构建长度任意的分片
8.client和server兼容接收分片消息与非分片消息
9.控制帧不允许分片,中间媒介不允许改变分片结构(即为控制帧分片)
10.如果使用保留位,中间媒介不知道其值表示的含义,那么中间媒介不允许改变消息的分片结构
11.如果协商扩展,中间媒介不知道,那么中间媒介不允许改变消息的分片结构,同样地,如果中间媒介不了解一个连接的握手信息,也不允许改变该连接的消息的分片结构
12.由于上述规则,一个消息的所有分片是同一数据类型(由第一个分片的opcode定义)的数据。因为控制帧不允许分片,所以一个消息的所有分片的数据类型是文本、二进制、opcode保留类型中的一种。
需要注意的是,如果控制帧不允许夹杂在一个消息的分片之间,延迟会较大,比如说当前正在传输一个较大的消息,此时的ping必须等待消息传输完成,才能发送出去,会导致较大的延迟。为了避免类似问题,需要允许控制帧夹杂在消息分片之间。
部分调试解析消息头代码如下:
#if RFC6455调试代码
#region 调试代码
if (receiveBuffer.Length > 2)
{
byte bt1 = receiveBuffer[0];
byte bt2 = receiveBuffer[1];
int Opcode = ((bt1) & 0xF);
Console.WriteLine(string.Format("FIN:{0},RSV1:{1} RSV2:{2} RSV3:{3};Opcode:{4},mask:{5};len:{6}"
, bt1 >> 7 & 1
, (bt1 >> 6) & 1
, (bt1 >> 5) & 1
, (bt1 >> 4) & 1
, (bt1 & 0xF).ToString("X2")
///第二字节
, bt2 >> 7 & 1 //mask
, bt2 & 0x7f //Payload len ==((byte)(bt2<<1)>>1)
));
//FIN:1位,用来表明这是一个消息的最后的消息片断,当然第一个消息片断也可能是最后的一个消息片断
//RSV1, RSV2, RSV3: 分别都是1位,如果双方之间没有约定自定义协议,那么这几位的值都必须为0,否则必须断掉WebSocket连接
//Opcode:4位操作码,定义有效负载数据,如果收到了一个未知的操作码,连接也必须断掉,以下是定义的操作码:
//* %x0 表示连续消息片断
//* %x1 表示文本消息片断
//* %x2 表未二进制消息片断
//* %x3-7 为将来的非控制消息片断保留的操作码
//* %x8 表示连接关闭
//* %x9 表示心跳检查的ping
//* %xA 表示心跳检查的pong
//* %xB-F 为将来的控制消息片断的保留操作码
}
#endregion
#endif
发送方法代码如下:
///
/// WebSocket Send 发送数据到客户端,打包服务器数据
///
/// 要发送的数据
/// 每次发送的最大数据包
public void Send(Byte[] bytes,int sendMax=65536)
{
bool canSend = true;
//每次最大发送 64Kb的数据
int SendMax = sendMax;
int num = 0;
//已经发送的字节数据
int taked = 0;
while (canSend)
{
//内容数据
byte[] contentBytes = null;
var sendArr = bytes.Skip(num * SendMax).Take(SendMax).ToArray();
taked += sendArr.Length;
if (sendArr.Length > 0)
{
//是否可以继续发送
canSend = bytes.Length > taked;
if (sendArr.Length < 126)
{
#region 一次发送小于126的数据
contentBytes = new byte[sendArr.Length + 2];
contentBytes[0] = (byte)(num == 0 ? 0x81 : (!canSend ? 0x80 : 0));
contentBytes[1] = (byte)sendArr.Length;
Array.Copy(sendArr, 0, contentBytes, 2, sendArr.Length);
canSend = false;
#endregion
}
else if (sendArr.Length < 0xFFFF)
{
#region 发送小于65535的数据
contentBytes = new byte[sendArr.Length + 4];
//首次不分片发送,大于128字节的数据一次发完
if (!canSend && num == 0)
{
contentBytes[0] = 0x81;
}
else
{
//一个分片的消息由起始帧(FIN为0,opcode非0),若干(0个或多个)帧(FIN为0,opcode为0),结束帧(FIN为1,opcode为0)。
contentBytes[0] = (byte)(num == 0 ? 0x01 : (!canSend ? 0x80 : 0));
}
contentBytes[1] = 126;
byte[] ushortlen = BitConverter.GetBytes((short)sendArr.Length);
contentBytes[2] = ushortlen[1];
contentBytes[3] = ushortlen[0];
Array.Copy(sendArr, 0, contentBytes, 4, sendArr.Length);
#endregion
}
else if (sendArr.LongLength < long.MaxValue)
{
#region 一次发送所有数据
//long数据一次发完
contentBytes = new byte[sendArr.Length + 10];
//首次不分片发送,大于128字节的数据一次发完
if (!canSend && num == 0)
{
contentBytes[0] = 0x81;
}
else
{
//一个分片的消息由起始帧(FIN为0,opcode非0),若干(0个或多个)帧(FIN为0,opcode为0),结束帧(FIN为1,opcode为0)。
contentBytes[0] = (byte)(num == 0 ? 0x01 : (!canSend ? 0x80 : 0));
}
contentBytes[1] = 127;
byte[] ulonglen = BitConverter.GetBytes((long)sendArr.Length);
contentBytes[2] = ulonglen[7];
contentBytes[3] = ulonglen[6];
contentBytes[4] = ulonglen[5];
contentBytes[5] = ulonglen[4];
contentBytes[6] = ulonglen[3];
contentBytes[7] = ulonglen[2];
contentBytes[8] = ulonglen[1];
contentBytes[9] = ulonglen[0];
Array.Copy(sendArr, 0, contentBytes, 10, sendArr.Length);
#endregion
}
}
try
{
if (contentBytes != null)
{
Socket.Send(contentBytes);
}
}
catch (NullReferenceException)
{
break;
}
catch (System.Net.Sockets.SocketException)
{
break;
}
catch (ObjectDisposedException)
{
break;
}
finally
{
num++;
}
}
}