我过去写过类似的东西。我多年前的研究表明,使用 异步 套接字编写自己的套接字实现是最好的选择。这意味着没有真正做任何事情的客户实际上需要相对较少的资源。发生的任何事情都由 .NET 线程池处理。
我把它写成一个管理服务器所有连接的类。
我只是使用一个列表来保存所有客户端连接,但如果您需要更快地查找更大的列表,您可以随意编写。
private List<xConnection> _sockets;
您还需要实际监听传入连接的套接字。
private System.Net.Sockets.Socket _serverSocket;
start 方法实际上是启动服务器套接字并开始侦听任何传入的连接。
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
{
//warning, only call this once, this is a bug in .net 2.0 that breaks if
// you're running multiple asynch accepts, this bug may be fixed, but
// it was a major pain in the rear previously, so make sure there is only one
//BeginAccept running
_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
{
//Finish accepting the connection
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);
}
//Queue receiving of data from the connection
conn.socket.BeginReceive(conn.buffer, 0, conn.buffer.Length, SocketFlags.None, new AsyncCallback(ReceiveCallback), conn);
//Queue the accept of the next incoming connection
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (SocketException e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
catch (Exception e)
{
if (conn.socket != null)
{
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
//Queue the next accept, think this should be here, stop attacks based on killing the waiting listeners
_serverSocket.BeginAccept(new AsyncCallback(acceptCallback), _serverSocket);
}
}
上面的代码基本上刚刚完成接受进来的连接,将BeginReceive排队,这是一个在客户端发送数据时运行的回调,然后将下一个acceptCallback排队,它将接受下一个客户端连接在。
BeginReceive 方法调用告诉套接字在接收到来自客户端的数据时要做什么。对于BeginReceive,你需要给它一个字节数组,当客户端发送数据时,它将复制数据。 ReceiveCallback 方法将被调用,这就是我们处理接收数据的方式。
private void ReceiveCallback(IAsyncResult result)
{
//get our connection from the callback
xConnection conn = (xConnection)result.AsyncState;
//catch any errors, we'd better not have any
try
{
//Grab our buffer and count the number of bytes receives
int bytesRead = conn.socket.EndReceive(result);
//make sure we've read something, if we haven't it supposadly means that the client disconnected
if (bytesRead > 0)
{
//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);
}
else
{
//Callback run but no data, close the connection
//supposadly means a disconnect
//and we still have to close the socket, even though we throw the event later
conn.socket.Close();
lock (_sockets)
{
_sockets.Remove(conn);
}
}
}
catch (SocketException e)
{
//Something went terribly wrong
//which shouldn't have happened
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 都不会延迟。
accept 回调通过调用 end receive 来完成对数据套接字的读取。这将填充开始接收函数中提供的缓冲区。一旦你在我留下评论的地方做任何你想做的事,我们就会调用下一个 BeginReceive 方法,如果客户端发送更多数据,它将再次运行回调。
现在真正棘手的部分是:当客户端发送数据时,您的接收回调可能只与部分消息一起调用。重新组装会变得非常非常复杂。我使用自己的方法并创建了一种专有协议来做到这一点。我把它省略了,但如果你要求,我可以添加它。这个处理程序实际上是我写过的最复杂的一段代码。
public bool Send(byte[] message, xConnection conn)
{
if (conn != null && conn.socket.Connected)
{
lock (conn.socket)
{
//we use a blocking mode send, no async on the outgoing
//since this is primarily a multithreaded application, shouldn't cause problems to send in blocking mode
conn.socket.Send(bytes, bytes.Length, SocketFlags.None);
}
}
else
return false;
return true;
}
上面的 send 方法实际上使用了同步的Send 调用。对我来说这很好,因为我的应用程序的消息大小和多线程性质。如果你想发送给每个客户端,你只需要遍历 _sockets 列表。
您在上面看到的 xConnection 类基本上是一个简单的套接字包装器,用于包含字节缓冲区,在我的实现中还有一些附加功能。
public class xConnection : xBase
{
public byte[] buffer;
public System.Net.Sockets.Socket socket;
}
这里还包含了usings 作为参考,因为当它们不被包含时我总是很生气。
using System.Net.Sockets;
我希望这会有所帮助。它可能不是最干净的代码,但它可以工作。代码也有一些细微差别,您应该对更改感到厌烦。一方面,任何时候都只能调用一个BeginAccept。过去有一个非常烦人的 .NET 错误,那是几年前的事了,所以我不记得细节了。
此外,在ReceiveCallback 代码中,我们在排队下一次接收之前处理从套接字接收到的任何内容。这意味着对于单个套接字,我们实际上在任何时间点都只在ReceiveCallback 中出现过一次,并且我们不需要使用线程同步。但是,如果您重新排序以在提取数据后立即调用下一个接收,这可能会更快一些,您需要确保正确同步线程。
此外,我修改了很多代码,但留下了正在发生的事情的本质。这应该是你设计的一个好的开始。如果您对此还有任何疑问,请发表评论。