转自http://www.2cto.com/kf/201210/161006.html
WebSocket握手协议
1、客户端握手请求(注意:键值之间有一个空格,行间有换行符号0x13x10或者说\r\n) GET /WebSocket/LiveVideo HTTP/1.1 Upgrade: WebSocket Connection: Upgrade Host: localhost:8080 (客户端请求主机) Origin: http://127.0.0.1 (来源网页地址) Sec-WebSocket-Key1: 23 asdfJKj,asdjk Sec_WebSocket-Key2: wewerw234 jij998 0x13x10 + 8个字节Sec_WebSocket-Key3值,没有键名(注意,这里的0x13x10的额外的,也就说有两个连续的0x13x10)
2、服务端握手回复 HTTP/1.1 101 Web Socket Protocol Handshake Upgrade: WebSocket Connection: Upgrade Sec-WebSocket-Origin: http://127.0.0.1 (来源网页地址) Sec-WebSocket-Location: ws://localhost:8080/WebSocket/LiveVideo 16个字节的加密KEY
加密KEY算法:
Sec_WebSocket-Key1的产生方式: (1)提取客户端请求的Sec_WebSocket-Key1中的数字符组成字符串k1 (2)转换字符串为8个字节的长整型intKey1 (3)统计客户端请求的Sec_WebSocket-Key1中的空格数k1Spaces (4)intK1/k1Spaces取整k1FinalNum (5)将k1FinalNum转换成字节数组再反转最终形成4个字节的Sec_WebSocket-Key1
Sec_WebSocket-Key2的产生方式: (1)提取客户端请求的Sec_WebSocket-Key2中的数字符组成字符串k2 (2)转换字符串为8个字节的长整型intKey2 (3)统计客户端请求的Sec_WebSocket-Key2中的空格数k2Spaces (4)intK2/k2Spaces取整k2FinalNum (5)将k2FinalNum转换成字节数组再反转最终形成4个字节的Sec_WebSocket-Key2
Sec_WebSocket-Key3的产生方式: 客户端握手请求的最后8个字节
将Sec_WebSocket-Key1、Sec_WebSocket-Key2、Sec_WebSocket-Key3合并成一个16字节数组 再进行MD5加密形成最终的16个字节的加密KEY
3、消息发送接收 客户端和服务端发送非握手文本消息时,消息以utf-8编码,并以0x00开头,0xFF结尾。
WebScoket 规范 4.1 握手协议 websocket 是 独立的基于TCP的协议, 其跟http协议的关系仅仅是 WebSocket 的握手被http 服务器当做 Upgrade request http包处理。 websocket 有自己的握手处理。 TCP连接建立后,client 发送websocket 握手请求. 请求包需求如下: 必须是有效的http request 格式 HTTP request method 必须是GET,协议应不小于1.1 如: Get /chat HTTP/1.1 必须包括Upgrade 头域,并且其值为“websocket” 必须包括"Connection" 头域,并且其值为 "Upgrade" 必须包括"Sec-WebSocket-Key"头域,其值采用base64编码的随机16字节长的字符序列, 服务器端根据该域来判断client 确实是websocket请求而不是冒充的,如http。响应方式是,首先要获取到请求头中的Sec-WebSocket-Key的值,再把这一段GUID "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"加到获取到的Sec-WebSocket-Key的值的后面,然后拿这个字符串做SHA-1 hash计算,然后再把得到的结果通过base64加密,就得到了返回给客户端的Sec-WebSocket-Accept的http响应头的值。 如果请求来自浏览器客户端,还必须包括Origin头域 。 该头域用于防止未授权的跨域脚本攻击,服务器可以从Origin决定是否接受该WebSocket连接。 必须包括"Sec-webSocket-Version" 头域,当前值必须是13. 可能包括"Sec-WebSocket-Protocol",表示client(应用程序)支持的协议列表,server选择一个或者没有可接受的协议响应之。 可能包括"Sec-WebSocket-Extensions", 协议扩展, 某类协议可能支持多个扩展,通过它可以实现协议增强 可能包括任意其他域,如cookie 示例如下: 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 接手到握手请求后应处理该请求包括: 处理请求包括处理GET 方法 验证Upgrader头域 验证Connection 头域 处理Sec-WebSocket-Key头域,方法见上; 处理Sec-WebSocket-Version 处理Origin头域,可选, 浏览器必须发送该头域 处理Sec-WebSocket-Protocol头域,可选 处理Sec-WebSocket-Extensions 头域,可选 处理其他头域,可选 Server 发送握手响应,这里只介绍服务器接受该连接情况下,包括: http Status-Line Upgrade 头域 ,值必须是"websocket" Conntion头域,值必须是:“Upgrade” Sec-WebSocket-Accept” 头域,该头域的值即处理Sec-WebSocket-Key" 域后的结果。 可选的"Sec-WebSocket-Protocol"头域 可选的"Sec-WebSocket-Extensions"头域 响应可能如下: HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat 4.2 数据传输 在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位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。 把消息分片处理主要是处于以下两个原因: 消息接收方事先并不知道消息大小, 而且也没必要预留一个足够大的buffer来处理; multiplexing 消息分片一些规则如下(不全): 为分片消息(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 被预留。 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 处理
Close 帧的opcode是0×8. 接收到 Close 帧后,如果之前没发送过Close帧,则其必须发送Close 帧响应,但其可以延迟发送Close响应帧,例如在其发送完数据之后发送;但是,协议不保证对方在发送Close 帧后仍会处理其后续的数据。Close帧可能Client发起也可能是Server发起。 Ping-Pong 帧
接收到Ping帧后将响应Pong帧, 主要用于检测网络连接情况。 Extensions
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个数据),后面跟客户端发送的内容数据,处理接收到的数据我们需要用取到的掩码依次轮流跟内容数据做异或(^)运算,第一个内容数据与第一个掩码异或,第二个内容数据与第二个掩码异或……第五个内容数据与第一个掩码异或……以此类推,一直到结束,然后对内容进行编码。 举例: 1 /// <summary> 2 ///判断传入数据是否存在掩码 3 /// 传入数据:hi 4 /// socket接收到的二进制数据: 5 /// 1000000110000010 1101011011101001 6 /// 111110 111000 10111110 10000000 7 /// 掩码异或的操作: 8 /// 111110 111000 10111110 10000000 9 /// 进行异或^ 111110 111001 11010110 11101001 10 /// 结果: 1101000 1101001 11 /// 数据样例: 12 /// [0] 129 byte 13 /// [1] 130 byte 14 /// [2] 214 byte 15 /// [3] 233 byte 16 /// [4] 62 byte 17 /// [5] 56 byte 18 /// [6] 190 byte 19 /// [7] 128 byte 20 /// </summary> 21 /// <returns></returns> 22 private string UnWrap() 23 { 24 string result = string.Empty; 25 26 // 计算非空位置 27 int lastStation = GetLastZero(); 28 29 // 利用掩码对org-data进行异或 30 int frame_masking_key = 1; 31 for (int i = 6; i <= lastStation; i++) 32 { 33 frame_masking_key = i % 4; 34 frame_masking_key = frame_masking_key == 0 ? 4 : frame_masking_key; 35 frame_masking_key = frame_masking_key == 1 ? 5 : frame_masking_key; 36 receivedDataBuffer[i] = Convert.ToByte(receivedDataBuffer[i] ^ receivedDataBuffer[frame_masking_key]); 37 } 38 39 System.Text.UTF8Encoding decoder = new System.Text.UTF8Encoding(); 40 result = decoder.GetString(receivedDataBuffer, 6, lastStation - 6 + 1); 41 42 return result; 43 44 } 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 }