Статьи

Windows Phone 7 Sockets: как получить сообщение

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

Основное внимание в этих постах уделяется разработчикам WP7. Если вы хотите лучше понять внутренние детали интернет-сокетов, в Википедии есть хорошее введение по этому вопросу.

Эта вторая часть будет продолжаться с того места, где остановилась первая часть : у нас открыто TCP-соединение, и теперь мы должны начать прослушивать сообщения, которые отправляет нам сервер.

основы

Как мы уже говорили ранее, Socket и SockectAsyncEventArgs являются двумя ключевыми классами при работе с сокетами на платформе Windows Phone. Они используются при создании соединения и используются снова, когда мы хотим получать сообщения с сервера. Мы продолжим работу с шаблоном, который мы создали в первом уроке: экземпляр SockectAsyncEventArgs всегда используется только один раз, а затем удаляется.

Для получения сообщения нам нужен буфер . Буфер — это просто массив байтов, который создается и присоединяется к экземпляру SocketAsyncEventArgs. Когда сервер отправляет сообщение нашему клиенту, оно может помещаться или не помещаться в один буфер. Если он не подходит, остальная часть сообщения (или следующая часть) может быть получена путем повторного создания нового экземпляра SocketAsyncEventArgs и буфера для него.

Создание нового буфера для каждого сообщения — не единственное доступное решение. Можно использовать кольцевой буфер для оптимизации производительности. Вы можете проверить «Круговой буфер для .NET» , проект от CodePlex, чтобы увидеть пример реализации.

Код

Как уже упоминалось ранее, в дополнение к байтовому массиву нам необходим экземпляр SocketAsyncEventArgs для получения сообщения от сервера. Это передается методу ReceiveAsync нашего экземпляра сокета.

private void ReceiveMessage()
{
    var responseListener = new SocketAsyncEventArgs();
    responseListener.Completed += OnMessageReceivedFromServer;
     
    var responseBuffer = new byte[bufferSize];
    responseListener.SetBuffer(responseBuffer, 0, bufferSize);
 
    connection.ReceiveAsync(responseListener);
}

Новый метод ReceiveMessage можно вызывать после открытия соединения с сервером. Теперь, когда сервер отправляет нам сообщение, выполняется наш метод OnMessageReceivedFromServer. В этом методе нам нужно сделать несколько вещей:

  1. Преобразуйте буфер (байтовый массив) в строку. Это сообщение (или сообщения), которое сервер отправил нам.
  2. Разберите сообщение и отреагируйте на него согласно IRC-спецификации.
  3. Вызовите метод ReceiveMessage, который создаст новый буфер и новый SocketAsyncEventArgs и начнет прослушивать следующее сообщение с сервера.


Один буфер может содержать несколько сообщений с сервера.
Сервер может отправлять нам несколько сообщений, что означает, что, хотя OnMessageReceivedFromServer выполняется только один раз, мы можем иметь дело с несколькими сообщениями. В протоколе IRC каждое сообщение заканчивается новой строкой, и мы можем использовать это для нашего преимущества: превратив байтовый массив в строку, мы разбиваем его по символам новой строки в строковый массив. Каждый экземпляр в массиве — это одно отдельное сообщение от сервера.

Но как мы можем быть уверены, что сообщение сервера помещается в наш буфер? Если последний символ в буфере является новой строкой, все хорошо. Но если это не так, наш буфер заполнен, и нас ждет новое сообщение. И мы должны сделать несколько сшивки: мы не можем обработать текущее сообщение, потому что оно не завершено, поэтому мы сохраняем его в переменной, ждем следующего сообщения и объединяем эти два. Использование ранее упомянутого кольцевого буфера сделало бы это несколько проще.

private string trailingMessage;
private void OnMessageReceivedFromServer(object sender, SocketAsyncEventArgs e)
{
    // Convert the received message into a string
    var message = Encoding.UTF8.GetString(e.Buffer, 0, e.BytesTransferred);
 
    var bufferWasPreviouslyFull = !string.IsNullOrWhiteSpace(trailingMessage);
    if (bufferWasPreviouslyFull)
    {
        message = trailingMessage + message;
        trailingMessage = null;
    }
 
    var isConnectionLost = string.IsNullOrWhiteSpace(message);
    if (isConnectionLost)
    {
        // We lost the connection for some reason
        // Handle the situation
        return;
    }
 
    // Convert the received string into a string array
    var lines = new List<string>(message.Split("\n\r".ToCharArray(), StringSplitOptions.None));
 
    var lastLine = lines.LastOrDefault();
    var isBufferFull = !string.IsNullOrWhiteSpace(lastLine);
    if (isBufferFull)
    {
        trailingMessage = lastLine;
        lines.Remove(lastLine);
    }
 
    foreach (var line in lines)
    {
        if (string.IsNullOrWhiteSpace(line))
            continue;
 
        ProcessIncomingMessage(line);
    }
 
    // Start listening for the next message
    ReceiveMessage();
}

Теперь нам просто нужно реализовать метод ProcessIncomingMessage. В этом методе мы будем анализировать сообщение и реагировать на него соответственно. Он может содержать личное сообщение другого пользователя, проверку сервера, чтобы убедиться, что мы все еще живы, или что-либо еще упомянутое в спецификациях IRC.

Как видите, мы предполагаем, что входящее сообщение имеет кодировку UTF8. К сожалению, мы не можем быть уверены в этом. Протокол IRC не определяет, какую кодировку следует использовать, и это может варьироваться между клиентами. UTF8 в настоящее время является наиболее используемой кодировкой, поэтому мы ее используем.

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

private void ProcessIncomingMessage(string ircMessage)
{
    Debug.WriteLine(ircMessage);
 
    // Future hook for handling the message in somewhere else.
    // It's most probably wise to put the parsing logic in some other class.
   if (IrcMessageReceivedFromServer != null)
       IrcMessageReceivedFromServer(this, new IrcMessageReceivedFromServer(ircMessage));
}

Текущая функциональность

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

образ

Но если вы позволите приложению работать достаточно долго, вы также заметите, что мы получаем два дополнительных сообщения. Один утверждает, что произошла ошибка, а другой — просто пустая строка. Первое сообщение получено, потому что мы не следовали спецификации IRC: сервер хочет, чтобы мы идентифицировали себя перед тем, как продолжить. Поскольку мы этого не сделали, сервер отключает наше соединение, и из-за этого получается пустая строка. Если мы попытаемся вызвать метод ReceiveMessage после получения пустой строки, мы получим исключение, потому что соединение больше не открыто.

Следующий шаг

Следующий шаг — начать следовать спецификации IRC и фактически отправлять сообщения на сервер. Мы пройдем через это в следующей части завтра.

Исходный код

Весь исходный код для этого урока доступен на GitHub.

связи