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 数据传输
该节主要参考了 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位,任意的应用数据,放在扩展数据之后,应用数据的长度=负载数据的长度-扩展数据的长度。
把消息分片处理主要是处于以下两个原因:
- 消息接收方事先并不知道消息大小, 而且也没必要预留一个足够大的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 被预留。
详细解析如下,来自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 处理
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个数据),后面跟客户端发送的内容数据,处理接收到的数据我们需要用取到的掩码依次轮流跟内容数据做异或(^)运算,第一个内容数据与第一个掩码异或,第二个内容数据与第二个掩码异或……第五个内容数据与第一个掩码异或……以此类推,一直到结束,然后对内容进行编码。
根据数据长度的不同,掩码的位置也不同:
从第9个字节开始是 1111101=125,掩码是第3-第6个数据
从第9个字节开始是 1111110=126,掩码是第5-第8个数据
从第9个字节开始是 1111111=126,掩码是第11-第14个数据
举例一:
1 hi
2 1000000110000010 1101011011101001
3 111110 111000 10111110 10000000
4 111110 111001 11010110 11101001
5 1101000 1101001
6
7 [0] 129 byte
8 [1] 130 byte
9 [2] 214 byte
10 [3] 233 byte
11 [4] 62 byte
12 [5] 56 byte
13 [6] 190 byte
14 [7] 128 byte
15
16
17 1234567890
18 [0] 129 byte
19 [1] 138 byte
20
21 [2] 108 byte
22 [3] 255 byte
23 [4] 86 byte
24 [5] 166 byte
25
26 [6] 93 byte
27 [7] 205 byte
28 [8] 101 byte
29 [9] 146 byte
30 [10] 89 byte
31 [11] 201 byte
32 [12] 97 byte
33 [13] 158 byte
34 [14] 85 byte
35 [15] 207 byte
举例二:
1 01234567890123456789012345678901234567890123456789
2 01234567890123456789012345678901234567890123456789
3 01234567890123456789012345678901234567890123456789
4 01234567890123456789012345678901234567890123456789
5
6 01234567890123456789012345678901
7 10000001111111100110010011001101
8
9 [0] 129 byte
10 [1] 254 byte
11 [2] 0 byte
12 [3] 201 byte
13
14 [4] 77 byte
15 [5] 175 byte
16 [6] 124 byte
17 [7] 107 byte
18
19 [8] 125 byte
20 [9] 158 byte
21 [10] 78 byte
22 [11] 88 byte
23 [12] 121 byte
24 [13] 154 byte
25 [14] 74 byte
26 [15] 92 byte
27 [16] 117 byte
28 [17] 150 byte
29 [18] 76 byte
30 [19] 90 byte
31 [20] 127 byte
32 [21] 156 byte
33 [22] 72 byte
34 [23] 94 byte
35 [24] 123 byte
36 [25] 152 byte
37 [26] 68 byte
38 [27] 82 byte
39 [28] 125 byte
40 [29] 158 byte
41 [30] 78 byte
42 [31] 88 byte
43 [32] 121 byte
44 [33] 154 byte
45 [34] 74 byte
46 [35] 92 byte
47 [36] 117 byte
48 [37] 150 byte
49 [38] 76 byte
50 [39] 90 byte
51 [40] 127 byte
52 [41] 156 byte
53 [42] 72 byte
54 [43] 94 byte
55 [44] 123 byte
56 [45] 152 byte
57 [46] 68 byte
58 [47] 82 byte
59 [48] 125 byte
60 [49] 158 byte
61 [50] 78 byte
62 [51] 88 byte
63 [52] 121 byte
64 [53] 154 byte
65 [54] 74 byte
66 [55] 92 byte
67 [56] 117 byte
68 [57] 150 byte
69 [58] 76 byte
70 [59] 90 byte
71 [60] 127 byte
72 [61] 156 byte
73 [62] 72 byte
74 [63] 94 byte
75 [64] 123 byte
76 [65] 152 byte
77 [66] 68 byte
78 [67] 82 byte
79 [68] 125 byte
80 [69] 158 byte
81 [70] 78 byte
82 [71] 88 byte
83 [72] 121 byte
84 [73] 154 byte
85 [74] 74 byte
86 [75] 92 byte
87 [76] 117 byte
88 [77] 150 byte
89 [78] 76 byte
90 [79] 90 byte
91 [80] 127 byte
92 [81] 156 byte
93 [82] 72 byte
94 [83] 94 byte
95 [84] 123 byte
96 [85] 152 byte
97 [86] 68 byte
98 [87] 82 byte
99 [88] 125 byte
100 [89] 158 byte
101 [90] 78 byte
102 [91] 88 byte
103 [92] 121 byte
104 [93] 154 byte
105 [94] 74 byte
106 [95] 92 byte
107 [96] 117 byte
108 [97] 150 byte
109 [98] 76 byte
110 [99] 90 byte
111 [100] 127 byte
112 [101] 156 byte
113 [102] 72 byte
114 [103] 94 byte
115 [104] 123 byte
116 [105] 152 byte
117 [106] 68 byte
118 [107] 82 byte
119 [108] 125 byte
120 [109] 158 byte
121 [110] 78 byte
122 [111] 88 byte
123 [112] 121 byte
124 [113] 154 byte
125 [114] 74 byte
126 [115] 92 byte
127 [116] 117 byte
128 [117] 150 byte
129 [118] 76 byte
130 [119] 90 byte
131 [120] 127 byte
132 [121] 156 byte
133 [122] 72 byte
134 [123] 94 byte
135 [124] 123 byte
136 [125] 152 byte
137 [126] 68 byte
138 [127] 82 byte
139 [128] 125 byte
140 [129] 158 byte
141 [130] 78 byte
142 [131] 88 byte
143 [132] 121 byte
144 [133] 154 byte
145 [134] 74 byte
146 [135] 92 byte
147 [136] 117 byte
148 [137] 150 byte
149 [138] 76 byte
150 [139] 90 byte
151 [140] 127 byte
152 [141] 156 byte
153 [142] 72 byte
154 [143] 94 byte
155 [144] 123 byte
156 [145] 152 byte
157 [146] 68 byte
158 [147] 82 byte
159 [148] 125 byte
160 [149] 158 byte
161 [150] 78 byte
162 [151] 88 byte
163 [152] 121 byte
164 [153] 154 byte
165 [154] 74 byte
166 [155] 92 byte
167 [156] 117 byte
168 [157] 150 byte
169 [158] 76 byte
170 [159] 90 byte
171 [160] 127 byte
172 [161] 156 byte
173 [162] 72 byte
174 [163] 94 byte
175 [164] 123 byte
176 [165] 152 byte
177 [166] 68 byte
178 [167] 82 byte
179 [168] 125 byte
180 [169] 158 byte
181 [170] 78 byte
182 [171] 88 byte
183 [172] 121 byte
184 [173] 154 byte
185 [174] 74 byte
186 [175] 92 byte
187 [176] 117 byte
188 [177] 150 byte
189 [178] 76 byte
190 [179] 90 byte
191 [180] 127 byte
192 [181] 156 byte
193 [182] 72 byte
194 [183] 94 byte
195 [184] 123 byte
196 [185] 152 byte
197 [186] 68 byte
198 [187] 82 byte
199 [188] 125 byte
200 [189] 158 byte
201 [190] 78 byte
202 [191] 88 byte
203 [192] 121 byte
204 [193] 154 byte
205 [194] 74 byte
206 [195] 92 byte
207 [196] 117 byte
208 [197] 150 byte
209 [198] 76 byte
210 [199] 90 byte
211 [200] 127 byte
212 [201] 156 byte
213 [202] 72 byte
214 [203] 94 byte
215 [204] 123 byte
216 [205] 152 byte
217 [206] 68 byte
218 [207] 82 byte
219 [208] 71 byte
代码分析掩码:
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 }
1 /// <summary>
2 /// 对传入数据进行无掩码转换
3 /// </summary>
4 /// <returns></returns>
5 public static byte[] Wrap(string msg, int maxBufferSize)
6 {
7 // 掩码开始位置
8 int masking_key_startIndex = 2;
9
10 byte[] msgByte = Encoding.UTF8.GetBytes(msg);
11
12 // 计算掩码开始位置
13 if (msgByte.Length <= 125)
14 {
15 masking_key_startIndex = 2;
16 }
17 else if (msgByte.Length > 65536)
18 {
19 masking_key_startIndex = 10;
20 }
21 else if (msgByte.Length > 125)
22 {
23 masking_key_startIndex = 4;
24 }
25
26 // 创建返回数据
27 byte[] result = new byte[msgByte.Length + masking_key_startIndex];
28
29 // 开始计算ws-frame
30 // frame-fin + frame-rsv1 + frame-rsv2 + frame-rsv3 + frame-opcode
31 result[0] = 0x81; // 129
32
33 // frame-masked+frame-payload-length
34 // 从第9个字节开始是 1111101=125,掩码是第3-第6个数据
35 // 从第9个字节开始是 1111110>=126,掩码是第5-第8个数据
36 if (msgByte.Length <= 125)
37 {
38 result[1] = Convert.ToByte(msgByte.Length);
39 }
40 else if (msgByte.Length > 65536)
41 {
42 result[1] = 0x7F; // 127
43 }
44 else if (msgByte.Length > 125)
45 {
46 result[1] = 0x7E; // 126
47 result[2] = Convert.ToByte(msgByte.Length >> 8);
48 result[3] = Convert.ToByte(msgByte.Length % 256);
49 }
50
51 // 将数据编码放到最后
52 Array.Copy(msgByte, 0, result, masking_key_startIndex, msgByte.Length);
53
54 return result;
55 }
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:
1 // 开始连接到服务器
2 var pollingInterval;
3 _ws = new WebSocket("ws://192.168.0.103:5002/WebIM5?uaid=200513807p8912-5a78ae8a-cabb-46ee-8d8a-85874bbc942c&re=0");
4 //_ws = new window.MozWebSocket("ws://192.168.0.103:5002/WebIM5?uaid=200513807p8912-5a78ae8a-cabb-46ee-8d8a-85874bbc942c&re=0");
5 _ws.onopen = function () {
6 alert("onopen");
7
8 _socketCreated = true;
9 var args
10 _ws.send("1234567890");
11 _ws.send("33322233");
12 };
13 _ws.onmessage = function (event) {
14 console.log("event.data=" + event.data);
15
16 };
17 _ws.onclose = function () {
18 alert("onclose");
19 console.log("onclose");
20 };
21 _ws.onerror = function () {
22 console.log("onerror");
23 };
24 function send() {
25 _ws.send(document.getElementById("msg").value);
26 }
其中,safari和chrome都是 :_ws = new WebSocket("ws://ip:port"); 但是firefox是:_ws = new window.MozWebSocket("ws://ip:port");
浏览器方面:
chrome可以查看WebSocket的访问日志: chrome://net-internals/