如何在单个线程中使用C#async / await正确读取和写入Socket / NetworkStream?

时间:2018-03-28 07:53:43

标签: sockets asynchronous async-await networkstream

我正在尝试用C#编写客户端以获得专有的TCP协议连接(发送/接收密钥+值对消息)。我想使用async的{​​{1}} / await功能,因此我的程序可以使用单个线程读取和写入套接字( a la JavaScript但是我对NetworkStream的工作方式有疑问。

这是我的纲要:

NetworkStream.ReadAsync

这里有一个问题:如果public static async Task Main(String[] args) { using( TcpClient tcp = new TcpClient() ) { await tcp.ConnectAsync( ... ); using( NetworkStream ns = tcp.GetStream() ) { while( true ) { await RunInnerAsync( ns ); } } } } private static readonly ConcurrentQueue<NameValueCollection> pendingMessagesToSend = new ConcurrentQueue<NameValueCollection>(); private static async Task RunInnerAsync( NetworkStream ns ) { // 1. Send any pending messages. // 2. Read any newly received messages. // 1: while( !pendingMessagesToSend.IsEmpty ) { // ( foreach Message, send down the NetworkStream here ) } // 2: Byte[] buffer = new Byte[1024]; while( ns.DataAvailable ) { Int32 bytesRead = await ns.ReadAsync( buffer, 0, buffer.Length ); if( bytesRead == 0 ) break; // ( process contents of `buffer` here ) } } 中没有要读取的数据(NetworkStream ns),那么DataAvailable == false中的while( true )循环会不断运行,而CPU永远不会空闲 - 这很糟糕。

因此,如果我更改代码以删除Main检查并且只是总是调用DataAvailable,那么调用会有效地“阻止”直到数据可用 - 所以如果没有数据到达,那么此客户端将永远不会发送任何到远程主机的消息。所以我考虑添加500ms的超时时间:

ReadAsync

However, this does not work!显然,接受// 2: Byte[] buffer = new Byte[1024]; while( ns.DataAvailable ) { CancellationTokenSource cts = new CancellationTokenSource( 500 ); Task<Int32> readTask = ns.ReadAsync( buffer, 0, buffer.Length, cts.Token ); await readTask; if( readTask.IsCancelled ) break; // ( process contents of `buffer` here ) } 的{​​{1}}重载在实际请求取消时不会中止或停止,它总是忽略它(这不是一个错误?)。

我链接的质量检查表明只需关闭NetworkStream.ReadAsync / CancellationToken的解决方法 - 这对我来说不合适,因为我需要保持连接活动,但只有休息一下< / em>从等待数据到达并发送一些数据。

One of the other answers建议共同等待Socket,如下所示:

NetworkStream

...但是,这确实会阻止程序等待无限期的网络读取操作,它不会自动停止读取操作 - 所以当我的程序完成发送任何待处理的消息时,它将会第二次调用Task.Delay,而它仍在等待数据到达 - 这意味着处理重叠IO并且不是我想要的。

I know when working with Socket directly and its BeginReceive / EndReceive methods您只是从// 2: Byte[] buffer = new Byte[1024]; while( ns.DataAvailable ) { Task maxReadTime = Task.Delay( 500 ); Task readTask = ns.ReadAsync( buffer, 0, buffer.Length ); await Task.WhenAny( maxReadTime, readTask ); if( maxReadTime.IsCompleted ) { // what do I do here to cancel the still-pending ReadAsync operation? } } 内拨打ReadAsync - 但是第一次如何安全地呼叫BeginReceive,尤其是在循环中 - 应该如何使用EndReceive API时会修改这些调用吗?

1 个答案:

答案 0 :(得分:0)

我通过从function0()获取Task并将其存储在设置为NetworkStream.ReadAsync的可变局部变量中(如果没有待处理的null操作),否则它是一个有效的ReadAsync实例。

不幸的是,由于Task方法不能包含async个参数,我需要将我的逻辑从ref移到Main

这是我的解决方案:

RunInnerAsync

每当修改private static readonly ConcurrentQueue<NameValueCollection> pendingMessagesToSend = new ConcurrentQueue<NameValueCollection>(); private static TaskCompletionSource<Object> queueTcs; public static async Task Main(String[] args) { using( TcpClient tcp = new TcpClient() ) { await tcp.ConnectAsync( ... ); using( NetworkStream ns = tcp.GetStream() ) { Task<Int32> nsReadTask = null; // <-- this! Byte[] buffer = new Byte[1024]; while( true ) { if( nsReadTask == null ) { nsReadTask = ns.ReadAsync( buffer, 0, buffer.Length ); } if( queueTcs == null ) queueTcs = new TaskCompletionSource<Object>(); Task completedTask = await Task.WhenAny( nsReadTask, queueTcs.Task ); if( completedTask == nsReadTask ) { while( ns.DataAvailable ) { Int32 bytesRead = await ns.ReadAsync( buffer, 0, buffer.Length ); if( bytesRead == 0 ) break; // ( process contents of `buffer` here ) } } else if( completedTask == queueTcs ) { while( !pendingMessagesToSend.IsEmpty ) { // ( foreach Message, send down the NetworkStream here ) } } } } } } 时,pendingMessagesToSend都会被实例化,如果为空,并且queueTcs被调用以取消等待SetResult(null)行。

自从构建此解决方案后,我感觉好像没有恰当地使用Task completedTask = await Task.WhenAny( nsReadTask, queueTcs.Task );,请参阅此问答:Is it acceptable to use TaskCompletionSource as a WaitHandle substitute?