最近碰到一个需求,就是有数千台设备,这些设备都是通过运营商的网络,基于TCP/IP协议发送一组信息给服务器,并且这些设备只是单向发送,不需要服务器返回信息,设备的信息发送频率在一秒钟一次。服务器端接受到之后,解析信息,然后入库。这是正常的操作。所以对服务器端的信息接受软件提出了较高的要求,这么多设备,这么高的频率,要保证服务器端接受软件的健壮,不能崩溃掉。在网上查找了相关文章之后,发现SocketAsyncEventArgs这个类,可以实现上述我想要的功能。参考了其他大神的文章之后,在下不才在这里滥竽充数一下,各位见谅。
SocketAsyncEventArgs这个是微软提供的一个类,可以在https://docs.microsoft.com/zh-cn/dotnet/api/system.net.sockets.socketasynceventargs?redirectedfrom=MSDN&view=netframework-4.8官网查看。但是微软的例子比较粗糙,没有达到我想要的目的,所以我这里进行了一些改造。首先创建一个类,我这里叫SocketServer。
在类SocketServer里面,首先定义一些变量,使得我的这个服务端的接收软件,可以支持较多的客户端,也就是设备。
private int m_maxConnectNum; //最大连接数
private int m_revBufferSize; //最大接收字节数
BufferManager m_bufferManager; //处理信息的工具
const int opsToAlloc = 2;
Socket listenSocket; //监听Socket
SocketEventPool m_pool;
int m_clientCount; //连接的客户端数量
Semaphore m_maxNumberAcceptedClients;
List m_clients; //客户端列表
其中BufferManager这个类是用来管理,客户端发来的信息的,不是特别重要,具体代码网上已经有人实现了,这里我就直接贴出来了:
// This class creates a single large buffer which can be divided up
// and assigned to SocketAsyncEventArgs objects for use with each
// socket I/O operation.
// This enables bufffers to be easily reused and guards against
// fragmenting heap memory.
//
// The operations exposed on the BufferManager class are not thread safe.
class BufferManager
{
int m_numBytes; // the total number of bytes controlled by the buffer pool
byte[] m_buffer; // the underlying byte array maintained by the Buffer Manager
Stack m_freeIndexPool; //
int m_currentIndex;
int m_bufferSize;
public BufferManager(int totalBytes, int bufferSize)
{
m_numBytes = totalBytes;
m_currentIndex = 0;
m_bufferSize = bufferSize;
m_freeIndexPool = new Stack();
}
// Allocates buffer space used by the buffer pool
public void InitBuffer()
{
// create one big large buffer and divide that
// out to each SocketAsyncEventArg object
m_buffer = new byte[m_numBytes];
}
// Assigns a buffer from the buffer pool to the
// specified SocketAsyncEventArgs object
//
// true if the buffer was successfully set, else false
public bool SetBuffer(SocketAsyncEventArgs args)
{
if (m_freeIndexPool.Count > 0)
{
args.SetBuffer(m_buffer, m_freeIndexPool.Pop(), m_bufferSize);
}
else
{
if ((m_numBytes - m_bufferSize) < m_currentIndex)
{
return false;
}
args.SetBuffer(m_buffer, m_currentIndex, m_bufferSize);
m_currentIndex += m_bufferSize;
}
return true;
}
// Removes the buffer from a SocketAsyncEventArg object.
// This frees the buffer back to the buffer pool
public void FreeBuffer(SocketAsyncEventArgs args)
{
m_freeIndexPool.Push(args.Offset);
args.SetBuffer(null, 0, 0);
}
}
SocketEventPool这个类用来异步管理客户端的,代码如下:
class SocketEventPool
{
Stack m_pool;
public SocketEventPool(int capacity)
{
m_pool = new Stack(capacity);
}
public void Push(SocketAsyncEventArgs item)
{
if (item == null) { throw new ArgumentNullException("Items added to a SocketAsyncEventArgsPool cannot be null"); }
lock (m_pool)
{
m_pool.Push(item);
}
}
// Removes a SocketAsyncEventArgs instance from the pool
// and returns the object removed from the pool
public SocketAsyncEventArgs Pop()
{
lock (m_pool)
{
return m_pool.Pop();
}
}
// The number of SocketAsyncEventArgs instances in the pool
public int Count
{
get { return m_pool.Count; }
}
public void Clear()
{
m_pool.Clear();
}
}
客户端过来的信息,在服务器接收端都是异步处理,用AsyncUserToken这个类来管理客户端(也就是设备),代码也有人已经实现了,下面贴出代码:
class AsyncUserToken
{
///
/// 客户端IP地址
///
public IPAddress IPAddress { get; set; }
///
/// 远程地址
///
public EndPoint Remote { get; set; }
///
/// 通信SOKET
///
public Socket Socket { get; set; }
///
/// 连接时间
///
public DateTime ConnectTime { get; set; }
/////
///// 所属用户信息
/////
//public UserInfoModel UserInfo { get; set; }
///
/// 数据缓存区
///
public List Buffer { get; set; }
public AsyncUserToken()
{
this.Buffer = new List();
}
}
上面这两段代码都是辅助类,没那么重要,这里不多说。下面重点讲讲,我们这个服务器接收端怎么来实现的。首先关心的是,服务器端接收到客户端发送来的信息,要进行处理,这里定义了一个委托来帮助我们,也就是说服务器接收到信息,就会进入这个函数进行处理:
///
/// 接收到客户端的数据
///
/// 客户端
/// 客户端数据
public delegate void OnReceiveData(AsyncUserToken token, byte[] buff);
///
/// 接收到客户端的数据事件
///
public event OnReceiveData ReceiveClientData;
这样调用我实现的服务器端时,就可以直接注册自定义的函数来绑定,例如下面代码:
_socketServer.ReceiveClientData += onReceiveData;
private void onReceiveData(AsyncUserToken token, byte[] buff)
{
//Dosomething;
}
好了,上面我们定义了一些参数,那么在SocketServer这个类初始化的时候,就需要把这些参数初始化,看下面代码:
///
/// 构造函数
///
/// 最大连接数
/// 缓存区大小
public SocketServer(int numConnections, int receiveBufferSize)
{
m_clientCount = 0;
m_maxConnectNum = numConnections;
m_revBufferSize = receiveBufferSize;
// allocate buffers such that the maximum number of sockets can have one outstanding read and
//write posted to the socket simultaneously
m_bufferManager = new BufferManager(receiveBufferSize * numConnections * opsToAlloc, receiveBufferSize);
m_pool = new SocketEventPool(numConnections);
m_maxNumberAcceptedClients = new Semaphore(numConnections, numConnections);
}
上面代码是类SocketServer的构造函数,在构造函数里我们设定了最大连接数以及每条消息的大小,因为我们要希望我们的服务器端软件能够接受更多客户端的信息。通过构造函数初始化参数之后,我们需要给每一个客户端或者说每一个Socket分配一定的内存,如下代码:
///
/// 初始化
///
public void Init()
{
// Allocates one large byte buffer which all I/O operations use a piece of. This gaurds
// against memory fragmentation
m_bufferManager.InitBuffer();
m_clients = new List();
// preallocate pool of SocketAsyncEventArgs objects
SocketAsyncEventArgs readWriteEventArg;
for (int i = 0; i < m_maxConnectNum; i++)
{
readWriteEventArg = new SocketAsyncEventArgs();
readWriteEventArg.Completed += new EventHandler(IO_Completed);
readWriteEventArg.UserToken = new AsyncUserToken();
// assign a byte buffer from the buffer pool to the SocketAsyncEventArg object
m_bufferManager.SetBuffer(readWriteEventArg);
// add SocketAsyncEventArg to the pool
m_pool.Push(readWriteEventArg);
}
}
void IO_Completed(object sender, SocketAsyncEventArgs 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:
ProcessSend(e);
break;
default:
throw new ArgumentException("The last operation completed on the socket was not a receive or send");
}
}
分配好内容之后,所有的准备工作就好了,下面我们可以直接写SocketServer这个类的启动函数了:
///
/// 启动服务
///
///
public bool Start(IPEndPoint localEndPoint)
{
try
{
m_clients.Clear();
listenSocket = new Socket(localEndPoint.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
listenSocket.Bind(localEndPoint);
// start the server with a listen backlog of 100 connections
listenSocket.Listen(m_maxConnectNum);
// post accepts on the listening socket
StartAccept(null);
return true;
}
catch (Exception)
{
return false;
}
}
// Begins an operation to accept a connection request from the client
//
// The context object to use when issuing
// the accept operation on the server's listening socket
public void StartAccept(SocketAsyncEventArgs acceptEventArg)
{
if (acceptEventArg == null)
{
acceptEventArg = new SocketAsyncEventArgs();
acceptEventArg.Completed += new EventHandler(AcceptEventArg_Completed);
}
else
{
// socket must be cleared since the context object is being reused
acceptEventArg.AcceptSocket = null;
}
m_maxNumberAcceptedClients.WaitOne();
if (!listenSocket.AcceptAsync(acceptEventArg))
{
ProcessAccept(acceptEventArg);
}
}
private void ProcessAccept(SocketAsyncEventArgs e)
{
try
{
Interlocked.Increment(ref m_clientCount);
// Get the socket for the accepted client connection and put it into the
//ReadEventArg object user token
SocketAsyncEventArgs readEventArgs = m_pool.Pop();
AsyncUserToken userToken = (AsyncUserToken)readEventArgs.UserToken;
userToken.Socket = e.AcceptSocket;
userToken.ConnectTime = DateTime.Now;
userToken.Remote = e.AcceptSocket.RemoteEndPoint;
userToken.IPAddress = ((IPEndPoint)(e.AcceptSocket.RemoteEndPoint)).Address;
lock (m_clients) { m_clients.Add(userToken); }
if (ClientNumberChange != null)
ClientNumberChange(1, userToken);
if (!e.AcceptSocket.ReceiveAsync(readEventArgs))
{
ProcessReceive(readEventArgs);
}
}
catch (Exception me)
{
LogHelper.WriteLog(me.Message + "\r\n" + me.StackTrace);
}
// Accept the next connection request
if (e.SocketError == SocketError.OperationAborted) return;
StartAccept(e);
}
通过上述步骤,我们就可以通过一个IP地址和一个端口来启动SocketServer服务了。为了使SocketServer这个服务器接收软件更加健壮,我们再添加一个停止服务的功能:
///
/// 停止服务
///
public void Stop()
{
foreach (AsyncUserToken token in m_clients)
{
try
{
listenSocket.Shutdown(SocketShutdown.Both);
}
catch (Exception) { }
}
listenSocket.Close();
int c_count = m_clients.Count;
lock (m_clients) { m_clients.Clear(); }
if (ClientNumberChange != null)
ClientNumberChange(-c_count, null);
}
上述代码通过循环来关闭每一个Socket连接。
在前面的代码中,我们在初始化的时候,已经注册了SocketAsyncEventArgs这个类中的Completed事件,这个事件的意思就是,服务器不管是接收还是发送,完成后的函数都是IO_Completed。
readWriteEventArg.Completed += new EventHandler(IO_Completed);
void IO_Completed(object sender, SocketAsyncEventArgs 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:
ProcessSend(e);
break;
default:
throw new ArgumentException("The last operation completed on the socket was not a receive or send");
}
}
而在委托的IO_Completed函数中,我们通过接收到的信息判断是接收消息还是发送消息,如果是接收,那么我们使用ProcessReceive(SocketAsyncEventArgs e)这个函数来处理接收到的信息。
// 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
AsyncUserToken token = (AsyncUserToken)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 (token.Buffer)
{
token.Buffer.AddRange(data);
}
if (ReceiveClientData != null)
{
ReceiveClientData(token, data);
}
//继续接收. 为什么要这么写,请看Socket.ReceiveAsync方法的说明
if (!token.Socket.ReceiveAsync(e))
this.ProcessReceive(e);
}
else
{
CloseClientSocket(e);
}
}
catch (Exception xe)
{
LogHelper.WriteLog(xe.Message + "\r\n" + xe.StackTrace);
}
}
上面的代码可以看出来,我们接收到信息后,是要再把信息委托给另外的注册委托函数进行处理:
if (ReceiveClientData != null)
{
ReceiveClientData(token, data);
}
到这里为止,我们就把SocketServer这个类的接收功能给实现了,如果有读者对客户端发送信息感兴趣,就直接参考下面的代码:
// 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)
{
// done echoing data back to the client
AsyncUserToken token = (AsyncUserToken)e.UserToken;
// read the next block of data send from the client
bool willRaiseEvent = token.Socket.ReceiveAsync(e);
if (!willRaiseEvent)
{
ProcessReceive(e);
}
}
else
{
CloseClientSocket(e);
}
}
//关闭客户端
private void CloseClientSocket(SocketAsyncEventArgs e)
{
AsyncUserToken token = e.UserToken as AsyncUserToken;
lock (m_clients) { m_clients.Remove(token); }
//如果有事件,则调用事件,发送客户端数量变化通知
if (ClientNumberChange != null)
ClientNumberChange(-1, token);
// close the socket associated with the client
try
{
token.Socket.Shutdown(SocketShutdown.Send);
}
catch (Exception) { }
token.Socket.Close();
// decrement the counter keeping track of the total number of clients connected to the server
Interlocked.Decrement(ref m_clientCount);
m_maxNumberAcceptedClients.Release();
// Free the SocketAsyncEventArg so they can be reused by another client
e.UserToken = new AsyncUserToken();
m_pool.Push(e);
}
///
/// 对数据进行打包,然后再发送
///
///
///
///
public void SendMessage(AsyncUserToken token, byte[] message)
{
if (token == null || token.Socket == null || !token.Socket.Connected)
return;
try
{
//新建异步发送对象, 发送消息
SocketAsyncEventArgs sendArg = new SocketAsyncEventArgs();
sendArg.UserToken = token;
sendArg.SetBuffer(byte, 0, byte.Length); //将数据放置进去.
token.Socket.SendAsync(sendArg);
}
catch (Exception e)
{
LogHelper.WriteLog("SendMessage - Error:" + e.Message);
}
}
上述的代码,都已提供下载,下载地址:
https://download.csdn.net/download/aplsc/11817545