Создание бизнес-приложений с помощью 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. Регистрация на сервере центра обработки звонков находится в процессе