如何使用C#4.0编写可伸缩的套接字服务器?

我想编写一个简单的套接字服务器,但是我希望它可以垂直扩展,例如,不是每个连接创建一个线程,也不是很长时间运行的任务,这可能会占用所有线程。

服务器接收包含查询的请求并流式传输任意大的结果。

我希望使用C#4中提供的技术和库来实现这一点的惯用方法,重点是简单的代码,而不是原始性能。

重新打开套接字服务器是可伸缩系统的有用部分。 如果要水平缩放,则有不同的技术。 如果您从未创建套接字服务器,则应该无法回答此问题。

我一直在做一两个类似的事情,所以希望我能帮助你一点点。

如果您专注于简单代码,我建议使用TcpClient和TcpListener类。 它们都使sockets更容易使用。 虽然它们自.NET Framework 1.1以来就已经存在,但它们已经更新,仍然是您最好的选择。

在如何利用.NET Framework 4.0编写简单代码方面,任务首先浮现在脑海中。 它们使编写异步代码变得更加痛苦,一旦C#5出现( 新的异步和等待关键字 ),迁移代码将变得更加容易。 以下是Tasks如何简化代码的示例:

而不是使用tcpListener.BeginAcceptTcpClient(AsyncCallback callback, object state); 并提供一个调用EndAcceptTcpClient();的回调方法EndAcceptTcpClient(); 并且可选地转换你的状态对象,C#4允许你利用闭包,lambda和Tasks来使这个过程更具可读性和可扩展性。 这是一个例子:

 private void AcceptClient(TcpListener tcpListener) { Task acceptTcpClientTask = Task.Factory.FromAsync(tcpListener.BeginAcceptTcpClient, tcpListener.EndAcceptTcpClient, tcpListener); // This allows us to accept another connection without a loop. // Because we are within the ThreadPool, this does not cause a stack overflow. acceptTcpClientTask.ContinueWith(task => { OnAcceptConnection(task.Result); AcceptClient(tcpListener); }, TaskContinuationOptions.OnlyOnRanToCompletion); } private void OnAcceptConnection(TcpClient tcpClient) { string authority = tcpClient.Client.RemoteEndPoint.ToString(); // Format is: IP:PORT // Start a new Task to handle client-server communication } 

FromAsync非常有用,因为Microsoft提供了许多可以简化常见异步操作的重载。 这是另一个例子:

 private void Read(State state) { // The int return value is the amount of bytes read accessible through the Task's Result property. Task readTask = Task.Factory.FromAsync(state.NetworkStream.BeginRead, state.NetworkStream.EndRead, state.Data, state.BytesRead, state.Data.Length - state.BytesRead, state, TaskCreationOptions.AttachedToParent); readTask.ContinueWith(ReadPacket, TaskContinuationOptions.OnlyOnRanToCompletion); readTask.ContinueWith(ReadPacketError, TaskContinuationOptions.OnlyOnFaulted); } 

State只是一个用户定义的类,通常只包含TcpClient实例,数据(字节数组),也可能包含读取的字节。

正如你所看到的,ContinueWith可以用来取代许多繁琐的try-catches ,直到现在它们都是必要的恶魔。

在你的post开头你提到不想为每个连接创建一个线程或创建很长时间运行的任务,我想我会在这一点上解决这个问题。 就个人而言,我没有看到为每个连接创建一个线程的问题。

但是,您必须小心使用Tasks(对ThreadPool的抽象)来进行长时间运行。 ThreadPool非常有用,因为创建新线程的开销不可忽略,对于诸如读取或写入数据以及处理客户端连接等简短任务,首选任务。

您必须记住ThreadPool是一个具有专门function的共享资源(避免花费更多时间创建线程而不是实际使用它的开销)。 因为它是共享的,如果你使用了一个线程,另一个资源就不能,这很快就会导致线程池饥饿和死锁场景。