本文章將講解基於TCP連接的Socket通訊,使用Socket異步功能,並且無粘包現象,通過事件驅動使用。
在編寫Socket代碼之前,我們得要定義一下Socket的基本功能。
作為一個TCP連接,不論是客戶端還是服務器端,它都得有以下接口:
public interface ISocket { /// <summary> /// 獲取是否已連接。 /// </summary> bool IsConnected { get; } /// <summary> /// 發送數據。 /// </summary> /// <param name="data">要發送的數據。</param> void Send(byte[] data); /// <summary> /// 異步發送數據。 /// </summary> /// <param name="data">要發送的數據。</param> void SendAsync(byte[] data); /// <summary> /// 斷開連接。 /// </summary> void Disconnect(); /// <summary> /// 異步斷開連接。 /// </summary> void DisconnectAsync(); /// <summary> /// 斷開完成時引發事件。 /// </summary> event EventHandler<SocketEventArgs> DisconnectCompleted; /// <summary> /// 接收完成時引發事件。 /// </summary> event EventHandler<SocketEventArgs> ReceiveCompleted; /// <summary> /// 發送完成時引發事件。 /// </summary> event EventHandler<SocketEventArgs> SendCompleted; }
用到的事件參數SocketEventArgs。
/// <summary> /// Socket事件參數 /// </summary> public class SocketEventArgs : EventArgs { /// <summary> /// 實例化Socket事件參數 /// </summary> /// <param name="socket">相關Socket</param> /// <param name="operation">操作類型</param> public SocketEventArgs(ISocket socket, SocketAsyncOperation operation) { if (socket == null) throw new ArgumentNullException("socket"); Socket = socket; Operation = operation; } /// <summary> /// 獲取或設置事件相關數據。 /// </summary> public byte[] Data { get; set; } /// <summary> /// 獲取數據長度。 /// </summary> public int DataLength { get { return Data == null ? 0 : Data.Length; } } /// <summary> /// 獲取事件相關Socket /// </summary> public ISocket Socket { get; private set; } /// <summary> /// 獲取事件操作類型。 /// </summary> public SocketAsyncOperation Operation { get; private set; } }
因為作為客戶端只管收發,比較簡單,所以這里從客戶端開始做起。
定義類TCPClient繼承接口ISocket和IDisposable
/// <summary> /// TCP客戶端 /// </summary> public class TCPClient : ISocket, IDisposable { /// <summary> /// 獲取是否已連接。 /// </summary> public bool IsConnected { get; } /// <summary> /// 發送數據。 /// </summary> /// <param name="data">要發送的數據。</param> public void Send(byte[] data) { } /// <summary> /// 異步發送數據。 /// </summary> /// <param name="data">要發送的數據。</param> public void SendAsync(byte[] data) { } /// <summary> /// 斷開連接。 /// </summary> public void Disconnect() { } /// <summary> /// 異步斷開連接。 /// </summary> public void DisconnectAsync() { } /// <summary> /// 斷開完成時引發事件。 /// </summary> public event EventHandler<SocketEventArgs> DisconnectCompleted; /// <summary> /// 接收完成時引發事件。 /// </summary> public event EventHandler<SocketEventArgs> ReceiveCompleted; /// <summary> /// 發送完成時引發事件。 /// </summary> public event EventHandler<SocketEventArgs> SendCompleted; /// <summary> /// 釋放資源。 /// </summary> public void Dispose() { } }
並在此之上,增加以下方法
/// <summary> /// 連接至服務器。 /// </summary> /// <param name="endpoint">服務器終結點。</param> public void Connect(IPEndPoint endpoint) { } /// <summary> /// 異步連接至服務器。 /// </summary> /// <param name="endpoint"></param> public void ConnectAsync(IPEndPoint endpoint) { }
下面我們開始編寫構造函數,實例化一個Socket並保存到私有變量里。
把IsConnected指向Socket.Connected。
private Socket Socket; private Stream Stream; /// <summary> /// 實例化TCP客戶端。 /// </summary> public TCPClient() { Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); } /// <summary> /// 獲取是否已連接。 /// </summary> public bool IsConnected { get { return Socket.Connected; } }
因為接下來我們開始編寫Socket的異步功能,所以在此之前,我們要做一個狀態類,用來保存異步狀態。
internal class SocketAsyncState { /// <summary> /// 是否完成。 /// </summary> public bool Completed { get; set; } /// <summary> /// 數據 /// </summary> public byte[] Data { get; set; } /// <summary> /// 是否異步 /// </summary> public bool IsAsync { get; set; } }
下面我們開始編寫TCP連接功能。
/// <summary> /// 連接至服務器。 /// </summary> /// <param name="endpoint">服務器終結點。</param> public void Connect(IPEndPoint endpoint) { //判斷是否已連接 if (IsConnected) throw new InvalidOperationException("已連接至服務器。"); if (endpoint == null) throw new ArgumentNullException("endpoint"); //鎖定自己,避免多線程同時操作 lock (this) { SocketAsyncState state = new SocketAsyncState(); //Socket異步連接 Socket.BeginConnect(endpoint, EndConnect, state).AsyncWaitHandle.WaitOne(); //等待異步全部處理完成 while (!state.Completed) { } } } /// <summary> /// 異步連接至服務器。 /// </summary> /// <param name="endpoint"></param> public void ConnectAsync(IPEndPoint endpoint) { //判斷是否已連接 if (IsConnected) throw new InvalidOperationException("已連接至服務器。"); if (endpoint == null) throw new ArgumentNullException("endpoint"); //鎖定自己,避免多線程同時操作 lock (this) { SocketAsyncState state = new SocketAsyncState(); //設置狀態為異步 state.IsAsync = true; //Socket異步連接 Socket.BeginConnect(endpoint, EndConnect, state); } } private void EndConnect(IAsyncResult result) { SocketAsyncState state = (SocketAsyncState)result.AsyncState; try { Socket.EndConnect(result); } catch { //出現異常,連接失敗。 state.Completed = true; //判斷是否為異步,異步則引發事件 if (state.IsAsync && ConnectCompleted != null) ConnectCompleted(this, new SocketEventArgs(this, SocketAsyncOperation.Connect)); return; } //連接成功。 //創建Socket網絡流 Stream = new NetworkStream(Socket); //連接完成 state.Completed = true; if (state.IsAsync && ConnectCompleted != null) { ConnectCompleted(this, new SocketEventArgs(this, SocketAsyncOperation.Connect)); } //開始接收數據 Handler.BeginReceive(Stream, EndReceive, state); }
/// <summary>
/// 連接完成時引發事件。
/// </summary>
public event EventHandler<SocketEventArgs> ConnectCompleted;
以上為連接服務器的代碼,EndConnect中最后的Handler為一個處理IO收發的類,這留到后面再說。
接下來我們開始做斷開服務器的方法。
/// <summary> /// 斷開與服務器的連接。 /// </summary> public void Disconnect() { //判斷是否已連接 if (!IsConnected) throw new InvalidOperationException("未連接至服務器。"); lock (this) { //Socket異步斷開並等待完成 Socket.BeginDisconnect(true, EndDisconnect, true).AsyncWaitHandle.WaitOne(); } } /// <summary> /// 異步斷開與服務器的連接。 /// </summary> public void DisconnectAsync() { //判斷是否已連接 if (!IsConnected) throw new InvalidOperationException("未連接至服務器。"); lock (this) { //Socket異步斷開 Socket.BeginDisconnect(true, EndDisconnect, false); } } private void EndDisconnect(IAsyncResult result) { try { Socket.EndDisconnect(result); } catch { } //是否同步 bool sync = (bool)result.AsyncState; if (!sync && DisconnectCompleted!=null) { DisconnectCompleted(this, new SocketEventArgs(this, SocketAsyncOperation.Disconnect)); } } //這是一個給收發異常准備的斷開引發事件方法 private void Disconnected(bool raiseEvent) { if (raiseEvent && DisconnectCompleted != null) DisconnectCompleted(this, new SocketEventArgs(this, SocketAsyncOperation.Disconnect)); }
至此,我們已經完成了客戶端的連接於斷開功能。
現在我們開始寫客戶端的發送接收功能。
對於Socket的發送與接收,在大量數據吞吐的時候,容易造成粘包問題,要解決這個問題,我們先定義一個ISocketHandler接口。
該接口定義了Socket的發送與接收。
public interface ISocketHandler { /// <summary> /// 開始接收 /// </summary> /// <param name="stream">Socket網絡流</param> /// <param name="callback">回調函數</param> /// <param name="state">自定義狀態</param> /// <returns>異步結果</returns> IAsyncResult BeginReceive(Stream stream, AsyncCallback callback, object state); /// <summary> /// 結束接收 /// </summary> /// <param name="asyncResult">異步結果</param> /// <returns>接收到的數據</returns> byte[] EndReceive(IAsyncResult asyncResult); /// <summary> /// 開始發送 /// </summary> /// <param name="data">要發送的數據</param> /// <param name="offset">數據偏移</param> /// <param name="count">發送長度</param> /// <param name="stream">Socket網絡流</param> /// <param name="callback">回調函數</param> /// <param name="state">自定義狀態</param> /// <returns>異步結果</returns> IAsyncResult BeginSend(byte[] data, int offset, int count, Stream stream, AsyncCallback callback, object state); /// <summary> /// 結束發送 /// </summary> /// <param name="asyncResult">異步結果</param> /// <returns>發送是否成功</returns> bool EndSend(IAsyncResult asyncResult); }
在TCPClient中添加一個屬性。
/// <summary> /// Socket處理程序 /// </summary> public ISocketHandler Handler { get; set; }
這個ISocketHandler在上面的EndConnect里有使用到BeginReceive()。
而使用BeginReceive的回調函數則是這個。
private void EndReceive(IAsyncResult result) { SocketAsyncState state = (SocketAsyncState)result.AsyncState; //接收到的數據 byte[] data = Handler.EndReceive(result); //如果數據長度為0,則斷開Socket連接 if (data.Length == 0) { Disconnected(true); return; } //再次開始接收數據 Handler.BeginReceive(Stream, EndReceive, state); //引發接收完成事件 if (ReceiveCompleted != null) ReceiveCompleted(this, new SocketEventArgs(this, SocketAsyncOperation.Receive) { Data = data }); }
有了這個回調函數,我們的客戶端就能持續的接收數據。
現在剩下發送數據的功能要完成。
/// <summary> /// 發送數據。 /// </summary> /// <param name="data">要發送的數據。</param> public void Send(byte[] data) { //是否已連接 if (!IsConnected) throw new SocketException(10057); //發送的數據不能為null if (data == null) throw new ArgumentNullException("data"); //發送的數據長度不能為0 if (data.Length == 0) throw new ArgumentException("data的長度不能為0"); //設置異步狀態 SocketAsyncState state = new SocketAsyncState(); state.IsAsync = false; state.Data = data; try { //開始發送數據 Handler.BeginSend(data, 0, data.Length, Stream, EndSend, state).AsyncWaitHandle.WaitOne(); } catch { //出現異常則斷開Socket連接 Disconnected(true); } } /// <summary> /// 異步發送數據。 /// </summary> /// <param name="data">要發送的數據。</param> public void SendAsync(byte[] data) { //是否已連接 if (!IsConnected) throw new SocketException(10057); //發送的數據不能為null if (data == null) throw new ArgumentNullException("data"); //發送的數據長度不能為0 if (data.Length == 0) throw new ArgumentException("data的長度不能為0"); //設置異步狀態 SocketAsyncState state = new SocketAsyncState(); state.IsAsync = true; state.Data = data; try { //開始發送數據並等待完成 Handler.BeginSend(data, 0, data.Length, Stream, EndSend, state); } catch { //出現異常則斷開Socket連接 Disconnected(true); } } private void EndSend(IAsyncResult result) { SocketAsyncState state = (SocketAsyncState)result.AsyncState; //是否完成 state.Completed = Handler.EndSend(result); //沒有完成則斷開Socket連接 if (!state.Completed) Disconnected(true); //引發發送結束事件 if (state.IsAsync && SendCompleted != null) { SendCompleted(this, new SocketEventArgs(this, SocketAsyncOperation.Send) { Data = state.Data }); } }
至此,客戶端的發送接收也完成了。
我們再寫一下釋放資源的方法。
/// <summary> /// 釋放資源 /// </summary> public void Dispose() { lock (this) { if (IsConnected) Socket.Disconnect(false); Socket.Close(); } }
整個客戶端編寫完成。
下一篇將講解ISocketHandler的實現方法,用ISocketHandler來完成Socket的IO工作。
你可以在ISocketHandler中定義你自己的Socket通訊協議。
原文地址:http://www.cnblogs.com/Kation/archive/2013/03/06/2946761.html