C#封装的websocket协议类
关于VB版之前已经写了,有需要可以进传送门《VB封装的WebSocket模块,拿来即用》,两个使用都差不多,这里简单概述一下:
连接完成后,没有握手就用Handshake()先完成握手
之后接收数据,先用AnalyzeHeader()得到数据帧结构(DataFrame)
然后再用PickDataV()或PickData()得到源数据,对于掩码数据是在这里反掩码
关于发送数据,则是:
服务端发送无需掩码用PackData()将数据组装一下就可以发送
而模拟客户端向服务器的发送需要加掩码,用PackMaskData()
相关资料下载:《WebSocket协议中文版.pdf》
*等有时间我再做个demo供下载吧,这个类使用还算简单
WebSocketProtocol10.cs
1 using System; 2 using System.Collections; 3 using System.Collections.Generic; 4 using System.Collections.Specialized; 5 using System.Globalization; 6 using System.Runtime.InteropServices; 7 using System.Security.Cryptography; 8 using System.Text; 9 /* 10 * 详见<5.2 基本帧协议>和<11.8.WebSocket 操作码注册> 11 0 1 2 3 12 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 13 +-+-+-+-+-------+-+-------------+-------------------------------+ 14 |F|R|R|R| opcode|M| Payload len | Extended payload length | 15 |I|S|S|S| (4) |A| (7) | (16/64) | 16 |N|V|V|V| |S| | (if payload len==126/127) | 17 | |1|2|3| |K| | | 18 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + 19 | Extended payload length continued, if payload len == 127 | 20 + - - - - - - - - - - - - - - - +-------------------------------+ 21 | |Masking-key, if MASK set to 1 | 22 +-------------------------------+-------------------------------+ 23 | Masking-key (continued) | Payload Data | 24 +-------------------------------- - - - - - - - - - - - - - - - + 25 : Payload Data continued ... : 26 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + 27 | Payload Data continued ... | 28 +---------------------------------------------------------------+ 29 */ 30 //2017-06-17 31 //By: 悠悠然 32 //QQ: 2860898817 33 //E-mail: ur1986@foxmail.com 34 namespace WebSocketProtocol10 35 { 36 /// <summary> 37 /// Opcode操作码是一个在 0 到 15(包括)之间的整数数字。 38 /// </summary> 39 public enum OpcodeType:byte 40 { 41 Contin = 0x0, //表示连续消息片断 42 Text = 0x1, //表示文本消息片断 43 Binary = 0x2, //表未二进制消息片断 44 // 0x3 - 0x7 非控制帧保留 45 Close = 0x8, //表示连接关闭 46 Ping = 0x9, //表示心跳检查的ping 47 Pong = 0xA, //表示心跳检查的pong 48 // 0xB - 0xF 控制帧保留 49 Unkown 50 }; 51 /// <summary> 52 /// 数据帧头,就是包头结构 53 /// </summary> 54 public struct DataFrame 55 { 56 /// <summary>0表示不是当前消息的最后一帧,后面还有消息,1表示这是当前消息的最后一帧;</summary> 57 public bool FIN; 58 /// <summary>1位,若没有自定义协议,必须为0,否则必须断开.</summary> 59 public bool RSV1; 60 /// <summary>1位,若没有自定义协议,必须为0,否则必须断开.</summary> 61 public bool RSV2; 62 /// <summary>1位,若没有自定义协议,必须为0,否则必须断开.</summary> 63 public bool RSV3; 64 /// <summary>4位操作码,定义有效负载数据,如果收到了一个未知的操作码,连接必须断开.</summary> 65 public OpcodeType Opcode; 66 /// <summary>1位,定义传输的数据是否有加掩码,如果有掩码则存放在MaskingKey</summary> 67 public bool MASK; 68 /// <summary>0或4个字节,客户端发送给服务端的数据,都是通过内嵌的一个32位值作为掩码的;掩码键只有在掩码位设置为1的时候存在。</summary> 69 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)] 70 public byte[] MaskingKey; 71 /// <summary>传输数据的长度</summary> 72 public int Payloadlen; 73 /// <summary>数据起始位</summary> 74 public int DataOffset; 75 } 76 77 public class WSProtocol 78 { 79 #region 握手 80 /// <summary> 81 /// 获取连接请求附带的参数 82 /// </summary> 83 public static NameValueCollection QueryString(byte[] recv) 84 { 85 //前端js如: ws = new WebSocket("ws://127.0.0.1:8899/ws?id=1&session=a1b2c3") 86 //该函数相当于ASP.NET中的Request.QueryString,就是取得参数 id 和 session 的 87 NameValueCollection NV = new NameValueCollection(); 88 string n = string.Empty; 89 string v = string.Empty; 90 bool tf1 = false; 91 bool tf2 = false; 92 foreach (byte b in recv) 93 { 94 if (tf1) 95 { 96 if (b == 32) 97 break; 98 else if (b == 61 && tf2 == false)//= 99 tf2 = true; 100 else if (b == 38)//& 101 { 102 tf2 = false; 103 if (!string.IsNullOrEmpty(n) && !string.IsNullOrEmpty(v)) 104 NV.Add(n, v); 105 n = string.Empty; 106 v = string.Empty; 107 } 108 else 109 { 110 if (tf2) 111 v += (char)b; 112 else 113 n += (char)b; 114 } 115 } 116 else if (b == 63)//? 117 tf1 = true; 118 else if (b == 10 || b == 13) break; 119 } 120 if (!string.IsNullOrEmpty(n) && !string.IsNullOrEmpty(v)) 121 NV.Add(n, v); 122 return NV; 123 } 124 public static byte[] Handshake(string request) 125 { 126 string webSocketKey = getCilentWSKey(request, "Sec-WebSocket-Key:"); 127 string acceptKey = produceAcceptKey(webSocketKey); 128 StringBuilder response = new StringBuilder(); //响应串 129 response.Append("HTTP/1.1 101 Web Socket Protocol Handshake\r\n"); 130 response.Append("Upgrade: WebSocket\r\n"); 131 response.Append("Connection: Upgrade\r\n"); 132 response.AppendFormat("Sec-WebSocket-Accept: {0}\r\n", acceptKey); 133 response.AppendFormat("WebSocket-Origin: {0}\r\n", getCilentWSKey(request, "Sec-WebSocket-Origin")); 134 response.AppendFormat("WebSocket-Location: {0}\r\n", getCilentWSKey(request, "Host")); 135 response.Append("\r\n"); 136 return Encoding.UTF8.GetBytes(response.ToString()); 137 } 138 private static string getCilentWSKey(string request, string kname) 139 { 140 int i = CultureInfo.InvariantCulture.CompareInfo.IndexOf(request, kname, CompareOptions.IgnoreCase); 141 if (i > 0) 142 { 143 i += kname.Length; 144 int j = request.IndexOf("\r\n", i); 145 if (j > 0) 146 return request.Substring(i, j - i).Trim(); 147 } 148 return string.Empty; 149 } 150 // 根据Sec-WebSocket-Key和MagicKey生成AcceptKey 151 private static string produceAcceptKey(string webSocketKey) 152 { 153 string MagicKey = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 154 Byte[] acceptKey = SHA1.Create().ComputeHash(Encoding.ASCII.GetBytes(webSocketKey + MagicKey)); 155 return Convert.ToBase64String(acceptKey); 156 } 157 #endregion 158 #region 数据帧解析 159 /// <summary> 160 /// 数据帧头的解析 161 /// </summary> 162 public static DataFrame AnalyzeHeader(byte[] data) 163 { 164 DataFrame df; 165 df.FIN = (data[0] & 0x80) == 0x80 ? true : false; 166 df.RSV1 = (data[0] & 0x40) == 0x40 ? true : false; 167 df.RSV2 = (data[0] & 0x40) == 0x20 ? true : false; 168 df.RSV3 = (data[0] & 0x40) == 0x10 ? true : false; 169 byte[] b = { data[0] }; 170 BitArray bit = new BitArray(b); 171 bit.Set(4, false); 172 bit.Set(5, false); 173 bit.Set(6, false); 174 bit.Set(7, false); 175 bit.CopyTo(b, 0); 176 df.Opcode = (OpcodeType)b[0]; 177 178 df.MASK = (data[1] & 0x80) == 0x80 ? true : false; 179 df.MaskingKey = new Byte[4]; 180 int len = data[1] & 0x7F; 181 /// 0-125 表示传输数据的长度; 182 /// 126 表示随后的两个字节是一个16进制无符号数,用来表示传输数据的长度; 183 /// 127 表示随后的是8个字节是一个64位无符合数,这个数用来表示传输数据的长度。 184 /// 多字节长度的数量是以网络字节的顺序表示。负载数据的长度为扩展数据及应用数据之和,扩展数据的长度可能为0,因而此时负载数据的长度就为应用数据的长度。 185 switch (len) 186 { 187 case 126: 188 df.Payloadlen = (UInt16)(data[2] << 8 | data[3]); 189 if(df.MASK) 190 { 191 Buffer.BlockCopy(data, 4, df.MaskingKey, 0, 4); 192 df.DataOffset = 8; 193 }else 194 df.DataOffset = 4; 195 break; 196 case 127: 197 Byte[] byteLen = new Byte[8]; 198 Buffer.BlockCopy(data, 4, byteLen, 0, 8); 199 df.Payloadlen = (int)BitConverter.ToUInt64(byteLen, 0); 200 if (df.MASK) 201 { 202 Buffer.BlockCopy(data, 10, df.MaskingKey, 0, 4); 203 df.DataOffset = 14; 204 } 205 else 206 df.DataOffset = 10; 207 break; 208 default: 209 if (len < 126) 210 { 211 df.Payloadlen = len; 212 if (df.MASK) 213 { 214 Buffer.BlockCopy(data, 2, df.MaskingKey, 0, 4); 215 df.DataOffset = 6; 216 } 217 else 218 df.DataOffset = 2; 219 } 220 else 221 { 222 df.Payloadlen = 0; 223 df.DataOffset = 0; 224 } 225 break; 226 } 227 return df; 228 } 229 #endregion 230 #region 处理数据--接收 231 /* 232 * PickDataV 方法是出于性能的考虑,用于有时数据只是为了接收,做一些逻辑判断,并不需要对数据块进行单独提炼 233 * PickData 有两个重载就不赘述了... 234 */ 235 /// <summary> 236 /// 如果数据存在掩码就反掩码,具体的使用数据就 237 /// </summary> 238 public static void PickDataV(byte[] data, DataFrame dtype) 239 { 240 if (dtype.MASK) 241 { 242 int j = 0; 243 for (int i = dtype.DataOffset; i < dtype.DataOffset + dtype.Payloadlen; i++) 244 { 245 data[i] ^= dtype.MaskingKey[j++]; 246 if (j == 4) 247 j = 0; 248 } 249 } 250 } 251 public static byte[] PickData(byte[] data, DataFrame dtype) 252 { 253 byte[] byt = new byte[dtype.Payloadlen]; 254 PickDataV(data, dtype); 255 Buffer.BlockCopy(data, dtype.DataOffset, byt, 0, dtpye.Payloadlen); 256 return byt; 257 } 258 public static string PickData(byte[] data,DataFrame dtype,Encoding encode) 259 { 260 PickDataV(data, dtype); 261 return encode.GetString(data, dtype.DataOffset, dtype.Payloadlen); 262 } 263 #endregion 264 #region 处理数据--发送 265 /* 266 * PackData 两个重载,用于组装无掩码数据 267 * PackMaskData 两个重载,用于将数据掩码后组装 268 */ 269 /// <summary> 270 /// 组装无掩码数据,一般用于服务端向客户端发送 271 /// </summary> 272 public static byte[] PackData(string data, Encoding encode, OpcodeType dwOpcode = OpcodeType.Text) 273 { 274 //字符默认用UTF8编码处理,如果有别的编码需求,自行处理后用下面的重载函数 275 byte[] buff = encode.GetBytes(data); 276 return PackData(buff, dwOpcode); 277 } 278 public static byte[] PackData(byte[] buff, OpcodeType dwOpcode = OpcodeType.Text) 279 { 280 List<byte> byt = new List<byte>(); 281 byt.Add((byte)(0x80 | (byte)dwOpcode)); 282 if (buff.Length < 126) 283 byt.Add((byte)buff.Length); 284 else if (buff.Length <= ushort.MaxValue) 285 { 286 ushort l = (ushort)buff.Length; 287 byte[] bl = BitConverter.GetBytes(l); 288 byt.Add(0x7e); 289 byt.Add(bl[1]); 290 byt.Add(bl[0]); 291 } 292 else 293 { 294 //由于用不到,未做测试 295 ulong l = (ulong)buff.Length; 296 byt.Add(0x7f); 297 byt.AddRange(BitConverter.GetBytes(l)); 298 } 299 byt.AddRange(buff); 300 return byt.ToArray(); 301 } 302 303 /// <summary> 304 /// 将数据掩码后组装,一般是客户端向服务端发送 305 /// </summary> 306 public static byte[] PackMaskData(string str, Encoding encode, OpcodeType dwOpcode = OpcodeType.Text) 307 { 308 byte[] byt = encode.GetBytes(str); 309 return PackMaskData(byt, dwOpcode); 310 } 311 public static byte[] PackMaskData(byte[] byt, OpcodeType dwOpcode = OpcodeType.Text) 312 { 313 List<byte> data = new List<byte>(); 314 //掩码我用的是固定值,有需要也可以自己做成随机的 315 byte[] maskey ={ 13, 113, 213, 177 }; 316 int j = 0; 317 for (int i = 0; i < byt.Length; i++) 318 { 319 data[i] ^= maskey[j++]; 320 if (j > 3) j = 0; 321 } 322 data.Add((byte)(0x80 | (byte)OpcodeType.Text));//第一字节,FIN+RSV1+RSV2+RSV3+opcode 323 if (byt.Length < 126) 324 { 325 data.Add((Byte)(0x80 | (Byte)byt.Length)); 326 } 327 else if (byt.Length <= ushort.MaxValue)//65535 328 { 329 data.Add(254);//固定 掩码位+126 330 byte[] b=BitConverter.GetBytes((ushort)byt.Length); 331 data.Add(b[1]);//反转 332 data.Add(b[0]); 333 } 334 else 335 { 336 //这部分没有经过实际测试,依靠协议文档推写出来的 337 //我的需求只是聊天通信,若有传送文件等需求,请自行测试 338 data.Add(255);//固定 掩码位+127 339 byte[] b = BitConverter.GetBytes((ulong)byt.Length); 340 Array.Reverse(b);//反转 341 data.AddRange(b); 342 } 343 data.AddRange(byt); 344 data.AddRange(maskey); 345 return data.ToArray(); 346 } 347 #endregion 348 #region 常用控制帧 349 /* 350 下面的 Ping、Pong、Close 是非掩码信号,用于服务端向客户端发送,如果客户端想服务端发送就需要掩码 351 使用的时候直接 socket.Send(WSProtocol.PingFrame, 0, WSProtocol.PingFrame.Length); 352 我用的0长度,其实是可以包含数据的,但是附带数据客户端处理又麻烦了 353 354 * 如果有附带信息的需求,也可以用上面[发送]里的函数,可选参数指定OpcodeType 355 * 特别注意:收到ping的时候,回应pong.而收到pong的时候,回应还是pong 356 * 在协议里,ping是最为要求应答信号的,而pong是作为单向心跳检测的 357 */ 358 private static byte[] dwPing = { 0x89, 0x0 }; 359 private static byte[] dwPong = { 0x8a, 0x0 }; 360 private static byte[] dwClose = { 0x88, 0x0 }; 361 public static byte[] PingFrame { get { return dwPing; } } 362 public static byte[] PongFrame { get { return dwPong; } } 363 public static byte[] CloseFrame { get { return dwClose; } } 364 #endregion 365 } 366 }
分享是追求进步的态度