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 basics of TCP. I am not sure if the combination of questions (or the one questions and while i'm at it the request for confirmation of a couple of points) is against the rules. Hope not.
I am trying to write a C# implementation of a TCP client, that communicates with an existing app containing a TCP server (I don't have access to its code, so no WCF). How do I connect to it, send and receive as needed as new info comes in or out, and ultimately disconnect. Using the following MSDN code as an example where they list "Send" and "Receive" asynchronous methods (or just TcpClient), and ignoring the connect and disconnect as trivial, how can I best go about continuously checking for new packets received and at the same time send when needed?
I initially used TCPClient and GetStream(), and the msdn code still seems to require the loop and sleep described in a bit (counter intuitively), where I run the receive method in a loop in a separate thread with a sleep(10) milliseconds, and Send in the main (or third) thread as needed. This allows me to send fine, and the receive method effectively polls at regular intervals to find new packets. The received packets are then added to a queue.
Is this really the best solution? Shouldn't there be a DataAvailable event equivalent (or something i'm missing in the msdn code) that allows us to receive when, and only when, there is new data available?
As an afterthought I noticed that the socket could be cut from the other side without the client becoming aware till the next botched send. To clarify then, the client is obliged to send regular keepalives (and receive isn't sufficient, only send) to determine if the socket is still alive. And the frequency of the keepalive determines how soon I will know that link is down. Is that correct? I tried Poll, socket.connected etc only to discover why each just doesn't help.
Lastly, to confirm (i believe not but good to make sure), in the above scenario of sending on demand and receiving if tcpclient.DataAvailable every ten seconds, can there be data loss if sending and receiving at the same time? If at the same time I am receiving I try and send will one fail, overwrite the other or any other such unwanted behaviour?
TCP sockets are used for communication between a server and a client process. The server's code runs first, which opens a port and listens for incoming connection requests from clients. Once a client connects to the same (server) port, the client or server may send a message.
On the Program Navigation tree, click PLC Communications>Physical>CPU Ethernet>TCP Servers. Click Add New. Define a port: click the fields to name the server, assign a port, set the Number of Connections, and check Keep Alive.
Steps to create a client using TCP/IP APICreate a socket with the socket() system call. Initialize the socket address structure as per the server and connect the socket to the address of the server using the connect() system call. Receive and send the data using the recv() and send().
Socket Programming in C/C++ What is socket programming? Socket programming is a way of connecting two nodes on a network to communicate with each other. One socket(node) listens on a particular port at an IP, while the other socket reaches out to the other to form a connection.
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:
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; }
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; } }
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With