我們在編寫與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應用程序時,不妨試試這種思路。