C# TcpClient.NetworkStream异步操作-取消/断开连接

C# TcpClient.NetworkStream异步操作-取消/断开连接,c#,asynchronous,task,tcpclient,C#,Asynchronous,Task,Tcpclient,免责声明:我的C#甚至不如我的C#好++ 我试图学习如何在C#中实现异步套接字,以便为我的组件编写一个测试应用程序。我读过很多方法,但最现代的方法似乎是使用TcpClient,从中获取NetworkStream,并调用NetworkStream上返回任务的异步方法 于是,我开始了我的冒险 不清楚如何正确地取消异步方法,或者是否需要取消异步方法。当我想断开连接时,是否应该手动取消它们?有人会认为TcpClient.Close会以某种优雅的方式为我解决这个问题 如果我使用手动取消,那么在主线程继续运

免责声明:我的C#甚至不如我的C#好++

我试图学习如何在C#中实现异步套接字,以便为我的组件编写一个测试应用程序。我读过很多方法,但最现代的方法似乎是使用TcpClient,从中获取NetworkStream,并调用NetworkStream上返回任务的异步方法

于是,我开始了我的冒险

不清楚如何正确地取消异步方法,或者是否需要取消异步方法。当我想断开连接时,是否应该手动取消它们?有人会认为TcpClient.Close会以某种优雅的方式为我解决这个问题

如果我使用手动取消,那么在主线程继续运行之前,似乎需要一些机制来等待所有异步方法退出。在Disconnect()方法的列表中指出。结果是,在MakeRequest()或Read()退出之前,主线程将继续处理和退出,因此会出现有关使用已处理对象的异常

在没有手动取消的测试中,我从异步读取中得到了相同的异常,关于使用已处理的对象,因为在退出这些方法之前,主线程仍在继续处理和退出

我想我可以加入我自己的同步机制……但正确的方法是什么?我应该取消吗?是否有一些内置的方法来等待这些方法退出?他们希望我们做什么

以下是我最新的代码尝试:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

using log4net;
using System.IO;
using System.Threading;

namespace IntegrationTests
{
    public class Client
    {
        private static readonly ILog log = LogManager.GetLogger("root");

        static private ulong m_lastId = 1;

        private ulong m_id;
        private string m_host;
        private uint m_port;
        private uint m_timeoutMilliseconds;
        private string m_clientId;
        private TcpClient m_tcpClient;
        private CancellationTokenSource m_cancelationSource;

        public Client(string host, uint port, string clientId, uint timeoutMilliseconds)
        {
            m_id = m_lastId++;
            m_host = host;
            m_port = port;
            m_clientId = clientId;
            m_timeoutMilliseconds = timeoutMilliseconds;
            m_tcpClient = null;
            m_cancelationSource = null;
        }

        ~Client()
        {
            Disconnect();
        }

        /// <summary>
        /// Attempts to connect to the hostname and port specified in the constructor
        /// </summary>
        /// <throws cref="System.ApplicationException" on failure
        public void Connect()
        {
            Disconnect();

            m_tcpClient = new TcpClient();
            m_cancelationSource = new CancellationTokenSource();

            try
            {
                m_tcpClient.Connect(m_host, (int)m_port);
            }
            catch (SocketException e)
            {
                string msg = string.Format("Client #{0} failed to connect to {1} on port {2}"
                                          , m_id, m_host, m_port);
                throw new System.ApplicationException(msg, e);
            }

            if (m_tcpClient.Connected)
            {
                log.Debug(string.Format("Client #{0} connnected to the Component on {1}"
                                      , m_id, m_tcpClient.Client.RemoteEndPoint.ToString()));
            }
        }

        public void Disconnect()
        {
            if (m_cancelationSource != null)
            {
                m_cancelationSource.Cancel();

                // TODO - There needs to be some kind of wait here until the async methods all return!
                //        How to do that?
                //        Are we even supposed to be manually canceling? One would think TcpClient.Close takes care of that,
                //        however when deleting all cancelation stuff, instead we get exceptions from the async methods about
                //        using TcpClient's members after it was disposed.

                m_cancelationSource.Dispose();
                m_cancelationSource = null;
            }

            if (m_tcpClient != null)
            {
                m_tcpClient.Close();
                m_tcpClient = null;
            }
        }

        public void Login()
        {
            string loginRequest = string.Format("loginstuff{0}", m_clientId);
            var data = Encoding.ASCII.GetBytes(loginRequest);

            NetworkStream stream = m_tcpClient.GetStream();
            Task writeTask = stream.WriteAsync(data, 0, data.Count());

            // This will block until the login is sent
            // We want block until the login is sent, so we can be sure we logged in before making requests
            if( !writeTask.Wait((int)m_timeoutMilliseconds) )
            {
                // Error - Send timed out
                log.Error(string.Format("Client #{0} Timed out while sending login request to the Component"
                                      , m_id));
            }
            else
            {
                log.Debug(string.Format("Client #{0} sent login request to the Component"
                                       , m_id));
            }
        }

        public async void Read()
        {
            byte[] buffer = new byte[1024];
            MemoryStream memoryStream = new MemoryStream();

            NetworkStream networkStream = m_tcpClient.GetStream();
            Task<int> readTask = null;

            bool disconnected = false;

            try
            {
                while (!disconnected)
                {
                    readTask = networkStream.ReadAsync(buffer, 0, buffer.Length, m_cancelationSource.Token);
                    int bytesReceived = await readTask;

                    if (readTask.Status == TaskStatus.RanToCompletion)
                    {
                        if( bytesReceived <= 0)
                        {
                            disconnected = true;
                            continue;
                        }

                        memoryStream.Write(buffer, 0, bytesReceived);

                        // TODO - Handle parsing of messages in the memory stream

                        memoryStream.Seek(0, SeekOrigin.Begin);
                    }
                    else if (readTask.Status == TaskStatus.Canceled)
                    {
                        // Error - Read was cancelled
                        log.Error(string.Format("Client #{0} Read operation was canceled."
                                              , m_id));
                        disconnected = true;
                        continue;
                    }
                    else
                    {
                        // Error - Unexpected status
                        log.Error(string.Format("Client #{0} Read operation has unexpected status after returning from await."
                                              , m_id));
                    }
                }
            }
            catch (System.Exception e)
            {
                log.Error(string.Format("Client #{0} Exception caught while reading from socket. Exception: {1}"
                                       , m_id, e.ToString()));
            }
        }

        public async void MakeRequest(string thingy)
        {
            string message = string.Format("requeststuff{0}", thingy);
            var data = Encoding.ASCII.GetBytes(message);

            NetworkStream networkStream = m_tcpClient.GetStream();
            Task writeTask = null;

            try
            {
                writeTask = networkStream.WriteAsync(data, 0, data.Count(), m_cancelationSource.Token);
                await writeTask;

                if (writeTask.Status == TaskStatus.RanToCompletion)
                {
                    log.Debug(string.Format("Client #{0} sent request for thingy {1} to the Component"
                                           , m_id, thingy));
                }
                else if (writeTask.Status == TaskStatus.Canceled)
                {
                    // Error - Write was cancelled
                    log.Error(string.Format("Client #{0} Write operation was canceled while requesting thingy {1} from the Component"
                                          , m_id, thingy));
                }
                else
                {
                    // Error - Unexpected status
                    log.Error(string.Format("Client #{0} Write operation has unexpected status after returning from await, while requesting thingy {1} from the Component"
                                          , m_id, thingy));
                }
            }
            catch (System.Exception e)
            {
                log.Error(string.Format("Client #{0} Exception caught while requesting thingy {1}. Exception: {2}" 
                                       , m_id, thingy, e.ToString()));
            }
        }
    }
}

WriteAsync
的一个重载使用
CancellationToken
取消调用
TcpClient
实现了
IDisposable
,因此当您摆脱它时,需要在它上面调用
Dispose
。你应该用
try...finally
来保护那些地方,以确保你清理了资源。是的,我读到了。但是,不清楚如何使用取消令牌取消以及何时取消。我是否需要在断开连接时取消连接,还是已经自动取消?另外,如果抛出异常,当主线程的执行指针移动并通过异步方法调用时,如何处理异常?无论哪种情况,调用方都会得到异常-如果使用
wait
,异步调用中的异常将被封送回调用方。我不明白你关于何时取消的问题——你想取消电话时就取消。如果你断开插座,它会断开连接;那不是取消。但是,您仍然需要调用
Dispose
。“当我想取消时”…好的,我想什么时候取消?如果连接断开,进程是否仍在尝试写入网络缓冲区?我有例外吗?我在哪里能抓到它?你说我从调用者那里得到异常以指示错误,但是如果我拔掉插头,任何catch块中都没有断点被命中,一切都像成功一样继续,但我的服务器被关闭。
WriteAsync
的一个重载使用
CancellationToken
来取消调用
TcpClient
实现了
IDisposable
,因此当您摆脱它时,需要在它上面调用
Dispose
。你应该用
try...finally
来保护那些地方,以确保你清理了资源。是的,我读到了。但是,不清楚如何使用取消令牌取消以及何时取消。我是否需要在断开连接时取消连接,还是已经自动取消?另外,如果抛出异常,当主线程的执行指针移动并通过异步方法调用时,如何处理异常?无论哪种情况,调用方都会得到异常-如果使用
wait
,异步调用中的异常将被封送回调用方。我不明白你关于何时取消的问题——你想取消电话时就取消。如果你断开插座,它会断开连接;那不是取消。但是,您仍然需要调用
Dispose
。“当我想取消时”…好的,我想什么时候取消?如果连接断开,进程是否仍在尝试写入网络缓冲区?我有例外吗?我在哪里能抓到它?你说我从调用者那里得到异常来指示错误,但是如果我拔掉插头,任何catch块中都不会出现断点,一切都会继续,就好像成功了一样,而我的服务器却关闭了。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using log4net;
using log4net.Config;

namespace IntegrationTests
{
    class Program
    {
        private static readonly ILog log = LogManager.GetLogger("root");

        static void Main(string[] args)
        {
            try
            {
                XmlConfigurator.Configure();
                log.Info("Starting Component Integration Tests...");

                Client client = new Client("127.0.0.1", 24001, "MyClientId", 60000);
                client.Connect();

                client.Read();
                client.Login();
                client.MakeRequest("Stuff");

                System.Threading.Thread.Sleep(60000);

                client.Disconnect();
            }
            catch (Exception e)
            {
                log.Error(string.Format("Caught an exception in main. Exception: {0}"
                                      , e.ToString()));
            }
        }
    }
}