Using C# Streams with TCP



Using C# Streams with TCP

Because handling messages on a TCP connection is often a challenge for programmers, the .NET Framework supplies some extra classes to help out. This section describes the NetworkStream class, which provides a stream interface for sockets, as well as two additional stream classes, StreamReader and StreamWriter, that can be used to send and receive text messages using TCP.

The NetworkStream Class

As described in Chapter 1, C# uses streams to help programmers move data in large chunks. The System.Net.Sockets namespace contains the NetworkStream class, which provides a stream interface to sockets.

There are several constructors that can be used to create a NetworkStream object. The easiest (and most popular) way to create the NetworkStream object is to simply use the Socket object:

Socket newsock = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
NetworkStream ns = new NetworkStream(newsock);

This creates a new NetworkStream object that can then be referenced instead of the Socket object. After the NetworkStream object has been created, there are several properties and methods for augmenting the functionality of the Socket object, listed in Figure.

Figure: NetworkStream Class Properties

Property

Description

CanRead

Is true if the NetworkStream supports reading

CanSeek

Is always false for NetworkStreams

CanWrite

Is true if the NetworkStream supports writing

DataAvailable

Is true if there is data available to be read

The property most often used, DataAvailable,quickly checks to see if there is data waiting in the socket buffer to be read.

The NetworkStream class contains a healthy supply of methods for accessing data in the stream. These methods are listed in Figure.

Figure: NetworkStream Class Methods

Method

Description

BeginRead()

Starts an asynchronous NetworkStream read

BeginWrite()

Starts an asynchronous NetworkStream write

Close()

Closes the NetworkStream object

CreateObjRef()

Creates an object used as a proxy for the NetworkStream

EndRead()

Finishes an asynchronous NetworkStream read

EndWrite()

Finishes an asynchronous NetworkStream write

Equals()

Determines if two NetworkStreams are the same

Flush()

Flushes all data from the NetworkStream

GetHashCode()

Obtains a hash code for the NetworkStream

GetLifetimeService()

Retrieves the lifetime service object for the NetworkStream

GetType()

Retrieves the type of the NetworkStream

InitializeLifetimeService()

Obtains a lifetime service object to control the lifetime policy for the NetworkStream

Read()

Reads data from the NetworkStream

ReadByte()

Reads a single byte of data from the NetworkStream

ToString()

Returns a string representation

Write()

Writes data to the NetworkStream

WriteByte()

Writes a single byte of data to the NetworkStream

Use the Read() method to read blocks of data from the NetworkStream. Its format is as follows, where buffer is a byte array buffer to hold the read data, offset is the buffer location at which to start placing the data, and size is the number of bytes to be read.

int Read(byte[] buffer, int offset, int size)

The Read() method returns an integer value representing the number of bytes actually read from the NetworkStream and placed in the buffer.

Similarly, the Write() method format is as follows, where buffer is the buffer from which to get the data to send, offset is the buffer location at which to start getting data, and size is the number of bytes to send:

void Write(byte[] buffer, int offset, int size)

Listing 5.9 shows another version of the TCP client program that uses the NetworkStream object for communication with the Socket object.

Listing 5.9: The NetworkStreamTcpClient.cs progam
Start example
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
class NetworkStreamTcpClient
{
  public static void Main()
  {
   byte[] data = new byte[1024];
   string input, stringData;
   int recv;
   IPEndPoint ipep = new IPEndPoint(
           IPAddress.Parse("127.0.0.1"), 9050);
   Socket server = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   try
   {
     server.Connect(ipep);
   } catch (SocketException e)
   {
     Console.WriteLine("Unable to connect to server.");
     Console.WriteLine(e.ToString());
     return;
   }
   NetworkStream ns = new NetworkStream(server);
   if (ns.CanRead)
   {
     recv = ns.Read(data, 0, data.Length);
     stringData = Encoding.ASCII.GetString(data, 0, recv);
     Console.WriteLine(stringData);
   }
   else
   {
     Console.WriteLine("Error: Can't read from this socket");
     ns.Close();
     server.Close();
     return;
   }
   while(true)
   {
     input = Console.ReadLine();
     if (input == "exit")
      break;
     if (ns.CanWrite)
     {
      ns.Write(Encoding.ASCII.GetBytes(input), 0, input.Length);
      ns.Flush();
     }
     recv = ns.Read(data, 0, data.Length);
     stringData = Encoding.ASCII.GetString(data, 0, recv);
     Console.WriteLine(stringData);
   }
   Console.WriteLine("Disconnecting from server...");
   ns.Close();
   server.Shutdown(SocketShutdown.Both);
   server.Close();
  }
}
End example

This program creates a NetworkStream object from the Socket object:

NetworkStream ns = new NetworkStream(server);

Once the NetworkStream object has been created, the Socket object is never again referenced until it is closed at the end of the program. All communication with the remote server is done through the NetworkStream object:

recv = ns.Read(data, 0, data.Length);
ns.Write(Encoding.ASCII.GetBytes(input), 0, input.Length);
ns.Flush();

The Flush() method is used after every Write() method to ensure that the data placed in the NetworkStream will immediately be sent to the remote system rather than waiting in the TCP buffer area for more data before being sent.

Since the NetworkStreamTcpClient program sends and receives the same data packets, you can test it with the original SimpleTcpSrvr program presented at the beginning of this chapter (Listing 5.1). It should behave the same way as the SimpleTcpClient program, sending a message to the server and receiving the echoed message back.

Although the NetworkStream object has some additional functionality over the Socket object, it is still limited in how it sends and receives data from the socket. The same unprotected message boundary problems exist, as when using the plain Socket object to send and receive messages. The next section describes two classes that can help you have more control of the data on the socket.

The StreamReader and StreamWriter Classes

The System.IO namespace contains the StreamReader and StreamWriter classes that control the reading and writing of text messages on a stream. Both of these classes can be deployed with a NetworkStream object to help define markers for TCP messages.

The StreamReader class has lots of constructor formats for many applications. The simplest format to use with NetworkStreams is as follows:

public StreamReader(Stream stream)

The stream variable can reference any type of Stream object, including a NetworkStream object. You have a generous selection of properties and methods available for use with the StreamReader object after it is created, as described in Figure.

Figure: StreamReader Class Methods

Method

Description

Close()

Closes the StreamReader object

CreateObjRef()

Creates an object used as a proxy for the StreamReader

DiscardBufferedData()

Discards the current data in the StreamReader

Equals()

Determines if two StreamReader objects are the same

GetHashCode()

Returns a hash code for the StreamReader object

GetLifetimeService()

Retrieves the lifetime service object for the StreamReader

GetType()

Retrieves the type of the StreamReader object

InitializeLifetimeService()

Creates a lifetime service object for the StreamReader

Peek()

Returns the next available byte of data from the stream without removing it from the stream

Read()

Reads one or more bytes of data from the StreamReader

ReadBlock()

Reads a group of bytes from the StreamReader stream and places it in a specified buffer location

ReadLine()

Reads data from the StreamReader object up to and including the first line feed character

ReadToEnd()

Reads the data up to the end of the stream

ToString()

Creates a string representation of the StreamReader object

Similar to StreamReader, the StreamWriter object can be created from a NetworkStream object:

public StreamWriter(Stream stream)

StreamWriter, too, has several associated properties and methods. And, as expected, most of the common methods of the StreamReader class are also found in the StreamWriter class. Figure shows the other important methods used for StreamWriter.

Figure: Unique StreamWriter Class Methods

Method

Description

Flush()

Sends all StreamWriter buffer data to the underlying stream

Write()

Sends one or more bytes of data to the underlying stream

WriteLine()

Sends the specified data plus a line feed character to the underlying stream

Tip 

The most interesting StreamReader method is the ReadLine() method. It reads characters from the stream until it comes across a line feed character. This feature allows you to use the line feed character as a message marker to separate text messages. This greatly simplifies receiving text messages with TCP. You may notice that the WriteLine() method is the perfect match to the StreamReader’s ReadLine() method. This was not an accident. Together, these two methods let you create message-based TCP communications using the line feed character as a message marker. This feature greatly simplifies TCP message programming—as long as you are using text messages.

The programs presented in the next two sections show you how to use the StreamReader and StreamWriter classes to modify the TCP server and client programs presented earlier to use text messages.

Stream Server

Listing 5.10 is the StreamTcpSrvr.cs program, which demonstrates using these principles in a server program.

Listing 5.10: The StreamTcpSrvr.cs program
Start example
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
class StreamTcpSrvr
{
  public static void Main()
  {
   string data;
   IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9050);
   Socket newsock = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   newsock.Bind(ipep);
   newsock.Listen(10);
   Console.WriteLine("Waiting for a client...");
   Socket client = newsock.Accept();
   IPEndPoint newclient = (IPEndPoint)client.RemoteEndPoint;
   Console.WriteLine("Connected with {0} at port {1}",
           newclient.Address, newclient.Port);
   NetworkStream ns = new NetworkStream(client);
   StreamReader sr = new StreamReader(ns);
   StreamWriter sw = new StreamWriter(ns);
   string welcome = "Welcome to my test server";
   sw.WriteLine(welcome);
   sw.Flush();
   while(true)
   {
     try
     {
      data = sr.ReadLine();
     } catch (IOException)
     {
      break;
     }
   
     Console.WriteLine(data);
     sw.WriteLine(data);
     sw.Flush();
   }
   Console.WriteLine("Disconnected from {0}", newclient.Address);
   sw.Close();
   sr.Close();
   ns.Close();
  }
}
End example

The StreamTcpSrvr program uses the StreamWriter WriteLine() method to send text messages terminated with a line feed. As is true for the NetworkStream object, it is best to use the Flush() method after each WriteLine() call to ensure that all of the data is sent from the TCP buffer.

You may have noticed one major difference between this program and the original SimpleTcpSrvr program: the way StreamTcpSrvr knows when the remote connection is disconnected. Because the ReadLine() method works on the stream and not the socket, it cannot return a 0 when the remote connection has disconnected. Instead, if the underlying socket disappears, the ReadLine() method will produce an Exception. It is up to you to catch the Exception produced when the socket has been disconnected:

try
{ 
 data = sr.ReadLine();
} catch (IOException)
{
  break;
}

Stream Client

Now that you have a server that accepts messages delimited by a line feed, you need a client that can send messages in that format. Listing 5.11 shows the StreamTcpClient program that demonstrates using the StreamReader and StreamWriter objects in a TCP client application.

Listing 5.11: The StreamTcpClient.cs program
Start example
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
class StreamTcpClient
{
  public static void Main()
  {
   string data;
   string input;
   IPEndPoint ipep = new IPEndPoint(
           IPAddress.Parse("127.0.0.1"), 9050);
   Socket server = new Socket(AddressFamily.InterNetwork,
           SocketType.Stream, ProtocolType.Tcp);
   try
   {
     server.Connect(ipep);
   } catch (SocketException e)
   {
     Console.WriteLine("Unable to connect to server.");
     Console.WriteLine(e.ToString());
     return;
   }
   NetworkStream ns = new NetworkStream(server);
   StreamReader sr = new StreamReader(ns);
   StreamWriter sw = new StreamWriter(ns);
   data = sr.ReadLine();
   Console.WriteLine(data);
   while(true)
   {
     input = Console.ReadLine();
     if (input == "exit")
      break;
     sw.WriteLine(input);
     sw.Flush();
     data = sr.ReadLine();
     Console.WriteLine(data);
   }
   Console.WriteLine("Disconnecting from server...");
   sr.Close();
   sw.Close();
   ns.Close();
   server.Shutdown(SocketShutdown.Both);
   server.Close();
  }
}
End example

This version of the client program performs the same functions as the SimpleTcpClient program, but it sends messages terminated with a line feed. There is one important difference for you to observe. If you try using the StreamTcpClient program with the original

SimpleTcpSrvr program, it won’t work. That’s because the greeting banner produced by the SimpleTcpSrvr program does not include a line feed at the end of the text. Without the line feed, the ReadLine() method in the client program does not complete, and blocks the program execution, waiting for the line feed to appear. Remember this behavior whenever you use the ReadLine() stream method.

Warning 

Another point to remember when using the ReadLine() method is to ensure that the data itself does not contain a line feed character. This will create a false marker for the ReadLine() method and affect the way the data is read.

 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows