Can a TCP c# client receive and send continuously/consecutively without sleep?

前端 未结 1 2006
我寻月下人不归
我寻月下人不归 2020-11-29 17:10

This is to a degree a \"basics of TCP\" question, yet at the same time I have yet to find a convincing answer elsewhere and believe i have a ok/good understanding of the bas

相关标签:
1条回答
  • 2020-11-29 17:46

    There's nothing wrong necessarily with grouping questions together, but it does make answering the question more challenging... :)

    The MSDN article you linked shows how to do a one-and-done TCP communication, that is, one send and one receive. You'll also notice it uses the Socket class directly where most people, including myself, will suggest using the TcpClient class instead. You can always get the underlying Socket via the Client property should you need to configure a certain socket for example (e.g., SetSocketOption()).

    The other aspect about the example to note is that while it uses threads to execute the AsyncCallback delegates for both BeginSend() and BeginReceive(), it is essentially a single-threaded example because of how the ManualResetEvent objects are used. For repeated exchange between a client and server, this is not what you want.

    Alright, so you want to use TcpClient. Connecting to the server (e.g., TcpListener) should be straightforward - use Connect() if you want a blocking operation or BeginConnect() if you want a non-blocking operation. Once the connection is establish, use the GetStream() method to get the NetworkStream object to use for reading and writing. Use the Read()/Write() operations for blocking I/O and the BeginRead()/BeginWrite() operations for non-blocking I/O. Note that the BeginRead() and BeginWrite() use the same AsyncCallback mechanism employed by the BeginReceive() and BeginSend() methods of the Socket class.

    One of the key things to note at this point is this little blurb in the MSDN documentation for NetworkStream:

    Read and write operations can be performed simultaneously on an instance of the NetworkStream class without the need for synchronization. As long as there is one unique thread for the write operations and one unique thread for the read operations, there will be no cross-interference between read and write threads and no synchronization is required.

    In short, because you plan to read and write from the same TcpClient instance, you'll need two threads for doing this. Using separate threads will ensure that no data is lost while receiving data at the same time someone is trying to send. The way I've approached this in my projects is to create a top-level object, say Client, that wraps the TcpClient and its underlying NetworkStream. This class also creates and manages two Thread objects, passing the NetworkStream object to each during construction. The first thread is the Sender thread. Anyone wanting to send data does so via a public SendData() method on the Client, which routes the data to the Sender for transmission. The second thread is the Receiver thread. This thread publishes all received data to interested parties via a public event exposed by the Client. It looks something like this:

    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;
        }
    }
    

    Notice that these are three separate .cs files but define different aspects of the same Client class. I use the Visual Studio trick described here to nest the respective Receiver and Sender files under the Client file. In a nutshell, that's the way I do it.

    Regarding the NetworkStream.DataAvailable/Thread.Sleep() question. I would agree that an event would be nice, but you can effectively achieve this by using the Read() method in combination with an infinite ReadTimeout. This will have no adverse impact on the rest of your application (e.g., UI) since it's running in its own thread. However, this complicates shutting down the thread (e.g., when the application closes), so you'd probably want to use something more reasonable, say 10 milliseconds. But then you're back to polling, which is what we're trying to avoid in the first place. Here's how I do it, with comments for explanation:

    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();
            }
        }
    }
    

    As far as 'keepalives' are concerned, there is unfortunately not a way around the problem of knowing when the other side has exited the connection silently except to try sending some data. In my case, since I control both the sending and receiving sides, I've added a tiny KeepAlive message (8 bytes) to my protocol. This is sent every five seconds from both sides of the TCP connection unless other data is already being sent.

    I think I've addressed all the facets that you touched on. I hope you find this helpful.

    0 讨论(0)
提交回复
热议问题