Mark Strawmyer Presents: .NET Nuts & Bolts
Multithreading is a powerful design tool for creating high-performance applications, especially those that require user interaction. Microsoft .NET has broken down the barriers that once existed in creating multithreaded applications. The last two installments of the .NET Nuts & Bolts column were Part 1 and Part 2 of the exploration of multithreading with the .NET Framework. In the first article (Part 1), we covered the background of threading, benefits of threading, and provided a demonstration. In the second article (Part 2), we looked at the basic methods involved with working with threads and the synchronization of thread activity. In this article, the third and final of the series on multithreading, we will look at how threads can be used to write a server application to accept multiple requests. This will involve using classes from the System.Threading namespace along with classes from the System.Net namespace.
Network Programming Basics
In order for us to write an application that accepts network requests, we must first have a basic understanding of the network components and terminology involved. I will not attempt to provide a full explanation on networking and how it all works; rather, I’ll provide the information essential to understanding this topic. Some basic definitions are as follows:
- TCP/IP—A communications protocol suite used by computers to communicate across a network. It is a routable protocol, which means that a router will forward the communications to the appropriate location if the destination does not rely on its network.
- Port—Each TCP/IP based application program has unique port numbers assigned to it. The port number identifies the logical communications channel that is to be used to connect to the application. Some protocols use a well-known port reserved specifically for their use. For example, if you want to connect to a Web server, you typically use port 80, which is reserved for HTTP.
- Socket—A socket is one endpoint of a two-way network connection between two programs. It is a mechanism for communication across processes on the same machine or on different computers connected by a network. A socket is connected to a specific port to communicate with the desired application.
Listener Application
Listener applications, also known as server applications, open a network port and then wait for clients to connect and make requests. Examples of such applications include but are not limited to Web servers, database servers, e-mail servers, and chat servers. Listener applications generally follow a similar algorithm. The algorithm is as follows:
- Open available port
- Wait to accept client socket connection through port
- Client connects through socket and either makes a request or the connection alone serves as the sole request
- Listener performs some process and sends a response
- Close the socket connection to the client
Sample Listener Code Listing
The following sample follows the basic algorithm above. It contains a console application that opens a port and waits for a socket connection to be made. Rather than have the client make any type of formal request for action, we’ll simply use the connection as the request. Once a client is connected, the server sends 10 date/time messages to the client with a pause between each message. The listener closes the connection and begins to wait for another client connection.
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace CodeGuru.MultithreadedPart3 { /// <remarks> /// Example console application demonstrating a listener/server /// application. /// Waits for connections to be made and responds with a message. /// </remarks> class HelloWorldServer { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main(string[] args) { try { DateTime now; String dateStr; // Choose a port other than 8080 if you have difficulty TcpListener listener = new TcpListener(IPAddress.Loopback, 8080); listener.Start(); Console.WriteLine("Waiting for clients to connect"); Console.WriteLine("Press Ctrl+c to Quit..."); while(true) { // Accept blocks until a client connects Socket clientSocket = listener.AcceptSocket(); for( int i = 0; i < 10; i++ ) { // Get the current date and time then build a // Byte Array to send now = DateTime.Now; dateStr = now.ToShortDateString() + " " + now.ToLongTimeString(); Byte[] byteDateLine = Encoding.ASCII.GetBytes( dateStr.ToCharArray()); // Send the data clientSocket.Send(byteDateLine, byteDateLine.Length, 0); Thread.Sleep(1000); Console.WriteLine("Sent {0}", dateStr); } clientSocket.Close(); } } catch( SocketException socketEx ) { Console.WriteLine("Socket error: {0}", socketEx.Message); } } } }
Sample Client Code Listing
The following sample code contains a client application that will connect to the listener and display the results to the console window.
using System; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; namespace CodeGuru.MultithreadedPart3 { /// <remarks> /// Example console application demonstrating a client /// application. /// Makes a connection to the server and displays the response. /// </remarks> class HelloWorldClient { /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main(string[] args) { bool isDone = false; Byte[] read = new Byte[32]; TcpClient client = new TcpClient("localhost", 8080); // Get the stream to read the input Stream s; try { s = client.GetStream(); } catch( InvalidOperationException ) { Console.WriteLine("Cannot connect to localhost"); return; } // Read the stream and convert it to ASII while( !isDone ) { int numBytes = s.Read(read, 0, read.Length); String data = Encoding.ASCII.GetString(read); if( numBytes == 0 ) { isDone = true; } else { Console.WriteLine("Received {0} bytes: {1}", numBytes, data); } } client.Close(); } } }
Testing the Listener Using the Client
Copy the listener and client samples into separate solutions and compile each. Open a command line and run the listener application. Open another command line and run the client application. You will see output similar to the following:
Figure 1—Listener
Figure 2—Client
Start the execution of the client application again. From a third command line, execute another instance of the client application. You should see that only one of them is receiving a response from the server. The other client is sitting, waiting for the server to respond. Once the first client completes, the second client will then receive response from the server.
Multithreaded Listener Application
The problem with the previous example is that the server will only process one connection at a time. That may be sufficient for our simple date/time example, but for other listener applications, such as Web servers, that won’t do. The reason that only one client is processed at a time is because our listener application is single threaded and therefore only has a single unit of execution. To process multiple client requests simultaneously, we would need to handle each client on a different thread.
Sample Listener Code Listing
The following sample code changes the previous listener application so that it can process multiple client requests at a time. This is handled by moving the processing into a separate method that is called on a new thread for each client connection. Each client socket is assigned to a new instance of the HelloWorldServer so that it can be passed to the new thread.
using System; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; namespace CodeGuru.MultithreadedPart3 { /// <remarks> /// Example console application demonstrating a listener/server /// application. /// Waits for connections to be made and responds with a message. /// </remarks> class HelloWorldServer { // Socket to use to accept client connections private Socket _socket; /// <summary> /// The main entry point for the application. /// </summary> [STAThread] static void Main(string[] args) { try { TcpListener listener = new TcpListener( IPAddress.Loopback, 8080); listener.Start(); Console.WriteLine("Waiting for clients to connect"); Console.WriteLine("Press Ctrl+c to Quit..."); while(true) { // Accept blocks until a client connects HelloWorldServer hwServer = new HelloWorldServer(); hwServer._socket = listener.AcceptSocket(); // Process the client connection on a new thread Thread sampleThread = new Thread(new ThreadStart( hwServer.Process)); sampleThread.Start(); } } catch( SocketException socketEx ) { Console.WriteLine("Socket error: {0}", socketEx.Message); } } /* * Get the current date and time and send it to the client. * Requires that a socket is created and connected to a client. * Closes the socket when complete. */ private void Process() { DateTime now; String dateStr; for( int i = 0; i < 10; i++ ) { // Get the current date and time then concatenate build // a Byte Array to send now = DateTime.Now; dateStr = now.ToShortDateString() + " " + now.ToLongTimeString(); Byte[] byteDateLine = Encoding.ASCII.GetBytes( dateStr.ToCharArray()); // Send the data this._socket.Send(byteDateLine, byteDateLine.Length, 0); Thread.Sleep(1000); Console.WriteLine("Sent {0}", dateStr); } this._socket.Close(); } } }
Testing the Multithreaded Listener Using the Client
Copy the updated listener application code into the appropriate solution and recompile. Run the listener application from a command line. Then run multiple instances of the client application from different command line prompts. You will notice that both clients now receive a response from the server.
Possible Enhancements
The samples given above contain a simple listener and client application. You can use them as a starting point to further explore the System.Net namespace. A couple of possibilities are as follows:
- Expand the client to send the server a request for a specific action. Modify the server to allow the client to make a request and respond with the appropriate action. For example, the client could be required to send “DATETIME” as a request, and the server would then respond to the client with the date and time.
- Remove the console output from the listener application. Change it into a Windows Service to allow it to start up automatically and run in the background.
Future Columns
The topic of the next column is yet to be determined. If you have something in particular that you would like to see explained here, you could reach me at mstrawmyer@crowechizek.com.
About the Author
Mark Strawmyer, MCSD, MCSE (NT4/W2K), MCDBA is a Senior Architect of .NET applications for large- and mid-size organizations. Mark is a technology leader with Crowe Chizek in Indianapolis, Indiana. He specializes in the architecture, design, and development of Microsoft-based solutions. You can reach Mark at mstrawmyer@crowechizek.com.
# # #