前一篇 《Windows 8 Metro 關於 StreamSocket MSDN示例閱讀》我們基本懂得如何通過StreamSocket 做監聽、連接、發送接收數據。
同時前一篇留下的幾個疑問,我們在這里進行實驗和解答。
在“原有的異步Socket”連接方式與現在WIN8 Metro App 的StreamSocket 如何通信呢?
首先解釋下這里說的“原有的異步Socket” 是什么。
在await/async 關鍵字出現前,我們的Socket異步是依靠System.Net 以及 System.Net.Sockets 命名空間下的類與方法實現的。
請求連接BeginConnect/ConnectAsync 接受連接 BeginAccept/AcceptAsync 發送信息BeginSend/SendAsyn 收信息BeginReceive/ReceiveAsync
上面列舉的都是Socket類下的異步操作主要方法。這就是前面文章說的“原有的異步Socket”。
前一篇里還有兩個問題分別是
StreamSocket在原有的數據包格式如何讀取到數據?(沒有DataWriter.WriteUInt32(),DataReader.ReadUInt32();)
WIN8 Metro App 如何像之前的Silverlight一樣與服務器進行異步收發數據(問答數據)?
這幾個問題我想用一個相對簡單合適的示例來講解。
所以我們先來設定一下場景。
我現在面對的情況是,已經有使用原異步Socket的成熟服務器程序以及解決方案了。
這個解決方案可能會用於各種客戶端的連接,比如 FLASH客戶端 Silverlight客戶端 WINFORM客戶端 HTML5客戶端 等等。
現在我們需要增加一個WIN8 metro APP 的客戶端。但是我們不希望重新開發服務器端的東西。
於是乎我們就有了WIN8 metro APP 與 原異步Socket 連接收發信息的需求。
首先是服務器方面的一些主要代碼:(如果您之前已經做了很多可以忽略這一部分)
首先建立一個WINFORM工程然后在默認的FORM1上添加一個LISTBOX 和一個 Button 控件

然后轉到后台代碼。
聲明一個Socket 以及一個存儲客戶端的LIST。 添加Button Click事件代碼:
button1_Click
1 IList<Client> ClientList = new List<Client>(); 2 Socket ServerSoket; 3 /// <summary> 4 /// 開始監聽 5 /// </summary> 6 /// <param name="sender"></param> 7 /// <param name="e"></param> 8 private void button1_Click(object sender, EventArgs e) 9 { 10 ServerSoket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 11 ServerSoket.Bind(new IPEndPoint(IPAddress.Any, 4518));//監聽端口為4518 12 ServerSoket.Listen(100);//100是相對穩定連接隊列數 13 ServerSoket.BeginAccept(new AsyncCallback(ClientConnectComplete), null); 14 OutPutMsg("服務器在端口4518開始監聽"); 15 }
ClientConnectComplete 代碼:
ClientConnectComplete
1 private void ClientConnectComplete(IAsyncResult async) 2 { 3 Client client = new Client(); 4 5 // 完成接受客戶端傳入的連接的這個異步操作,並返回客戶端連入的 socket 6 try 7 { 8 client.Socket = ServerSoket.EndAccept(async); 9 } 10 catch (Exception) 11 { 12 13 return; 14 } 15 16 //把連接進來的客戶端保存起來 17 ClientList.Add(client); 18 19 OutPutMsg(client.Socket.RemoteEndPoint + " 連接成功!"); 20 21 try 22 { 23 // 開始異步接收客戶端傳入的數據 24 client.Socket.BeginReceive(client.Buffer, 0, client.Buffer.Length, SocketFlags.None, new AsyncCallback(ClientReceiveComplete), client); 25 } 26 catch (SocketException ex) 27 { 28 // 處理異常 29 } 30 31 ServerSoket.BeginAccept(new AsyncCallback(ClientConnectComplete), null);//繼續接受下一個連接 32 }
ClientReceiveComplete代碼:
ClientReceiveComplete
1 private void ClientReceiveComplete(IAsyncResult async) 2 { 3 Client client = async.AsyncState as Client; 4 5 int _receiveCount = 0;//用於統計本次接收到的字節數 6 7 try 8 { 9 // 完成接收數據的這個異步操作,並返回接收的字節數 10 if (client.Socket.Connected) 11 { 12 _receiveCount = client.Socket.EndReceive(async); 13 } 14 else 15 { 16 } 17 } 18 catch (SocketException ex) 19 { 20 // 處理異常 21 } 22 23 char startChar = System.BitConverter.ToChar(client.Buffer, 0);//獲取到包頭 24 Int16 msgType = System.BitConverter.ToInt16(client.Buffer, 2);//獲取到消息類型 25 Int32 dataSize = System.BitConverter.ToInt32(client.Buffer, 4);//獲取到包長度 26 char endtChar = System.BitConverter.ToChar(client.Buffer, dataSize-2);//獲取到包尾 27 28 //驗證數據包的正確性 29 if (startChar.Equals(Convert.ToChar(2)) && endtChar.Equals(Convert.ToChar(3))) 30 { 31 //數據驗證成功輸出信息 32 OutPutMsg(client.Socket.RemoteEndPoint + "數據驗證成功。"); 33 string receivedContent = UTF32Encoding.UTF8.GetString(client.Buffer,8, dataSize - 10);//獲取到數據內容 34 OutPutMsg("接收到的數據內容是:" + receivedContent); 35 36 //服務器應答機制,制造回發給客戶端消息 37 byte[] sendMsg = GetSendMsg(UTF32Encoding.UTF8.GetBytes("服務器發回數據:" + receivedContent),2); 38 39 try 40 { 41 //發送給所有客戶端 42 foreach (var clientItem in ClientList) 43 { 44 clientItem.Socket.BeginSend(sendMsg, 0, sendMsg.Length, SocketFlags.None, new AsyncCallback(SendToClientComplete), clientItem); 45 } 46 47 } 48 catch (Exception ex) 49 { 50 51 OutPutMsg(ex.Message); 52 } 53 } 54 55 try 56 { 57 //繼續開始接收客戶端傳入的數據 58 if (client.Socket.Connected) 59 { 60 client.Socket.BeginReceive(client.Buffer, 0, client.Buffer.Length, 0, new AsyncCallback(ClientReceiveComplete), client); 61 } 62 } 63 catch (SocketException ex) 64 { 65 // 處理異常 66 } 67 }
這里數據接收以及驗證數據都是比較簡單粗略的,沒有考慮掉包拆包復雜的數據驗證等等。
SendToClientComplete代碼:
SendToClientComplete
1 private void SendToClientComplete(IAsyncResult async) 2 { 3 Client client = async.AsyncState as Client; 4 try 5 { 6 // 完成將信息發送到客戶端的這個異步操作 7 if (client.Socket.Connected) 8 { 9 client.Socket.EndSend(async); 10 } 11 } 12 catch (SocketException ex) 13 { 14 // 處理異常 15 } 16 OutPutMsg("服務器數據回發完畢。"); 17 }
OutPutMsg代碼:
OutPutMsg
1 /// <summary> 2 /// 正常消息輸出 3 /// </summary> 4 /// <param name="msg"></param> 5 private void OutPutMsg(string msg) 6 { 7 this.BeginInvoke(new System.EventHandler(AddToList), msg); 8 }
AddToList代碼:
AddToList
1 /// <summary> 2 /// 添加到窗體列表顯示 3 /// </summary> 4 /// <param name="sender"></param> 5 /// <param name="e"></param> 6 private void AddToList(object sender, EventArgs e) 7 { 8 this.listBox1.Items.Add(sender.ToString()); 9 try 10 { 11 this.listBox1.SelectedIndex = this.listBox1.Items.Count - 1; 12 } 13 catch (Exception) 14 { 15 //throw; 16 } 17 18 if (this.listBox1.Items.Count > 100) 19 { 20 this.listBox1.Items.RemoveAt(0); 21 } 22 }
GetSendMsg代碼:
1 /// <summary> 2 /// 組裝消息包 3 /// </summary> 4 /// <param name="BArr">內容字節流</param> 5 /// <param name="MsgType">消息類型</param> 6 /// <returns></returns> 7 private byte[] GetSendMsg(byte[] BArr, Int16 MsgType) 8 { 9 //數據包格式為 包頭 + 消息類型 + 包長度 + 消息內容 + 包尾 10 //開始字節流 包頭 11 byte[] bs = System.BitConverter.GetBytes(Convert.ToChar(2)); 12 //結束字節流 包尾 13 byte[] be = System.BitConverter.GetBytes(Convert.ToChar(3)); 14 //消息體類型 15 byte[] b1 = System.BitConverter.GetBytes(MsgType); 16 17 //總長度 18 int size = BArr == null ? 10 : BArr.Length + 10; 19 20 byte[] b2 = System.BitConverter.GetBytes(size); 21 22 23 byte[] b = new byte[size]; 24 25 Array.Copy(bs, 0, b, 0, bs.Length); 26 Array.Copy(b1, 0, b, 2, b1.Length); 27 Array.Copy(b2, 0, b, 4, b2.Length); 28 if (BArr != null) 29 { 30 Array.Copy(BArr, 0, b, 8, BArr.Length); 31 } 32 Array.Copy(be, 0, b, size - 2, be.Length); 33 34 35 return b; 36 37 }
這個方法需要講解下。前一篇博文我已經說到,做原異步socket通信時需要設計的數據包格式。
我的數據包格式 包頭 + 消息類型 + 包長度 + 消息內容 + 包尾
當然METRO APP 通信時也是需要設計數據包的,只是數據長度可以單獨出來。
class Client 代碼:
class Client
1 public class Client 2 { 3 /// <summary> 4 /// 客戶端 Socket 5 /// </summary> 6 public Socket Socket { get; set; } 7 8 private byte[] _buffer; 9 /// <summary> 10 /// 為該客戶端 Socket 開辟的緩沖區 11 /// </summary> 12 public byte[] Buffer 13 { 14 get 15 { 16 if (_buffer == null) 17 _buffer = new byte[102400]; 18 19 return _buffer; 20 } 21 } 22 }
到這里簡單的原異步socket服務器就設計完畢了。
服務器做的事情:接收連接放入客戶端列表,接收客戶端發來的數據驗證處理並回發給所有客戶端。
現在我們看看WIN8 METRO APP 的 設計。
首先新建一個 Windows 應用商店 空應用程序。
使用Blend + SketchFlow Preview for Visual Studio 2012 打開工程。

在默認的MainPage.xaml 里添加 兩個 button,一個 TextBox, 一個ListBox 控件。
在第一個Button事件內我們寫入StreamSocket 的連接以及對已連接上的StreamSocket 實例進行消息監聽。
Button_Click_1代碼:
Button_Click_1
1 StreamSocket sk = new StreamSocket(); 2 private async void Button_Click_1(object sender, Windows.UI.Xaml.RoutedEventArgs e) 3 { 4 5 await sk.ConnectAsync(new HostName("127.0.0.1"), "4518"); 6 await ShowMsg("發送連接完畢。"); 7 8 if (sk.Information!= null) 9 { 10 await BeginReceived(); 11 } 12 13 }
這段代碼我就不解釋了,看過前一篇的朋友應該都懂。
BeginReceived 代碼:
BeginReceived
private async Task BeginReceived() { //綁定已連接的StreamSocket到DataReader DataReader reader = new DataReader(sk.InputStream); while (true) { try { //嘗試從StreamSocket 讀取到數據 讀取2個字節的數據(包頭) uint sizeFieldCount = await reader.LoadAsync(sizeof(UInt16)); if (sizeFieldCount != sizeof(UInt16)) { return; } } catch (Exception exception) { return; } //臨時處理讀取器 的字節流數組 byte[] tempByteArr; tempByteArr = new byte[2]; //將剛才reader讀取到的數據填充到tempByteArr reader.ReadBytes(tempByteArr); char startChar = System.BitConverter.ToChar(tempByteArr, 0);//獲取到包頭 //如果不是包頭 if (!startChar.Equals(Convert.ToChar(2))) { return; } //在讀取下2個字節 await reader.LoadAsync(sizeof(UInt16)); reader.ReadBytes(tempByteArr); Int16 msgType = System.BitConverter.ToInt16(tempByteArr,0);//獲取到消息類型 await ShowMsg("獲取到的消息類型:" + msgType.ToString()); tempByteArr = new byte[4]; //在讀取下4個字節 await reader.LoadAsync(sizeof(uint)); reader.ReadBytes(tempByteArr); uint dataSize = System.BitConverter.ToUInt32(tempByteArr, 0);//獲取到包長度 await ShowMsg("獲取到的數據包長度:" + dataSize.ToString()); uint contentByteLenth = dataSize - 8;//內容字節流長度 //根據長度讀取內容字節流 uint actualStringLength = await reader.LoadAsync(contentByteLenth); if (contentByteLenth != actualStringLength) { //內容數據流未發送完成 await ShowMsg("內容數據流未發送完成"); return; } //填充 tempByteArr = new byte[contentByteLenth]; reader.ReadBytes(tempByteArr); await ShowMsg(System.Text.UnicodeEncoding.UTF8.GetString(tempByteArr, 0, int.Parse(contentByteLenth.ToString()))); } }
BeginReceived方法內我們用一個 DataReader 綁定已經連接上的StreamSocket 實例的InputStream
然后我們利用循環進行監聽,監聽時需要進行初步的數據讀取以便查看是否有數據到達。
我講解一下BeginReceived內的代碼。因為這是這篇博文比較重要的部分。
代碼:
try { //嘗試從StreamSocket 讀取到數據 讀取2個字節的數據(包頭) uint sizeFieldCount = await reader.LoadAsync(sizeof(UInt16)); if (sizeFieldCount != sizeof(UInt16)) { return; } } catch (Exception exception) { return; }
這里要說下為什么要讀取 2個字節的數據長度來做驗證呢?
前面的服務器設計里我們數據包的格式包頭就是2個字節。
DataReader.LoadAsync() 方法的機制是:
從StreamSocket的InputStream讀取一定長度的數據字節數到DataReader內,且讀取完畢后InputStream內被讀取到的數據將被刪除。
所以我這里用讀取2個字節的包頭來做是否有數據的判斷。
接下來的代碼:
1 //臨時處理讀取器 的字節流數組 2 byte[] tempByteArr; 3 tempByteArr = new byte[2]; 4 5 //將剛才reader讀取到的數據填充到tempByteArr 6 reader.ReadBytes(tempByteArr); 7 8 char startChar = System.BitConverter.ToChar(tempByteArr, 0);//獲取到包頭 9 //如果不是包頭 10 if (!startChar.Equals(Convert.ToChar(2))) 11 { 12 return; 13 }
定義一個臨時的字節數組 tempByteArr 且數組大小與需要讀取到的數據長度一樣。
DataReader.ReadBytes()方法是根據傳入的字節數組長度讀取數據的。
所以接下來的代碼:
1 //再讀取下2個字節 2 await reader.LoadAsync(sizeof(UInt16)); 3 reader.ReadBytes(tempByteArr); 4 Int16 msgType = System.BitConverter.ToInt16(tempByteArr,0);//獲取到消息類型 5 await ShowMsg("獲取到的消息類型:" + msgType.ToString()); 6 7 tempByteArr = new byte[4]; 8 //再讀取下4個字節 9 await reader.LoadAsync(sizeof(uint)); 10 reader.ReadBytes(tempByteArr); 11 uint dataSize = System.BitConverter.ToUInt32(tempByteArr, 0);//獲取到包長度 12 await ShowMsg("獲取到的數據包長度:" + dataSize.ToString()); 13 14 uint contentByteLenth = dataSize - 8;//內容字節流長度 15 //根據長度讀取內容字節流 16 uint actualStringLength = await reader.LoadAsync(contentByteLenth); 17 if (contentByteLenth != actualStringLength) 18 { 19 //內容數據流未發送完成 20 await ShowMsg("內容數據流未發送完成"); 21 return; 22 } 23 24 //填充 25 tempByteArr = new byte[contentByteLenth]; 26 reader.ReadBytes(tempByteArr); 27 28 await ShowMsg(System.Text.UnicodeEncoding.UTF8.GetString(tempByteArr, 0, int.Parse(contentByteLenth.ToString())));
我們讀取到包長度后就能順利的得到我們的消息內容了。
接下來的Button2 發送信息 代碼:
Button_Click_2
1 DataWriter writer; 2 private async void Button_Click_2(object sender, Windows.UI.Xaml.RoutedEventArgs e) 3 { 4 if (this.SendText.Text != string.Empty) 5 { 6 if (writer == null) 7 { 8 writer = new DataWriter(sk.OutputStream); 9 } 10 11 //需要發送的數據 12 byte[] sendMsg = GetSendMsg(System.Text.UnicodeEncoding.UTF8.GetBytes(this.SendText.Text), 1); 13 14 //把數據寫入到發送流 15 writer.WriteBytes(sendMsg); 16 17 //異步發送 18 await writer.StoreAsync(); 19 20 } 21 }
ShowMsg GetSendMsg 代碼:
ShowMsg GetSendMsg
1 protected async Task ShowMsg(string s) 2 { 3 MsgList.Items.Insert(0, s); 4 } 5 6 /// <summary> 7 /// 組裝消息包 8 /// </summary> 9 /// <param name="BArr">內容字節流</param> 10 /// <param name="MsgType">消息類型</param> 11 /// <returns></returns> 12 private byte[] GetSendMsg(byte[] BArr, Int16 MsgType) 13 { 14 //數據包格式為 包頭 + 消息類型 + 包長度 + 消息內容 + 包尾 15 //開始字節流 包頭 16 byte[] bs = System.BitConverter.GetBytes(Convert.ToChar(2)); 17 //結束字節流 包尾 18 byte[] be = System.BitConverter.GetBytes(Convert.ToChar(3)); 19 //消息體類型 20 byte[] b1 = System.BitConverter.GetBytes(MsgType); 21 22 //總長度 23 int size = BArr == null ? 10 : BArr.Length + 10; 24 25 byte[] b2 = System.BitConverter.GetBytes(size); 26 27 28 byte[] b = new byte[size]; 29 30 Array.Copy(bs, 0, b, 0, bs.Length); 31 Array.Copy(b1, 0, b, 2, b1.Length); 32 Array.Copy(b2, 0, b, 4, b2.Length); 33 if (BArr != null) 34 { 35 Array.Copy(BArr, 0, b, 8, BArr.Length); 36 } 37 Array.Copy(be, 0, b, size - 2, be.Length); 38 39 40 return b; 41 42 }
到這里基本解答了
在“原有的異步Socket”連接方式與現在WIN8 Metro App 的StreamSocket 如何通信呢?
StreamSocket在原有的數據包格式如何讀取到數據?(沒有DataWriter.WriteUInt32(),DataReader.ReadUInt32();)
WIN8 Metro App 如何像之前的Silverlight一樣與服務器進行異步收發數據(問答數據)?
最后給上 示例的源碼:點擊這里下載
國際慣例如果您看得起鄙人,轉載請注明出處謝謝。
