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

时间:2011-10-04 22:10:10

标签: c# sockets c#-4.0 task-parallel-library tcplistener

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

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

我希望使用C#4中提供的技术和库来实现这一目标,并强调简单的代码,而不是原始性能。

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

1 个答案:

答案 0 :(得分:15)

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

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

在如何利用.NET Framework 4.0编写简单代码方面,任务首先浮现在脑海中。它们使编写异步代码变得更加痛苦,而且一旦C#5出现(new async and await keywords),迁移代码就会变得更加容易。以下是Tasks如何简化代码的示例:

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

private void AcceptClient(TcpListener tcpListener)
{
    Task<TcpClient> acceptTcpClientTask = Task.Factory.FromAsync<TcpClient>(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<int> readTask = Task<int>.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,直到现在还是一种必要的邪恶。

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

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

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