Compartir a través de


Programación asincrónica

Sockets TCP asincrónicos como alternativa a WCF

James McCaffrey

Descargar el código de ejemplo

En los entornos con tecnologías Microsoft, Windows Communication Foundation (WCF) es una elección muy común para la creación de sistemas cliente/servidor. Existen muchas alternativas a WCF, por supuesto, cada una con sus propias ventajas y desventajas: por ejemplo HTTP Web Services, Web API, DCOM, las tecnologías web AJAX, la programación de canalizaciones con nombre y la programación de sockets TCP puros. Pero si tomamos en cuenta factores tales como el trabajo de desarrollo, la facilidad de administración, escalabilidad, rendimiento y seguridad, en muchas situaciones el uso de WCF es la opción más eficiente.

Sin embargo, WCF puede ser extremadamente complicado y podría ser excesivo en algunas situaciones de programación. Antes del lanzamiento de Microsoft .NET Framework 4.5, en la mayoría de los casos la programación de sockets asincrónicos era, en mi opinión, demasiado difícil para justificar su uso. Pero la sencillez de las nuevas características await y async en C# altera totalmente el equilibrio, así que el uso de sockets para los sistemas cliente/servidor es ahora una opción más atractiva que antes. Este artículo explica cómo usar estas nuevas características asincrónicas de .NET Framework 4.5 para crear sistemas cliente/servidor asincrónicos de bajo nivel con un excelente rendimiento.

La mejor forma de entender lo que pretendo es echar una mirada al sistema cliente/servidor de demostración de la figura 1. En el extremo superior de la imagen, un shell de comandos ejecuta un servicio basado en sockets TCP que acepta solicitudes para calcular el valor promedio o mínimo de un conjunto de valores numéricos. En la parte central de la imagen aparece una aplicación Windows Forms (WinForm) que envió una solicitud para calcular el promedio de (3, 1, 8). Observe que el cliente es asincrónico: después de enviar la solicitud, mientras espera que el servicio responda, el usuario puede hacer clic en el botón rotulado “Say Hello” (“Saludar”) tres veces, y la aplicación responde correctamente.

Demo TCP-Based Service with Two Clients
Figura 1 Servicio de demostración basado en TCP con dos clientes

En el extremo inferior de la figura 1 vemos una aplicación cliente web. El cliente envió una solicitud asincrónica para encontrar el valor mínimo de (5, 2, 7, 4). Aunque esto no se pueda apreciar en la captura de pantalla, mientras la aplicación web espera la respuesta del servicio, sigue respondiendo a las acciones del usuario.

En las siguientes secciones mostraré cómo codificar el servicio, el cliente WinForm y el cliente web. A lo largo del recorrido examinaré los pros y contras del uso de sockets. Este artículo da por entendido que usted tiene al menos conocimientos intermedios de programación en C#, pero no supone que tenga conocimientos profundos ni experiencia considerable con la programación asincrónica. El código descargable que acompaña este artículo contiene el código fuente completo de los tres programas que aparecen en la figura 1. Eliminé la mayoría de las funciones de comprobación de errores normales para que las ideas centrales quedaran más claras.

Creación del servicio

La estructura general del servicio de demostración, con algunos cambios menores para ahorrar espacio, se presenta en la figura 2. Para crear el servicio, inicié Visual Studio 2012, que cuenta con el .NET Framework 4.5 requerido, y creé una nueva aplicación de consola en C# llamada DemoService. Como los servicios basados en sockets tienden a realizar funciones específicas y limitadas, en las situaciones reales se prefiere usar nombres más descriptivos.

Figura 2 Estructura del programa del servicio de demostración

using System;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading.Tasks;
namespace DemoService
{
  class ServiceProgram
  {
    static void Main(string[] args)
    {
      try
      {
        int port = 50000;
        AsyncService service = new AsyncService(port);
        service.Run();
        Console.ReadLine();
      }
      catch (Exception ex)
      {
        Console.WriteLine(ex.Message);
        Console.ReadLine();
      }
    }
  }
  public class AsyncService
  {
    private IPAddress ipAddress;
    private int port;
    public AsyncService(int port) { . . }
    public async void Run() { . . }
    private async Task Process(TcpClient tcpClient) { . . }
    private static string Response(string request)
    private static double Average(double[] vals) { . . }
    private static double Minimum(double[] vals) { . . }
  }
}

Después de cargar el código de la plantilla en el editor, modifiqué las instrucciones using al comienzo del código fuente para incluir System.Net y System.Net.Sockets. En la ventana del Explorador de soluciones cambié el nombre del archivo Program.cs a ServiceProgram.cs, y Visual Studio cambió automáticamente el nombre de la clase Program. Iniciar el servicio es muy sencillo:

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

Cada servicio personalizado basado en sockets en un servidor debe usar un puerto único. Generalmente se usan los puertos número 49152 a 65535 para los servicios personalizados. Evitar las colisiones de puertos puede ser bastante difícil. Existe la posibilidad de reservar números de puertos en un servidor mediante el elemento ReservedPorts del registro del sistema. El servicio emplea un diseño de programación orientada en objetos, donde las instancias se crean mediante un constructor que acepta el número del puerto. Como los números de los puertos de servicio son fijos, el número del puerto se puede codificar de forma rígida en vez de pasarlo como parámetro. El método Run contiene un bucle while que aceptará y procesará las solicitudes de los clientes hasta que el shell de la consola reciba la señal de la tecla <Entrar>.

La clase AsyncService tiene dos miembros privados, ipAddress y port. Estos dos valores definen, en esencia, un socket. El constructor acepta un número de puerto y determina en forma programática la dirección IP del servidor. El método público Run realiza todo el trabajo de aceptar las solicitudes, calcular las respuestas y de enviarlas. El método Run llama el método auxiliar Process que, a su vez, llama el método auxiliar Response. El método Response llama los métodos auxiliares Average y Minimum.

Existen muchas formas de organizar un servidor basado en sockets. La estructura que empleé en esta demostración trata de encontrar el equilibrio entre la modularidad y la sencillez y me ha dado buenos resultados en la práctica.

Constructor del servicio y método Run

Los dos métodos públicos del servicio de demostración basado en sockets se muestran en la figura 3. Después de almacenar el nombre del puerto, el constructor usa el método GetHostName para determinar el nombre del servidor y recuperar luego una estructura que contiene información sobre el servidor. La colección AddressList contiene diferentes direcciones de máquina: entre ellas, direcciones IPv4 e IPv6. El valor de la enumeración InterNetwork se refiere a una dirección IPv4.

Figura 3 Constructor del servicio y método Run

public AsyncService(int port)
{
  this.port = port;
  string hostName = Dns.GetHostName();
  IPHostEntry ipHostInfo = Dns.GetHostEntry(hostName);
  this.ipAddress = null;
  for (int i = 0; i < ipHostInfo.AddressList.Length; ++i) {
    if (ipHostInfo.AddressList[i].AddressFamily ==
      AddressFamily.InterNetwork)
    {
      this.ipAddress = ipHostInfo.AddressList[i];
      break;
    }
  }
  if (this.ipAddress == null)
    throw new Exception("No IPv4 address for server");
}
public async void Run()
{
  TcpListener listener = new TcpListener(this.ipAddress, this.port);
  listener.Start();
  Console.Write("Array Min and Avg service is now running"
  Console.WriteLine(" on port " + this.port);
  Console.WriteLine("Hit <enter> to stop service\n");
  while (true) {
    try {
      TcpClient tcpClient = await listener.AcceptTcpClientAsync();
      Task t = Process(tcpClient);
      await t;
    }
    catch (Exception ex) {
      Console.WriteLine(ex.Message);
    }
  }
}

En este caso, el servidor se limita a escuchar solo las solicitudes con la primera dirección IPv4 asignada del servidor. Una alternativa más sencilla podría permitir que el servidor acepte las solicitudes que se envían a cualquier dirección, al asignar simplemente el campo miembro como this.ipAddress = IPAddress.Any.

Observe que la firma del método Run del servicio usa el modificador async para indicar que en el cuerpo del método se llamará algún método asincrónico junto con la palabra clave await. El método devuelve void en vez del Task más usual, ya que Run es llamado por el método Main que, como caso especial, no permite el modificador async. Otra alternativa sería definir el método Run para que devuelva el tipo Task y luego llamar el método como service.Run().Wait.

El método Run del servicio crea una instancia de TcpListener con la dirección IP del servidor y el número del puerto. El método Start del agente de escucha comienza a supervisar el puerto especificado mientras espera una solicitud de conexión.

Dentro del bucle de procesamiento principal while, se crea un objeto TcpClient (que podemos imaginar como un socket inteligente), que espera una conexión mediante el método AcceptTcpClientAsync. Antes de .NET Framework 4.5, tendríamos que haber usado BeginAcceptTcpClient y luego escribir código personalizado para la coordinación asincrónica, lo que no es nada fácil. .NET Framework 4.5 cuenta con muchos métodos nuevos que, por convención, terminan en “Async”. Gracias a estos métodos nuevos, en conjunto con las palabras claves async y await, la programación asincrónica es muchísimo más fácil.

El método Run llama el método Process mediante dos instrucciones. También podríamos emplear una sintaxis abreviada y llamar el método Process con una sola instrucción: await Process(tcpClient).

Para resumir, el servicio usa los objetos TcpListener y TcpClient para ocultar la complejidad de la programación de los sockets básicos y utiliza el nuevo método AcceptTcpClientAsync junto con las nuevas palabras clave async y await para ocultar la complejidad de la programación asincrónica. El método Run establece y coordina las actividades de conexión y llama primero el método Process para procesar las solicitudes y luego una segunda instrucción para esperar el Task devuelto.

Métodos Process y Response del servicio

Los métodos Process y Response del objeto de servicio se muestran en la figura 4. La firma del método Process usa el modificador async y devuelve el tipo Task.

Figura 4 Métodos Process y Response del servicio de demostración

private async Task Process(TcpClient tcpClient)
{
  string clientEndPoint =
    tcpClient.Client.RemoteEndPoint.ToString();
  Console.WriteLine("Received connection request from "
    + clientEndPoint);
  try {
    NetworkStream networkStream = tcpClient.GetStream();
    StreamReader reader = new StreamReader(networkStream);
    StreamWriter writer = new StreamWriter(networkStream);
    writer.AutoFlush = true;
    while (true) {
      string request = await reader.ReadLineAsync();
      if (request != null) {
        Console.WriteLine("Received service request: " + request);
        string response = Response(request);
        Console.WriteLine("Computed response is: " + response + "\n");
        await writer.WriteLineAsync(response);
      }
      else
        break; // Client closed connection
    }
    tcpClient.Close();
  }
  catch (Exception ex) {
    Console.WriteLine(ex.Message);
    if (tcpClient.Connected)
      tcpClient.Close();
  }
}
private static string Response(string request)
{
  string[] pairs = request.Split('&');
  string methodName = pairs[0].Split('=')[1];
  string valueString = pairs[1].Split('=')[1];
  string[] values = valueString.Split(' ');
  double[] vals = new double[values.Length];
  for (int i = 0; i < values.Length; ++i)
    vals[i] = double.Parse(values[i]);
  string response = "";
  if (methodName == "average") response += Average(vals);
  else if (methodName == "minimum") response += Minimum(vals);
  else response += "BAD methodName: " + methodName;
  int delay = ((int)vals[0]) * 1000; // Dummy delay
  System.Threading.Thread.Sleep(delay);
  return response;
}

Una de las ventajas de los sockets de bajo nivel en comparación con Windows Communication Foundation (WCF) es que podemos insertar fácilmente instrucciones WriteLine de diagnóstico donde queramos. En la demostración, reemplacé clientEndPoint con la dirección IP de prueba 123.45.678.999 por razones de seguridad.

Las tres líneas clave en el método Process son:

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

Podemos interpretar la primera instrucción como “leer una línea de la solicitud en forma asincrónica y permitir que se ejecuten otras instrucciones, si fuera necesario”. Una vez que se obtiene la cadena de la solicitud, esta se pasa al método auxiliar Response. Luego la respuesta se envía en forma asincrónica de vuelta al cliente solicitante.

El servidor emplea un ciclo de lectura-de-la-solicitud y escritura-de-la-respuesta. Es sencillo, pero hay varias salvedades que debemos tener presentes. Si el servidor lee sin escribir, no es capaz de detectar una situación abierta a medias. Si el servidor escribe sin leer (por ejemplo, al responder con una gran cantidad de datos), podría producirse un interbloqueo con el cliente. El diseño de lectura/escritura es aceptable en el caso de los servicios sencillos de uso interno, pero no se debería usar para servicios críticos o abiertos al público.

El método Response acepta la cadena de solicitud, analiza la solicitud y calcula una cadena de respuesta. Una fortaleza y una debilidad al mismo tiempo de los servicios basados en sockets es que debemos confeccionar algún tipo de protocolo personalizado. En este caso, se espera que las solicitudes se vean así:

method=average&data=1.1 2.2 3.3&eor

En otras palabras, el servicio espera el literal “method=”, seguido por la cadena “average” o “minimum”, luego el carácter Y comercial (“&”), seguido por el literal “data=”. Los datos de entrada mismos deben ir separados por espacios. La solicitud termina con un “&”, seguido por el literal “eor”, que representa el fin de la solitud (o “end-of-request” en inglés). Una desventaja de los servicios basados en sockets, comparado con WCF, es que la serialización de los tipos de parámetros complejos a veces puede ser un poco difícil.

En esta demostración de ejemplo, la respuesta del servicio es sencilla, simplemente una cadena que representa el valor promedio o mínimo de una matriz con valores numéricos. En muchas situaciones cliente/servidor personalizadas, sin embargo, tendremos que diseñar algún protocolo para la respuesta del servicio. Por ejemplo, en vez de enviar una respuesta simplemente como “4.00”, podríamos enviar la respuesta como “average=4.00”.

El método Process utiliza un método relativamente burdo para cerrar una conexión en el caso de una excepción. Una alternativa sería emplear la instrucción using de C# (que cierra automáticamente todas las conexiones) y eliminar la llamada explícita al método Close.

Los métodos auxiliares Average y Minimum se definen como:

private static double Average(double[] vals)
{
  double sum = 0.0;
  for (int i = 0; i < vals.Length; ++i)
    sum += vals[i];
  return sum / vals.Length;
}
private static double Minimum(double[] vals)
{
  double min = vals[0]; ;
  for (int i = 0; i < vals.Length; ++i)
    if (vals[i] < min) min = vals[i];
  return min;
}

En la mayoría de las situaciones, si usamos una estructura de programa similar a la del servicio de demostración, los métodos auxiliares en este punto se conectarían con algún origen de datos para recuperar algún tipo de datos. Una ventaja de los servicios de bajo nivel es que entregan un control mayor sobre el método de acceso a los datos. Por ejemplo, si recibimos datos desde SQL, podemos usar ADO.NET clásico, Entity Framework u otro método de acceso a datos.

Una desventaja del método de bajo nivel es que debemos determinar explícitamente cómo controlar los errores en el sistema. Aquí, si el servicio de demostración no es capaz de analizar satisfactoriamente la cadena de solicitud, entonces en vez de devolver una respuesta válida (en forma de cadena) devuelve un mensaje de error. En mi experiencia, hay muy pocos principios generales que nos sirvan de guía. Cada servicio requiere de un control de errores personalizado.

Observe que el método Response tiene un retraso artificial:

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

Este retraso, basado arbitrariamente en el primer valor numérico de la solicitud, se insertó para poner freno al servicio, para permitir que los clientes WinForm y de aplicación web demuestren la capacidad de respuesta de la interfaz de usuario mientras esperan una respuesta.

El cliente de demostración WinForm

Para crear el cliente WinForm que aparece en la figura 1, inicié Visual Studio 2012 y creé una nueva aplicación WinForm en C#, llamada DemoFormClient. Observe que, de manera predeterminada, Visual Studio modulariza las aplicaciones WinForm en varios archivos que separan el código de la interfaz de usuarios del código con la lógica. Para el código de descarga que viene con este artículo, refactoricé el código modularizado de Visual Studio en un archivo de código fuente único. Usted puede compilar la aplicación al iniciar un shell de comando de Visual Studio (que sabe dónde se encuentra el compilador de C) y ejecutar el comando: csc.exe /target:winexe DemoFormClient.cs.

Con las herramientas de diseño de Visual Studio, agregué un control ComboBox, un control TextBox, dos controles Button y un control ListBox, junto con cuatro controles Label. Para el control ComboBox, agregué las cadenas “average” y “minimum” a la propiedad de colección Items. Cambié las propiedades Text de button1 y button2 a Send Async y Say Hello, respectivamente. Luego, en la vista de diseño, hice doble clic en los controles button1 y button2 para registrar los controladores de eventos. Edité los controladores de clic, tal como se aprecia en la figura 5.

Figura 5 Controladores de clic de botón para el programa de demostración WinForm

private async void button1_Click(object sender, EventArgs e)
{
  try {
    string server = "mymachine.network.microsoft.com";
    int port = 50000;
    string method = (string)comboBox1.SelectedItem;
    string data = textBox1.Text;
    Task<string> tsResponse = 
      SendRequest(server, port, method, data);
    listBox1.Items.Add("Sent request, waiting for response");
    await tsResponse;
    double dResponse = double.Parse(tsResponse.Result);
    listBox1.Items.Add("Received response: " +
     dResponse.ToString("F2"));
  }
  catch (Exception ex) {
    listBox1.Items.Add(ex.Message);
  }
}
private void button2_Click(object sender, EventArgs e)
{
  listBox1.Items.Add("Hello");
}

Observe que la firma del controlador de clics del control button1 se cambió para incluir el modificador async. El controlador configura un nombre de equipo codificado de manera rígida en forma de una cadena y un número de puerto. Como al usar servicios de bajo nivel basados en sockets no hay ningún mecanismo automático de detección, los clientes deben tener acceso a la información sobre el nombre del servidor o la dirección IP y el puerto.

Las líneas claves del código son:

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

SendRequest es un método asincrónico definido por el programa. La llamada se puede interpretar libremente como “enviar una solicitud asincrónica que devolverá una cadena y, al finalizar, seguir con la ejecución en la instrucción ‘await tsResponse’ que ocurre más adelante”. Esto permite que la aplicación lleve a cabo otras acciones mientras espera la respuesta. Como la respuesta está encapsulada en una Task, la cadena resultante se debe extraer con la propiedad Result. Esta cadena resultante se convierte en el tipo double para poder presentarla convenientemente con dos cifras decimales.

Otra forma para realizar la llamada es:

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

Aquí la palabra clave await se escribe insertada junto con la llamada asincrónica a SendRequest. Esto simplifica un poco el código que genera la llamada y también permite recuperar la cadena de devolución sin la llamada a Task.Result. La elección de usar la llamada await de manera insertada o de usar una llamada await en una instrucción independiente varía en los diferentes casos, pero ,como regla general, es mejor evitar el uso explícito de la propiedad Result del objeto Task.

La mayor parte del trabajo asincrónico se lleva a cabo en el método Send­Request, que se muestra en la figura 6. Como SendRequest es asincrónico, sería preferible llamarlo SendRequestAsync o MySendRequestAsync.

Figura 6 Método SendRequest del cliente de demostración WinForm

private static async Task<string> SendRequest(string server,
  int port, string method, string data)
{
  try {
    IPAddress ipAddress = null;
    IPHostEntry ipHostInfo = Dns.GetHostEntry(server);
    for (int i = 0; i < ipHostInfo.AddressList.Length; ++i) {
      if (ipHostInfo.AddressList[i].AddressFamily ==
        AddressFamily.InterNetwork)
      {
        ipAddress = ipHostInfo.AddressList[i];
        break;
      }
    }
    if (ipAddress == null)
      throw new Exception("No IPv4 address for server");
    TcpClient client = new TcpClient();
    await client.ConnectAsync(ipAddress, port); // Connect
    NetworkStream networkStream = client.GetStream();
    StreamWriter writer = new StreamWriter(networkStream);
    StreamReader reader = new StreamReader(networkStream);
    writer.AutoFlush = true;
    string requestData = "method=" + method + "&" + "data=" +
      data + "&eor"; // 'End-of-request'
    await writer.WriteLineAsync(requestData);
    string response = await reader.ReadLineAsync();
    client.Close();
    return response;
  }
  catch (Exception ex) {
    return ex.Message;
  }
}

SendRequest acepta una cadena que representa el nombre del servidor y comienza por resolver ese nombre en una dirección IP mediante la misma lógica en código que se empleó para el constructor de la clase del servicio. Otra alternativa más sencilla sería pasar simplemente el nombre del servidor: await client.ConnectAsync(server, port).

Una vez que se determinó la dirección IP del servidor, se crea una instancia del objeto de socket inteligente TcpClient y se usa el método Connect­Async del objeto para enviar una solicitud de conexión al servidor. Después de configurar un objeto StreamWriter de red para enviar datos al servidor y un objeto StreamReader para recibir los datos del servidor, se crea una cadena de solicitud con el formato esperado por el servidor. La solicitud se envía y recibe de manera asincrónica y el método la devuelve en forma de cadena.

El cliente de demostración web

Creé la aplicación web de demostración que aparece en la figura 1 en dos pasos. Primero usé Visual Studio para crear un sitio web para hospedar la aplicación y luego escribí la aplicación con el Bloc de notas. Inicié Visual Studio 2012 y creé un nuevo Sitio web vacío en C#, llamado DemoClient at http://localhost/. Esto configuró toda la estructura necesaria de IIS para hospedar una aplicación y creó la ubicación física asociada con el sitio web en C:\inetpub\wwwroot\DemoClient\. El proceso también creó un archivo de configuración básico Web.config, que contiene la información necesaria para permitir que las aplicaciones del sitio puedan acceder a las funciones asincrónicas de .NET Framework 4.5:

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

Luego inicié el Bloc de notas con privilegios administrativos. Al crear aplicaciones ASP.NET simples, a veces prefiero usar el Bloc de notas en vez de Visual Studio para dejar todo el código de la aplicación en un solo archivo .aspx, en vez de generar varios archivos y código de ejemplo indeseado. Guardé el archivo vacío como DemoWeb­Client.aspx en C:\inetpub\wwwroot\DemoClient.

La estructura general de la aplicación web se muestra en la figura 7.

Figura 7 Estructura de la aplicación web cliente de demostración

    <%@ Page Language="C#" Async="true" AutoEventWireup="true"%>
    <%@ Import Namespace="System.Threading.Tasks" %>
    <%@ Import Namespace="System.Net" %>
    <%@ Import Namespace="System.Net.Sockets" %>
    <%@ Import Namespace="System.IO" %>
    <script runat="server" language="C#">
      private static async Task<string> SendRequest(string server,
      private async void Button1_Click(object sender, System.EventArgs e) { . . }
    </script>
    <head>
      <title>Demo</title>
    </head>
    <body>
      <form id="form1" runat="server">
      <div>
      <p>Enter service method:
        <asp:TextBox ID="TextBox1" runat="server"></asp:TextBox></p>
      <p>Enter data:
        <asp:TextBox ID="TextBox2" runat="server"></asp:TextBox></p>
      <p><asp:Button Text="Send Request" id="Button1"
        runat="server" OnClick="Button1_Click"> </asp:Button> </p>
      <p>Response:
        <asp:TextBox ID="TextBox3" runat="server"></asp:TextBox></p>
      <p>Dummy responsive control:
        <asp:TextBox ID="TextBox4" runat="server"></asp:TextBox></p>
      </div>
      </form>
    </body>
    </html>

En el extremo superior de la página agregué instrucciones Import para incluir los espacios de nombres pertinentes de .NET en el ámbito, además de una directiva Page que incluye el atributo Async=true.

La región del script en C# contiene dos métodos, SendRequest y Button1_Click. El cuerpo de la aplicación cuenta con dos controles TextBox y un control Button para la entrada, un control TextBox de salida para contener la respuesta del servicio y un control TextBox genérico, sin usar, para demostrar la capacidad de respuesta de la interfaz de usuario mientras la aplicación espera que el servicio responda a la solicitud.

El código del método SendRequest de la aplicación web es exactamente igual al código de Send­Request de la aplicación WinForm. El código del controlador Button1_Click de la aplicación web solo difiere ligeramente del controlador button1_Click de WinForm, para adaptarse a la interfaz de usuario diferente:

try {
  string server = "mymachine.network.microsoft.com";
  int port = 50000;
  string method = TextBox1.Text;
  string data = TextBox2.Text;
  string sResponse = await SendRequest(server, port, method, data);
  double dResponse = double.Parse(sResponse);
  TextBox3.Text = dResponse.ToString("F2");
}
catch (Exception ex) {
  TextBox3.Text = ex.Message;
}

Aunque el código de la aplicación web es esencialmente el mismo código para la aplicación WinForm, el mecanismo de llamada es un poco diferente. Cuando un usuario realiza una solicitud con WinForm, WinForm emite la llamada directamente al servicio y este responde directamente a WinForm. Cuando el usuario realiza una solicitud desde la aplicación web, esta envía la información de la solicitud al servidor web que hospeda la aplicación, el servidor web realiza la llamada al servicio, el servicio responde al servidor web, el servidor web construye la página de respuesta que incluye la respuesta, y la página de respuesta se envía de vuelta al explorador cliente.

En resumen

Entonces, ¿en qué circunstancias deberíamos considerar el uso de sockets TCP asincrónicos en vez de WCF? Como hace diez años, antes de la creación de WCF y la tecnología precursora ASP.NET Web Services, si queríamos crear un sistema cliente/servidor, frecuentemente usar sockets era la opción más lógica. El lanzamiento de WCF fue un gran avance, pero, debido a la cantidad enorme de situaciones en las que debe funcionar, usarlo para sistemas cliente/servidor sencillos puede ser excesivo en algunos casos. Aunque la última versión de WCF se configura más fácilmente que las versiones anteriores, todavía puede resultar complicado trabajar con WCF.

En aquellas situaciones donde el cliente y el servidor se encuentran en redes diferentes y donde la seguridad es un factor importante, siempre uso WCF. Pero muchos sistemas cliente/servidor donde el cliente y el servidor se ubican en una misma red empresarial segura, frecuentemente prefiero los sockets TCP.

Otro método relativamente nuevo para implementar sistemas cliente/servidor es usar el marco ASP.NET Web API para servicios basados en HTTP en combinación con la biblioteca SignalR de ASP.NET para los métodos asincrónicos. En muchos casos, este método se puede implementar más fácilmente que WCF y evita muchos de los detalles de bajo nivel de los métodos con sockets.

Dr. James McCaffrey trabaja para Microsoft Research en Redmond, Washington. Ha colaborado en el desarrollo de varios productos de Microsoft como, por ejemplo, Internet Explorer y Bing. Puede ponerse en contacto con él en jammc@microsoft.com.

Gracias a los siguientes expertos técnicos por sus consejos y por revisar este artículo: Piali Choudhury (MS Research), Stephen Cleary (consultant), Adam Eversole (MS Research) Lynn Powers (MS Research) y Stephen Toub (Microsoft)