使用 .Net 4.5 异步特性进行Socket编程

43

我之前使用过 BeginAccept()BeginRead(),但是现在我想在我的套接字服务器程序中使用新的异步 (async, await) 特性。

我该如何完成 AcceptAsyncReceiveAsync 函数呢?

using System.Net;
using System.Net.Sockets;

namespace OfficialServer.Core.Server
{
    public abstract class CoreServer
    {
        private const int ListenLength = 500;
        private const int ReceiveTimeOut = 30000;
        private const int SendTimeOut = 30000;
        private readonly Socket _socket;

        protected CoreServer(int port, string ip = "0.0.0.0")
        {
            _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            _socket.Bind(new IPEndPoint(IPAddress.Parse(ip), port));
            _socket.Listen(ListenLength);
            _socket.ReceiveTimeout = ReceiveTimeOut;
            _socket.SendTimeout = SendTimeOut;
            _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
            _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.DontLinger, true);
        }

        public void Start()
        {    
        }
    }
}

发送超时和接收超时不仅适用于同步情况吗? - Vroomfondel
2个回答

62

因为你非常决心,我为你准备了一个非常简单的示例,展示如何编写一个回声服务器,帮助你开始入门。接收到的任何内容都会被回送给客户端。服务器将保持运行60秒。尝试在本地主机端口6666上使用telnet连接它。花时间仔细理解这里发生了什么。

void Main()
{
    CancellationTokenSource cts = new CancellationTokenSource();
    TcpListener listener = new TcpListener(IPAddress.Any, 6666);
    try
    {
        listener.Start();
        //just fire and forget. We break from the "forgotten" async loops
        //in AcceptClientsAsync using a CancellationToken from `cts`
        AcceptClientsAsync(listener, cts.Token);
        Thread.Sleep(60000); //block here to hold open the server
    }
    finally
    {
        cts.Cancel();
        listener.Stop();
    }
}

async Task AcceptClientsAsync(TcpListener listener, CancellationToken ct)
{
    var clientCounter = 0;
    while (!ct.IsCancellationRequested)
    {
        TcpClient client = await listener.AcceptTcpClientAsync()
                                            .ConfigureAwait(false);
        clientCounter++;
        //once again, just fire and forget, and use the CancellationToken
        //to signal to the "forgotten" async invocation.
        EchoAsync(client, clientCounter, ct);
    }

}
async Task EchoAsync(TcpClient client,
                     int clientIndex,
                     CancellationToken ct)
{
    Console.WriteLine("New client ({0}) connected", clientIndex);
    using (client)
    {
        var buf = new byte[4096];
        var stream = client.GetStream();
        while (!ct.IsCancellationRequested)
        {
            //under some circumstances, it's not possible to detect
            //a client disconnecting if there's no data being sent
            //so it's a good idea to give them a timeout to ensure that 
            //we clean them up.
            var timeoutTask = Task.Delay(TimeSpan.FromSeconds(15));
            var amountReadTask = stream.ReadAsync(buf, 0, buf.Length, ct);
            var completedTask = await Task.WhenAny(timeoutTask, amountReadTask)
                                          .ConfigureAwait(false);
            if (completedTask == timeoutTask)
            {
                var msg = Encoding.ASCII.GetBytes("Client timed out");
                await stream.WriteAsync(msg, 0, msg.Length);
                break;
            }
            //now we know that the amountTask is complete so
            //we can ask for its Result without blocking
            var amountRead = amountReadTask.Result;
            if (amountRead == 0) break; //end of stream.
            await stream.WriteAsync(buf, 0, amountRead, ct)
                        .ConfigureAwait(false);
        }
    }
    Console.WriteLine("Client ({0}) disconnected", clientIndex);
}

3
Socket.ReceiveAsync是一个奇特的方法。它与.net4.5中的async/await特性无关。它被设计为一种替代Socket API,可以减少像BeginReceive/EndReceive这样的方法对内存的冲击,并且只需要在最强大的服务器应用程序中使用。我们运行的服务器使用BeginXXXX/EndXXXX方法轻松支持5000个连接的客户端,这可以让你感受到其规模之大。我从未有过使用ReceiveAsync的需求,在考虑使用ReceiveAsync和SocketAsyncEventArgs进行重写之前,我可能会升级硬件,因为这样做可以更节省我的时间成本。 - spender
4
...或者迁移到已经取代它们的async/await方法,就像我的例子一样。 - spender
这是只有我还是只有在超时(15秒)后才回显到客户端? - user3822370
在访问Result之前,难道不需要加上if (amountReadTask.IsFaulted || amountReadTask.IsCanceled) break;吗?如果任务出现故障,你将会得到一个隐形的异常。 - Bart Calixto
@spender 我理解套接字如何接收或超时,但我想了解如何在未先从客户端接收消息的情况下向客户端发送消息。 - Paul Stanley
显示剩余13条评论

16

您可以使用TaskFactory.FromAsyncBegin/End成对的操作包装成可以支持async的操作。

Stephen Toub在他的博客上有一个可等待的Socket,它包装了更高效的*Async端点。我建议将其与TPL Dataflow结合使用,创建一个完全兼容asyncSocket组件。


4
我不想包装Begin和End(如果我理解你的意思正确的话)。我想做的是使用.AcceptAsync代替.BeginAccept,并使用.ReceiveAsync代替.BeginReceive。 - Daniel Eugen
4
AcceptAsyncReceiveAsync 采用一种特殊的异步 API 形式(详情见 http://msdn.microsoft.com/en-us/library/system.net.sockets.socketasynceventargs.aspx),该形式仅适用于 Socket 类。 它们与 asyncawait 没有任何关系。 - Stephen Cleary
2
是的,那正是我想要的,但我无法使用SocketAsyncEventArgs实现,我不知道该怎么做。如果您能提供一个接受连接、使用这些方法从中接收数据的示例,我将非常感激。 - Daniel Eugen
3
请跟随我上一个评论中的链接,该MSDN页面上有一个长示例。请注意,使用MSDN套接字示例时通常需要注意:它可能无法正确处理边缘情况,并且几乎肯定需要更改才能适用于任何实际协议。 - Stephen Cleary
现在UdpClient已经内置了异步支持,因此您可以使用ReceiveAsync - Stephen Cleary
显示剩余3条评论

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