Nov. 6, 2009, 8:35 a.m.
posted by vendetta
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.
|
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.
|
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.
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();
}
}
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.
|
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.
|
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.
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();
}
}
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.
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();
}
}
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. |