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");