之前在文章基于SocketAsyncEventArgs(IOCP)的高性能TCP服务器实现(二)——服务端信息接收窗体实现(C#)这篇文章中,我介绍了一个高性能的TCP服务器,目的是接受数千台基于TCP协议的设备发送的信息,并且这些设备只是单向发送,不需要服务器返回信息,设备的信息发送频率在一秒钟一次。服务器端接受到之后,解析信息,然后入库。并且在文章中,我也实现了一个软件窗体,为了有效的检验这个软件,我需要大量的设备同时向这个服务器软件发送信息,但是一般情况,在开发中不可能同时提供这么大量的设备,因此需要我们做一个模拟的软件,在网络上搜索了很久,发现都不太符合我个人的需求,那么在翻阅大量大神的文章之后,自己做了一个模拟软件,不太成熟,欢迎各位指正。
这个窗体很简单,通过设置服务器和客户端IP地址之后,可以自定义模拟设备的数量,非常方便。窗体中发送信息,是我根据项目实际的信息格式放的一个样例,读者可以自行替换成其他信息。下面详细介绍下 我是如何实现的。
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; }
}
// 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 就是客户端与服务器端连接的套接字。
实现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;
}
// 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判断是否连接成功,如果连接成功则对收发参数进行初始化。
连接成功后,对收发参数进行初始化
///
/// 初始化收发参数
///
///
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);
}
///
/// 初始化发送参数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;
}
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中判断当前套接字是接收还是发送,然后分派给不同的处理函数。 // 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是在类外面定义的委托函数,比如接收到服务器信息后,需要展示处理或者入库之类的,读者可以自己实现。
重头戏,客户端向服务器发送信息的函数
// 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类,帮我们进行信息的处理,包括服务器信息接收和向服务器发送信息,甚至可以接收服务器服务停止的状态处理。
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;
}
按下停止测试后,主要是把Break 变量设置为true即可,然后就是记录一下运行时间反馈到窗体上。
通过上述两大步骤,我们就可以实现了基于SocketAsyncEventArgs(IOCP)的高并发TCP客户端,这测试过一万个客户端,可以正常运行。这里唯一的问题就是,虽然是为每个客户端分配了不同的线程,但是对于服务器来说还不算是同时接收到客户端信息,如果改进?请有识之士不吝赐教!文章最后,提供下我的实现完整代码,下载链接:基于SocketAsyncEventArgs(IOCP)实现的高并发TCP客户端