WebScoket 规范
websocket 是 独立的基于TCP的协议, 其跟http协议的关系仅仅是 WebSocket 的握手被http 服务器当做 Upgrade request http包处理。 websocket 有自己的握手处理。 TCP连接建立后,client 发送websocket 握手请求. 请求包需求如下:
示例如下:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
Server 接手到握手请求后应处理该请求包括:
响应可能如下:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
该节主要参考了 http://blog.csdn.net/fenglibing/article/details/6852497。 在WebSocket 协议中,使用序列frames方式来传输数据。一个frame的标准格式如下:
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,否则必须断掉WebSocket连接;
Opcode:4位操作码,定义有效负载数据,如果收到了一个未知的操作码,连接也必须断掉,以下是定义的操作码:
%x0 表示连续消息分片
%x1 表示文本消息分片%x2 表未二进制消息分片
%x3-7 为将来的非控制消息片断保留的操作码
%x8 表示连接关闭 %x9 表示心跳检查的ping
%xA 表示心跳检查的pong
%xB-F 为将来的控制消息片断的保留操作码
Mask: 定义传输的数据是否有加掩码,如果设置为1,掩码键必须放在masking-key区域,客户端发送给服务端的所有消息,此位的值都是1;
Payload length: 传输数据的长度,以字节的形式表示:7位、7+16位、或者7+64位。如果 这个值以字节表示是0-125这个范围,那这个值就表示传输数据的长度;如果这个值是126,则随后的两个字节表示的是一个16进制无符号数,用来表示传 输数据的长度;如果这个值是127,则随后的是8个字节表示的一个64位无符合数,这个数用来表示传输数据的长度。多字节长度的数量是以网络字节的顺序表 示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度。注意Payload length不包括Masking-key在内。
Masking-key: 0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在。 数据Mask方法是,第 i byte 数据 = orig-data ^ (i % 4) .
Payload data: (x+y)位,负载数据为扩展数据及应用数据长度之和。
Extension data:x位,如果客户端与服务端之间没有特殊约定,那么扩展数据的长度始终为0,任何的扩展都必须指定扩展数据的长度,或者长度的计算方式,以及在握手时如何确定正确的握手方式。如果存在扩展数据,则扩展数据就会包括在负载数据的长度之内。
Application data:y位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。
把消息分片处理主要是处于以下两个原因:
消息分片一些规则如下(不全):
- 为分片消息(single-frame) 其FIN置为1,并且opcode code 不是 0;
- 分片消息序列如下, 第一帧FIN置为0,opcode code不是0; 接着是FIN置为0,opcode code也是0; 最后帧 FIN为1,opcode code为0.
- 在分片消息发送期间可能插入了控制帧
- 控制帧不能分片
控制帧的opcode符号位为1, 目前控制帧包括 0×8(Close), 0×9(Ping) 0xA (Pong). 0xB-0xF 被预留。
详细解析如下,来自http://blog.csdn.net/fenglibing/article/details/6852497:
ws-frame = frame-fin
frame-rsv1
frame-rsv2
frame-rsv3
frame-opcode
frame-masked
frame-payload-length
[ frame-masking-key ]
frame-payload-data
frame-fin = %x0 ; 表示这不是当前消息的最后一帧,后面还有消息
/ %x1 ; 表示这是当前消息的最后一帧
frame-rsv1 = %x0
; 1 bit, 如果没有扩展约定,该值必须为0
frame-rsv2 = %x0
; 1 bit, 如果没有扩展约定,该值必须为0
frame-rsv3 = %x0
; 1 bit, 如果没有扩展约定,该值必须为0
frame-opcode = %x0 ; 表示这是一个连续帧消息
/ %x1 ; 表示文本消息
/ %x2 ; 表示二进制消息
/ %x3-7 ; 保留
/ %x8 ; 表示客户端发起的关闭
/ %x9 ; ping(用于心跳)
/ %xA ; pong(用于心跳)
/ %xB-F ; 保留
frame-masked = %x0 ; 数据帧没有加掩码,后面没有掩码key
/ %x1 ; 数据帧加了掩码,后面有掩码key
frame-payload-length = %x00-7D
/ %x7E frame-payload-length-16
/ %x7F frame-payload-length-63
; 表示数据帧的长度
frame-payload-length-16 = %x0000-FFFF
; 表示数据帧的长度
frame-payload-length-63 = %x0000000000000000-7FFFFFFFFFFFFFFF
; 表示数据帧的长度
frame-masking-key = 4( %0x00-FF ) ; 掩码key,只有当掩码位为1时出现
frame-payload-data = (frame-masked-extension-data
frame-masked-application-data)
; 当掩码位为1时,这里的数据为带掩码的数据,扩展数据及应用数据都带掩码
/ (frame-unmasked-extension-data
frame-unmasked-application-data) ;
当掩码位为0时,这里的数据为不带掩码的数据,扩展数据及应用数据都不带掩码
frame-masked-extension-data = *( %x00-FF ) ; 目前保留,以后定义
frame-masked-application-data = *( %x00-FF )
frame-unmasked-extension-data = *( %x00-FF ) ; 目前保留,以后定义
frame-unmasked-application-data = *( %x00-FF )
Close 帧的opcode是0×8. 接收到 Close 帧后,如果之前没发送过Close帧,则其必须发送Close 帧响应,但其可以延迟发送Close响应帧,例如在其发送完数据之后发送;但是,协议不保证对方在发送Close 帧后仍会处理其后续的数据。Close帧可能Client发起也可能是Server发起。
接收到Ping帧后将响应Pong帧, 主要用于检测网络连接情况。
WebSocket 支持协议扩展。 例如增加一个认证处理或者速率控制等,这通过client-server 协商完成。在WebSocket 握手处理时,通过头域Sec-WebSocket-Extensions来完成协商。 例如:
Sec-WebSocket-Extensions: mux; max-channels=4; flow-control, deflate-stream
服务器接收一个或多个extensiions 通过再起响应的Sec-WebSocket-Extensions头域增加一个或多个extension完成。
说明:
服务器建立成功之后,如果有客户端请求连接本服务器,需要用socket_accept等方法建立一个新的socket连接,并接收客户端的请求信息,处理之后,返回响应信息,然后握手成功。
接下来是字符串通信,客户端send过来一段字符串信息,服务器端接收到并返回给客户端这个字符串。 首先我们处理接收到的信息,根据上篇文章介绍的数据传输格式,并firefox的FIN一直为1,RSV1,2,3为0,如果是文本消息,那么 opcode为1,所以数据包的第一个数据是0x81,然后是一位mask值,firefox发来的数据是加了掩码的,所以mask值为1,后面跟7位是 数据信息长度,我们以客户端发送hi为例,那么长度就是2个字节,则第二个数据就是0x82,这里没有约定扩展数据,所以不存在扩展数据长度字节,接下来 是4个数据的掩码(因为我们这里是发送hi,2个字节的信息,小于125个字节,所以掩码是第3-第6个数据,根据数据长度的不同,掩码的位置也不同,如 果取到那7位表示的值是126,则掩码为第5-第8个数据,如果取到那7位表示的值是127,则掩码为第11-第14个数据),后面跟客户端发送的内容数 据,处理接收到的数据我们需要用取到的掩码依次轮流跟内容数据做异或(^)运算,第一个内容数据与第一个掩码异或,第二个内容数据与第二个掩码异或……第 五个内容数据与第一个掩码异或……以此类推,一直到结束,然后对内容进行编码。
根据数据长度的不同,掩码的位置也不同:
从第9个字节开始是 1111101=125,掩码是第3-第6个数据
从第9个字节开始是 1111110=126,掩码是第5-第8个数据
从第9个字节开始是 1111111=126,掩码是第11-第14个数据
举例一:
举例二:
01234567890123456789012345678901234567890123456789
01234567890123456789012345678901234567890123456789
01234567890123456789012345678901234567890123456789
01234567890123456789012345678901234567890123456789
01234567890123456789012345678901
10000001111111100110010011001101
[0] 129 byte
[1] 254 byte
[2] 0 byte
[3] 201 byte
[4] 77 byte
[5] 175 byte
[6] 124 byte
[7] 107 byte
[8] 125 byte
[9] 158 byte
[10] 78 byte
[11] 88 byte
[12] 121 byte
[13] 154 byte
[14] 74 byte
[15] 92 byte
[16] 117 byte
[17] 150 byte
[18] 76 byte
[19] 90 byte
[20] 127 byte
[21] 156 byte
[22] 72 byte
[23] 94 byte
[24] 123 byte
[25] 152 byte
[26] 68 byte
[27] 82 byte
[28] 125 byte
[29] 158 byte
[30] 78 byte
[31] 88 byte
[32] 121 byte
[33] 154 byte
[34] 74 byte
[35] 92 byte
[36] 117 byte
[37] 150 byte
[38] 76 byte
[39] 90 byte
[40] 127 byte
[41] 156 byte
[42] 72 byte
[43] 94 byte
[44] 123 byte
[45] 152 byte
[46] 68 byte
[47] 82 byte
[48] 125 byte
[49] 158 byte
[50] 78 byte
[51] 88 byte
[52] 121 byte
[53] 154 byte
[54] 74 byte
[55] 92 byte
[56] 117 byte
[57] 150 byte
[58] 76 byte
[59] 90 byte
[60] 127 byte
[61] 156 byte
[62] 72 byte
[63] 94 byte
[64] 123 byte
[65] 152 byte
[66] 68 byte
[67] 82 byte
[68] 125 byte
[69] 158 byte
[70] 78 byte
[71] 88 byte
[72] 121 byte
[73] 154 byte
[74] 74 byte
[75] 92 byte
[76] 117 byte
[77] 150 byte
[78] 76 byte
[79] 90 byte
[80] 127 byte
[81] 156 byte
[82] 72 byte
[83] 94 byte
[84] 123 byte
[85] 152 byte
[86] 68 byte
[87] 82 byte
[88] 125 byte
[89] 158 byte
[90] 78 byte
[91] 88 byte
[92] 121 byte
[93] 154 byte
[94] 74 byte
[95] 92 byte
[96] 117 byte
[97] 150 byte
[98] 76 byte
[99] 90 byte
[100] 127 byte
[101] 156 byte
[102] 72 byte
[103] 94 byte
[104] 123 byte
[105] 152 byte
[106] 68 byte
[107] 82 byte
[108] 125 byte
[109] 158 byte
[110] 78 byte
[111] 88 byte
[112] 121 byte
[113] 154 byte
[114] 74 byte
[115] 92 byte
[116] 117 byte
[117] 150 byte
[118] 76 byte
[119] 90 byte
[120] 127 byte
[121] 156 byte
[122] 72 byte
[123] 94 byte
[124] 123 byte
[125] 152 byte
[126] 68 byte
[127] 82 byte
[128] 125 byte
[129] 158 byte
[130] 78 byte
[131] 88 byte
[132] 121 byte
[133] 154 byte
[134] 74 byte
[135] 92 byte
[136] 117 byte
[137] 150 byte
[138] 76 byte
[139] 90 byte
[140] 127 byte
[141] 156 byte
[142] 72 byte
[143] 94 byte
[144] 123 byte
[145] 152 byte
[146] 68 byte
[147] 82 byte
[148] 125 byte
[149] 158 byte
[150] 78 byte
[151] 88 byte
[152] 121 byte
[153] 154 byte
[154] 74 byte
[155] 92 byte
[156] 117 byte
[157] 150 byte
[158] 76 byte
[159] 90 byte
[160] 127 byte
[161] 156 byte
[162] 72 byte
[163] 94 byte
[164] 123 byte
[165] 152 byte
[166] 68 byte
[167] 82 byte
[168] 125 byte
[169] 158 byte
[170] 78 byte
[171] 88 byte
[172] 121 byte
[173] 154 byte
[174] 74 byte
[175] 92 byte
[176] 117 byte
[177] 150 byte
[178] 76 byte
[179] 90 byte
[180] 127 byte
[181] 156 byte
[182] 72 byte
[183] 94 byte
[184] 123 byte
[185] 152 byte
[186] 68 byte
[187] 82 byte
[188] 125 byte
[189] 158 byte
[190] 78 byte
[191] 88 byte
[192] 121 byte
[193] 154 byte
[194] 74 byte
[195] 92 byte
[196] 117 byte
[197] 150 byte
[198] 76 byte
[199] 90 byte
[200] 127 byte
[201] 156 byte
[202] 72 byte
[203] 94 byte
[204] 123 byte
[205] 152 byte
[206] 68 byte
[207] 82 byte
[208] 71 byte
代码分析掩码:
/// <summary>
///判断传入数据是否存在掩码
/// 传入数据:hi
/// socket接收到的二进制数据:
/// 1000000110000010 1101011011101001
/// 111110 111000 10111110 10000000
/// 掩码异或的操作:
/// 111110 111000 10111110 10000000
/// 进行异或^ 111110 111001 11010110 11101001
/// 结果: 1101000 1101001
/// 数据样例:
/// [0] 129 byte
/// [1] 130 byte
/// [2] 214 byte
/// [3] 233 byte
/// [4] 62 byte
/// [5] 56 byte
/// [6] 190 byte
/// [7] 128 byte
/// </summary>
/// <returns></returns>
private string UnWrap()
{
string result = string.Empty;
// 计算非空位置
int lastStation = GetLastZero();
// 利用掩码对org-data进行异或
int frame_masking_key = 1;
for (int i = 6; i <= lastStation; i++)
{
frame_masking_key = i % 4;
frame_masking_key = frame_masking_key == 0 ? 4 : frame_masking_key;
frame_masking_key = frame_masking_key == 1 ? 5 : frame_masking_key;
receivedDataBuffer[i] = Convert.ToByte(receivedDataBuffer[i] ^ receivedDataBuffer[frame_masking_key]);
}
System.Text.UTF8Encoding decoder = new System.Text.UTF8Encoding();
result = decoder.GetString(receivedDataBuffer, 6, lastStation - 6 + 1);
return result;
}
/// <summary>
/// 对传入数据进行无掩码转换
/// </summary>
/// <returns></returns>
public static byte[] Wrap(string msg, int maxBufferSize)
{
// 掩码开始位置
int masking_key_startIndex = 2;
byte[] msgByte = Encoding.UTF8.GetBytes(msg);
// 计算掩码开始位置
if (msgByte.Length <= 125)
{
masking_key_startIndex = 2;
}
else if (msgByte.Length > 65536)
{
masking_key_startIndex = 10;
}
else if (msgByte.Length > 125)
{
masking_key_startIndex = 4;
}
// 创建返回数据
byte[] result = new byte[msgByte.Length + masking_key_startIndex];
// 开始计算ws-frame
// frame-fin + frame-rsv1 + frame-rsv2 + frame-rsv3 + frame-opcode
result[0] = 0x81; // 129
// frame-masked+frame-payload-length
// 从第9个字节开始是 1111101=125,掩码是第3-第6个数据
// 从第9个字节开始是 1111110>=126,掩码是第5-第8个数据
if (msgByte.Length <= 125)
{
result[1] = Convert.ToByte(msgByte.Length);
}
else if (msgByte.Length > 65536)
{
result[1] = 0x7F; // 127
}
else if (msgByte.Length > 125)
{
result[1] = 0x7E; // 126
result[2] = Convert.ToByte(msgByte.Length >> 8);
result[3] = Convert.ToByte(msgByte.Length % 256);
}
// 将数据编码放到最后
Array.Copy(msgByte, 0, result, masking_key_startIndex, msgByte.Length);
return result;
}
WebSocket 协议:
public enum WebSocketProtocol { /* * * Request GET /WebIM5?uaid=200513807p8912-8de8c7e2-c963-4f67-8aca-8028797efbc1&re=0 HTTP/1.1 Upgrade: WebSocket Connection: Upgrade Host: 10.10.150.60:5002 Origin: https://localhost:444 Sec-WebSocket-Key1: 3+3 1 8kgV"m 0 8 64u43 Sec-WebSocket-Key2: 3_7891 6 4 `50 `8 * * Response HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Origin: https://localhost:444 Sec-WebSocket-Location: ws://192.168.110..... Sec-WebSocket-Protocol: WebIM5 * * asdfalskdfa * */ draft_00 = 0, /* * * Request GET /WebIM5?uaid=200513807p8912-2e695e5b-9b46-4511-b59e-28981b4ab327&re=0 HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 10.10.150.60:5002 Origin: https://localhost:444 Sec-WebSocket-Key: 1o4Jk9XPGvTX66OxmNMaww== Sec-WebSocket-Version: 13 * * Response HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: WebIM5 * */ draft_17 = 17 }
支持safari+chrome+firefox:
public enum WebSocketProtocol { /* * * Request GET /WebIM5?uaid=200513807p8912-8de8c7e2-c963-4f67-8aca-8028797efbc1&re=0 HTTP/1.1 Upgrade: WebSocket Connection: Upgrade Host: 10.10.150.60:5002 Origin: https://localhost:444 Sec-WebSocket-Key1: 3+3 1 8kgV"m 0 8 64u43 Sec-WebSocket-Key2: 3_7891 6 4 `50 `8 * * Response HTTP/1.1 101 WebSocket Protocol Handshake Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Origin: https://localhost:444 Sec-WebSocket-Location: ws://192.168.110..... Sec-WebSocket-Protocol: WebIM5 * * asdfalskdfa * */ draft_00 = 0, /* * * Request GET /WebIM5?uaid=200513807p8912-2e695e5b-9b46-4511-b59e-28981b4ab327&re=0 HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 10.10.150.60:5002 Origin: https://localhost:444 Sec-WebSocket-Key: 1o4Jk9XPGvTX66OxmNMaww== Sec-WebSocket-Version: 13 * * Response HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: WebIM5 * */ draft_17 = 17 }
其中,safari和chrome都是 :_ws = new WebSocket("ws://ip:port"); 但是firefox是:_ws = new window.MozWebSocket("ws://ip:port");
浏览器方面:
chrome可以查看WebSocket的访问日志: chrome://net-internals/