之前在文章基於SocketAsyncEventArgs(IOCP)的高性能TCP服務器實現(二)——服務端信息接收窗體實現(C#)這篇文章中,我介紹了一個高性能的TCP服務器,目的是接受數千臺基於TCP協議的設備發送的信息,並且這些設備只是單向發送,不需要服務器返回信息,設備的信息發送頻率在一秒鐘一次。服務器端接受到之後,解析信息,然後入庫。並且在文章中,我也實現了一個軟件窗體,爲了有效的檢驗這個軟件,我需要大量的設備同時向這個服務器軟件發送信息,但是一般情況,在開發中不可能同時提供這麼大量的設備,因此需要我們做一個模擬的軟件,在網絡上搜索了很久,發現都不太符合我個人的需求,那麼在翻閱大量大神的文章之後,自己做了一個模擬軟件,不太成熟,歡迎各位指正。
這個窗體很簡單,通過設置服務器和客戶端IP地址之後,可以自定義模擬設備的數量,非常方便。窗體中發送信息,是我根據項目實際的信息格式放的一個樣例,讀者可以自行替換成其他信息。下面詳細介紹下 我是如何實現的。
一、基於SocketAsyncEventArgs封裝的客戶端
- 創建一個自定義的類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; } }
- 實現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 就是客戶端與服務器端連接的套接字。
-
實現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; }
- 連接服務器完成後,通過委託函數實現一些功能
// 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判斷是否連接成功,如果連接成功則對收發參數進行初始化。
-
連接成功後,對收發參數進行初始化
/// <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); }
- 初始化發送參數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; }
- 發送信息和接收信息的處理委託
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(); }
- 客戶端接收到服務器端信息的處理函數
// 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是在類外面定義的委託函數,比如接收到服務器信息後,需要展示處理或者入庫之類的,讀者可以自己實現。
-
重頭戲,客戶端向服務器發送信息的函數
// 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); } }
上面的發送函數就是通過異步發送進行。
-
銷燬函數
// Disposes the instance of SocketClient. public void Dispose() { autoConnectEvent.Close(); if (clientSocket.Connected) { clientSocket.Close(); } }
通過上述10個步驟,我們實現了一個封裝的SocketClient類,幫我們進行信息的處理,包括服務器信息接收和向服務器發送信息,甚至可以接收服務器服務停止的狀態處理。
二、客戶端信息發送窗體實現(C#)
- 創建一個Windows窗體,這個對有一定C#基礎的讀者不是難事,這裏就不詳細介紹了。建好的窗體如下圖:圖中主要是服務器和客戶端的IP地址,以及一個信息發送的樣例。中間是顯示發送信息狀態的ListView,最下面是幾個按鈕。
- 在窗體的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類。
-
“創建客戶端”按鈕的實現代碼
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]數組,然後遍歷這個數組,使得每個客戶端連接服務器,並註冊服務器停止服務的處理函數。
-
“開始測試”按鈕的實現代碼
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線程池裏面,這樣線程池自動分配線程。
-
信息發送函數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(); }
- “停止測試”按鈕的實現代碼
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; }
通過上述兩大步驟,我們就可以實現了基於SocketAsyncEventArgs(IOCP)的高併發TCP客戶端,這測試過一萬個客戶端,可以正常運行。這裏唯一的問題就是,雖然是爲每個客戶端分配了不同的線程,但是對於服務器來說還不算是同時接收到客戶端信息,如果改進?請有識之士不吝賜教!文章最後,提供下我的實現完整代碼,下載鏈接:基於SocketAsyncEventArgs(IOCP)實現的高併發TCP客戶端