我們在編寫與Socket有關的應用程序時,在發送軟為復雜的數據時,可能我們最常做的是把各個部分的數據轉換為字符串,然後將這些字符串用一個分隔符連接起來進行發送。不過,不知道你有沒有想過這樣做還是有問題的。
比如,我用#來分隔各個字符串,在根據客戶端輸入的內容到服務器端進行查找,然後返回結果,萬一用戶輸入的查找關鍵字中就包含#,那麼就會影響我們對字符串進行分割了。
不知道各位有沒有想過,把序列化和反序列化的技術也用到socket上?先定義一個封裝數據的類,發送前將該類的對象序列化,然後發送;接收時,先接收字節流,然後再反序列化,得到某個對象。
這樣一來是不是比單單發送字符串好多了。
下面我舉的這個例子,服務器端用WPF開發,客戶端是Windows Store App,當然我這裡只是舉例,其實這可以用於所有類型的應用程序,包括Windows Phone應用,原理是不變的。
一、服務器端
首先我們定義一個用於封裝數據的類,這裡就以一個產品信息類做演示,這個類在服務器端和客戶端都要定義一遍,這樣才方便序列化和反序列化,你也可以特立寫到一個類庫中,然後服務器端和客戶端都引用該類庫。
[DataContract(Name = "product")] public class Product { /// <summary> /// 產品編號 /// </summary> [DataMember(Name = "product_no")] public string ProductNo { get; set; } /// <summary> /// 產品名稱 /// </summary> [DataMember(Name = "product_name")] public string ProductName { get; set; } /// <summary> /// 產品單價 /// </summary> [DataMember(Name = "product_price")] public decimal ProductPrice { get; set; } }
WPF窗口的XAML
<Grid> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <Grid Grid.Row="0" Margin="11"> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto"/> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> <RowDefinition Height="auto"/> </Grid.RowDefinitions> <TextBlock Text="產品編號:" Grid.Column="0" Grid.Row="0" Style="{DynamicResource tbLabel}"/> <TextBlock Text="產品名稱:" Grid.Column="0" Grid.Row="1" Style="{DynamicResource tbLabel}"/> <TextBlock Text="產品單價:" Grid.Column="0" Grid.Row="2" Style="{DynamicResource tbLabel}"/> <TextBox x:Name="txtProductNo" Grid.Column="1" Grid.Row="0" Style="{DynamicResource txtInput}"/> <TextBox x:Name="txtProductName" Grid.Column="1" Grid.Row="1" Style="{DynamicResource txtInput}"/> <TextBox x:Name="txtProductPrice" Grid.Column="1" Grid.Row="2" Style="{DynamicResource txtInput}"/> </Grid> <StackPanel Grid.Row="1" Margin="9" Orientation="Horizontal"> <Button x:Name="btnStartListen" Content="開始偵聽" Padding="10,6" Click="OnStartListen"/> <Button x:Name="btnStopListen" Content="停止偵聽" Padding="10,6" IsEnabled="False" Margin="18,0,0,0" Click="OnStopListen"/> </StackPanel> </Grid>
處理代碼如下。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.IO; using System.Net; using System.Net.Sockets; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; namespace ServerApp { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> public partial class MainWindow : Window { TcpListener listener = null; const int LOCAL_PORT = 2785;//監聽端口 public MainWindow() { InitializeComponent(); } private void OnStartListen(object sender, RoutedEventArgs e) { this.listener = new TcpListener(IPAddress.Any, LOCAL_PORT); this.listener.Start(); btnStartListen.IsEnabled = false; btnStopListen.IsEnabled = true; // 接受傳入連接 decimal d; if (decimal.TryParse(txtProductPrice.Text, out d) == false) { d = 0.00M; } Product prd = new Product { ProductNo = txtProductNo.Text == "" ? "000" : txtProductNo.Text, ProductName = txtProductName.Text == "" ? "No Name" : txtProductName.Text, ProductPrice = d }; listener.BeginAcceptTcpClient(new AsyncCallback(EndAcceptClientMethod), prd); MessageBox.Show("正在接受連接。"); } private void EndAcceptClientMethod(IAsyncResult ar) { Product prd = ar.AsyncState as Product; TcpClient client = null; // 發送消息 byte[] sendBuffer = this.SerializeObject<Product>(prd); try { client = listener.EndAcceptTcpClient(ar); var networkStream = client.GetStream(); // 先發送數據長度 byte[] bfLen = BitConverter.GetBytes(sendBuffer.Length); networkStream.Write(bfLen, 0, 4); // 發送數據 networkStream.Write(sendBuffer, 0, sendBuffer.Length); } catch (SocketException ex) { System.Diagnostics.Debug.WriteLine(ex.Message); } catch (ObjectDisposedException dex) { System.Diagnostics.Debug.WriteLine("對象已釋放。" + dex.Message); } catch (Exception eex) { System.Diagnostics.Debug.WriteLine(eex.Message); } finally { if (client != null) { client.Close(); } } // 繼續接受連接 try { listener.BeginAcceptTcpClient(new AsyncCallback(EndAcceptClientMethod), prd); } catch { } } private void OnStopListen(object sender, RoutedEventArgs e) { if (this.listener != null) { this.listener.Stop(); } btnStartListen.IsEnabled = true; btnStopListen.IsEnabled = false; } /// <summary> /// 將對象序列化 /// </summary> /// <typeparam name="T">要進行序列化的類型</typeparam> /// <param name="t">要序列化的對象</param> /// <returns>序列化後的字節數組</returns> private byte[] SerializeObject<T>(T t) where T : class { byte[] buffer = null; // 開始序列化 using (MemoryStream ms = new MemoryStream()) { DataContractJsonSerializer ss = new DataContractJsonSerializer(t.GetType()); ss.WriteObject(ms, t); buffer = ms.ToArray(); } return buffer; } } [DataContract(Name = "product")] public class Product { /// <summary> /// 產品編號 /// </summary> [DataMember(Name = "product_no")] public string ProductNo { get; set; } /// <summary> /// 產品名稱 /// </summary> [DataMember(Name = "product_name")] public string ProductName { get; set; } /// <summary> /// 產品單價 /// </summary> [DataMember(Name = "product_price")] public decimal ProductPrice { get; set; } } }
由於只做演示,接受連接後只發送一次數據就關閉連接。
二、客戶端
這裡只放出核心代碼。
namespace ClientApp { /// <summary> /// 可用於自身或導航至 Frame 內部的空白頁。 /// </summary> public sealed partial class MainPage : Page { public MainPage() { this.InitializeComponent(); } /// <summary> /// 在此頁將要在 Frame 中顯示時進行調用。 /// </summary> /// <param name="e">描述如何訪問此頁的事件數據。Parameter /// 屬性通常用於配置頁。</param> protected override void OnNavigatedTo(NavigationEventArgs e) { } /// <summary> /// 反序列化對象 /// </summary> /// <typeparam name="T">要反序列化的類型</typeparam> /// <param name="buffer">字節數組</param> /// <returns>反序列化後的對象</returns> private T DeSerializeObject<T>(byte[] buffer) where T : class { T t = default(T); using (MemoryStream ms = new MemoryStream(buffer)) { ms.Position = 0; DataContractJsonSerializer ss = new DataContractJsonSerializer(typeof(T)); t = (T)ss.ReadObject(ms); } return t; } private async void OnClick(object sender, RoutedEventArgs e) { StreamSocket socket = new StreamSocket(); HostName host = new HostName(txtHost.Text); // 連接服務器 await socket.ConnectAsync(host, txtPort.Text); // 讀取數據 DataReader reader = new DataReader(socket.InputStream); reader.ByteOrder = ByteOrder.LittleEndian; // 加載4字節,讀取長度 await reader.LoadAsync(sizeof(int)); int len = reader.ReadInt32(); // 加載剩余字節 await reader.LoadAsync((uint)len); IBuffer readBuffer = reader.ReadBuffer((uint)len); // 反序列化 Product prd = this.DeSerializeObject<Product>(readBuffer.ToArray()); if (prd != null) { this.tbContent.Text = string.Format("產品編號:{0}\n產品名稱:{1}\n產品單價:{2}", prd.ProductNo, prd.ProductName, prd.ProductPrice.ToString("C2")); } } } [DataContract(Name = "product")] public class Product { /// <summary> /// 產品編號 /// </summary> [DataMember(Name = "product_no")] public string ProductNo { get; set; } /// <summary> /// 產品名稱 /// </summary> [DataMember(Name = "product_name")] public string ProductName { get; set; } /// <summary> /// 產品單價 /// </summary> [DataMember(Name = "product_price")] public decimal ProductPrice { get; set; } } }
下在是測試結果。
本例我使用的是JSON序列化和反序列化。
這個例子只是知識與技巧的綜合,沒有涉及到新的概念,所以就不多解釋了。用socket收發數據比較好的做法是先將要發送的數據的長度先發送給對方,然後再發數據內容,這樣可以保證正確的讀取數據。
大家在編寫socket應用程序時,不妨試試這種思路。