[.NETWorld] Asynchronous TCP Sockets as an Alternative to WCF


In a Microsoft technologies environment, using Windows Communication Foundation (WCF) is a common approach for creating a client-server system. There are many alternatives to WCF, of course, each with its own advantages and disadvantages, including HTTP Web Services, Web API, DCOM, AJAX Web technologies, named pipe programming and raw TCP socket programming. But if you take into account factors such as development effort, manageability, scalability, performance and security, in many situations using WCF is the most efficient approach.

However, WCF can be extremely complicated and might be overkill for some programming situations. Prior to the release of the Microsoft .NET Framework 4.5, asynchronous socket programming was, in my opinion, too difficult in most cases to justify its use. But the ease of using the new C# await and async language features changes the balance, so using socket programming for asynchronous client-server systems is now a more attractive option than it used to be. This article explains how to use these new asynchronous features of the .NET Framework 4.5 to create low-level, high-­performance asynchronous client-server software systems.

The best way to see where I’m headed is to take a look at the demo client-server system shown in Figure 1. At the top of the image a command shell is running an asynchronous TCP socket-based service that accepts requests to compute the average or minimum of a set of numeric values. In the middle part of the image is a Windows Forms (WinForm) application that has sent a request to compute the average of (3, 1, 8). Notice the client is asynchronous—after the request is sent, while waiting for the service to respond, the user is able to click on the button labeled Say Hello three times, and the application is responsive.

Demo TCP-Based Service with Two Clients
Figure 1 Demo TCP-Based Service with Two Clients

The bottom part of Figure 1 shows a Web application client in action. The client has sent an asynchronous request to find the minimum value of (5, 2, 7, 4). Although it’s not apparent from the screenshot, while the Web application is waiting for the service response, the application is responsive to user input.

In the sections that follow, I’ll show how to code the service, the WinForm client and the Web application client. Along the way I’ll discuss the pros and cons of using sockets. This article assumes you have at least intermediate-level C# programming skill, but does not assume you have deep understanding or significant experience with asynchronous programming. The code download that accompanies this article has the complete source code for the three programs shown in Figure 1. I have removed most normal error checking to keep the main ideas as clear as possible.

Creating the Service

The overall structure of the demo service, with a few minor edits to save space, is presented in Figure 2. To create the service, I launched Visual Studio 2012, which has the required .NET Framework 4.5, and created a new C# console application named DemoService. Because socket-based services tend to have specific, limited functionality, using a more descriptive name would be preferable in a real-life scenario.

Figure 2 The Demo Service Program Structure

  1. using System;
  2. using System.Net;
  3. using System.Net.Sockets;
  4. using System.IO;
  5. using System.Threading.Tasks;
  6. namespace DemoService
  7. {
  8.   class ServiceProgram
  9.   {
  10.     static void Main(string[] args)
  11.     {
  12.       try
  13.       {
  14.         int port = 50000;
  15.         AsyncService service = new AsyncService(port);
  16.         service.Run();
  17.         Console.ReadLine();
  18.       }
  19.       catch (Exception ex)
  20.       {
  21.         Console.WriteLine(ex.Message);
  22.         Console.ReadLine();
  23.       }
  24.     }
  25.   }
  26.   public class AsyncService
  27.   {
  28.     private IPAddress ipAddress;
  29.     private int port;
  30.     public AsyncService(int port) { . . }
  31.     public async void Run() { . . }
  32.     private async Task Process(TcpClient tcpClient) { . . }
  33.     private static string Response(string request)
  34.     private static double Average(double[] vals) { . . }
  35.     private static double Minimum(double[] vals) { . . }
  36.   }
  37. }

After the template code loaded into the editor, I modified the using statements at the top of the source code to include System.Net and System.Net.Sockets. In the Solution Explorer window, I renamed file Program.cs to ServiceProgram.cs and Visual Studio automatically renamed class Program for me. Starting the service is simple:

  1. int port = 50000;
  2. AsyncService service = new AsyncService(port);
  3. service.Run();

Each custom socket-based service on a server must use a unique port. Port numbers between 49152 and 65535 are generally used for custom services. Avoiding port number collisions can be tricky. It’s possible to reserve port numbers on a server using the system registry ReservedPorts entry. The service uses an object-oriented programming (OOP) design and is instantiated via a constructor that accepts the port number. Because service port numbers are fixed, the port number can be hardcoded rather than passed as a parameter. The Run method contains a while loop that will accept and process client requests until the console shell receives an <enter> key press.

The AsyncService class has two private members, ipAddress and port. These two values essentially define a socket. The constructor accepts a port number and programmatically determines the IP address of the server. Public method Run does all the work of accepting requests, then computing and sending responses. The Run method calls helper method Process, which in turn calls helper Response. Method Response calls helpers Average and Minimum.

There are many ways to organize a socket-based server. The structure used in the demo tries to strike a balance between modularity and simplicity, and has worked well for me in practice.

The Service Constructor and Run Methods

The two public methods of the socket-based demo service are presented in Figure 3. After storing the port name, the constructor uses method GetHostName to determine the name of the server, and to then fetch a structure that contains information about the server. The AddressList collection holds different machine addresses, including IPv4 and IPv6 addresses. The InterNetwork enum value means an IPv4 address.

Figure 3 Service Constructor and Run Methods

  1. public AsyncService(int port)
  2. {
  3.   this.port = port;
  4.   string hostName = Dns.GetHostName();
  5.   IPHostEntry ipHostInfo = Dns.GetHostEntry(hostName);
  6.   this.ipAddress = null;
  7.   for (int i = 0; i < ipHostInfo.AddressList.Length; ++i) {
  8.     if (ipHostInfo.AddressList[i].AddressFamily ==
  9.       AddressFamily.InterNetwork)
  10.     {
  11.       this.ipAddress = ipHostInfo.AddressList[i];
  12.       break;
  13.     }
  14.   }
  15.   if (this.ipAddress == null)
  16.     throw new Exception(“No IPv4 address for server”);
  17. }
  18. public async void Run()
  19. {
  20.   TcpListener listener = new TcpListener(this.ipAddress, this.port);
  21.   listener.Start();
  22.   Console.Write(“Array Min and Avg service is now running”
  23.   Console.WriteLine(” on port ” + this.port);
  24.   Console.WriteLine(“Hit <enter> to stop service\n”);
  25.   while (true) {
  26.     try {
  27.       TcpClient tcpClient = await listener.AcceptTcpClientAsync();
  28.       Task t = Process(tcpClient);
  29.       await t;
  30.     }
  31.     catch (Exception ex) {
  32.       Console.WriteLine(ex.Message);
  33.     }
  34.   }
  35. }

This approach restricts the server to listen to requests using only the server’s first assigned IPv4 address. A simpler alternative could allow the server to accept requests sent to any of its addresses by just assigning the member field as this.ipAddress = IPAddress.Any.

Notice the service’s Run method signature uses the async modifier, indicating that in the body of the method some asynchronous method will be called in conjunction with the await keyword. The method returns void rather than the more usual Task because Run is called by the Main method, which, as a special case, does not allow the async modifier. An alternative is to define method Run to return type Task and then call the method as service.Run().Wait.

The service’s Run method instantiates a TcpListener object using the server’s IP address and port number. The listener’s Start method begins monitoring the specified port, waiting for a connection request.

Inside the main processing while loop, a TcpClient object, which you can think of as an intelligent socket, is created and waits for a connection via the AcceptTcpClientAsync method. Prior to the .NET Framework 4.5, you’d have to use BeginAcceptTcpClient and then write custom asynchronous coordination code, which, believe me, is not simple. The .NET Framework 4.5 adds many new methods that, by convention, end with “Async.” These new methods, combined with the async and await keywords, make asynchronous programming much, much easier.

Method Run calls method Process using two statements. An alternative is to use shortcut syntax and call method Process in a single statement: await Process(tcpClient).

To summarize, the service uses TcpListener and TcpClient objects to hide the complexity of raw socket programming, and uses the new AcceptTcpClientAsync method in conjunction with the new async and await keywords to hide the complexity of asynchronous programming. Method Run sets up and coordinates connection activities, and calls method Process to process requests and then a second statement to await on the return Task.

The Service Process and Response Methods

The Process and Response methods of the service object are presented in Figure 4. The Process method’s signature uses the async modifier and returns type Task.

Figure 4 The Demo Service Process and Response Methods

  1. private async Task Process(TcpClient tcpClient)
  2. {
  3.   string clientEndPoint =
  4.     tcpClient.Client.RemoteEndPoint.ToString();
  5.   Console.WriteLine(“Received connection request from “
  6.     + clientEndPoint);
  7.   try {
  8.     NetworkStream networkStream = tcpClient.GetStream();
  9.     StreamReader reader = new StreamReader(networkStream);
  10.     StreamWriter writer = new StreamWriter(networkStream);
  11.     writer.AutoFlush = true;
  12.     while (true) {
  13.       string request = await reader.ReadLineAsync();
  14.       if (request != null) {
  15.         Console.WriteLine(“Received service request: ” + request);
  16.         string response = Response(request);
  17.         Console.WriteLine(“Computed response is: ” + response + “\n”);
  18.         await writer.WriteLineAsync(response);
  19.       }
  20.       else
  21.         break; // Client closed connection
  22.     }
  23.     tcpClient.Close();
  24.   }
  25.   catch (Exception ex) {
  26.     Console.WriteLine(ex.Message);
  27.     if (tcpClient.Connected)
  28.       tcpClient.Close();
  29.   }
  30. }
  31. private static string Response(string request)
  32. {
  33.   string[] pairs = request.Split(‘&’);
  34.   string methodName = pairs[0].Split(‘=’)[1];
  35.   string valueString = pairs[1].Split(‘=’)[1];
  36.   string[] values = valueString.Split(‘ ‘);
  37.   double[] vals = new double[values.Length];
  38.   for (int i = 0; i < values.Length; ++i)
  39.     vals[i] = double.Parse(values[i]);
  40.   string response = “”;
  41.   if (methodName == “average”) response += Average(vals);
  42.   else if (methodName == “minimum”) response += Minimum(vals);
  43.   else response += “BAD methodName: ” + methodName;
  44.   int delay = ((int)vals[0]) * 1000; // Dummy delay
  45.   System.Threading.Thread.Sleep(delay);
  46.   return response;
  47. }

One of the advantages of using low-level sockets instead of Windows Communication Foundation (WCF) is that you can easily insert diagnostic WriteLine statements anywhere you choose. In the demo, I replaced clientEndPoint with the dummy IP address value 123.45.678.999 for security reasons.

The three key lines in method Process are:

  1. string request = await reader.ReadLineAsync();
  2. string response = Response(request);
  3. await writer.WriteLineAsync(response);

You can interpret the first statement to mean, “read a line of the request asynchronously, allowing other statements to execute if necessary.” Once the request string is obtained, it’s passed to the Response helper. Then the response is sent back to the requesting client asynchronously.

The server is using a read-request, write-response cycle. It’s simple, but there are several caveats of which you should be aware. If the server reads without writing, it can’t detect a half-open situation. If the server writes without reading (for example, responding with a large amount of data), it could deadlock with the client. A read-write design is acceptable for simple in-house services but shouldn’t be used for services that are critical or public-facing.

The Response method accepts the request string, parses the request and computes a response string. A simultaneous strength and weakness of a socket-based service is that you must craft some sort of custom protocol. In this case, requests are assumed to look like:

method=average&data=1.1 2.2 3.3&eor

In other words, the service expects the literal “method=” followed by the string “average” or “minimum,” then an ampersand character (“&”) followed by the literal “data=”. The actual input data must be in space-delimited form. The request is terminated by an “&” followed by the literal “eor,” which stands for end-of-request. A disadvantage of socket-based services compared to WCF is that serializing complex parameter types can be a bit tricky sometimes.

In this demo example, the service response is simple, just a string representation of the average or minimum of an array of numeric values. In many custom client-server situations, you’ll have to design some protocol for the service response. For example, instead of sending a response just as “4.00,” you might want to send the response as “average=4.00.”

Method Process uses a relatively crude approach to close a connection if an Exception occurs. An alternative is to use the C# using statement (which will automatically close any connection) and remove the explicit call to method Close.

Helper methods Average and Minimum are defined as:

  1. private static double Average(double[] vals)
  2. {
  3.   double sum = 0.0;
  4.   for (int i = 0; i < vals.Length; ++i)
  5.     sum += vals[i];
  6.   return sum / vals.Length;
  7. }
  8. private static double Minimum(double[] vals)
  9. {
  10.   double min = vals[0]; ;
  11.   for (int i = 0; i < vals.Length; ++i)
  12.     if (vals[i] < min) min = vals[i];
  13.   return min;
  14. }

In most situations, if you’re using a program structure similar to the demo service, your helper methods at this point would connect to some data source and fetch some data. An advantage of low-level services is that you have greater control over your data-access approach. For example, if you’re getting data from SQL, you can use classic ADO.NET,  the Entity Framework or any other data access method.

A disadvantage of a low-level approach is you must explicitly determine how to handle errors in your system. Here, if the demo service is unable to satisfactorily parse the request string, instead of returning a valid response (as a string), the service returns an error message. Based on my experience, there are very few general principles on which to rely. Each service requires custom error handling.

Notice the Response method has a dummy delay:

  1. int delay = ((int)vals[0]) * 1000;
  2. System.Threading.Thread.Sleep(delay);

This response delay, arbitrarily based on the first numeric value of the request, was inserted to slow the service down so that the WinForm and Web application clients could demonstrate UI responsiveness while waiting for a response.

The WinForm Application Demo Client

To create the WinForm client shown in Figure 1, I launched Visual Studio 2012 and created a new C# WinForm application named DemoFormClient. Note that, by default, Visual Studio modularizes a WinForm application into several files that separate the UI code from the logic code. For the code download that accompanies this article, I refactored the modularized Visual Studio code into a single source code file. You can compile the application by launching a Visual Studio command shell (which knows where the C# compiler is), and executing the command: csc.exe /target:winexe DemoFormClient.cs.

Using the Visual Studio design tools, I added a ComboBox control, a TextBox control, two Button controls and a ListBox control, along with four Label controls. For the ComboBox control, I added strings “average” and “minimum” to the control’s Items collection property. I changed the Text properties of button1 and button2 to Send Async and Say Hello, respectively. Then, in design view, I double-clicked on the button1 and button2 controls to register their event handlers. I edited the click handlers as shown inFigure 5.

Figure 5 WinForm Demo Client Button Click Handlers

  1. private async void button1_Click(object sender, EventArgs e)
  2. {
  3.   try {
  4.     string server = “mymachine.network.microsoft.com”;
  5.     int port = 50000;
  6.     string method = (string)comboBox1.SelectedItem;
  7.     string data = textBox1.Text;
  8.     Task<string> tsResponse =
  9.       SendRequest(server, port, method, data);
  10.     listBox1.Items.Add(“Sent request, waiting for response”);
  11.     await tsResponse;
  12.     double dResponse = double.Parse(tsResponse.Result);
  13.     listBox1.Items.Add(“Received response: ” +
  14.      dResponse.ToString(“F2”));
  15.   }
  16.   catch (Exception ex) {
  17.     listBox1.Items.Add(ex.Message);
  18.   }
  19. }
  20. private void button2_Click(object sender, EventArgs e)
  21. {
  22.   listBox1.Items.Add(“Hello”);
  23. }

Notice the signature of the button1 control’s click handler was changed to include the async modifier. The handler sets up a hardcoded server machine name as a string and port number. When using low-level socket-based services, there’s no automatic discovery mechanism and so clients must have access to the server name or IP address and port information.

The key lines of code are:

  1. Task<string> tsResponse = SendRequest(server, port, method, data);
  2. // Perform some actions here if necessary
  3. await tsResponse;
  4. double dResponse = double.Parse(tsResponse.Result);

SendRequest is a program-defined asynchronous method. The call can be loosely interpreted as “send an asynchronous request that will return a string, and when finished continue execution at the statement ‘await tsResponse,’ which occurs later.” This allows the application to perform other actions while waiting for the response. Because the response is encapsulated in a Task, the actual string result must be extracted using the Result property. That string result is converted to type double so that it can be formatted nicely to two decimal places.

An alternative calling approach is:

  1. string sResponse = await SendRequest(server, port, method, data);
  2. double dResponse = double.Parse(sResponse);
  3. listBox1.Items.Add(“Received response: ” + dResponse.ToString(“F2”));

Here, the await keyword is placed in-line with the asynchronous call to SendRequest. This simplifies the calling code a bit and also allows the return string to be fetched without a call to Task.Result. The choice of using an inline await call or using a separate-statement await call will vary from situation to situation, but as a general rule of thumb, it’s better to avoid the explicit use of a Task object’s Result property.

Most of the asynchronous work is performed in the Send­Request method, which is listed in Figure 6. Because SendRequest is asynchronous, it might better be named SendRequestAsync or MySendRequestAsync.

Figure 6 WinForm Demo Client SendRequest Method

  1. private static async Task<string> SendRequest(string server,
  2.   int port, string method, string data)
  3. {
  4.   try {
  5.     IPAddress ipAddress = null;
  6.     IPHostEntry ipHostInfo = Dns.GetHostEntry(server);
  7.     for (int i = 0; i < ipHostInfo.AddressList.Length; ++i) {
  8.       if (ipHostInfo.AddressList[i].AddressFamily ==
  9.         AddressFamily.InterNetwork)
  10.       {
  11.         ipAddress = ipHostInfo.AddressList[i];
  12.         break;
  13.       }
  14.     }
  15.     if (ipAddress == null)
  16.       throw new Exception(“No IPv4 address for server”);
  17.     TcpClient client = new TcpClient();
  18.     await client.ConnectAsync(ipAddress, port); // Connect
  19.     NetworkStream networkStream = client.GetStream();
  20.     StreamWriter writer = new StreamWriter(networkStream);
  21.     StreamReader reader = new StreamReader(networkStream);
  22.     writer.AutoFlush = true;
  23.     string requestData = “method=” + method + “&” + “data=” +
  24.       data + “&eor”; // ‘End-of-request’
  25.     await writer.WriteLineAsync(requestData);
  26.     string response = await reader.ReadLineAsync();
  27.     client.Close();
  28.     return response;
  29.   }
  30.   catch (Exception ex) {
  31.     return ex.Message;
  32.   }
  33. }

SendRequest accepts a string representing the server name and begins by resolving that name to an IP address using the same code logic that was used in the service class constructor. A simpler alternative is to just pass the name of the server: await client.ConnectAsync(server, port).

After the server’s IP address is determined, a TcpClient intelligent-socket object is instantiated and the object’s Connect­Async method is used to send a connection request to the server. After setting up a network StreamWriter object to send data to the server and a StreamReader object to receive data from the server, a request string is created using the formatting expected by the server. The request is sent and received asynchronously and returned by the method as a string.

The Web Application Demo Client

I created the demo Web application client shown in Figure 1 in two steps. First, I used Visual Studio to create a Web site to host the application, and then I coded the Web application using Notepad. I launched Visual Studio 2012 and created a new C# Empty Web Site named DemoClient at http://localhost/. This set up all the necessary IIS plumbing to host an application and created the physical location associated with the Web site at C:\inetpub\wwwroot\DemoClient\. The process also created a basic configuration file, Web.config, which contains information to allow applications in the site to access async functionality in the .NET Framework 4.5:

<?xml version="1.0"?>
<configuration>
  <system.web>
    <compilation debug="false" targetFramework="4.5" />
    <httpRuntime targetFramework="4.5" />
  </system.web>
</configuration>

Next, I launched Notepad with administrative privileges. When creating simple ASP.NET applications, I sometimes prefer using Notepad instead of Visual Studio so I can keep all application code in a single .aspx file, rather than generating multiple files and unwanted example code. I saved the empty file as DemoWeb­Client.aspx at C:\inetpub\wwwroot\DemoClient.

The overall structure of the Web application is shown in Figure 7.

Figure 7 Web Application Demo Client Structure

  1. <%@ Page Language=”C#” Async=”true” AutoEventWireup=”true”%>
  2. <%@ Import Namespace=”System.Threading.Tasks” %>
  3. <%@ Import Namespace=”System.Net” %>
  4. <%@ Import Namespace=”System.Net.Sockets” %>
  5. <%@ Import Namespace=”System.IO” %>
  6. <script runat=”server” language=”C#”>
  7.   private static async Task<string> SendRequest(string server,
  8.   private async void Button1_Click(object sender, System.EventArgs e) { . . }
  9. </script>
  10. <head>
  11.   <title>Demo</title>
  12. </head>
  13. <body>
  14.   <form id=”form1″ runat=”server”>
  15.   <div>
  16.   <p>Enter service method:
  17.     <asp:TextBox ID=”TextBox1″ runat=”server”></asp:TextBox></p>
  18.   <p>Enter data:
  19.     <asp:TextBox ID=”TextBox2″ runat=”server”></asp:TextBox></p>
  20.   <p><asp:Button Text=”Send Request” id=”Button1″
  21.     runat=”server” OnClick=”Button1_Click”> </asp:Button> </p>
  22.   <p>Response:
  23.     <asp:TextBox ID=”TextBox3″ runat=”server”></asp:TextBox></p>
  24.   <p>Dummy responsive control:
  25.     <asp:TextBox ID=”TextBox4″ runat=”server”></asp:TextBox></p>
  26.   </div>
  27.   </form>
  28. </body>
  29. </html>

At the top of the page I added Import statements to bring the relevant .NET namespaces into scope, and a Page directive that includes the Async=true attribute.

The C# script region contains two methods, SendRequest and Button1_Click. The application page body has two TextBox controls and one Button control for input, an output TextBox control to hold the service response, and a dummy, unused TextBox control to demonstrate UI responsiveness while the application waits for the service to respond to a request.

The code for the Web application’s SendRequest method is exactly the same as the code in the WinForm application’s Send­Request. The code for the Web application’s Button1_Click handler differs only slightly from the WinForm’s button1_Click handler to accommodate the different UI:

  1. try {
  2.   string server = “mymachine.network.microsoft.com”;
  3.   int port = 50000;
  4.   string method = TextBox1.Text;
  5.   string data = TextBox2.Text;
  6.   string sResponse = await SendRequest(server, port, method, data);
  7.   double dResponse = double.Parse(sResponse);
  8.   TextBox3.Text = dResponse.ToString(“F2”);
  9. }
  10. catch (Exception ex) {
  11.   TextBox3.Text = ex.Message;
  12. }

Even though the code for the Web application is essentially the same as the code for the WinForm application, the calling mechanism is quite a bit different. When a user makes a request using the WinForm, the WinForm issues the call directly to the service and the service responds directly to the WinForm. When a user makes a request from the Web application, the Web application sends the request information to the Web server that’s hosting the application, the Web server makes the call to the service, the service responds to the Web server, the Web server constructs a response page that includes the response and the response page is sent back to the client browser.

Wrapping Up

So, when should you consider using asynchronous TCP sockets instead of WCF? Roughly 10 years ago, before the creation of WCF and its predecessor technology ASP.NET Web Services, if you wanted to create a client-server system, using sockets was often the most logical option. The introduction of WCF was a big advance, but because of the huge number of scenarios WCF is designed to handle, using it for simple client-server systems might be overkill in some situations. Although the latest version of WCF is easier to configure than previous versions, it can still be tricky to work with WCF.

For situations where the client and server are on different networks, making security a major consideration, I always use WCF. But for many client-server systems where client and server are located on a single secure enterprise network, I often prefer using TCP sockets.

A relatively new approach for implementing client-server systems is to use the ASP.NET Web API framework for HTTP-based services combined with the ASP.NET SignalR library for asynchronous methods. This approach, in many cases, is simpler to implement than using WCF and avoids many of the low-level details involved with a socket approach.


Dr. James McCaffrey works for Microsoft Research in Redmond, Wash. He has worked on several Microsoft products including Internet Explorer and Bing. He can be reached at jammc@microsoft.com.

Thanks to the following technical experts for their advice and for reviewing this article: Piali Choudhury (MS Research), Stephen Cleary (consultant), Adam Eversole (MS Research) Lynn Powers (MS Research) and Stephen Toub (Microsoft)

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s