基於SocketAsyncEventArgs(IOCP)實現的高併發TCP客戶端

之前在文章基於SocketAsyncEventArgs(IOCP)的高性能TCP服務器實現(二)——服務端信息接收窗體實現(C#)這篇文章中,我介紹了一個高性能的TCP服務器,目的是接受數千臺基於TCP協議的設備發送的信息,並且這些設備只是單向發送,不需要服務器返回信息,設備的信息發送頻率在一秒鐘一次。服務器端接受到之後,解析信息,然後入庫。並且在文章中,我也實現了一個軟件窗體,爲了有效的檢驗這個軟件,我需要大量的設備同時向這個服務器軟件發送信息,但是一般情況,在開發中不可能同時提供這麼大量的設備,因此需要我們做一個模擬的軟件,在網絡上搜索了很久,發現都不太符合我個人的需求,那麼在翻閱大量大神的文章之後,自己做了一個模擬軟件,不太成熟,歡迎各位指正。

這個窗體很簡單,通過設置服務器和客戶端IP地址之後,可以自定義模擬設備的數量,非常方便。窗體中發送信息,是我根據項目實際的信息格式放的一個樣例,讀者可以自行替換成其他信息。下面詳細介紹下 我是如何實現的。

一、基於SocketAsyncEventArgs封裝的客戶端

  1. 創建一個自定義的類SocketClient,繼承自IDisposable。然後添加自定義的一些變量
        private const Int32 BuffSize = 200;
    
            // The socket used to send/receive messages.  
            private Socket clientSocket;
    
            // Flag for connected socket.  
            private Boolean connected = false;
    
            // Listener endpoint.  
            private IPEndPoint hostEndPoint;
    
            // Signals a connection.  
            private static AutoResetEvent autoConnectEvent = new AutoResetEvent(false);
    
            BufferManager m_bufferManager;
            //定義接收數據的對象  
            List<byte> m_buffer;
            //發送與接收的MySocketEventArgs變量定義.  
            private List<MySocketEventArgs> listArgs = new List<MySocketEventArgs>();
            private MySocketEventArgs receiveEventArgs = new MySocketEventArgs();
            int tagCount = 0;
    
            /// <summary>  
            /// 當前連接狀態  
            /// </summary>  
            public bool Connected { get { return clientSocket != null && clientSocket.Connected; } }
    
            //服務器主動發出數據受理委託及事件  
            public delegate void OnServerDataReceived(byte[] receiveBuff);
            public event OnServerDataReceived ServerDataHandler;
    
            //服務器主動關閉連接委託及事件  
            public delegate void OnServerStop();
            public event OnServerStop ServerStopEvent;

    上面MySocketEventArgs 的實現很簡單,參考下面代碼:

    public class MySocketEventArgs : SocketAsyncEventArgs
        {
    
            /// <summary>  
            /// 標識,只是一個編號而已  
            /// </summary>  
            public int ArgsTag { get; set; }
    
            /// <summary>  
            /// 設置/獲取使用狀態  
            /// </summary>  
            public bool IsUsing { get; set; }
    
        }

     

  2. 實現SocketClient的構造函數(帶參數)
        // Create an uninitialized client instance.  
            // To start the send/receive processing call the  
            // Connect method followed by SendReceive method.  
            internal SocketClient(String ip, Int32 port)
            {
                // Instantiates the endpoint and socket.  
                hostEndPoint = new IPEndPoint(IPAddress.Parse(ip), port);
                clientSocket = new Socket(hostEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
                m_bufferManager = new BufferManager(BuffSize * 2, BuffSize);
                m_buffer = new List<byte>();
            }

    上面的BufferManager類的實現可直接參考基於SocketAsyncEventArgs(IOCP)的高性能TCP服務器實現(一)——封裝SocketAsyncEventArgs這篇文章,clientSocket 就是客戶端與服務器端連接的套接字。

  3. 實現SocketClient的連接函數,讓客戶端連接到服務器

         /// <summary>  
            /// 連接到主機  
            /// </summary>  
            /// <returns>0.連接成功, 其他值失敗,參考SocketError的值列表</returns>  
            internal SocketError Connect()
            {
                SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs();
                connectArgs.UserToken = clientSocket;
                connectArgs.RemoteEndPoint = hostEndPoint;
                connectArgs.Completed += new EventHandler<SocketAsyncEventArgs>(OnConnect);
    
                clientSocket.ConnectAsync(connectArgs);
                //autoConnectEvent.WaitOne(); //阻塞. 讓程序在這裏等待,直到連接響應後再返回連接結果  
                return connectArgs.SocketError;
            }

     

  4. 連接服務器完成後,通過委託函數實現一些功能
        // Calback for connect operation  
            private void OnConnect(object sender, SocketAsyncEventArgs e)
            {
                // Signals the end of connection.  
                //autoConnectEvent.Set(); //釋放阻塞.  
                // Set the flag for socket connected.  
                connected = (e.SocketError == SocketError.Success);
                //如果連接成功,則初始化socketAsyncEventArgs  
                if (connected)
                    initArgs(e);
            }

    通過第三步的委託回調函數OnConnect判斷是否連接成功,如果連接成功則對收發參數進行初始化。

  5. 連接成功後,對收發參數進行初始化

         /// <summary>  
            /// 初始化收發參數  
            /// </summary>  
            /// <param name="e"></param>  
            private void initArgs(SocketAsyncEventArgs e)
            {
                m_bufferManager.InitBuffer();
                //發送參數  
                initSendArgs();
                //接收參數  
                receiveEventArgs.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed);
                receiveEventArgs.UserToken = e.UserToken;
                receiveEventArgs.ArgsTag = 0;
                m_bufferManager.SetBuffer(receiveEventArgs);
    
                //啓動接收,不管有沒有,一定得啓動.否則有數據來了也不知道.  
                if (!e.ConnectSocket.ReceiveAsync(receiveEventArgs))
                    ProcessReceive(receiveEventArgs);
            }

     

  6. 初始化發送參數MySocketEventArgs 
        /// <summary>  
            /// 初始化發送參數MySocketEventArgs  
            /// </summary>  
            /// <returns></returns>  
            MySocketEventArgs initSendArgs()
            {
                MySocketEventArgs sendArg = new MySocketEventArgs();
                sendArg.Completed += new EventHandler<SocketAsyncEventArgs>(IO_Completed);
                sendArg.UserToken = clientSocket;
                sendArg.RemoteEndPoint = hostEndPoint;
                sendArg.IsUsing = false;
                Interlocked.Increment(ref tagCount);
                sendArg.ArgsTag = tagCount;
                lock (listArgs)
                {
                    listArgs.Add(sendArg);
                }
                return sendArg;
            }

     

  7. 發送信息和接收信息的處理委託
            void IO_Completed(object sender, SocketAsyncEventArgs e)
            {
                MySocketEventArgs mys = (MySocketEventArgs)e;
                // determine which type of operation just completed and call the associated handler  
                switch (e.LastOperation)
                {
                    case SocketAsyncOperation.Receive:
                        ProcessReceive(e);
                        break;
                    case SocketAsyncOperation.Send:
                        mys.IsUsing = false; //數據發送已完成.狀態設爲False  
                        ProcessSend(e);
                        break;
                    default:
                        throw new ArgumentException("The last operation completed on the socket was not a receive or send");
                }
            }
    
    
            // This method is invoked when an asynchronous send operation completes.    
            // The method issues another receive on the socket to read any additional   
            // data sent from the client  
            //  
            // <param name="e"></param>  
            private void ProcessSend(SocketAsyncEventArgs e)
            {
                if (e.SocketError != SocketError.Success)
                {
                    ProcessError(e);
                }
            }
    
    
            // Close socket in case of failure and throws  
            // a SockeException according to the SocketError.  
            private void ProcessError(SocketAsyncEventArgs e)
            {
                Socket s = (Socket)e.UserToken;
                if (s.Connected)
                {
                    // close the socket associated with the client  
                    try
                    {
                        s.Shutdown(SocketShutdown.Both);
                    }
                    catch (Exception)
                    {
                        // throws if client process has already closed  
                    }
                    finally
                    {
                        if (s.Connected)
                        {
                            s.Close();
                        }
                        connected = false;
                    }
                }
                //這裏一定要記得把事件移走,如果不移走,當斷開服務器後再次連接上,會造成多次事件觸發.  
                foreach (MySocketEventArgs arg in listArgs)
                    arg.Completed -= IO_Completed;
                receiveEventArgs.Completed -= IO_Completed;
    
                if (ServerStopEvent != null)
                    ServerStopEvent();
            }
     在IO_Completed中判斷當前套接字是接收還是發送,然後分派給不同的處理函數。
  8. 客戶端接收到服務器端信息的處理函數
            // This method is invoked when an asynchronous receive operation completes.   
            // If the remote host closed the connection, then the socket is closed.    
            // If data was received then the data is echoed back to the client.  
            //  
            private void ProcessReceive(SocketAsyncEventArgs e)
            {
                try
                {
                    // check if the remote host closed the connection  
                    Socket token = (Socket)e.UserToken;
                    if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
                    {
                        //讀取數據  
                        byte[] data = new byte[e.BytesTransferred];
                        Array.Copy(e.Buffer, e.Offset, data, 0, e.BytesTransferred);
                        lock (m_buffer)
                        {
                            m_buffer.AddRange(data);
                        }
                        DoReceiveEvent(data);
                         
                        //繼續接收  
                        if (!token.ReceiveAsync(e))
                            this.ProcessReceive(e);
                    }
                    else
                    {
                        ProcessError(e);
                    }
                }
                catch (Exception xe)
                {
                    Console.WriteLine(xe.Message);
                }
            }
    
            /// <summary>  
            /// 使用新進程通知事件回調  
            /// </summary>  
            /// <param name="buff"></param>  
            private void DoReceiveEvent(byte[] buff)
            {
                if (ServerDataHandler == null) return;
                //ServerDataHandler(buff); //可直接調用.  
                //但我更喜歡用新的線程,這樣不拖延接收新數據.  
                Thread thread = new Thread(new ParameterizedThreadStart((obj) =>
                {
                    ServerDataHandler((byte[])obj);
                }));
                thread.IsBackground = true;
                thread.Start(buff);
            }

    上面的ServerDataHandler是在類外面定義的委託函數,比如接收到服務器信息後,需要展示處理或者入庫之類的,讀者可以自己實現。

  9. 重頭戲,客戶端向服務器發送信息的函數

             // Exchange a message with the host.  
            internal void Send(byte[] sendBuffer)
            {
                if (connected)
                {
                    //先對數據進行包裝,就是把包的大小作爲頭加入,這必須與服務器端的協議保持一致,否則造成服務器無法處理數據.  
                    byte[] buff = new byte[sendBuffer.Length + 4];
                    Array.Copy(BitConverter.GetBytes(sendBuffer.Length), buff, 4);
                    Array.Copy(sendBuffer, 0, buff, 4, sendBuffer.Length);
                    //查找有沒有空閒的發送MySocketEventArgs,有就直接拿來用,沒有就創建新的.So easy!  
                    MySocketEventArgs sendArgs = listArgs.Find(a => a.IsUsing == false);
                    if (sendArgs == null)
                    {
                        sendArgs = initSendArgs();
                    }
                    lock (sendArgs) //要鎖定,不鎖定讓別的線程搶走了就不妙了.  
                    {
                        sendArgs.IsUsing = true;
                        sendArgs.SetBuffer(buff, 0, buff.Length);
                    }
                    clientSocket.SendAsync(sendArgs);
                }
                else
                {
                    throw new SocketException((Int32)SocketError.NotConnected);
                }
            }

    上面的發送函數就是通過異步發送進行。

  10. 銷燬函數

            // Disposes the instance of SocketClient.  
            public void Dispose()
            {
                autoConnectEvent.Close();
                if (clientSocket.Connected)
                {
                    clientSocket.Close();
                }
            }

    通過上述10個步驟,我們實現了一個封裝的SocketClient類,幫我們進行信息的處理,包括服務器信息接收和向服務器發送信息,甚至可以接收服務器服務停止的狀態處理。

二、客戶端信息發送窗體實現(C#)

  1. 創建一個Windows窗體,這個對有一定C#基礎的讀者不是難事,這裏就不詳細介紹了。建好的窗體如下圖:圖中主要是服務器和客戶端的IP地址,以及一個信息發送的樣例。中間是顯示發送信息狀態的ListView,最下面是幾個按鈕。
  2. 在窗體的cs文件中定義一些變量
            private int _serverPort = 0;
            private bool Break = false;
            private bool _clientCreatedSuccess = false;
            private SocketClient[] _socketClients;
            private IPAddress _serverIP;
            private IPEndPoint _remoteEndPoint;
            private delegate void winDelegate(MessageInfo msg);
            private int msgCount = 0;
            private byte[] _testMessage;
            private DateTime _startTime;
            private DateTime _endTime;
            RegisteredWaitHandle rhw;

    比較重要的是_socketClients這個變量,他是一個數組,可以用來模擬大量的客戶端,數組的長度就是客戶端的數量,他的類型就是剛剛我們創建好的SocketClient類。

  3. “創建客戶端”按鈕的實現代碼

            private void btnCreateClient_Click(object sender, EventArgs e)
            {
                initTestMessageDate();
                Break = false;
                createSocketClient();
            }
    
            //把發送信息轉換爲字節數組
            private void initTestMessageDate()
            {
                string tst = txtMessage.Text.Trim();
                string[] tstArray = tst.Split(' ');
                _testMessage = new byte[tstArray.Length];
                for (int i=0;i< tstArray.Length;i++)
                {
                    // Convert the number expressed in base-16 to an integer.
                    string hex = tstArray[i];
                    byte value = Convert.ToByte(hex,16);
                    _testMessage[i] = value;
                }
                //return _testMessage;
            }
    
    
    
            private void createSocketClient()
            {
                int tcRes = 0;
                IPAddress arRes = null;
                IPAddress lcarRes = null;
                int ptRes = 0;
                bool arBool = IPAddress.TryParse(txtIPAddress.Text.Trim(), out arRes);
                bool tcBool = int.TryParse(txtThreadCount.Text.Trim(), out tcRes);
                bool ptBool = int.TryParse(txtPort.Text.Trim(), out ptRes);
                bool lcarBool = IPAddress.TryParse(txtLocalIP.Text.Trim(), out lcarRes);
                try
                {
                    if (arBool && tcBool && ptBool )
                    {
                        _serverPort = ptRes;
                        _socketClients = new SocketClient[tcRes];
                        for (int i = 0; i < tcRes; i++)
                        {
                            _socketClients[i] = new SocketClient(txtIPAddress.Text.Trim(), _serverPort);
                            _socketClients[i].ServerStopEvent += OnServerStop;
                            _socketClients[i].Connect();
                            LogHelper.WriteLog("第" + i.ToString() + "個客戶端創建!");
                        }
                    }
                    else
                    {
                        MessageBox.Show("參數設置錯誤"); _clientCreatedSuccess = false;return;
                    }
                    
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message); return;
                }
                MessageBox.Show("創建客戶端成功!");
                _clientCreatedSuccess = true;
            }
    
    
            private void OnServerStop()
            {
                Break = true;
                
            }

    createSocketClient函數中,我們首先從文本框中獲取創建模擬客戶端的數量,然後創建此數量長度的SocketClient[tcRes]數組,然後遍歷這個數組,使得每個客戶端連接服務器,並註冊服務器停止服務的處理函數。

  4. “開始測試”按鈕的實現代碼

            private void btnTest_Click(object sender, EventArgs e)
            {
                int tcRes = 0;
                IPAddress arRes = null;
                int ptRes = 0;
                bool arBool = IPAddress.TryParse(txtIPAddress.Text.Trim(), out arRes);
                bool tcBool = int.TryParse(txtThreadCount.Text.Trim(), out tcRes);
                bool ptBool = int.TryParse(txtPort.Text.Trim(), out ptRes);
                string msg = txtMessage.Text.Trim();
                rhw = ThreadPool.RegisterWaitForSingleObject(new AutoResetEvent(false), this.CheckThreadPool, null, 1000, false);
                if (arBool && tcBool && ptBool && _clientCreatedSuccess)
                {
                    _serverIP = arRes;
                    _serverPort = ptRes;
                    LogHelper.WriteLog(DateTime.Now.ToString("f")+"開始創建線程池");
                    for (int i = 0; i < tcRes; i++)
                    {
                        SocketClientInfo sci = new SocketClientInfo(i + 1, _socketClients[i]);
                        ThreadPool.QueueUserWorkItem(new WaitCallback(sendMessage2Server), sci);//將方法排入隊列等待執行,並傳入該方法所用參數
                    }
                    _startTime = DateTime.Now;
    
                    LogHelper.WriteLog(DateTime.Now.ToString("f") + "線程創建分配完畢" );
                }
                else
                {
                    MessageBox.Show("參數設置錯誤");
                }
                
            }
    
    
    
        public class SocketClientInfo
        {
            private int _clientId = -1;
            private SocketClient _client = null;
    
            public SocketClientInfo(int clientId, SocketClient client)
            {
                ClientId = clientId;
                Client = client;
            }
    
            public int ClientId { get => _clientId; set => _clientId = value; }
            public SocketClient Client { get => _client; set => _client = value; }
        }

    在上面函數中,主要是爲每一個模擬客戶端分配一個線程,這樣就實現了高併發狀態,儘可能的模擬多客戶端的情況。SocketClientInfo這個類是我自定義的,用處傳遞客戶端狀態信息的。每一個客戶端的的信息發送函數sendMessage2Server都放到了ThreadPool線程池裏面,這樣線程池自動分配線程。

  5. 信息發送函數sendMessage2Server的實現

            private void sendMessage2Server(object client)
            {
                while (true)
                {
                    Thread.Sleep(1000);
                    msgCount++;
                    SocketClientInfo sci= client as SocketClientInfo;
                    SocketClient c= sci.Client;
                    if(c.Connected)
                    {
                        c.Send(_testMessage);
                    }
                    if (Break)
                    {
                        return;
                    }
                    MessageInfo mi = new MessageInfo(sci.ClientId, "當前客戶端連接狀態:" + c.Connected.ToString());
                    this.Invoke(new winDelegate(updateListBox), new object[] { mi });//異步委託
                }
            }
    
    
        public class MessageInfo
        {
            private int _number = -1;
            private string _message = "";
            private string _threadId = "";
    
            public MessageInfo(int number,string message)
            {
                _number = number;
                _message = message;
            }
            public MessageInfo(int number, string message,string threadId)
            {
                _number = number;
                _message = message;
                _threadId = threadId;
            }
            public MessageInfo(string message, string threadId)
            {
                _message = message;
                _threadId = threadId;
            }
    
            public int Number { get => _number; set => _number = value; }
            public string Message { get => _message; set => _message = value; }
            public string ThreadId { get => _threadId; set => _threadId = value; }
        }

    上面代碼中,首先判斷連接是否成功,成功的話就發送信息到服務器,然後判斷服務器端服務是否停止。最後,發送成功後對窗體中的ListView進行數據更新,代碼如下:

            private void updateListBox(MessageInfo msg)
            {
                new Thread((ThreadStart)(delegate ()
                {
                    // 此處警惕值類型裝箱造成的"性能陷阱"
                    listView1.Invoke((MethodInvoker)delegate ()
                    {
                        ListViewItem lviItem = new ListViewItem();
                        ListViewItem.ListViewSubItem lviSubItem;
                        lviItem.Text = "模擬客戶端C"+msg.Number.ToString();
    
                        lviSubItem = new ListViewItem.ListViewSubItem();
                        lviSubItem.Text = msg.Message;
                        lviItem.SubItems.Add(lviSubItem);
                        
    
                        listView1.Items.Add(lviItem);
                    });
                    tsslMessageCount.Text = "共發送消息" + msgCount.ToString() + "條";
                }))
                .Start();
            }

     

  6. “停止測試”按鈕的實現代碼
            private void btnStop_Click(object sender, EventArgs e)
            {
                Break = true;
                _endTime = DateTime.Now;
                int days = (_endTime - _startTime).Days;
                int hours = (_endTime - _startTime).Hours;
                int minutes = (_endTime - _startTime).Minutes;
                int seconds = (_endTime - _startTime).Seconds;
    
                string t = "共運行時間:"+days.ToString()+"天"+hours.ToString()+"小時"+minutes.ToString()+"分鐘"+seconds.ToString()+"秒";
                tsslTimespan.Text = t;
            }
     按下停止測試後,主要是把Break 變量設置爲true即可,然後就是記錄一下運行時間反饋到窗體上。

 

通過上述兩大步驟,我們就可以實現了基於SocketAsyncEventArgs(IOCP)的高併發TCP客戶端,這測試過一萬個客戶端,可以正常運行。這裏唯一的問題就是,雖然是爲每個客戶端分配了不同的線程,但是對於服務器來說還不算是同時接收到客戶端信息,如果改進?請有識之士不吝賜教!文章最後,提供下我的實現完整代碼,下載鏈接:基於SocketAsyncEventArgs(IOCP)實現的高併發TCP客戶端

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章