TCP c#客户端可以在不休眠的情况下连续/连续接收和发送吗?

Posted

技术标签:

【中文标题】TCP c#客户端可以在不休眠的情况下连续/连续接收和发送吗?【英文标题】:Can a TCP c# client receive and send continuously/consecutively without sleep? 【发布时间】:2014-01-08 18:02:04 【问题描述】:

这在一定程度上是一个“TCP 基础”问题,但与此同时,我还没有在其他地方找到令人信服的答案,并且相信我对 TCP 的基础知识有很好的了解。我不确定问题的组合(或一个问题,当我在它时要求确认几点)是否违反规则。希望不会。

我正在尝试编写 TCP 客户端的 C# 实现,它与包含 TCP 服务器的现有应用程序通信(我无权访问其代码,因此没有 WCF)。我如何连接它,根据需要发送和接收新信息,并最终断开连接。以the following MSDN code 为例,他们列出了“发送”和“接收”异步方法(或只是 TcpClient),并且忽略了连接和断开连接,我怎样才能最好地继续检查接收到的新数据包,同时需要的时候发送?

我最初使用 TCPClient 和 GetStream(),msdn 代码似乎仍然需要稍微描述的循环和睡眠(直观地反击),我在一个单独的线程中循环运行接收方法,并使用 sleep( 10) 毫秒,并根据需要在主(或第三个)线程中发送。这使我可以正常发送,并且接收方法会定期有效地轮询以查找新数据包。然后将接收到的数据包添加到队列中。

这真的是最好的解决方案吗?难道不应该有一个等效的 DataAvailable 事件(或者我在 msdn 代码中缺少的东西)允许我们在且仅当有新数据可用时接收?

作为事后的想法,我注意到可以从另一侧切断套接字,而客户端不会意识到直到下一次发送失败。为了澄清,客户端有义务发送常规的keepalives(接收是不够的,只发送)以确定套接字是否仍然存在。 keepalive 的频率决定了我多久会知道该链接已断开。那是对的吗?我尝试了 Poll、socket.connected 等只是为了发现为什么每个都没有帮助。

最后,确认一下(我相信不是,但最好确定一下),在上述按需发送和接收的场景中,如果 tcpclient.DataAvailable 每十秒一次,如果同时发送和接收会不会有数据丢失?如果我在接收的同时尝试发送,是否会失败,覆盖另一个或任何其他此类不需要的行为?

【问题讨论】:

还有一个 TCPListener,它会不断地监听连接。 recv 阻塞,直到它真正获取数据,你不需要为它睡眠。如果您想要更多事件驱动的东西,请查找异步套接字。 IO完成端口等。 您是否使用 BeginReceive 作为那个 msdn 示例?我认为你应该阅读它是如何工作的。异步接收是您正在寻找的。​​span> 好的,所以 BeginReceive 是非阻塞的,并且完全符合我的要求……但对于 TcpClient,我需要睡眠。 BeginReceive 会与来自另一个线程的发送协同工作吗? "对于 TcpClient,我需要睡眠" -- 不。对于TcpClient,假设您不打算使用底层Client 套接字(因此可以访问所有相同的异步I/O,如BeginReceive()),您改为调用GetStream(),并使用异步I/哦,就像BeginRead() 或现在的ReadAsync() 【参考方案1】:

将问题组合在一起并没有什么错,但它确实使回答问题更具挑战性...... :)

您链接的 MSDN 文章显示了如何进行一次性 TCP 通信,即一次发送和一次接收。您还会注意到它直接使用Socket 类,而大多数人(包括我自己)会建议改用TcpClient 类。如果您需要配置某个套接字(例如,SetSocketOption()),您始终可以通过Client 属性获取底层Socket

关于该示例需要注意的另一个方面是,虽然它使用线程来执行 BeginSend()BeginReceive()AsyncCallback 委托,但它本质上是一个单线程示例,因为 ManualResetEvent 对象被使用。对于客户端和服务器之间的重复交换,这不是您想要的。

好的,所以你想使用TcpClient。连接到服务器(例如,TcpListener)应该很简单——如果你想要一个阻塞操作,使用Connect(),如果你想要一个非阻塞操作,使用BeginConnect()。建立连接后,使用GetStream() 方法获取NetworkStream 对象用于读写。对阻塞 I/O 使用 Read()/Write() 操作,对非阻塞 I/O 使用 BeginRead()/BeginWrite() 操作。请注意,BeginRead()BeginWrite() 使用与 Socket 类的 BeginReceive()BeginSend() 方法相同的 AsyncCallback 机制。

此时要注意的关键事项之一是 NetworkStream 的 MSDN 文档中的这个小简介:

读写操作可以同时在一个 NetworkStream 类的实例,而不需要 同步。 只要有一个唯一的线程可以写 操作和一个用于读取操作的唯一线程,将 读写线程之间没有交叉干扰并且没有 需要同步。

简而言之,因为您计划从同一个 TcpClient 实例读取和写入,所以您需要两个线程来执行此操作。使用单独的线程将确保在有人尝试发送的同时接收数据时不会丢失数据。我在我的项目中处理这个问题的方法是创建一个***对象,比如Client,它包装了TcpClient 及其底层NetworkStream。该类还创建和管理两个Thread 对象,在构造过程中将NetworkStream 对象传递给每个对象。第一个线程是Sender 线程。任何想要发送数据的人都可以通过Client 上的公共SendData() 方法发送数据,该方法将数据路由到Sender 进行传输。第二个线程是Receiver 线程。此线程通过Client 公开的公共事件将所有收到的数据发布给相关方。它看起来像这样:

客户端.cs

public sealed partial class Client : IDisposable

    // Called by producers to send data over the socket.
    public void SendData(byte[] data)
    
        _sender.SendData(data);
    

    // Consumers register to receive data.
    public event EventHandler<DataReceivedEventArgs> DataReceived;

    public Client()
    
        _client = new TcpClient(...);
        _stream = _client.GetStream();

        _receiver = new Receiver(_stream);
        _sender   = new Sender(_stream);

        _receiver.DataReceived += OnDataReceived;
    

    private void OnDataReceived(object sender, DataReceivedEventArgs e)
    
        var handler = DataReceived;
        if (handler != null) DataReceived(this, e);  // re-raise event
    

    private TcpClient     _client;
    private NetworkStream _stream;
    private Receiver      _receiver;
    private Sender        _sender;

Client.Receiver.cs

private sealed partial class Client

    private sealed class Receiver
    
        internal event EventHandler<DataReceivedEventArgs> DataReceived;

        internal Receiver(NetworkStream stream)
        
            _stream = stream;
            _thread = new Thread(Run);
            _thread.Start();
        

        private void Run()
        
            // main thread loop for receiving data...
        

        private NetworkStream _stream;
        private Thread        _thread;
    

Client.Sender.cs

private sealed partial class Client

    private sealed class Sender
    
        internal void SendData(byte[] data)
        
            // transition the data to the thread and send it...
        

        internal Sender(NetworkStream stream)
        
            _stream = stream;
            _thread = new Thread(Run);
            _thread.Start();
        

        private void Run()
        
            // main thread loop for sending data...
        

        private NetworkStream _stream;
        private Thread        _thread;
    

请注意,这是三个独立的 .cs 文件,但定义了同一 Client 类的不同方面。我使用 here 描述的 Visual Studio 技巧将各自的 ReceiverSender 文件嵌套在 Client 文件下。简而言之,我就是这样做的。

关于NetworkStream.DataAvailable/Thread.Sleep()的问题。我同意一个事件会很好,但是您可以通过结合使用Read() 方法和无限ReadTimeout 来有效地实现这一点。这不会对应用程序的其余部分(例如 UI)产生不利影响,因为它在自己的线程中运行。但是,这会使关闭线程变得复杂(例如,当应用程序关闭时),因此您可能希望使用更合理的时间,比如 10 毫秒。但是你又回到了投票,这是我们一开始就试图避免的。下面是我的做法,用 cmets 来解释:

private sealed class Receiver

    private void Run()
    
        try
        
            // ShutdownEvent is a ManualResetEvent signaled by
            // Client when its time to close the socket.
            while (!ShutdownEvent.WaitOne(0))
            
                try
                
                    // We could use the ReadTimeout property and let Read()
                    // block.  However, if no data is received prior to the
                    // timeout period expiring, an IOException occurs.
                    // While this can be handled, it leads to problems when
                    // debugging if we are wanting to break when exceptions
                    // are thrown (unless we explicitly ignore IOException,
                    // which I always forget to do).
                    if (!_stream.DataAvailable)
                    
                        // Give up the remaining time slice.
                        Thread.Sleep(1);
                    
                    else if (_stream.Read(_data, 0, _data.Length) > 0)
                    
                        // Raise the DataReceived event w/ data...
                    
                    else
                    
                        // The connection has closed gracefully, so stop the
                        // thread.
                        ShutdownEvent.Set();
                    
                
                catch (IOException ex)
                
                    // Handle the exception...
                
            
        
        catch (Exception ex)
        
            // Handle the exception...
        
        finally
        
            _stream.Close();
        
    

就'keepalives'而言,不幸的是,除了尝试发送一些数据外,没有办法解决知道对方何时静默退出连接的问题。就我而言,由于我同时控制发送方和接收方,我在我的协议中添加了一个很小的KeepAlive 消息(8 个字节)。除非已发送其他数据,否则每五秒钟从 TCP 连接的两端发送一次。

我想我已经解决了你提到的所有方面。希望对您有所帮助。

【讨论】:

这是一个非常令人印象深刻的答案。感觉它应该被框起来(-:感谢您花时间提出详细、全面和易于理解的解释,包括示例! 非常感谢您花时间做出如此精彩的回答! @BVB,感谢漂亮的 cmets。我很高兴你们俩都觉得它很有帮助。保重。 我也非常感谢。如果没有内存泄漏导致我的应用程序内存使用量在这里增长和增长***.com/questions/24268434/…,我无法理解在异步场景中使用它 这个答案的唯一错误是,作者认为将他的解决方案放在一个 zip 文件中让我们下载而不是复制粘贴他的代码是不合适的。

以上是关于TCP c#客户端可以在不休眠的情况下连续/连续接收和发送吗?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不强制断开连接的情况下使用 Go TLS 手动验证客户端证书?

C#:如何在启用 SSL 的情况下连接到 Active Directory?

如何在不关闭 TCP 连接的情况下关闭处理 TCP 请求的线程?

TCP与UDP

在不停止整个程序的情况下休眠 Lua 脚本?

我可以在不创建实体类的情况下对大型 sql 使用休眠命名查询吗?