8.6 NetworkStream

only for RuBoard

The NetworkStream class abstracts the transfer of data over networks. The underlying mechanism used for transmission is System.Net.Sockets.Socket , which is actually used by all the classes in the System.Net namespace that pertain to network communication. Socket can be used directly, but like Stream , higher-level classes make interaction over the network much simpler. This chapter discusses two of these classes: TcpClient and TcpListener .

8.6.1 TCP

It is hard to find a machine that doesn't have several TCP-based clients already on it. Most have an email client, a web browser, or maybe even a Napster client (that sadly does not do a thing). TcpClient , which uses the Socket class, makes it easy to connect, send, and receive data over a network, facilitating the creation of these types of clients.

Most Internet applications work as follows : a connection is established to a server on a specified port, data is sent in a specified format, and a response is received. It's as simple as that. The difficult part has always been working with sockets, but .NET abstracts the difficulty away.

By using TcpClient , creating a TCP-based client is easy. For instance, Example 8-16 contains the listing for a finger client. Finger used to be more popular than it is today. It associates information with an email address, so someone with a finger client can type

 finger  xyz@mail.com  

and retrieve all kinds of informationpossibly the real name of the person, where the person currently works, and so forth. You can find finger servers running on most .edu domains, but they are rare on .com s.

RFC 742 describes the finger protocol. To summarize: connect to the domain where the finger will occur on port 79, send the name of the user in question, and wait for the response, which will be the user 's finger information.

Here is a testament to .NET. Not knowing anything about finger, reading the RFC document consumed most of the time involved in writing Example 8-16. The entire journey from ignorance to implementation took 28 minutes. Time was wasted because the carriage return and line feeds were originally left off the data being sent to the server. It took a few more minutes of debugging to figure out what was going on. RFCs must be followed to the letter!

In Example 8-16, notice the use of streams. The example uses the same paradigm as that used to read and write to a file.

Example 8-16. Finger client using TcpClient
 'vbc /t:exe /r:system.dll finger.vb     Imports System Imports System.IO Imports System.Net Imports System.Net.Sockets     Public Class Finger       Public Const Port As Integer = 79  'TCP Port 79       Public Function Finger(ByVal EmailAddress As String) As String         Dim params( ) As String = EmailAddress.Split("@"c)         Try           Dim client As New TcpClient( )       client.Connect(params(1), Port)           'Get NetworkStream       Dim baseStream As NetworkStream = client.GetStream( )       'Wrap it with a buffered stream       Dim stream As New BufferedStream(baseStream)           Dim writer As StreamWriter = New StreamWriter(stream)       writer.Write(String.Format("{0}{1}", params(0), Environment.NewLine))       writer.Flush( )           Dim reader As StreamReader = New StreamReader(stream)       Dim response As String = reader.ReadToEnd( )           'Close TCP connection and stream       client.Close( )           Return response         Catch e As SocketException       Return e.Message     Catch e As Exception       Return e.Message     End Try       End Function     End Class     Friend Class FingerClient       Public Shared Sub Main(ByVal args( ) As String)     Dim f As New Finger( )     Console.WriteLine(f.Finger(args(0)))     Console.ReadLine( )   End Sub     End Class 

When the program is run, the account being fingered is nabbed from the command line and passed to the Finger class. The domain is extracted from the account and passed to the Connect method of TcpClient along with the portin this case, 79.

Once the connection is opened, the TcpClient object's GetStream method is called to retrieve an instance of NetworkStream . Although it is overkill in this example, the NetworkStream is then wrapped in a BufferedStream object. This is really just to demonstrate the buffered stream. While buffered streams are useful in network applications, finger servers typically do not return much data. Thus, providing a buffer is really unnecessary, but it does no harm, either. The buffered stream can be used like any other stream. The fact that data might stream in from halfway around the world is of no consequence. The concept of streams should be familiar to you by now.

The BufferedStream is passed to the constructor of a StreamWriter , making it possible to easily write a string to the stream. Notice that the account name is written back to the stream first with a call to StreamWriter.Write , which just writes the data to the internal buffer. The data is not actually sent to the server until the call to StreamWriter.Flush , which occurs on the next line.

Finally, the BufferedStream is passed to an instance of StreamReader , which reads back the response from the server. StreamReader.ReadToEnd gets the response back in one shot in the form of a string. Relying on a single read operation is safe for the most part, because finger does not usually return significant amounts of data. Finally, TcpClient.Close is called. It closes not only the client, but also the BufferedStream .

Ten lines of code are responsible for the finger. Developers who have done sockets programming in C on Unix systems can surely appreciate this. Otherwise, understand that this fact is pretty amazing. MIT has a really good finger server. After you have compiled the example, check it out:

 finger help@mit.edu 

8.6.2 Writing a Server

Building applications that accept connections from TCP clients is as easy as writing a TCP client. Just use System.Net.Sockets.TcpListener to wait for incoming connections. Use the following steps:

  1. Create a new TcpListener , specifying the port on which to listen.

  2. Call TcpListener.Start to begin listening.

  3. Call TcpListener.AcceptTcpClient . This is a blocking call; it causes program execution to halt until a TCP connection request is received and a connection is established. The return value of this call is a TcpClient instance.

  4. Call the GetStream method of the returned TcpClient object. The method will return an instance of NetworkStream . At this point, it is possible to read from the stream (if accepting data), write back to the stream, or both.

  5. Close the stream, the TcpListener , and the TcpClient .

Actually, Example 8-17 is a little more complicated. It contains the code for a War of the Worlds server. This server provides copies of H.G. Wells' book War of Worlds over a network connection. The text is long enough to put the example through its paces, and it is public domain. The book's text can be downloaded from the Gutenberg Project, along with many other classics, at http://www.gutenberg.org.

The example demonstrates several important concepts regarding servers and the transfer of data across a network. For one, it is multithreaded, so it can handle multiple requests simultaneously . To provide greater scalability, each thread reads from the data stream asynchronously and writes back to the client asynchronously. The example is several pages long, but that is the price you must pay for reality-based coding. Go over the example, but get ready to step through it afterward for an explanation.

Example 8-17. War of the Worlds server
 'vbc /t:exe /r:system.dll wow.vb      Imports System Imports System.IO Imports System.Net Imports System.Net.Sockets Imports System.Text Imports System.Threading     Public Class WarOfTheWorldsServer       Private port As Integer   Private listener As TcpListener       Public Sub New(ByVal port As Integer)     Me.Port = Port   End Sub       'Start listening for incoming requests   Public Sub Start( )         listener = New TcpListener(Port)     listener.Start( )     Console.WriteLine("War of the Worlds Server 1.0")         While (True)       'If there is a pending request, create a new        'thread to handle it       If (listener.Pending( )) Then         Dim requestThread As New Thread( _             New ThreadStart(AddressOf ThreadProc))         requestThread.Start( )       End If       Thread.Sleep(1000)     End While       End Sub       'Handles incoming requests   Private Sub ThreadProc( )         'Get client     Dim client As TcpClient = listener.AcceptTcpClient( )         'And pass to handler that will     'do asynchronous writes back to the client     Dim handler As New ClientHandler(client)     handler.SendDataToClient( )       End Sub       'Provides asynchronous quote support!   Private Class ClientHandler         Private Const BUFFER_SIZE As Integer = 256     Private buffer(BUFFER_SIZE) As Byte         Private client As TcpClient     Private rawClient As NetworkStream     Private clientStream As BufferedStream     Private dataStream As FileStream     Private writeCallback As AsyncCallback     Private readCallback As AsyncCallback         Public Sub New(ByVal client As TcpClient)           'Buffer the client stream       rawClient = client.GetStream( )       clientStream = New BufferedStream(rawClient)           'Callback used to handle writing       writeCallback = New AsyncCallback(AddressOf _                           Me.OnWriteComplete)       readCallback = New AsyncCallback(AddressOf _                          Me.OnReadComplete)         End Sub         'Start sending the data to the client     Public Sub SendDataToClient( )       'Open stream to data and start reading it       dataStream = New FileStream("war of the worlds.txt", _                        FileMode.Open, _                        FileAccess.Read)       dataStream.BeginRead(buffer, 0, buffer.Length, _           readCallback, Nothing)     End Sub         Private Sub OnReadComplete(ByVal ar As IAsyncResult)       Dim bytesRead As Integer = dataStream.EndRead(ar)       If (bytesRead > 0) Then         clientStream.BeginWrite(buffer, 0, _             bytesRead, writeCallback, Nothing)       Else         dataStream.Close( )         clientStream.Close( )       End If         End Sub         Private Sub OnWriteComplete(ByVal ar As IAsyncResult)       clientStream.EndWrite(ar)       clientStream.Flush( )       dataStream.BeginRead(buffer, 0, buffer.Length, _           readCallback, Nothing)     End Sub       End Class     End Class     Friend Class Application   Public Shared Sub Main(ByVal args( ) As String)     If args.Length < 1 Then       Console.WriteLine("usage: wow <port number>")       Return     End If     Dim wow As New WarOfTheWorldsServer( _                    Convert.ToInt32(args(0)))     wow.Start( )   End Sub End Class 

The action begins in Sub Main , where the port number the server uses is acquired from the command line. This port number is the input argument to the constructor of the server class, WarOfTheWorldsServer . The server starts listing on the supplied port number by creating a new instance of TcpListener and calling its Start method:

 listener = New TcpListener(port) listener.Start( ) 

The server sits in an infinite loop until a pending request comes in. This is OK because the server runs in a console window, and everything terminates properly when the window is shut down. A real server would be better implemented as a Windows Service. Implementing it in this way is not hard to do, but it would make the example considerably longer. When a request is received, a new thread is created and the work of handling the request is given to it. If there is not a pending request, the current application thread is put to sleep for one second. This is good behavior on the servers' part that allows the rest of the programs running on the machine to get some work done. The following block of code does this:

 While (True)   'If there is a pending request, create a new thread    'to handle it   If (listener.Pending( )) Then       Dim requestThread As New Thread( _           New ThreadStart(AddressOf ThreadProc))       requestThread.Start( )   End If   Thread.Sleep(1000) End While 

When the thread procedure assumes program control, TcpListener.AcceptTcpClient is ordered to get an instance of TcpClient that represents the client that currently requests data from the server. It also creates an instance of ClientHandler , which is a private class used to read and the write data to the client:

 'Handles incoming requests Private Sub ThreadProc( )         'Get client     Dim client As TcpClient = listener.AcceptTcpClient( )         'And pass to handler that will     'do asynchronous writes back to the client     Dim handler As New ClientHandler(client)     handler.SendDataToClient( )     End Sub 

ClientHandler reads data asynchronously and then writes it to the client, 256 bytes at a time. This is definitely not optimal. The size of the buffer is purposely small for two reasons:

  • To make the server work harder

  • To make the server able to work with smaller amounts of data

In the constructor of ClientHandler , a call is made to GetStream on the incoming TcpClient instance. This call returns a NetworkStream to the client, which is then wrapped by a buffered stream. Next, two delegates are declared. The first is the address of a method called OnWriteComplete , which is called whenever a write operation has finished. The second is the address of the OnReadComplete method, which is called when a read operation is completed. The ClientHandler constructor is as follows:

 Public Sub New(ByVal client As TcpClient)       'Buffer the client stream   rawClient = client.GetStream( )   clientStream = New BufferedStream(rawClient)       'Callback used to handle writing   writeCallback = New AsyncCallback(AddressOf Me.OnWriteComplete)   readCallback = New AsyncCallback(AddressOf Me.OnReadComplete)     End Sub 

Once the handler is initialized , ClientHandler.SendDataToClient is called in order to start the process of sending text to the client. A FileStream object opens the War of the Worlds text file, and BeginRead is called to read the first 256 bytes of the file into a buffer. The callback delegate is also passed so that OnReadComplete will be called after the operation is finished. SendDataToClient has the following code:

 'Start sending the data to the client Public Sub SendDataToClient( )   'Open stream to data and start reading it   dataStream = New FileStream("war of the worlds.txt", _                    FileMode.Open, _                    FileAccess.Read)   dataStream.BeginRead(buffer, 0, buffer.Length, _                        readCallback, Nothing) End Sub 

When OnReadComplete is called, an IAsyncResult interface is obtained that allows the number of bytes actually read to be determined (which is done by calling Stream.EndRead ). If data was read, then the buffer is written to the client stream. This time, the OnWriteCompleted delegate is used in the call. As soon as the write operation is finished, it is called. If no data is left, the War of the Worlds stream is closed along with the client stream. At this point, the process ultimately ends. The code for the OnReadComplete procedure is:

 Private Sub OnReadComplete(ByVal ar As IAsyncResult)   Dim bytesRead As Integer = dataStream.EndRead(ar)   If (bytesRead > 0) Then     clientStream.BeginWrite(buffer, 0, bytesRead, _       writeCallback, Nothing)   Else     dataStream.Close( )     clientStream.Close( )   End If End Sub 

When OnWriteComplete is called, Stream.EndWrite is called on the incoming IAsyncResult interface, ending the write operation. The client stream is flushed, causing the client to receive the first 256 bytes of data. Finally, Stream.BeginRead is called on the data stream in order to fetch the next 256 bytes of data. Then, the read/write process begins again, over and over, until there is no more data:

 Private Sub OnWriteComplete(ByVal ar As IAsyncResult)   clientStream.EndWrite(ar)   clientStream.Flush( )   dataStream.BeginRead(buffer, 0, buffer.Length, readCallback, Nothing) End Sub 

8.6.3 Writing a Client to Test the Server

After the server is compiled, it can be run from the command line along with the desired port:

 C:\>wow 1969 

A client is needed to test the server. The one listed in Example 8-18 will work just fine. Open a second console window to run it. It requires the IP address of the server and the port that it listens on. Localhost will work:

 C:\>wow-client 127.0.0.1 1969 

The client example uses asynchronous read operations to get the data, which means that other work can be done while the data is retrieved.

Example 8-18. War of the Worlds client
 'vbc /t:exe /r:system.dll get-quote.vb     Imports System Imports System.IO Imports System.Net.Sockets Imports System.Text Imports System.Threading     Public Class WarOfTheWorldsClient         Private server As String     Private port As Integer         Private client As TcpClient     Private buffer(256) As Byte     Private readCallback As AsyncCallback     Private stream As BufferedStream         Public Sub New(ByVal Server As String, ByVal Port As String)         Try             client = New TcpClient( )             client.Connect(Server, Convert.ToInt32(Port))             Dim raw As NetworkStream = client.GetStream( )             stream = New BufferedStream(raw)             readCallback = New AsyncCallback(AddressOf Me.OnReadComplete)             stream.BeginRead(buffer, 0, buffer.Length, _                 readCallback, Nothing)         Catch e As Exception             Console.WriteLine(e.Message)         End Try     End Sub         Private Sub OnReadComplete(ByVal ar As IAsyncResult)         Dim bytesRead As Integer = stream.EndRead(ar)         If (bytesRead > 0) Then             Dim output As String = _                 Encoding.ASCII.GetString(buffer, 0, bytesRead)             Console.WriteLine(output)             stream.BeginRead(buffer, 0, buffer.Length, _                 readCallback, Nothing)         Else             stream.Close( )             client.Close( )         End If     End Sub     End Class     Friend Class Application     Public Shared Sub Main( )         Dim args( ) As String = Environment.GetCommandLineArgs( )         If args.Length < 3 Then             Console.WriteLine("usage: wow <server> <port>")             Return         End If         Dim client As New WarOfTheWorldsClient(args(1), args(2))     End Sub End Class 
only for RuBoard


Object-Oriented Programming with Visual Basic. Net
Object-Oriented Programming with Visual Basic .NET
ISBN: 0596001460
EAN: 2147483647
Year: 2001
Pages: 112
Authors: J.P. Hamilton

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net