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

8
我想编写一个简单的套接字服务器,但我希望它具有垂直可扩展性,例如,不为每个连接或非常长时间运行的任务创建线程,这可能会消耗所有线程。
服务器接收包含查询的请求并流传任意大的结果。
我想使用C# 4中可用的技术和库的成语方式来完成这项工作,重点是简单的代码而不是原始性能。
重新开放 套接字服务器是可扩展系统的有用部分。如果您要进行水平扩展,则有不同的技术。如果您从未创建过套接字服务器,则可能无法回答此问题。
1个回答

15

我已经在类似的项目上工作了一两周,希望能对你有所帮助。

如果你的重点是简单的代码,我建议使用TcpClient和TcpListener类。它们都使套接字更易于使用。虽然它们自从.NET Framework 1.1以来就存在,但它们已经得到更新,并且仍然是您最好的选择。

在编写简单代码时如何利用.NET Framework 4.0,Tasks是我首先想到的东西。它们使编写异步代码变得不那么痛苦,而且一旦C#5发布(新async和await关键字),迁移代码将变得更加容易。以下是Tasks简化代码的示例:

与其使用 tcpListener.BeginAcceptTcpClient(AsyncCallback callback, object state); 并提供一个回调方法,该方法会调用 EndAcceptTcpClient(); 并可选择地转换状态对象,C#4允许您利用闭包、lambda和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(线程池的抽象)进行长时间运行的操作。线程池很有用,因为创建新线程的开销是不可忽略的,对于读取或写入数据以及处理客户端连接等短期任务,优先选择Tasks。

您必须记住,线程池是共享资源,具有专门的功能(避免花费更多时间创建线程而不是实际使用它)。因为它是共享的,如果您使用了一个线程,另一个资源将无法使用,这可能会迅速导致线程池饥饿和死锁情况。


5
使用每个连接一个线程,连接数量为100万,这个方案如何? - Shift
如果有一百万个连接:请使用Linux。如果您确实必须使用Windows,请使用静态Select方法在一个线程中处理多个套接字,并进行智能分组。不要使用TcpClient,因为这样可以真正节省任何开销。 - Matthias

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接