Windows 8 Metro 关于StreamSocket与原异步Socket
前一篇 《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事件代码:
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 代码:
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代码:
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代码:
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代码:
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代码:
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 代码:
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代码:
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 代码:
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 发送信息 代码:
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 代码:
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一样与服务器进行异步收发数据(问答数据)?
最后给上 示例的源码:点击这里下载
国际惯例如果您看得起鄙人,转载请注明出处谢谢。