[C#]手把手教你打造Socket的TCP通訊連接(一)


本文章將講解基於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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM