本系列服务端双工通信包括两种实现方式:一、使用Socket构建;二、使用WCF构建。本文为使用Socket构建服务端的双工通信,客户端同样使用Html5的WebSocket技术进行调用。
一、网页客户端:

1 <!DOCTYPE html> 2 <html xmlns="http://www.w3.org/1999/xhtml"> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 5 <title></title> 6 <script src="scripts/jquery-1.10.2.min.js"></script> 7 <script> 8 var socket; 9 //url必须使用ws或者wss(加密)作为头,这个url设定好后,在javascript脚本中可以通过访问websocket对象的url来重新获取 10 //通信建立连接后,就可以双向通信了,使用websocket对象的send方法加json数据便可将任何形式数据传往服务器 11 12 //通过onmessage事件来接收服务器传送过来数据: 13 //通过onopern事件监听socket打开事件 14 //通过onclose监听socket关闭事件 15 //通过webSocket.close()方法关闭socket连接; 16 //通过readyState属性获取websocket对象状态: 17 //CONNECTION 0 正在连接 18 //OPEN 1 已经连接 19 //CLOSING 2 正在关闭 20 //CLOSE 3 已经关闭 21 $(function () { 22 $('#conn').click(function () { 23 //ws = new WebSocket('ws://' + window.location.hostname + ':' + '4649/Echo/'); 24 socket = new WebSocket('ws://localhost:8021/'); 25 $('#tips').text('正在连接'); 26 27 socket.addEventListener("open", function (e) { 28 $('#tips').html( 29 '<div>Connected. Waiting for messages...</div>'); 30 //window.setInterval(function () { 31 // socket.send("the time is " + new Date()); 32 //}, 1000); 33 }, false) 34 35 socket.addEventListener("message", function (evt) { 36 $('#tips').text(evt.data); 37 }); 38 39 socket.onerror = function (evt) { 40 $('#tips').text(JSON.stringify(evt)); 41 } 42 socket.onclose = function () { 43 $('#tips').text('已经关闭'); 44 } 45 }); 46 47 $('#close').click(function () { 48 socket.close(); 49 }); 50 51 $('#send').click(function () { 52 if (socket.readyState == WebSocket.OPEN) { 53 socket.send($('#content').val()); 54 } 55 else { 56 $('#tips').text('连接已经关闭'); 57 } 58 }); 59 }); 60 </script> 61 </head> 62 <body> 63 <form id="form1"> 64 <div> 65 <input id="conn" type="button" value="连接" /> 66 <input id="close" type="button" value="关闭" /> 67 <span id="tips"></span> 68 <input id="content" type="text" /> 69 <input id="send" type="button" value="发送" /> 70 </div> 71 </form> 72 </body> 73 </html>
二、服务端:
创建控制台应用程序,代码如下:

class Program { //客户端集合 static List<Socket> clients = new List<Socket>(); static byte[] buffer = new byte[1024]; //static bool IsWebSocketClient = false; //客户端连接是否为websocket static void Main(string[] args) { //创建一个新的Socket,这里我们使用最常用的基于TCP的Stream Socket(流式套接字) Socket SeverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //将该socket绑定到主机上面的某个端口 SeverSocket.Bind(new IPEndPoint(IPAddress.Any, 8021)); //设置Socket为监听状态并设置允许的最大队列数为4 SeverSocket.Listen(4); //服务端开始异步接受客户端的连接请求 SeverSocket.BeginAccept(AsyncAcceptCallback, SeverSocket); Console.WriteLine("Sever is ready"); //创建一个时钟,每隔1分钟发送一个心跳包给客户端 SendHeartPackToClients(); Console.Read(); } #region 创建一个时钟,每隔10秒发送一个心跳包给客户端 private static void SendHeartPackToClients() { System.Timers.Timer time = new System.Timers.Timer(); time.Interval = 10 * 1000; time.Enabled = true; time.Elapsed += time_Elapsed; time.Start(); } static void time_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { SendMsgToAllClients("hi," + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")); } /// <summary> /// 发送消息给所有连接的客户端 /// </summary> private static void SendMsgToAllClients(string msg) { try { foreach (var client in clients) { if (client.Connected) { client.Send(PackageServerData(msg)); } } } catch (Exception) { //TODO } } #endregion /// <summary> /// 服务端异步接受连接的回调处理方法 /// </summary> /// <param name="ar"></param> private static void AsyncAcceptCallback(IAsyncResult ar) { var ServerSocket = ar.AsyncState as Socket; //异步接受传入的连接,并创建客户端Socket var ClientSocket = ServerSocket.EndAccept(ar); //将客户端加入集合中 clients.Add(ClientSocket); //开始异步接收该客户端发送的消息 ClientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, AsyncReceiveCallback, ClientSocket); //服务端开始异步接受下一个客户端的连接请求 ServerSocket.BeginAccept(AsyncAcceptCallback, ServerSocket); } /// <summary> /// 异步接收消息的回调处理方法 /// </summary> /// <param name="ar"></param> private static void AsyncReceiveCallback(IAsyncResult ar) { try { var ClientSocket = ar.AsyncState as Socket; int RevLength = ClientSocket.EndReceive(ar); string RevMsg = Encoding.UTF8.GetString(buffer, 0, RevLength); #region WebSocket处理代码 //判断是否为浏览器websocket发过来的请求,若是,则打包服务器握手数据,实现第4次握手 if (RevMsg.Contains("Sec-WebSocket-Key")) { //IsWebSocketClient = true; Console.WriteLine(RevMsg); ClientSocket.Send(PackageHandShakeData(buffer, RevLength)); } else { string AnalyzeMsg = AnalyzeClientData(buffer, RevLength); Console.WriteLine(AnalyzeMsg); ClientSocket.Send(PackageServerData("收到您的信息!")); } #endregion //继续接收该客户端下一条发送的消息 ClientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, AsyncReceiveCallback, ClientSocket); } catch (Exception ex) { Console.WriteLine(ex.Message); } } #region 客户端和服务端的响应 /* * 客户端向服务器发送请求 * * GET / HTTP/1.1 * Origin: http://localhost:1416 * Sec-WebSocket-Key: vDyPp55hT1PphRU5OAe2Wg== * Connection: Upgrade * Upgrade: Websocket *Sec-WebSocket-Version: 13 * User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko * Host: localhost:8064 * DNT: 1 * Cache-Control: no-cache * Cookie: DTRememberName=admin * * 服务器给出响应 * * HTTP/1.1 101 Switching Protocols * Upgrade: websocket * Connection: Upgrade * Sec-WebSocket-Accept: xsOSgr30aKL2GNZKNHKmeT1qYjA= * * 在请求中的“Sec-WebSocket-Key”是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个魔幻字符串 * “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”。使用 SHA-1 加密,之后进行 BASE-64编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端 */ #endregion /// <summary> /// 打包服务器握手数据 /// </summary> /// <returns>The hand shake data.</returns> /// <param name="handShakeBytes">Hand shake bytes.</param> /// <param name="length">Length.</param> private static byte[] PackageHandShakeData(byte[] handShakeBytes, int length) { string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, length); string key = string.Empty; Regex reg = new Regex(@"Sec\-WebSocket\-Key:(.*?)\r\n"); Match m = reg.Match(handShakeText); if (m.Value != "") { key = Regex.Replace(m.Value, @"Sec\-WebSocket\-Key:(.*?)\r\n", "$1").Trim(); } byte[] secKeyBytes = SHA1.Create().ComputeHash( Encoding.ASCII.GetBytes(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")); string secKey = Convert.ToBase64String(secKeyBytes); var responseBuilder = new StringBuilder(); responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + "\r\n"); responseBuilder.Append("Upgrade: websocket" + "\r\n"); responseBuilder.Append("Connection: Upgrade" + "\r\n"); responseBuilder.Append("Sec-WebSocket-Accept: " + secKey + "\r\n\r\n"); //如果把上一行换成下面两行,才是thewebsocketprotocol-17协议,但居然握手不成功,目前仍没弄明白! //responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine); //responseBuilder.Append("Sec-WebSocket-Protocol: chat" + Environment.NewLine); return Encoding.UTF8.GetBytes(responseBuilder.ToString()); } #region 处理接收的数据 /// <summary> /// 处理接收的数据 /// 参考 http://www.cnblogs.com/smark/archive/2012/11/26/2789812.html /// </summary> /// <param name="recBytes"></param> /// <param name="length"></param> /// <returns></returns> private static string AnalyzeClientData(byte[] recBytes, int length) { int start = 0; // 如果有数据则至少包括3位 if (length < 2) return ""; // 判断是否为结束针 bool IsEof = (recBytes[start] >> 7) > 0; // 暂不处理超过一帧的数据 if (!IsEof) return ""; start++; // 是否包含掩码 bool hasMask = (recBytes[start] >> 7) > 0; // 不包含掩码的暂不处理 if (!hasMask) return ""; // 获取数据长度 UInt64 mPackageLength = (UInt64)recBytes[start] & 0x7F; start++; // 存储4位掩码值 byte[] Masking_key = new byte[4]; // 存储数据 byte[] mDataPackage; if (mPackageLength == 126) { // 等于126 随后的两个字节16位表示数据长度 mPackageLength = (UInt64)(recBytes[start] << 8 | recBytes[start + 1]); start += 2; } if (mPackageLength == 127) { // 等于127 随后的八个字节64位表示数据长度 mPackageLength = (UInt64)(recBytes[start] << (8 * 7) | recBytes[start] << (8 * 6) | recBytes[start] << (8 * 5) | recBytes[start] << (8 * 4) | recBytes[start] << (8 * 3) | recBytes[start] << (8 * 2) | recBytes[start] << 8 | recBytes[start + 1]); start += 8; } mDataPackage = new byte[mPackageLength]; for (UInt64 i = 0; i < mPackageLength; i++) { mDataPackage[i] = recBytes[i + (UInt64)start + 4]; } Buffer.BlockCopy(recBytes, start, Masking_key, 0, 4); for (UInt64 i = 0; i < mPackageLength; i++) { mDataPackage[i] = (byte)(mDataPackage[i] ^ Masking_key[i % 4]); } return Encoding.UTF8.GetString(mDataPackage); } #endregion #region 发送数据 /// <summary> /// 把发送给客户端消息打包处理(拼接上谁什么时候发的什么消息) /// </summary> /// <returns>The data.</returns> /// <param name="message">Message.</param> private static byte[] PackageServerData(string msg) { byte[] content = null; byte[] temp = Encoding.UTF8.GetBytes(msg); if (temp.Length < 126) { content = new byte[temp.Length + 2]; content[0] = 0x81; content[1] = (byte)temp.Length; Buffer.BlockCopy(temp, 0, content, 2, temp.Length); } else if (temp.Length < 0xFFFF) { content = new byte[temp.Length + 4]; content[0] = 0x81; content[1] = 126; content[2] = (byte)(temp.Length & 0xFF); content[3] = (byte)(temp.Length >> 8 & 0xFF); Buffer.BlockCopy(temp, 0, content, 4, temp.Length); } return content; } #endregion }
同系列其他文章:如何使用HTML5的WebSocket实现网页与服务器的双工通信(二)