The code presented in the question has a number of issues, which i decided to address in several sections of my answer.
It is worth to note that the code is from a blog post (a sort of brief tutorial) published about six years ago and which already exhibits some of the issues addressed in this answer.
1. Why is the server program exiting immediately?
The reason for the server program exiting immediately is rather simple: When the Main method exits, the whole process of the program is shutdown, including any thread belonging to this process (such as the thread started in the constructor of the Server class). To fix this problem, the Main method needs to be prevented from exiting.
One common approach is to add Console.ReadKey()
or Console.ReadLine()
as last line in the Main method, which will not let the server program exit until the user provides some keyboard input.
However, for the particular code given here this approach would not be such an easy fix because the client connection handler method (HandleClientComm) does also read console keyboard input (which itself might become a problem, see section 5 below).
As i do not want to start my answer with elaborating on keyboard input, i suggest a different and admittedly more primitive solution: adding an infinite loop at the end of the Main method (which has also been included in the edited question):
static void Main(string[] args)
{
Server srv = new Server();
for (;;) {}
}
Since the tutorial code of the server is essentially a console application, it can still be terminated by pressing CTRL+C.
2. The server still doesn't seem to do anything. Why?
Most (if not all) errors or problems occurring in classes and methods of the .NET frameworks are reported via .NET's exception mechanism.
And here the tutorial code makes a grave mistake in the server method for handling client connections:
try
{
//blocks until a client sends a message
bytesRead = clientStream.Read(message, 0, 4096);
}
catch
{
//a socket error has occured
break;
}
What is that try-catch block doing? Well, the only thing which it does is catching any exception -- which could answer the question Why? --
and then silently and unceremoniously discards this useful information. Ugh...!
Of course it makes sense to catch exceptions in the client connection handler threads. Otherwise, uncaught exceptions would cause the shutdown not only of the failed client connection handler thread but of the whole server console application. But these exceptions need to be handled in a meaningful way, so that no information is lost which can help in troubleshooting problems.
To provide the valuable information of exceptions thrown in our simple tutorial code, the catch block will output the exception information to the console (and also taking care of outputting any possible "inner exceptions").
Additionally, using statements are being utilized to ensure that both the NetworkStream as well as the TcpClient objects are being properly closed/disposed, even in situations where exceptions cause the client connection thread to exit.
private void HandleClientComm(object client)
{
using ( TcpClient tcpClient = (TcpClient) client )
{
EndPoint remoteEndPoint = tcpClient.Client.RemoteEndPoint;
try
{
using (NetworkStream clientStream = tcpClient.GetStream() )
{
byte[] message = new byte[4096];
for (;;)
{
//blocks until a client sends a message
int bytesRead = clientStream.Read(message, 0, 4096);
if (bytesRead == 0)
{
//the client has disconnected from the server
Console.WriteLine("Client at IP address {0} closed connection.", remoteEndPoint);
break;
}
//message has successfully been received
Console.WriteLine(Encoding.ASCII.GetString(message, 0, bytesRead));
// Console.ReadLine() has been removed.
// See last section of the answer about why
// Console.ReadLine() was of little use here...
}
}
}
catch (Exception ex)
{
// Output exception information
string formatString = "Client IP address {2}, {0}: {1}";
do
{
Console.WriteLine(formatString, ex.GetType(), ex.Message, remoteEndPoint);
ex = ex.InnerException;
formatString = "\tInner {0}: {1}";
}
while (ex != null);
}
}
}
(You might notice the code is remembering the client's (public) IP address in remoteEndPoint. The reason is that the Socket object in the property tcpClient.Client will be disposed when the NetworkStream is being closed - which happens when the scope of the respective using statement is being left, which in turn will make it impossible to access tcpClient.Client.RemoteEndPoint in the catch block afterwards.)
By letting the server output information from exceptions on the console, we will be able to see the following information from the server when the client attempts to send a message:
Unable to read data from the transport connection: An existing connection was forcibly closed by the remote host.
This is a pretty strong indication that either something is wrong with the client or that some network device has some strange problem. As it happened, the O/P runs the client and the server software on the same computer using the IP address "127.0.0.1", which makes worrying about malfunctioning network devices a moot argument and rather points at a problem with the client software.
3. What's wrong with the client?
Running the client does not throw any exception. Looking at the source code, one might falsely believe that the code seems to be mostly okay: The connection with the server is established, the byte buffer filled with the message and written to the NetworkStream, then the NetworkStream is flushed. The NetworkStream is not closed, though -- which certainly is related to the problem.
But even if the NetworkStream has not been closed explicitly, flushing the stream should have sent the message to the server anyway, or...?
The previous paragraph contains two wrong assumptions. The first wrong assumption is related to the issue of the NetworkStream not being closed properly. What happens in the client code is that directly after writing the message to the NetworkStream, the client program exits.
When the client program exits, a "Connection terminated" signal is sent to the server (simplified speaking). If at that point in time the message still lingers in some TCP/IP-related buffer on the sender side, the buffer would be simply discarded and the message not being send anymore. But even if the message has been received by the server-side TCP/IP stack and kept in a TCP/IP-related receive buffer, the more or less immediately following "Connection terminated" signal still invalidates this receive buffer before the TCP/IP stack could acknowledge receiving this message and thus lets NetworkStream.Read() fail with the aforementioned error.
The other false assumption (and which is easily overlooked by someone not doing regular network-related programming in .NET) is in how the code tries to utilize NetworkStream.Flush() in an attempt to force transmission of the message.
Let's take a look at the "Remarks" section of the MSDN documentation of NetworkStream.Flush():
The Flush method implements the Stream.Flush method; however, because NetworkStream is not buffered, it has no affect on network streams.
Yes, you did read that correctly: NetworkStream.Flush() does exactly... Nothing!
(...obviously, since NetworkStream does not buffer any data)
So, to make the client program work properly, all that needs to be done is to close the client connection properly (which also ensures that the message is being send and received by the server before the connection is severed). As already demonstrated in the server code above, we will make use of the using statement, which will take care of properly closing and disposing of the NetworkStream and TcpClient objects under any circumstances. Also, the misleading and useless call of NetworkStream.Flush() is being removed:
static void Main(string[] args)
{
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);
using ( TcpClient client = new TcpClient() )
{
client.Connect(serverEndPoint);
using ( NetworkStream clientStream = client.GetStream() )
{
byte[] buffer = Encoding.ASCII.GetBytes("Hello Server!");
clientStream.Write(buffer, 0, buffer.Length);
}
}
}
4. Advices regarding reading and writing messages from/to the NetworkStream
What always should be kept in mind is that NetworkStream.Read(...) does not guarantee to read the complete message at once.
Depending on the size of the message, NetworkStream.Read(...) might read just fragments of a message. The reasons for this are related to how the client software sends data and how TCP/IP manages the transport of data through the network.
(Granted, with such a short message like "Hello world!" it is unlikely that you will not encounter such a situation. But, depending on your server and client OS and the used NICs + drivers, you might start observing messages being fragmented if they become larger than 500-something bytes.)
Thus, to make the server more robust and less error-prone, the server code should be changed to cater for fragmented messages.
If you stick with ASCII encoding you might choose an approach as illustrated in Alexander Brevig's answer -- simply write the received message fragments with ASCII bytes into a StringBuilder. However, this approach only works reliably because any ASCII character is represented by a single byte.
As soon as you use another encoding which could encode a single character in multiple bytes (any UTF encoding, for example) this approach won't work reliably anymore. A possible fragmentation of the byte data could split the byte sequence of a multi-byte character in such a way that one NetworkStream.Read invocation just reads the first byte of such a character, and only a subsequent invocation of NetworkStream.Read would fetch the remaining bytes of this multi-byte character. This would upset the character decoding if each message fragment would be decoded individually. Thus, it generally is safer for such encodings to store the complete message in a single byte buffer (array) before doing any text decoding.
One problem is still present with reading messages of different lengths. How does the server know when the complete message has been sent? Now, in the tutorial given here, the client only sends one message and then disconnects. Thus, the server knows that the complete message has been received simply via the fact of the client disconnecting.
But what if your client wants to send multiple messages, or what if the server needs to send a response to the client? In both scenarios, the client cannot simply disconnect after sending the first message. So, how will the server know when a message has been completely sent?
A very simple and reliable method is to let the client prepend the message with a short integer value (2 bytes) or integer value (4 bytes) which specifies the message length in bytes. Thus, after receiving those 2 bytes (or 4 bytes) the server will know how many more bytes have to be read to obtain the complete message.
Now, you do not need to implement such a mechanism all by yourself. The good thing is that .NET already provides classes with methods which do all this work for you: BinaryWriter and BinaryReader, which make even sending of messages composed of different data types almost as easy and enjoyable as the proverbial walk in the park.
The client using BinaryWriter.Write(string):
static void Main(string[] args)
{
IPEndPoint serverEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 3000);
using ( TcpClient client = new TcpClient() )
{
client.Connect(serverEndPoint);
using ( BinaryWriter writer = new BinaryWriter(client.GetStream(), Encoding.ASCII) )
{
writer.Write("Hello Server!");
}
}
}
The server's client connection handler using BinaryReader.ReadString():
private void HandleClientComm(object client)
{
using ( TcpClient tcpClient = (TcpClient) client )
{
EndPoint remoteEndPoint = tcpClient.Client.RemoteEndPoint;
try
{
using ( BinaryReader reader = new BinaryReader(tcpClient.GetStream(), Encoding.ASCII) )
{
for (;;)
{
string message = reader.ReadString();
Console.WriteLine(message);
}
}
}
catch (EndOfStreamException ex)
{
Console.WriteLine("Client at IP address {0} closed the connection.", remoteEndPoint);
}
catch (Exception ex)
{
string formatString = "Client IP address {2}, {0}: {1}";
do
{
Console.WriteLine(formatString, ex.GetType(), ex.Message, remoteEndPoint);
ex = ex.InnerException;
formatString = "\tInner {0}: {1}";
}
while (ex != null);
}
}
}
You might notice the special handling of the EndOfStreamException exception. This exception is used by BinaryReader to indicate that the end of the stream is reached; i.e., the connection has been closed by the client.
Instead of printing it out like any other exception (and which thus could be misunderstood as an error - which it indeed might be in different application scenarios), a specific message is printed out on the console to make the fact of the connection being closed very clear.
(Side note: If you intent to let your client software connect and exchange data with a 3rd-party server software, BinaryWriter.Write(string) might or might not be a feasible option, as it uses ULEB128 for encoding the byte length of a string.
In cases where BinaryWriter.Write(string) is not feasible you can very likely still make good use of some combination of BinaryWriter.Write(short)/BinaryWriter.Write(int) in conjunction with BinaryWriter.Write(byte[]) to prepend your message byte data with a messageLength value.)
5. Console keyboard input
The client connection handler method HandleClientComm in the code from the question waits for keyboard input after receiving a message from a client before it will continue waiting and reading for the next message.
Which is rather pointless, as HandleClientComm can just continue waiting for the next message without requiring explicit keyboard input.
But perhaps your intention is to use the console keyboard input as a response that will be sent back to the client -- i don't know. As long as you just toy around with one single simple client this approach would do it just fine, i guess.
However, as soon as you have two or more clients which concurrently access the server, even some simple test/toy scenario might require you to take measures to ensure that several client connection handler threads do not interleave their console output and that access to console keyboard input for these threads is managed in an understandable manner. That said, it really depends on what you want to do in detail -- it might very well also just be a non-issue...