服务器端概览
服务器端的启动包括构造socket,绑定其至某地址,建立监听队列。代码如下:
private Socket _serverSocket; private void SetupServerSocket() { //解析本地机器信息 IPHostEntry localMachineInfo = Dns.GetHostEntry(Dns.GetHostName()); IPEndPoint myEndpoint = new IPEndPoint( localMachineInfo.AddressList[0], _port); // 构造socket,绑定并开始监听 _serverSocket = new Socket(myEndpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); _serverSocket.Bind(myEndpoint); _serverSocket.Listen((int)SocketOptionName.MaxConnections); }
有两种方法可以避免创建单独的线程处理服务器端的socket。第一种是Select 方法——传统的在一个单独的线程里面处理多个I/O流。第二种是使用异步I/O。
线程化服务器端
线程化服务器端需要管理好所有对socket进行处理的线程。首先要考虑的情况就是调用Accept方法。在同步服务器端,调用Accept 方法必须在其本身的线程里运行,以确保socket及时被接收。一下代码展示的是如何创建新的线程处理接收连接。
class ThreadedServer { private Socket _serverSocket; private int _port; public ThreadedServer(int port) { _port = port; } private class ConnectionInfo { public Socket Socket; public Thread Thread; } private Thread _acceptThread; private List<ConnectionInfo> _connections = new List<ConnectionInfo>(); public void Start() { SetupServerSocket(); _acceptThread = new Thread(AcceptConnections); _acceptThread.IsBackground = true; _acceptThread.Start(); } private void SetupServerSocket() { IPHostEntry localMachineInfo = Dns.GetHostEntry(Dns.GetHostName()); IPEndPoint myEndpoint = new IPEndPoint( localMachineInfo.AddressList[0], _port); _serverSocket = new Socket(myEndpoint.Address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); _serverSocket.Bind(myEndpoint); _serverSocket.Listen((int)SocketOptionName.MaxConnections); } private void AcceptConnections() { while (true) { // 接收连接 Socket socket = _serverSocket.Accept(); ConnectionInfo connection = new ConnectionInfo(); connection.Socket = socket; // 为处理连接创建新线程 connection.Thread = new Thread(ProcessConnection); connection.Thread.IsBackground = true; connection.Thread.Start(connection); // 保存socket lock (_connections) _connections.Add(connection); } } private void ProcessConnection(object state) { ConnectionInfo connection = (ConnectionInfo)state; byte[] buffer = new byte[255]; try { while (true) { int bytesRead = connection.Socket.Receive(buffer); if (bytesRead > 0) { lock (_connections) { foreach (ConnectionInfo conn in _connections) { if (conn != connection) { conn.Socket.Send( buffer, bytesRead, SocketFlags.None); } } } } else if (bytesRead == 0) return; } } catch (SocketException exc) { Console.WriteLine("Socket exception: " + exc.SocketErrorCode); } catch (Exception exc) { Console.WriteLine("Exception: " + exc); } finally { connection.Socket.Close(); lock (_connections) _connections.Remove(connection); } } }
可以看出,一旦socket被传递进来,一个新线程就会被创建以接收该socket。这个线程只是接收socket的数据并对其进行处理。
这种线程化的方法如果作为只有很少的客户端的服务器端性能是非常好的,并且也容易编写代码。但是,当连接数量变大时就不行了。主要的缺点就是太多的线程数目被创建和销毁,而这会消耗太多的系统资源。
使用Select 处理多个I/O
有很多种方法可以避免为每个连接单独创建一个线程。第一个是使用线程池——一种只需要对以上线程化的服务器端做最少更改的方法。这种方法的好处是控制了同时出现的线程的数目,在连接存在时间较短的情况下是一种切实可行的方法。但是,如果每个连接都需要保持很长的时间的话,该方法就不那么适合了。一个更好的解决办法是使用静态的Socket.Select 方法。使用该方法需要传递三个要监视的socket列表,一个为可读性,一个为可写性,另外一个为处理错误情况。该方法也接收一个微秒的参数,指示该方法反应之前等待的时间。
每次调用Select方法,都需要创建需要监控的socket的列表。Select返回后,这个列表被修改得只包含需要服务的socket。尽管看起来不错,实际上是非常没有效率的。设想如果同时有100个等待处理的I/O操作,第100个socket会一直在等待,直到前面99个socket的服务或者列表完成。socket服务同时也会阻止代码重新进入Select,这会造成更过潜在的空闲线程。在重新调用Select 方法之前必须保证处理了所有socket的I/O;如果没有做到这些会导致同一个I/O 的多余一次的提醒。
这种方法还有别的性能上的缺陷。如果客户端的连接超过一千,性能上就会有显著的降低,这是因为核心必须查找每一个socket确保其数据的可用性。相比每个连接一个线程的方法,尽管你可以用这种方法处理明显多的连接,但是这个量也不是很大。而且你需要管理三个不同的列表,并且对每一个反复声明以处理每一个请求。从线程使用的观点来看这很有效,但反应灵敏性却下降了很多。
异步 I/O
异步I/O降低了创建和管理线程的需求,这直接会使代码相对简化,并且也是一个更高效的I/O模型。异步I/O使用回调函数处理传进的数据和连接,这就意味着将不需要建立socket列表,也不需要创建新的线程处理等待的I/O。
每一个基于.NET的应用程序都有一个线程池与其相连。当一个异步I/O函数有数据需要处理时,就会使用.NET线程池里的一个线程执行该回调函数。该回调函数执行完成以后,该线程就会被回收到线程池中。这种方法与线程池中的一个线程处理一个特定的请求是不同的;在那种情况下,线程池中的线程只被用来处理一个单独的I/O 操作。
.NET使用一个相当简单的模型处理异步操作。一切所需要做的就是调用相关的Begin 方法(BeginAccept, BeginSend, BeginReceive等等),利用合适的回调委托,在回调函数里面调用相应的End 方法(EndAccept, EndSend, EndReceive等等)以得到相应的结果。所有的异步Begin 方法都允许使用者传递一个内容状态对象,它可以是任意需要的对象。异步操作结束后,这个对象就是传递到回调函数里的IAsyncResult 一部分。
前面两种方法创建的服务器端中,线程化的服务器端运行较快,但是接收不了太多的连接;而基于Select方法的服务器端没有连接数的限制,但是付出了性能降低的代价。异步服务器端消除了连接数目的限制,并且没有性能方面的降低。实际上,这种服务器端比线程模型的性能还要优越,因为代码里面并没有不断的创建和销毁线程。
在异步服务器端里,首先调用BeginAccept方法。在接受连接操作完成后,下一步是异步读取的排队。这允许服务器端读取socket,而不必查找或者创建线程。异步读取从对BeginRead方法的调用开始,对用EndRead后返回结果。