我曾经写过类似的东西。从多年前的研究来看,编写自己的套接字实现是最好的选择,使用异步套接字。这意味着实际上没有做任何事情的客户端需要相对较少的资源。任何发生的事件都由.NET线程池处理。
我把它写成一个管理服务器所有连接的类。
我只是用一个列表来保存所有客户端连接,但如果你需要更快的查找大型列表,可以按照自己的方式编写。
private List<xConnection> _sockets
同时,您需要确保套接字实际上正在监听传入的连接。
private System.Net.Sockets.Socket _serverSocket;
开始方法实际上启动了服务器套接字并开始侦听任何传入连接。
public bool Start()
{
System.Net.IPHostEntry localhost = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName());
System.Net.IPEndPoint serverEndPoint;
try
{
serverEndPoint = new System.Net.IPEndPoint(localhost.AddressList[0], _port);
}
catch (System.ArgumentOutOfRangeException e)
{
throw new ArgumentOutOfRangeException("Port number entered would seem to be invalid, should be between 1024 and 65000", e);
}
try
{
_serverSocket = new System.Net.Sockets.Socket(serverEndPoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
}
catch (System.Net.Sockets.SocketException e)
{
throw new ApplicationException("Could not create socket, check to make sure not duplicating port", e);
}
try
{
_serverSocket.Bind(serverEndPoint);
_serverSocket.Listen(_backlog);
}
catch (Exception e)
{
throw new ApplicationException("An error occurred while binding socket. Check inner exception", e);
}
try
{
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
throw new ApplicationException("An error occurred starting listeners. Check inner exception", e);
}
return true;
}
我想指出异常处理代码看起来不太好,但原因是我在其中有异常抑制代码,以便任何异常都将被抑制并返回
false
,如果设置了配置选项,但为了简洁起见,我想删除它。
上面的_serverSocket.BeginAccept(new AsyncCallback(acceptCallback)), _serverSocket)基本上将我们的服务器套接字设置为在用户连接时调用acceptCallback方法。此方法从.NET线程池运行,该线程池自动处理创建额外的工作线程,如果您有许多阻塞操作,则应该最优地处理服务器上的任何负载。
private void acceptCallback(IAsyncResult result)
{
xConnection conn = new xConnection();
try
{
System.Net.Sockets.Socket s = (System.Net.Sockets.Socket)result.AsyncState;
conn = new xConnection();
conn.socket = s.EndAccept(result);
conn.buffer = new byte[_bufferSize];
lock (_sockets)
{
_sockets.Add(conn);
}
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
}
上述代码其实只是完成了接受连接,排队
BeginReceive
,当客户端发送数据时会运行一个回调函数,然后排队下一个
acceptCallback
用于接受下一个客户端连接。
BeginReceive
方法告诉套接字在接收到来自客户端的数据时该做什么。对于
BeginReceive
,您需要提供一个字节数组,这是客户端发送数据时它将复制数据的位置。我们使用
ReceiveCallback
方法来处理接收到的数据。
private void ReceiveCallback(IAsyncResult result)
{
xConnection conn = (xConnection)result.AsyncState;
try
{
int bytesRead = conn.socket.EndReceive(result);
if (bytesRead > 0)
{
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
}
else
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
}
编辑:在这个模式中,我忘了提到在代码的这个区域:
//put whatever you want to do when you receive data here
//Queue the next receive
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
通常情况下,在任何你想要的代码中,我会将数据包重新组装成消息,并将其作为工作项添加到线程池中。这样,当处理消息的代码运行时,来自客户端的下一个数据块的BeginReceive不会被延迟。
接受回调通过调用EndReceive完成了对数据套接字的读取,这填充了在BeginReceive函数中提供的缓冲区。一旦你在我的注释处完成了你想要做的事情,我们就调用下一个BeginReceive
方法,如果客户端发送更多数据,它将再次运行回调函数。
现在是真正棘手的部分:当客户端发送数据时,你的接收回调可能只会调用部分消息。重新组装可能变得非常复杂。我使用了自己的方法并创建了一种类似专有协议的方式来实现这一点。我省略了它,但如果你需要,我可以添加进去。这个处理程序实际上是我写过的最复杂的代码。
public bool Send(byte[] message, xConnection conn)
{
if (conn != null && conn.socket.Connected)
{
lock (conn.socket)
{
conn.socket.Send(bytes, bytes.Length, SocketFlags.None);
}
}
else
return false;
return true;
}
上面的发送方法实际上使用了同步的
Send
调用。对我来说,由于消息大小和应用程序的多线程性质,这是可以接受的。如果您想要向每个客户端发送消息,只需要循环遍历 _sockets 列表。
您在上面看到的 xConnection 类基本上是一个简单的包装器,用于包含字节缓冲区的套接字,并在我的实现中添加了一些额外功能。
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
另外作为参考,这里是我经常包含的using
,因为当它们没有被包含时,我总是感到很烦。
using System.Net.Sockets;
我希望这有所帮助。代码可能不是最干净的,但它能运行。代码中还有一些细微之处需要注意,不要轻易修改。首先,同时只调用一个
BeginAccept
方法。很久以前,.NET 中就存在一个非常恼人的 bug,但具体细节我已经记不清了。
另外,在 ReceiveCallback
代码中,我们在将数据加入队列之前处理从套接字接收到的任何内容。这意味着对于单个套接字,我们实际上只在任何时候都在 ReceiveCallback
中一次,而且我们不需要使用线程同步。然而,如果你重新排列代码以便在提取数据后立即调用下一个接收操作(这可能会更快),则需要确保正确同步线程。
此外,我删去了很多代码,但留下了核心过程。这应该是你设计的良好起点。如果你有更多关于此问题的问题,请在下面留言。