之前在文章基于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; } - 发送信息和接收信息的处理委托
在IO_Completed中判断当前套接字是接收还是发送,然后分派给不同的处理函数。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(); } - “停止测试”按钮的实现代码
按下停止测试后,主要是把Break 变量设置为true即可,然后就是记录一下运行时间反馈到窗体上。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客户端

本文介绍了一种基于SocketAsyncEventArgs(IOCP)的高并发TCP客户端设计方法,通过封装SocketClient类,实现数千台设备向服务器单向发送信息的功能。文章详细讲解了客户端连接、信息发送与接收的实现过程,以及如何通过线程池模拟多客户端并发测试。
676

被折叠的 条评论
为什么被折叠?



