Опубликован: 12.02.2013 | Доступ: свободный | Студентов: 791 / 47 | Длительность: 17:51:00
Специальности: Программист
Лекция 7:

Средства Windows Phone для работы с сетью

17.2. Создание подключения по протоколу UDP

Рассмотрим процесс создания программы, которая будет использовать протокол UDP для обмена сообщениями с удалённой службой под названием echo. Эта служба принимает входящие сообщения и отправляет их назад. Такая служба полезна для тестирования сетевых подключений. Это одна из самых первых Интернет-служб, и ей выделен порт с номером 7. Службу echo можно запустить на компьютере и использовать её для обмена дейтаграммами.

Создадим метод, который будет использовать протокол UDP для отправки сообщения на удалённый сервер. Пользователь должен будет ввести в текстовые поля программы сообщение для отправки и адрес целевого узла, а программа выполнит все необходимые действия. Метод может выглядеть так:

/// <summary>
/// Отправляет сообщение по указанному адресу, используя протокол UDP
/// </summary>
/// <param name="message">сообщение для отправки</param>
/// <param name="hostUrl">URL узла</param>
/// <param name="portNumber">порт</param>
/// <returns>сообщение об ошибке</returns>
public string SendMessageUDP(string message, string hostUrl, int portNumber)
{  
    // создать сокет и отправить сообщение
    // возвратить сообщение об ошибке, если оправка сообщения завершится неудачей
    // возвратить пустую строку, если сообщение будет передано успешно
}

Этот метод можно использовать так:

const int ECHO_PORT = 7;
string sendResponse = SendMessageUDP(MessageTextBox.Text, HostTextBox.Text,
    ECHO_PORT);

Этот код передаёт методу SendMessageUDP строки из текстовых полей, которые содержат текст сообщения и адрес узла, и номер порта на удалённом сервере. В данном случае будет использоваться порт 7.

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

Класс Socket

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

Несмотря на то, что Windows Phone поддерживает несколько способов подключения к Интернету, программа будет использовать ту же самую технологию сокетов для управления подключением. Класс Socket выбирает наиболее подходящий способ подключения. Если доступная сеть Wi-Fi, то будет использоваться она, а при её отсутствии будет установлено подключение к сети оператора сотовой связи.

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

Класс Socket описан в пространстве имён System.Net.Sockets, ссылку на которое рекомендуется добавить с помощью директивы using. Обычно файл библиотеки для работы с сетью уже является частью проекта, и его не нужно добавлять в проект вручную.

Для начала необходимо создать экземпляр класса Socket, который программа будет использовать для работы с сетевым подключением, и указать необходимые параметры для конструктора класса:

Socket hostSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram,
    ProtocolType.Udp);

Этот код создаёт экземпляр класса Socket, который использует адресацию IPv4 и дейтаграммы протокола UDP.

Класс SocketAsyncEventArgs

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

Для начала нужно задать конечную точку подключения, с которой будет взаимодействовать программа. Конечная точка состоит из адреса узла и номера порта на этом узле. Номер порта фактически идентифицирует программу на компьютере, с которой будет взаимодействовать наша программа. В нашем примере будет производиться передача сообщений в программу echo, которая использует порт 7.

Класс SocketAsyncEventArgs содержит свойство RemoteEndPoint, которое описывает конечную точку подключения:

SocketAsyncEventArgs socketEventArgs = new SocketAsyncEventArgs();
socketEventArgs.RemoteEndPoint = new DnsEndPoint(hostUrl, portNumber);

Этот код создаёт новый экземпляр класса SocketAsyncEventArgs и использует класс DnsEndPoint для указания Интернет-адреса узла и номера порта. При создании экземпляра класса DnsEndPoint указывается строка URL целевой системы, и класс будет использовать систему доменных имён, чтобы найти фактический IP-адрес узла, который необходим для создания подключения. Если этот адрес не будет найден, класс сгенерирует исключение.

После указания конечной точки подключения необходимо указать сообщение, которое необходимо передать программе echo, чтобы она отправила его назад:

// поместить сообщение в буфер
byte[] messageBytes = Encoding.UTF8.GetBytes(message);
socketEventArgs.SetBuffer(messageBytes, 0, messageBytes.Length);

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

Чтобы преобразовать текст в последовательность 8-битовых значений, можно использовать класс Encoding, который описан в пространстве System.Text. Этот класс использует метод GetBytes для преобразования строки, закодированной в системе UTF-8, в массив байтов. Кодировка UTF-8 используется в системе Windows Phone для кодирования символов.

Создание метода SendMessageUDP

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

Класс SocketAsyncEventArgs содержит свойство Completed, которое генерирует событие при завершении взаимодействия с сетью. При наступлении события можно выполнить код, который проверит результат выполнения сетевого запроса. Этот код необходимо связать со свойством Completed.

public string SendMessageUDP(string message, string hostUrl, int portNumber)
{
    // строка для хранения ответного сообщения
    string response = "";

    // создание аргумента для запроса
    SocketAsyncEventArgs socketEventArgs = new SocketAsyncEventArgs();
    // создание конечной точки подключения
    socketEventArgs.RemoteEndPoint = new DnsEndPoint(hostUrl, portNumber,
        AddressFamily.InterNetwork);

    // поместить сообщение в буфер
    byte[] messageBytes = Encoding.UTF8.GetBytes(message);
    socketEventArgs.SetBuffer(messageBytes, 0, messageBytes.Length);

    // подключить обработчик события завершения запроса
    socketEventArgs.Completed += new EventHandler<SocketAsyncEventArgs>
        (delegate(object s, SocketAsyncEventArgs e)
    {
        if (e.SocketError == SocketError.Success)
        {
            response = "";
        }
        else 
        {
            response = e.SocketError.ToString();
        }
        // запустить все ожидающие потоки
        transferDoneFlag.Set();
    });

    ...
}

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

Событие Completed использует ссылку на объект класса SocketAsyncEventArgs, который описывает состояние транзакции. Он устанавливает значение флага состояния SocketError, которое сообщает, успешно ли было передано сообщение. Если передача сообщения завершилась неудачей, в переменную response записывается сообщение об ошибке.

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

В .NET асинхронные операции выполняются в отдельных потоках. Запускаемая в Windows Phone программа, по сути, является потоком, который становится активным потоком устройства. Программы могут запускать другие потоки для выполнения асинхронных операций, и иногда один поток должен ожидать завершения другого потока. Среда .NET предоставляет возможность взаимодействия потоков с помощью класса ManualResetEvent:

static ManualResetEvent transferDoneFlag = new ManualResetEvent(false);

Программа содержит переменную transferDoneFlag, которая может использоваться в качестве флага. Переменная объявлена как static, чтобы все экземпляры класса использовали один флаг, поскольку можно создать ограниченное количество экземпляров класса ManualResetEvent.

Переменная transferDoneFlag может находиться в одном из двух состояний. Изначально значение флага равно false, т.е. флаг сброшен. Поток может ожидать завершения другого потока, чтобы установить этот флаг, т.е. изменить его значение на true, используя метод WaitOne. Это остановит текущий поток, пока другой поток не установит флаг, вызвав метод Set.

В описанном подходе имеется проблема, которая состоит в том, что метод WaitOne приостановит текущий поток, пока флаг не будет установлен. Если флаг никогда не будет установлен, то поток не возобновит выполнение. Чтобы избежать подобной проблемы, при вызове метода WaitOne можно указать значение тайм-аута в миллисекундах. В этом случае ожидающий поток продолжит работу, если флаг будет установлен другим потоком, или по истечении заданного времени.

Этот подход особенно полезен на платформе Windows Phone, поскольку в мобильных устройствах высока вероятность, что сетевой запрос завершится неудачей. При создании приложения, которое использует сетевые подключения, рекомендуется задавать тайм-аут и обрабатывать ситуацию, когда подключение недоступно. Кроме того, ожидание завершения других потоков позволяет экономить системные ресурсы.

Фактически, в нашей программе будет использоваться главный поток, который создаётся при запуске программы, и фоновый поток, который запускается для выполнения сетевой операции. Если главный поток должен ожидать, пока флаг не будет установлен, перед этим флаг нужно сбросить, вызвав метод Reset, иначе программа не будет ожидать завершения потока, поскольку флаг может быть установлен перед вызовом метода WaitOne.

После выполнения всех необходимых настроек, можно отправить сообщение удалённому компьютеру:

// отправка сообщения
hostSocket.SendToAsync(socketEventArgs);

// ожидание события
// работа будет продолжена после получения ответа или по истечению времени тайм-аута
transferDoneFlag.WaitOne(MESSAGE_TIMEOUT_MSECS);

Полная версия метода SendMessageUDP будет выглядеть следующим образом:

static ManualResetEvent transferDoneFlag = new ManualResetEvent(false);
const int MESSAGE_TIMEOUT_MSECS = 10000;

public string SendMessageUDP(string message, string hostUrl, int portNumber)
{
    string response = "Истекло время ожидания";

    SocketAsyncEventArgs socketEventArgs = new SocketAsyncEventArgs();

    socketEventArgs.RemoteEndPoint = new DnsEndPoint(hostUrl, portNumber,
        AddressFamily.InterNetwork);

    byte[] messageBytes = Encoding.UTF8.GetBytes(message);
    socketEventArgs.SetBuffer(messageBytes, 0, messageBytes.Length);


    socketEventArgs.Completed += new EventHandler<SocketAsyncEventArgs>
        (delegate(object s, SocketAsyncEventArgs e)
    {
        if (e.SocketError == SocketError.Success)
        {
            response = "";
        }
        else
        {
            response = e.SocketError.ToString();
        }
        transferDoneFlag.Set();
    });

    transferDoneFlag.Reset();

    hostSocket.SendToAsync(socketEventArgs);

    transferDoneFlag.WaitOne(MESSAGE_TIMEOUT_MSECS);

    return response;
}

В этом методе переменной response изначально присваивается значение "Истекло время ожидания". Если подключение не отвечает в течение заданного времени, метод возвращает эту строку, что будет свидетельствовать о возникновении ошибки. Если сетевой запрос выполнится успешно, то метод вернёт пустую строку.

Отправка и получение сообщения

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

string sendResponse = SendMessageUDP(MessageTextBox.Text, HostTextBox.Text,
    ECHO_PORT);

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

Для того чтобы получить ответ от службы echo после отправки сообщения нужно настроить другое подключение с удалённым узлом. Можно создать другой метод для отправки запроса на чтение:

public string ReceiveMessageUDP(int portNumber, out string result)
{
    string response = "Ошибка: тайм-аут запроса";
    string message ="";

    SocketAsyncEventArgs socketEventArgs = new SocketAsyncEventArgs();
    socketEventArgs.RemoteEndPoint = new IPEndPoint(IPAddress.Any, portNumber);

    byte[] responseBytes = new byte[MAX_BUFFER_SIZE];
    socketEventArgs.SetBuffer(responseBytes, 0, MAX_BUFFER_SIZE);

    socketEventArgs.Completed += new EventHandler<SocketAsyncEventArgs>
        (delegate(object s, SocketAsyncEventArgs e)
    {
        if (e.SocketError == SocketError.Success)
        {
            response = "";
            message = Encoding.UTF8.GetString(e.Buffer, e.Offset, e.BytesTransferred);
            message = message.Trim('\0');
        }
        else
        {
            response = e.SocketError.ToString();
        }

        transferDoneFlag.Set();
    });

    // сбросить флаг, он будет установлен после обработки запроса
    transferDoneFlag.Reset();

    // отправить запрос на чтение
    hostSocket.ReceiveFromAsync(socketEventArgs);

    // приостановить поток, пока не будет получен ответ или не истечёт время ожидания
    transferDoneFlag.WaitOne(MESSAGE_TIMEOUT_MSECS);

    // скопировать полученный текст в выходной параметр
    result = message;
    // вернуть результат запроса
    // если запрос выполнился успешно, возвращается пустая строка
    return response;  
}

Этот метод похож на метод SendMessageUDP. Он будет прослушивать указанный порт для получения ответа из сети. Однако, теперь адрес удалённого узла указывается с помощью класса IPEndPoint. Адрес конечной точки задаётся значением IPAddress.Any, что соответствует любому адресу, т.е. программа получит сообщение, отправленное на заданный порт с любого узла в сети. Если нужно получать сообщения только от одного адреса, здесь можно указать этот адрес.

Для получения сообщения с удалённого узла создаётся буфер, в котором будут сохраняться байты полученного сообщения. Созданный буфер указывается при вызове метода SetBuffer.

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

Если запрос успешно считывает сообщение, оно должно быть преобразовано из последовательности 8-битовых значений в тип данных, с которым будет работать программа. Поскольку программа работает с текстовыми сообщениями, для декодирования сообщения используется класс Encoding для получения строки в кодировке UTF-8. Последним действием обработчик событий устанавливает флаг для обеспечения синхронизации потоков.

Созданный метод можно использовать так:

string receiveResponse;
string message;

receiveResponse = ReceiveMessageUDP(ECHO_PORT, out message);

Отправка дейтаграммы службе echo

Теперь можно создать код, который отправит сообщение службе echo. Сначала нужно вызвать метод SendMessageUDP для отправки сообщения службе, и затем вызвать ReceiveMessageUDP для получения ответа.

string sendResponse = SendMessageUDP(MessageTextBox.Text, HostTextBox.Text,
    ECHO_PORT);

if (sendResponse.Length == 0)
{
    string receiveResponse;
    string message;

    receiveResponse = ReceiveMessageUDP(ECHO_PORT, out message);
    if (receiveResponse.Length == 0)
    {
        ResponseTextBox.Text = message;
    }
    else
    {
        ErrorTextBox.Text = "Ошибка получения: " + receiveResponse;
    }
}
else
{
    ErrorTextBox.Text = "Ошибка отправки: " + sendResponse;
}

Если метод SendMessageUDP возвращает пустую строку, это значит, что сообщение отправлено успешно, и программа может вызвать метод ReceiveMessageUDP, чтобы получить ответное сообщение от службы echo.

Для проверки правильности работы программы необходима система, в которой есть служба echo. Такая служба есть в операционной системе Windows, но по умолчанию эта служба не устанавливается. Чтобы использовать службу echo, нужно в панели управления выбрать пункт Включение и отключение компонентов Windows и поставить галочку напротив пункта Простые службы TCPIP (такие как echo, datetime и т.п.). После этого можно либо перезагрузить компьютер, либо в оснастке Службы запустить службу Простые службы TCP/IP. Описанная последовательность действий работает в Windows 7, а в других версиях Windows может отличаться некоторыми деталями.

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