本系列服務端雙工通信包括兩種實現方式:一、使用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實現網頁與服務器的雙工通信(二)
