Асинхронное программирование

Асинхронные TCP-сокеты как альтернатива WCF

Джеймс Маккафри

Продукты и технологии:

Visual Studio 2012, Microsoft .NET Framework 4.5, C#

В статье рассматриваются:

  • подготовка сервиса на основе TCP-сокета;
  • создание демонстрационного клиентского приложения Windows Forms;
  • создание демонстрационного клиентского веб-приложения;

Исходный код можно скачать по ссылке.

В среде технологий Microsoft применение Windows Communication Foundation (WCF) является распространенным подходом при создании клиент-серверной системы. Конечно, есть много альтернатив WCF, каждая со своими плюсами и минусами, например HTTP Web Services, Web API, DCOM, веб-технологии AJAX, программирование именованных каналов и исходных TCP-сокетов. Но, если учесть такие факторы, как усилия в разработке, управляемость, масштабируемость, производительность и безопасность, во многих ситуациях использование WCF оказывается самым эффективным подходом.

Однако использование WCF может быть крайне сложным и оказаться совершенно излишним в некоторых сценариях программирования. До выпуска Microsoft .NET Framework 4.5 программирование асинхронных сокетов в большинстве случаев было, по моему мнению, слишком трудным, чтобы оправдать усилия на него. Но легкость применения новых языковых средств await и async в C# меняет баланс, поэтому программирование сокетов для асинхронных клиент-серверных систем теперь является более привлекательным вариантом, чем раньше. В этой статье поясняется, как использовать эти новые асинхронные средства .NET Framework 4.5 для создания низкоуровневых, высокопроизводительных асинхронных клиент-серверных программных систем.

Лучший способ понять, куда я клоню, — взглянуть на демонстрационную клиент-серверную систему, показанную на рис. 1. В верхней части иллюстрации представлена командная оболочка, выполняющая сервис на основе асинхронного TCP-сокета, который принимает запросы на вычисление среднего или минимального значения из набора чисел. В середине изображено окно приложения Windows Forms (WinForm), которое посылает запрос на вычисление среднего в (3, 1, 8). Заметьте, что клиент асинхронный: после отправки запроса в процессе ожидания ответа от сервиса пользователь может успеть трижды щелкнуть кнопку с меткой «Say Hello», и приложение остается отзывчивым.

Демонстрационный сервис на основе TCP с двумя клиентами
Рис. 1. Демонстрационный сервис на основе TCP с двумя клиентами

В нижней части рис. 1 показано клиентское веб-приложение в действии. Клиент отправил асинхронный запрос, чтобы найти минимальное значение в (5, 2, 7, 4). Хотя это не очевидно на экранном снимке, пока веб-приложение ожидает ответ от сервиса, оно остается отзывчивым на пользовательский ввод.

В следующих разделах я продемонстрирую, как кодировать сервис, клиент WinForm и клиентское веб-приложение. Попутно мы обсудим все за и против использования сокетов. В этой статье предполагается, что вы по крайней мере на среднем уровне владеете навыками программирования на C#, но глубокого понимания или существенного опыта асинхронного программирования от вас не требуется. К данной статье можно скачать полный пакет исходного кода для всех трех программ, представленных на рис. 1. Большая часть обычной обработки ошибок убрана для большей ясности основных идей.

Создание сервиса

Общая структура демонстрационного сервиса с небольшой правкой для экономии места приведена на рис. 2. Чтобы создать этот сервис, я запустил Visual Studio 2012, в которой есть нужный .NET Framework 4.5, и создал новое консольное приложение на C# с именем DemoService. Поскольку сервисы на основе сокетов обычно имеют специфическую, ограниченную функциональность, на практике желательно присваивать им более описательные названия.

Рис. 2. Структура демонстрационного сервиса

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) { . . }
  }
}

После загрузки кода шаблона в редактор я модифицировал выражения using в начале исходного кода, чтобы включить System.Net и System.Net.Sockets. В окне Solution Explorer я переименовал файл Program.cs в ServiceProgram.cs, и Visual Studio автоматически переименовал класс Program за меня. Запустить сервис легко:

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

Каждый пользовательский сервис на основе сокета, размещенный на сервере, должен работать с уникальным портом. Обычно для пользовательских сервисов выделяются номера портов 49152–65535. Избежать конфликтов с номерами портов может оказаться совсем не просто. Вы можете зарезервировать номера портов на сервере, используя параметр системного реестра ReservedPorts. Сервис использует парадигму объектно-ориентированного программирования (ООП), и его экземпляр создается через конструктор, который принимает номер порта. Поскольку номера портов для сервисов фиксированы, номер порта можно «зашить» в код, не передавая его в качестве параметра. Метод Run содержит цикл while, который принимает и обрабатывает клиентские запросы, пока консольная оболочка не получит нажатия клавиши <enter>.

Каждый пользовательский сервис на основе сокета, размещенный на сервере, должен работать с уникальным портом.

Класс AsyncService содержит два закрытых члена: ipAddress и port. Эти два значения фактически определяют сокет. Конструктор принимает номер порта и программным способом определяет IP-адрес сервера. Открытый метод Run выполняет всю работу по приему запросов, расчетам и отправке ответов. Метод Run вызывает вспомогательный метод Process, а тот — вспомогательный метод Response. Метод Response вызывает вспомогательные методы Average и Minimum.

Существует много способов организации сервера на основе сокетов. В структуре, примененной в демонстрации, я попытался найти баланс между модульностью и простотой, и на практике это отлично сработало для меня.

Конструктор сервиса и методы Run

Два открытых метода демонстрационного сервиса на основе сокета показаны на рис. 3. Сохранив имя порта, конструктор использует метод GetHostName, чтобы узнать имя сервера, а затем получить структуру, которая содержит информацию о сервере. В наборе AddressList хранятся различные адреса машины, в том числе адреса по IPv4 и IPv6. Перечислимое значение InterNetwork указывает IPv4-адрес.

Рис. 3. Конструктор сервиса и методы 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);
    }
  }
}

Этот подход ограничивает сервер прослушиванием запросов только по первому назначенному ему IPv4-адресу. Более простой альтернативой могло бы быть разрешение серверу принимать запросы по любому из его адресов, присвоив this.ipAddress = IPAddress.Any.

Заметьте, что в сигнатуре метода Run сервиса используется модификатор async, указывая, что в теле этого метода будет вызываться какой-то асинхронный метод в сочетании с ключевым словом await. Этот метод возвращает void, а не более привычный Task, поскольку Run вызывается методом Main, который в качестве особого случая не разрешает использования модификатора async. Альтернатива — определить метод Run так, чтобы он возвращал тип Task, а затем вызывал метод как service.Run().Wait.

Метод Run сервиса создает экземпляр объекта TcpListener, используя IP-адрес и номер порта сервера. Метод Start слушателя начинает отслеживать указанный порт, ожидая запрос на соединение.

В основном цикле обработки while создается объект TcpClient, который можно считать интеллектуальным сокетом, и он ждет соединения через метод AcceptTcpClientAsync. До появления .NET Framework 4.5 вам пришлось бы использовать BeginAcceptTcpClient, а затем писать собственный код для координации асинхронных операций, что, поверьте мне, совсем не просто. В .NET Framework 4.5 добавлено много новых методов, имена которых, по соглашению, заканчиваются на «Async». Эти новые методы в сочетании с ключевыми словами async и await резко упрощают асинхронное программирование.

Метод Run вызывает метод Process, используя два выражения. Альтернатива этому — использовать сокращенный синтаксис и вызывать метод Process одним выражением: await Process(tcpClient).

Одно из преимуществ применения низкоуровневых сокетов вместо WCF заключается в том, что можно легко вставлять диагностические выражения WriteLine в любой точке кода.

Итак, сервис использует объекты TcpListener и TcpClient, чтобы скрыть сложность программирования низкоуровневых сокетов, а с помощью нового метода AcceptTcpClientAsync в сочетании с новыми ключевыми словами async и await скрывает и сложность асинхронного программирования. Метод Run настраивает и координирует операции соединения, вызывает метод Process для обработки запросов, а затем использует второе выражение для ожидания возвращаемого Task.

Методы Process и Response сервиса

Эти методы объекта сервиса представлены на рис. 4. В сигнатуре метода Process используется модификатор async и возвращаемый тип Task.

Рис. 4. Методы Process и Response демонстрационного сервиса

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; // клиент закрыл соединение
    }
    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; // искусственная задержка
  System.Threading.Thread.Sleep(delay);
  return response;
}

Одно из преимуществ применения низкоуровневых сокетов вместо Windows Communication Foundation (WCF) заключается в том, что можно легко вставлять диагностические выражения WriteLine в любой точке кода. В демонстрационной программе я заменил clientEndPoint фальшивым IP-адресом 123.45.678.999 из соображений безопасности.

Три основные строки кода в методе Process:

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

Вы можете интерпретировать первое выражение как «асинхронно считать строку запроса, при необходимости позволив выполняться другим выражениям». Как только строка запроса получена, она передается вспомогательному методу Response. Затем ответ асинхронно возвращается запросившему клиенту.

Сервер использует цикл чтения запроса и записи ответа. Это просто, но не без нескольких подвохов, о которых вы должны знать. Если сервер читает без записи, он не сможет обнаружить полузакрытое соединение. Если сервер пишет без чтения (например, отвечает большим объемом данных), он может вызвать взаимоблокировку с клиентом. Архитектура чтения-записи приемлема для простых внутренних сервисов, но не должна применяться для критически важных или общедоступных сервисов.

Метод Response принимает строку запроса, разбирает запрос и вычисляет строку ответу. Сильная и слабая стороны сервиса на основе сокета в том, что вы должны придумать нечто вроде собственного протокола. В данном случае предполагается, что запрос выглядит так:

method=average&data=1.1 2.2 3.3&eor

Иначе говоря, сервис ожидает литерал «method=», за которым следуют строка «average» или «minimum», символ «&» и литерал «data=». Сами входные данные должны быть в виде значений, разделяемых пробелами. Запрос завершается символом «&» и литералом «eor» (аббревиатура от «end-of-request»). Недостаток сервисов на основе сокетов в сравнении с WCF состоит в том, что сериализация параметров со сложными типами может оказаться весьма нелегким делом.

В этом примере ответ сервиса прост: это строковое представление среднего или минимального значения для массива числовых величин. Во многих собственных клиент-серверных системах вам придется создавать какой-то протокол для ответа сервиса. Скажем, вместо отправки ответа просто в виде «4.00» может потребоваться передача ответа в форме «average=4.00».

Метод Process использует сравнительно лобовой подход для закрытия соединения, если возникает исключение. Альтернатива — применение C#-выражения using (которое будет автоматически закрывать любое соединение) и удаление явного вызова метода Close.

Преимущество низкоуровневых сервисов в том, что вы получаете больший контроль над доступом к данным.

Вспомогательные методы Average и Minimum определены следующим образом:

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;
}

В большинстве ситуаций, если вы используете структуру программы, похожую на таковую у демонстрационного сервиса, ваши вспомогательные методы в этот момент соединялись бы с каким-то источником и получали бы некие данные. Преимущество низкоуровневых сервисов в том, что вы получаете больший контроль над доступом к данным. Например, если вы получаете данные от SQL, то можете использовать традиционный ADO.NET, Entity Framework или любой другой метод доступа к данным.

Недостаток низкоуровневого подхода в том, что вы должны явным образом определять, как обрабатывать ошибки в вашей системе. Здесь, если демонстрационный сервис не в состоянии удовлетворительно разобрать строку запроса, вместо возврата корректного ответа (в виде строки) он вернет сообщение об ошибке. Исходя из опыта, существует очень немного универсальных принципов, на которые можно полагаться. Каждый сервис требует собственной обработки ошибок.

Заметьте, что в метод Response введена искусственная задержка:

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

Эта задержка, произвольно выставляемая на основе первого числового значения в запросе, включена для замедления сервиса, чтобы в WinForm- и веб-клиентах можно было продемонстрировать отзывчивость UI в ожидании ответа.

Демонстрационное клиентское приложение WinForm

Чтобы создать WinForm-клиент, показанный на рис. 1, я запустил Visual Studio 2012 и создал новое приложение WinForm на C# с названием DemoFormClient. Заметьте, что по умолчанию Visual Studio разбивает приложение WinForm на несколько файлов, отделяющих UI-код от логики. В сопутствующем этой статье пакете исходного кода я переработал код, разбитый Visual Studio на модули, и поместил его в один файл исходного кода. Вы можете скомпилировать приложение, запустив командную оболочку Visual Studio (которой известно, где находится компилятор C#) и выполнив команду: csc.exe /target:winexe DemoFormClient.cs.

Используя дизайнерские средства Visual Studio, я добавил ряд элементов управления: ComboBox, TextBox, два Button, ListBox и четыре Label. В свойство-набор Items элемента управления ComboBox я включил строки «average» и «minimum». Значения свойств Text элементов button1 и button2 я изменил на Send Async и Say Hello соответственно. Затем в режиме дизайнера я дважды щелкнул button1 и button2, чтобы зарегистрировать их обработчики событий. Эти обработчики я отредактировал, как показано на рис. 5.

Рис. 5. Обработчики событий щелчка кнопок в 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");
}

Заметьте, что сигнатура обработчика щелчков кнопки button1 была изменена и теперь включает модификатор async. Обработчик формирует имя сервера из строки и номера порта. При использовании сервисов на основе низкоуровневых сокетов механизм автоматического обнаружения отсутствует, поэтому у клиентов должен быть доступ к имени или IP-адресу сервера, а также к информации о портах.

Ключевые строки кода:

Task<string> tsResponse = SendRequest(server, port, method, data);
// Здесь при необходимости выполняем какие-то операции
await tsResponse;
double dResponse = double.Parse(tsResponse.Result);

SendRequest — асинхронный метод, определенный в программе. Его вызов можно вольно трактовать так: «отправить асинхронный запрос, который вернет строку, а по окончании продолжить выполнение с выражения await tsResponse, которое встретится позже». Это позволяет приложению выполнять другие операции, ожидая ответ. Поскольку ответ инкапсулирован в Task, реальная строка результата должна быть извлечена из свойства Result. Этот строковый результат преобразуется в тип double для форматирования как числа с двумя разрядами после точки.

Альтернативный подход к вызову:

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

Недостаток низкоуровневого подхода в том, что вы должны явным образом определять, как обрабатывать ошибки в вашей системе.

Здесь ключевое слово await подставляется в строку с вызовом SendRequest. Это немного упрощает вызывающий код, а также позволяет извлекать возвращаемую строку без вызова Task.Result. Какой вариант вы будете использовать, зависит от конкретной ситуации, но, как правило, лучше избегать явного использования свойства Result объекта Task.

Большая часть асинхронная работы выполняется в методе SendRequest (рис. 6). Так как SendRequest является асинхронным, лучше именовать его SendRequestAsync или MySendRequestAsync.

Рис. 6. Метод SendRequest в 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); // соединение
    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 принимает строку, представляющую имя сервера, и разрешает это имя в IP-адрес, используя тот же код, что и в конструкторе класса сервиса. Более простой вариант — передача имени сервера: await client.ConnectAsync(server, port).

После определения IP-адреса сервера создается экземпляр объекта TcpClient на основе сокета и используется метод ConnectAsync этого объекта для отправки запроса на соединение с сервером. После настройки объекта StreamWriter для передачи данных серверу по сети и объекта StreamReader для приема данных от сервера создается строка запроса с применением форматирования, ожидаемого сервером. Запрос отправляется и принимается асинхронно и возвращается методом в виде строки.

Демонстрационное клиентское веб-приложение

Я создал это приложение, показанное на рис. 1, в два этапа. Сначала с помощью Visual Studio я создал веб-сайт для хостинга приложения, а затем написал веб-приложение, используя Notepad. Я запустил Visual Studio 2012 и создал новый проект C# Empty Web Site с именем DemoClient по адресу http://localhost/. Это позволило подготовить необходимую инфраструктуру IIS к хостингу приложения и создать физическое местоположение, сопоставленное с веб-сайтом в C:\inetpub\wwwroot\DemoClient\. Кроме того, был создан базовый конфигурационный файл Web.config, который содержит информацию, позволяющую приложениям на сайте обращаться к асинхронной функциональности в .NET Framework 4.5:

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

Потом я запустил Notepad с административными привилегиями. Создавая простые приложения ASP.NET, я иногда предпочитаю пользоваться Notepad вместо Visual Studio, чтобы хранить весь код приложения в одном файле .aspx, не генерируя множество файлов и ненужные образцы кода. Я сохранил пустой файл как DemoWebClient.aspx в C:\inetpub\wwwroot\DemoClient.

Общая структура веб-приложения приведена на рис. 7.

Рис. 7. Структура демонстрационного клиентского веб-приложения

<%@ 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>

В начало страницы я добавил выражение Import, чтобы ввести в область видимости релевантные пространства имен .NET, а также директиву Page, включающую атрибут Async=true.

Блок script для C# содержит два метода: SendRequest и Button1_Click. В теле страницы приложения имеются два элемента управления TextBox и один Button для ввода, плюс элемент управления TextBox для вывода, в котором хранится ответ сервиса, а также неиспользуемый TextBox для демонстрации отзывчивости UI при ожидании приложением ответа на запрос от сервиса.

Код для метода SendRequest веб-приложения идентичен коду SendRequest в WinForm-приложении, а код для обработчика Button1_Click в веб-приложении лишь немногим отличается от такового в WinForm-приложении:

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;
}

Хотя код веб-приложения и WinForm-приложения во многом совпадает, механизм вызова отличается весьма заметно. Когда пользователь выдает запрос из WinForm, этот клиент адресует вызов непосредственно сервису, и тот посылает ответ прямо WinForm. А когда пользователь выдает запрос из веб-приложения, оно посылает информацию запроса веб-серверу, на котором размещено данное приложение, потом веб-сервер вызывает сервис, тот отвечает веб-серверу, а веб-сервер конструирует страницу ответа. После этого страница ответа отправляется клиентскому браузеру.

Заключение

Итак, когда следует задуматься о применении асинхронных TCP-сокетов вместо WCF? Лет десять назад, до создания WCF и предшествующей ей технологии ASP.NET Web Services, если вы хотели создать клиент-серверную систему, использование сокетов зачастую было самым логичным вариантом. Введение WCF было большим достижением, но, поскольку WCF рассчитана на огромное количество разнообразных сценариев, ее использование в простых клиент-серверных системах может оказаться перебором. Хотя новейшую версию WCF легче конфигурировать, чем предыдущие версии, она все равно остается весьма сложной.

В ситуациях, где клиент и сервер находятся в разных сетях, на первый план выходит вопрос безопасности, и в этом случае я всегда использую WCF. Но во многих клиент-серверных системах, где клиент и сервер размещены в одной защищенной корпоративной сети, я часто предпочитаю работать с TCP-сокетами.

Сравнительно новый подход к реализации клиент-серверных систем заключается в применении инфраструктуры ASP.NET Web API для HTTP-сервисов в сочетании с ASP.NET-библиотекой SignalR для поддержки асинхронных методов. Во многих случаях этот подход проще реализовать, чем при использовании WCF, и он позволяет избегать многих низкоуровневых деталей, неизбежных при работе с сокетами.


Джеймс Маккафри (Dr. James McCaffrey) работает на Microsoft Research в Редмонде (штат Вашингтон). Принимал участие в создании нескольких продуктов Microsoft, в том числе Internet Explorer и Bing. С ним можно связаться по адресу jammc@microsoft.com.

Выражаю благодарность за рецензирование статьи экспертам Пиали Чоудхури (Piali Choudhury) из MS Research, Стивену Клири (Stephen Cleary) (консультант), Адаму Эверсоулу (Adam Eversole) из MS Research, Линну Пауэрсу (Lynn Powers) из MS Research и Стефену Таубу (Stephen Toub) из Microsoft.