基于SocketAsyncEventArgs(IOCP)实现的高并发TCP客户端

之前在文章基于SocketAsyncEventArgs(IOCP)的高性能TCP服务器实现(二)——服务端信息接收窗体实现(C#)这篇文章中,我介绍了一个高性能的TCP服务器,目的是接受数千台基于TCP协议的设备发送的信息,并且这些设备只是单向发送,不需要服务器返回信息,设备的信息发送频率在一秒钟一次。服务器端接受到之后,解析信息,然后入库。并且在文章中,我也实现了一个软件窗体,为了有效的检验这个软件,我需要大量的设备同时向这个服务器软件发送信息,但是一般情况,在开发中不可能同时提供这么大量的设备,因此需要我们做一个模拟的软件,在网络上搜索了很久,发现都不太符合我个人的需求,那么在翻阅大量大神的文章之后,自己做了一个模拟软件,不太成熟,欢迎各位指正。

基于SocketAsyncEventArgs(IOCP)实现的高并发TCP客户端_第1张图片

这个窗体很简单,通过设置服务器和客户端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 m_buffer;
            //发送与接收的MySocketEventArgs变量定义.  
            private List listArgs = new List();
            private MySocketEventArgs receiveEventArgs = new MySocketEventArgs();
            int tagCount = 0;
    
            ///   
            /// 当前连接状态  
            ///   
            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
        {
    
            ///   
            /// 标识,只是一个编号而已  
            ///   
            public int ArgsTag { get; set; }
    
            ///   
            /// 设置/获取使用状态  
            ///   
            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();
            }

    上面的BufferManager类的实现可直接参考基于SocketAsyncEventArgs(IOCP)的高性能TCP服务器实现(一)——封装SocketAsyncEventArgs这篇文章,clientSocket 就是客户端与服务器端连接的套接字。

  3. 实现SocketClient的连接函数,让客户端连接到服务器

         ///   
            /// 连接到主机  
            ///   
            /// 0.连接成功, 其他值失败,参考SocketError的值列表  
            internal SocketError Connect()
            {
                SocketAsyncEventArgs connectArgs = new SocketAsyncEventArgs();
                connectArgs.UserToken = clientSocket;
                connectArgs.RemoteEndPoint = hostEndPoint;
                connectArgs.Completed += new EventHandler(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. 连接成功后,对收发参数进行初始化

         ///   
            /// 初始化收发参数  
            ///   
            ///   
            private void initArgs(SocketAsyncEventArgs e)
            {
                m_bufferManager.InitBuffer();
                //发送参数  
                initSendArgs();
                //接收参数  
                receiveEventArgs.Completed += new EventHandler(IO_Completed);
                receiveEventArgs.UserToken = e.UserToken;
                receiveEventArgs.ArgsTag = 0;
                m_bufferManager.SetBuffer(receiveEventArgs);
    
                //启动接收,不管有没有,一定得启动.否则有数据来了也不知道.  
                if (!e.ConnectSocket.ReceiveAsync(receiveEventArgs))
                    ProcessReceive(receiveEventArgs);
            }

     

  6. 初始化发送参数MySocketEventArgs 
        ///   
            /// 初始化发送参数MySocketEventArgs  
            ///   
            ///   
            MySocketEventArgs initSendArgs()
            {
                MySocketEventArgs sendArg = new MySocketEventArgs();
                sendArg.Completed += new EventHandler(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  
            //  
            //   
            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);
                }
            }
    
            ///   
            /// 使用新进程通知事件回调  
            ///   
            ///   
            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#基础的读者不是难事,这里就不详细介绍了。建好的窗体如下图:基于SocketAsyncEventArgs(IOCP)实现的高并发TCP客户端_第2张图片图中主要是服务器和客户端的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客户端

你可能感兴趣的:(C#,TCP)