Создание бизнес-приложений с помощью Silverlight - Асинхронные циклы ввода/вывода

ОГЛАВЛЕНИЕ

Асинхронные циклы ввода/вывода

.NET Framework 3.5 представила новое асхинхронное программирование API-сокеты; это методы, заканчивающиеся на Async(). На данном сервере я буду использовать методы Socket.AcceptAsync, Socket.SendAsync и Socket.ReceiveAsync. Методы Async оптимизированы для серверных приложений с высокой пропускной способностью, путем использования портов завершения ввода/вывода, а также эффективного управления буфером отправки и получения через пригодный к многократному использованию класс SocketAsyncEventArgs.

Поскольку Silverlight не позволено создавать слушателей TCP, его класс Socket поддерживает лишь ConnectAsync, SendAsync и ReceiveAsync. Silverlight поддерживает лишь асинхронную модель программирования, и это касается не только API сокетов, но и любого сетевого взаимодействия.

Поскольку я буду использовать асинхронную модель программирования как на сервере, так и на клиенте, давайте познакомимся поближе с шаблонами разработки. Одним из повторяющихся шаблонов разработки является цикл ввода/вывода, применимый ко всем асинхронным операциям. Сперва стоит взглянуть на типичное синхронное исполнение цикла допуска сокета:

_listener.Bind(localEndPoint);
 _listener.Listen(50);
 while (true)
 {
  Socket acceptedSocket = _listener.Accept();
  RepConnection repCon = new 
  RepConnection(acceptedSocket);
  Thread receiveThread = new Thread(ReceiveLoop);
  receiveThread.Start(repCon);
 }

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

Чтобы Silverlight могла хорошо работать со средой выполнения обозревателя, она должен занимать как можно меньше ресурсов. Все вызовы в псевдокоде «допуска сокета», показанном выше, блокируют потоки на которых исполняются, тем самым негативно влияя на масштабируемость. По этой причине Silverlight налагает очень серьезные ограничения на блокирования вызовов и на деле допускает только асинхронное взаимодействие с сетевыми ресурсами. Асинхронные циклы требует корректировки мысленной модели, чтобы представить себе невидимое окно сообщений, в котором должно находиться минимум одно сообщение, чтобы цикл сработал.

Рис. 4 показывает цикл receive (получения) – более завершенная его реализация содержится в прилагающемся к статье загружаемом коде. Здесь нет программных конструкций бесконечного цикла, подобных циклу while (true), который можно было увидеть в синхронном псевдокоде допуска сокета выше. Для разработчика Silverlight важно привыкнуть к программированию такого рода. Чтобы цикл получения продолжал получать данные после того, как сообщение было получено и обработано, в очереди должен быть минимум один запрос к порту завершения ввода/вывода, связанному с подключенным сокетом. Типичный асинхронный цикл проиллюстрирован на рис. 5 и применим к ConnectAsync, ReceiveAsync и SendAsync. AcceptAsync можно добавить к этому списку на сервере, где будет использоваться .NET Framework 3.5.

Рис. 4. Асинхронные циклы отправки/получения с помощью сокетов Silverlight

public class CallNetworkClient
{
  private Socket _socket;
  private ReceiveBuffer _receiveBuffer;

  public event EventHandler<EventArgs> OnConnectError;
  public event EventHandler<ReceiveArgs> OnReceive;
  public SocketAsyncEventArgs _receiveArgs;
  public SocketAsyncEventArgs _sendArgs;
//removed for space
  public void ReceiveAsync()
  {
  ReceiveAsync(_receiveArgs);
  }

  private void ReceiveAsync(SocketAsyncEventArgs recvArgs)
  {
  if (!_socket.ReceiveAsync(recvArgs))
  {
  ReceiveCallback(_socket, recvArgs);
  }
  }

  void ReceiveCallback(object sender, SocketAsyncEventArgs e)
  {
  if (e.SocketError != SocketError.Success)
  {
  return;
  }
  _receiveBuffer.Offset += e.BytesTransferred;
  if (_receiveBuffer.IsMessagePresent())
  {
  if (OnReceive != null)
  {
  NetworkMessage msg = 
  NetworkMessage.Deserialize(_receiveBuffer.Buffer);
  _receiveBuffer.AdjustBuffer();
  OnReceive(this, new ReceiveArgs(msg));
  }
  }
  else
  {
  //adjust the buffer pointer
  e.SetBuffer(_receiveBuffer.Offset, _receiveBuffer.Remaining);
  }
  //queue an async read request
  ReceiveAsync(_receiveSocketArgs);
  }
  public void SendAsync(NetworkMessage msg) { ... }

  private void SendAsync(SocketAsyncEventArgs sendSocketArgs)  
  { 
  ... 
  }

  void SendCallback(object sender, SocketAsyncEventArgs e)  
  { 
  ... 
  }
}

 

Рис. 5. Шаблон асинхронного цикла сокета

В Реализации цикла получения, показанной на рис. 4, ReceiveAsync – это оболочка для повторно входящего метода ReceiveAsync(SockeAsyncEventArgs recvArgs), который поставит в очередь запрос на порте завершения ввода/вывода сокета. SocketAsyncEventArgs, представленный в .NET Framework 3.5 играет похожую роль в реализации сокета Silverlight и может быть повторно использован в ходе нескольких запросов, избегая смешивания с собираемым мусором. За извлечение сообщения, инициацию события обработки сообщения и установки в очередь следующего элемента для продолжения цикла будет отвечать процедура обратного вызова.

Чтобы обрабатывать случаи частичного получения сообщений, ReceiveCallback корректирует буфер перед установкой в очередь следующего вопроса. NetworkMessage заключается в экземпляр ReceiveArgs и передается внешнему обработчику событий для обработки полученного сообщения.

Буфер сбрасывается для каждого завершенного получения NetworkMessage, после копирования частичного сообщения, если такое есть, в начало буфера. Похожая схема используется на сервере, но практические реализации могут выиграть от кольцевых буферов.

Для реализации сценария «принятия вызова», необходимо создать расширяемую архитектуру сообщения, позволяющую сериализовывать и десериализовывать сообщения с произвольным содержанием, не требуя переписки логики сериализации для каждого нового сообщения.

 

Рис. 6. Компоновка сериализованных типов NetworkMessage

Архитектура сообщения довольно проста: каждый дочерний объект NetworkMessage заявляет свою подпись в момент создания экземпляра с помощью соответствующего MessageAction. Реализации NetworkMessage.Serialize и Deserialize будут работать на Silverlight и .NET Framework 3.5 (на сервере), благодаря совместимости на уровне исходного кода. Сериализованное сообщение будет иметь компоновку, показанную на рис. 6.

Вместо вставки длины в начале сообщения можно использовать маркеры "begin" и "end" («начало» и «конец») с соответствующими escape-последовательностями. Закодировать длину в сообщение гораздо проще для обработки буферов.

Первые четыре байта каждого сериализованного сообщения будут включать число байтов в сериализованном объекте, следующем за этими 4 байтами. Silverlight поддерживает XmlSerializer, расположенный внутри System.Xml.dll, являющегося частью Silverlight SDK. Код сериализации прилагается к статье в загружаемом файле. Можно заметить, что у него нет прямых зависимостей от дочерних классов, таких как RegisterMessage или других сообщений, включая UnregisterMessage и AcceptMessage. Серия пометок XmlInclude поможет сериализатору адекватно разрешать типы.NET при сериализации дочерних классов.

Использование NetworkMessage.Serlialize и Deserialize показано в ReceiveCallback и SendAsync на рис. 4. В цикле получения собственно обработка сообщения выполняется обработчиком событий, присоединенным к событию NetworkClient.OnReceive. Я мог бы обработать сообщение внутри CallNetworkConnection, но написание обработчика получения для обработки сообщения поможет расширяемости путем отделения обработчика от CallNetworkConnection во время разработки.

Рис. 7 показывает приложение Silverlight, RootVisual, запускающее CallNetworkClient (показанный на рис. 4). Все элементы управления Silverlight присоединены к одному потоку интерфейса пользователя и обновления интерфейса пользователя могут проводится только когда код исполняется в контексте этого потока интерфейса пользователя. Асинхронная программная модель Silverlight исполняет код доступа к сети и действующие обработчики на рабочих потоках пула потоков. Все классы, производные от FrameworkElement (такие как Control («Элемент управления»), Border («Граница»), Panel («Панель») и большая часть элементов интерфейса пользователя наследуют свойство Dispatcher (от DispatcherObject), которое может исполнять код на потоке интерфейса пользователя.

На рис. 7 случай MessageAction.RegisterResponse обновит интерфейс пользователя подробностями смены центра обработки звонков через анонимный делегат. Обновленный интерфейс пользователя, к которому привело исполнение делегата, показан на рис. 8.

Рис. 7. Пользовательский элемент управления Silverlight UserControl, обрабатывающие входящие сообщения

public partial class Page : UserControl
{
  public Page()
  {
  InitializeComponent();
  ClientGlobals.socketClient = new CallNetworkClient();
  ClientGlobals.socketClient.OnReceive += new 
  EventHandler<ReceiveArgs>(ReceiveCallback);
  ClientGlobals.socketClient.Connect(4530);
  //code omitted for brevity
  }
  void ReceiveCallback(object sender, ReceiveArgs e)
  {
  NetworkMessage msg = e.Result;
  ProcessMessage(msg);
  }
  void ProcessMessage(NetworkMessage msg)
  {
  switch(msg.GetMessageType())
  {
  case MessageAction.RegisterResponse:
  RegisterResponse respMsg = msg as RegisterResponse;
  //the if is unncessary as the code always executes in the 
  //background thread
  this.Dispatcher.BeginInvoke(
  delegate()
  {
  ClientGlobals.networkPopup.CloseDialog();
  this.registrationView.Visibility = Visibility.Collapsed;
  this.callView.Visibility = Visibility.Visible;
  this.borderWaitView.Visibility = Visibility.Visible;
  this.tbRepDisplayName.Text = this.txRepName.Text;
  this.tbRepDisplayNumber.Text = respMsg.RepNumber;
  this.tbCallServerName.Text = 
  respMsg.CallManagerServerName;
  this.tbCallStartTime.Text = 
  respMsg.RegistrationTimestamp.ToString(); 
  });
  break;
  case MessageAction.Call:
  CallMessage callMsg = msg as CallMessage;
  //Code omitted for brevity
  if (!this.Dispatcher.CheckAccess())
  {
  this.Dispatcher.BeginInvoke(
  delegate()
  { 
  ClientGlobals.notifyCallPopup.ShowDialog(true); 
  });
  }
  break;
  //
  //Code omitted for brevity  
  //
  default:
  break;
  }
  }
}

Рис. 8. Регистрация на сервере центра обработки звонков находится в процессе

 

Рис. 9. Регистрация на сервере центра обработки звонков находится в процессе