我們在編寫與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應用程式時,不妨試試這種思路。本文的例子我稍候上傳到資源區。