Малышев_Сетевое программирование 19.12.18
.pdf51
жен выделять ресурсы для каждого клиента в отдельности – сколько бы клиентов ни слушали сервер, на нем будут расходоваться одни и те же ресурсы.
Механизм, не устанавливающий соединения, имеет и свои недостатки. Например, нет никакой гарантии, что данные получит хоть кто-нибудь. Если необходимо обеспечить надежность доставки, то придется добавить собственный механизм квитирования (подтверждения) на более высоком уровне, чем UDP.
При широковещательной передаче для любой станции (под)сетиадресата возникает проблема производительности, т. к. каждая станция этой (под)сети должна проверять, нужен ли ей полученный пакет. Сообщения могут интересовать все станции в сети, и поэтому они поднимаются вверх по стеку протоколов на каждой станции вплоть до транспортного уровня, где только и можно установить их релевантность. Кроме того широковещательными сообщениями существует и другая проблема – они не пересекают границы (под)сетей, т. к. маршрутизаторы не пропускают широковещательные сообщения, иначе быстро произошло перенасыщение сети. Таким образом, широковещательная передача может распространяться только внутри конкретной подсети, т. е. в пределах ЛВС до маршрутизатора (в сетях с IP v.4).
Широковещательная передача полезна, если несколько узлов одной подсети должны получать информацию одновременно. Примером полезного использования широковещательной передачи служит NTP (Network Time Protocol) [9].
Послать широковещательное сообщение легче, чем принять его. Чтобы отправить сообщение, достаточно указать IP-адрес получателя (сетевая подсистема впоследствии преобразует его в МАС-адрес сетевого адаптера принимающей стороны). А чтобы получить сообщение, сетевой адаптер должна прослушивать сеть, выявляя сообщенияс заданным МАС- адресом. Проблема заключается в том, что у широковещательного сообщения один адрес, а в подсети все компьютеры имеют разные МАС- адреса. В результате было решено, что, когда программа посылает широковещательное сообщение, ядро автоматически назначает ему МАС-адрес, состоящий из всех единиц (FF:FF:FF:FF:FF:FF). Он служит сигналом для всех сетевых адаптеров принять сообщение, даже если на конкретном компьютере нет программ, ожидающих широковещательных сообщений
[24].
Работа в широковещательном режиме
52
Включить широковещательный режим можно с помощью параметра сокета SO_BROADCAST. В остальном все остается прежним: программа создает обычный дейтаграммный сокет.
Листинг 3.
/*** Создание широковещательного дейтаграммного сокета ***/
const int on=l;
sd = socket(PF_INET, SOCK_DGRAM, 0);
if (setsockopt(sd, SOL_SOCKET, SO_BROADCAST, Son, sizeof(on)) != 0 ) // устанавливает параметры
panic("set broadcast failed"); // это встроенная функция, которая
//останавливает обычный поток управления
bzero(&addr, sizeof(addr)); //заполняет первые n байт, начиная с
//адреса addr, нулевыми значениями addr.sin_family = AF_INET; addr.sin_port = htons(port); addr.sin_addr.s_addr=INADDR_ANY;
if (bind(sd, Saddr, sizeof(addr)) != 0) panic("bind failed"); addr.sin_port = htons(atoin(strings[2]));
if (inet_aton(strings[1], Saddr.sin_addr) == 0 ) panic("inet_aton failed (%s)", strings[l]);
Активизировав широковещательный режим, можно отправлять сообщения по широковещательному адресу.
Обычно создается отдельный процесс или поток для посылающей и принимающей сторон. В отличие от других соединений, где на одно сообщение приходит один ответ, в широковещании на каждое посланное сообщение может прийти несколько ответов.
Поэтому в одном из каналов необходимо отключить входную оче-
редь:
/***Разделение обязанностей при отправке и приеме сообщений***/
/*** На отправляющей стороне входной канал закрывается. ***/
if ( fork() ) Receiver(sd); else
{
shutdown(sd, SHUT RD); /* закрываем входной канал */ Sender(sd);
}
wait(0);
53
Для общей рассылки также может использоваться функция Broadcast(), которая и реализует широковещательную рассылку по маске IP адреса.
В Java не поддерживается широковещание. Возможно, это связано с тем, что данный режим применяется все реже.
7. МОДЕЛИ ВВОДА-ВЫВОДА
Ранее были рассмотрены понятия блокирующих и неблокирующих socket’ов, которые участвуют в WinSock-моделях ввода/вывода (I/OModels). Модель ввода/вывода – это метод, используемый для управления программным процессом, который взаимодействует с сетевым вводом и выводом. Другими словами, модель ввода/вывода определяет, как приложение будет обрабатывать операции ввода/вывода для определённого socket’a. WinSock обеспечивает несколько моделей для разработки плана ввода / вы-
вода [12].
Cкорость сети не бесконечна, поэтому получение или отправление данных по сети не может быть мгновенным и иногда требует достаточно длительного времени. При этом часто необходимо делать другие операции, в то время как ожидается завершение процесса обмена данными. Рассматриваемые модели различным способом позволяют решать эту проблему.
|
|
Модели ввода/выводы. |
Таблица 2 |
||||
|
|
|
|
||||
|
|
|
|
|
|
|
|
|
Модель |
Блокирующий |
|
Способ уведомления |
|||
|
|
режим |
|
|
|
|
|
|
|
отсутст- |
|
На сетевое |
При заверше- |
|
|
|
|
|
|
|
|||
|
|
|
вует |
|
событие |
нии |
|
|
|
|
|
|
|
|
|
1 |
Блокирующие |
Блокирующий |
+ |
|
|
|
|
|
socket'ы |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
Polling |
Heблокирующий |
+ |
|
|
|
|
|
|
|
|
|
|
|
|
3 |
Select |
Оба режима |
|
|
Избранное |
|
|
|
|
|
|
|
блокирова- |
|
|
|
|
|
|
|
ние |
|
|
4 |
WSAAsyncSelect |
Heблокирующий |
|
|
Оконное |
|
|
|
|
|
|
|
сообщение |
|
|
5 |
WSAEventSelect |
Heблокирующий |
|
|
Объекты |
|
|
|
|
|
|
|
событий |
|
|
|
|
|
|
|
|
|
|
6 |
Перекрытый |
Не доступен |
|
|
|
Вызов блоки- |
|
|
ввод/вывод: бло- |
|
|
|
|
рования |
|
|
кирование |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54
Окончание таблицы 2
|
Модель |
Блокирующий |
|
Способ уведомления |
||
|
|
режим |
отсут- |
|
На сетевое |
При заверше- |
|
|
|
ствует |
|
событие |
нии |
|
|
|
|
|
|
|
7 |
Перекрытый |
Не доступен |
+ |
|
|
|
|
ввод/вывод: |
|
|
|
|
|
|
Select |
|
|
|
|
|
|
|
|
|
|
|
|
8 |
Перекрытый |
Не доступен |
|
|
|
Функция об- |
|
ввод/вывод: |
|
|
|
|
ратного вызо- |
|
процедуры за- |
|
|
|
|
ва |
|
вершения |
|
|
|
|
|
|
|
|
|
|
|
|
9 |
Перекрытый |
Не доступен |
|
|
|
Закрытие пор- |
|
ввод/вывод: за- |
|
|
|
|
та |
|
крытие порта |
|
|
|
|
|
Первые пять моделей часто используются и довольно удобны в применении. Последние четыре имеют одинаковую модель (перекрытый ввод/вывод – overlapped I/O), но разные методы реализации. На практике перекрытые модели используют, если только необходимо написать программу, поддерживающую тысячи соединений.
Один из критериев разделения моделей ввода/вывода основан на блокирующем режиме, который использует модель. Как видно из таблицы, единственной моделью, использующей блокирующий режим, являются блокирующие socket’ы (к этой модели можно добавить модель select, но в ней отсутствует явное блокирование и используются оба режима: блокирующий и неблокирующий). Блокирующий режим не применим к перекрытому вводу/выводу, потому что эти операции всегда работают асинхронно.
Другой критерий классификации моделей – по способу уведомления. Как видно из таблицы, существует три типа:
отсутствует – нет никакого уведомления, операция просто завершается успешно или терпит неудачу (1, 2 и 7 модели);
на сетевое событие – уведомление посылается в ответ на определенное сетевое событие. Операция терпит неудачу, если не может быть мгновенно завершена. Этот способ уведомления может использоваться для определения удачного времени повторной попытки операции (3 – 5 модели); при завершении – уведомление отправляется, когда сетевая операция была завершена. Операция либо мгновенно завершается (успешно) – посылается уведомление, либо нет – нет уведомления. При таком типе уве-
55
домления, сообщается, когда операция закончится, избавляя от необходимости повторить операцию позже (6, 8, 9 модели).
Блокирующий режим (1) не использует уведомлений. Основой процесс программы будет заблокирован пока операция ввода-вывода, такая как send и recv не завершится.
В блокирующем режиме вызовы Winsock-функций, которые выполняют операции, ждут пока операция завершится, прежде чем отдать управление приложению. В неблокирующем режиме Winsock-функции отдают управление приложению сразу. Приложения, которые работают на Windows CE и Windows 95 (Winsock 1) платформах, которые поддерживают только некоторые модели ввода-вывода, вынуждены выполнять определенные действия с блокирующими и неблокирующими сокетами для коректной отработки разных ситуаций.
WSAAsyncSelect (4) – пример модели, отвечающей на сетевое событие. Это означает, что будет отправлено оконное сообщение, когда произойдет тот или иной сетевой процесс.
Способ уведомления при завершении используется исключительно с перекрытыми моделями (6, 8, 9), которые связаны непосредственно с операциями.
Большое различие между уведомлением при завершении и на сетевое событие в том, что уведомление о завершении будет вызвано только при завершении определенной операции, а «сетевое событие» может возникнуть в любой момент и по любой причине.
Кроме того, перекрытые модели ввода/вывода могут – как видно из названия – перекрываться, т. е. многократные запросы ввода/вывода могут образовывать очередь.
7.1. НЕБЛОКИРУЮЩИЙ РЕЖИМ
Socket может быть установлен в неблокирующий режим, используя функцию ioctlsocket (с параметром FIONBIO). Некоторые функции, используемые в моделях ввода / вывода, неявно устанавливают socket в неблокирующее состояние. В неблокирующем режиме WinSock-функции, работающие с socket’ом, никогда не заблокируют основной процесс, а всегда будут немедленно завершены, возможно, неудачей (т. к. не хватило времени, чтобы выполнить операцию). Неблокирующие socket’ы используют новый код WinSock-ошибки, который, в отличие от других ошибок не вызывает исключительную ситуацию. Кодом ошибки WinSock-функции, при невозможности немедленно выполнить операцию на неблокирующем
56
socket’е является константа WSAEWOULDBLOCK. Этот код ошибки можно получить, вызвав функцию WSAGetLastError после того, как функция WinSock потерпела неудачу.
Однако, это не является реальной ошибкой, т. к. такое значение может возникнуть в любой момент, при использовании неблокирующих socket’ов. WSAEWOULDBLOCK говорит о том, что операция не может быть завершена корректно в данный момент и нужно повторить попытку позже. Модель ввода/вывода обычно сама определяет наилучшее время для повторной попытки.
Примечание: во многих методах будет рассмотрен вариант, в котором WinSock-функция терпит неудачу (посредством WSAEWOULDBLOCK).
7.2. БЛОКИРУЮЩИЕ SOCKET’Ы
Блокирующие socket’ы самые легкие в использовании, они использовались уже в первых реализациях socket’ов. Когда операция, выполняемая блокирующим socket’ом, не может немедленно завершиться, socket блокирует основной процесс (т. е. останавливает его выполнение) до тех пор, пока операция не завершится. При вызове WinSock-функции типа send или recv, для их завершения может потребоваться долгое время (по сравнению с другими API-вызовами) (рис. 12).
Рис. 12. Блокирующие сокеты
По умолчанию socket находится в блокирующем режиме и ведет себя так, как показано выше. Рассмотрим пример «диалога» программы и
WinSock:
Программа: «Отправь-ка эти данные»
WinSock: «Хорошо, но мне может понадобится некоторое время»
…….
…….
…….
57
WinSock: «Сделано!»
Блокирующие сокеты [13] создают некоторые неудобства, потому что вызов любой из Winsock-API-функций блокируют главный поток на некоторое время.
Большинство Winsock-приложений считывают либо записывают определенное количество байт и выполняют их обработку. Следующий листинг иллюстрирует эту модель:
Листинг 4. SOCKET sock; char buffer[256]; int done = 0, err; while(!done) { // прием данных:
err = recv(sock, buffer, sizeof (buffer)); if (err == SOCKET_ERROR)
{
// обработка ошибки приема:
printf("recv failed with error %dn", WSAGetLastError());
//- возвращает код ошибки return;
}
//обработка данных: ProcessReceivedData(buffer); }
Проблема в приведенном коде состоит в том, что функция recv может
никогда не вернуть управление приложению, если на этот сокет не придут какие-то данные. Некоторые программисты проверяют наличие ожидающих данных на сокете вызовом функции recv с флагом MSG_PEEK либо ioctlsocket с FIONREAD опцией. Однако проверка наличия ожидающих данных на сокете без их приема считается в программировании плохим тоном, этого надо избегать любой ценной чтением данных из системного буфера. Для избежания этого способа, необходимо не дать приложению полностью застыть из-за отсутствия ожидающих данных без вызова проверки их наличия. Решением данной проблемы может быть разделение приложения на два потока: читающий и обрабатывающий данные, разделяя общий буфер данных между ними. Доступ к буферу осуществляется синхронизирующим обьектом как событие(event) или мьютекс(mutex). Задача читающего потока состоит в считывании поступающих данных из сети на сокет в общий буфер. Когда читающий поток считал минимальное
58
необходимое количество данных, предназначенных для обрабатывающего потока, он переключает событие в сигнализирующее состояние, таким образом давая знать обрабатывающему потоку о наличии в общем буфере данных для обработки. Обрабатывающий поток в свою очередь забирает из буфера данные и обрабатывает их.
Следующий листинг показывает реализацию данного способа, используя два потока: один обеспечивая чтение данных из сети
(ReadingThread), другой – обработку данных (ProcessingThread):
Листинг 5.
#define MAX_BUFFER_SIZE 4096
//Инициализация critical section
//и события с автосбросом перед инициализацией потоков
CRITICAL_SECTION data;
HANDLE hEvent; //дескриптор для описания объекта «событие»
SOCKET sock;
CHAR buffer[MAX_BUFFER_SIZE];
//создание читающего сокета
//читающий поток
void ReadingThread(void)
{
int nTotal = 0, nRead = 0, nLeft = 0, nBytes = 0;
while (true)
{
nTotal = 0;
nLeft = NUM_BYTES_REQUIRED; //необходимо while (nTotal < NUM_BYTES_REQUIRED)
{
EnterCriticalSection(&data);//захват критической секции
nRead = recv(sock, &(buffer[MAX_BUFFER_SIZE - nBytes]), nLeft, 0); if (nRead == -1)
{
printf("error"); ExitThread(); } nTotal += nRead; nLeft -= nRead; nBytes += nRead;
LeaveCriticalSection(&data);// «освобождение» критической секции
59
}
SetEvent(hEvent); //меняет состояние на сигнализированное //(включено)
}
}
//обрабатывающий поток void ProcessingThread(void)
{
while (true)
{
//ждем данных
WaitForSingleObject(hEvent); //останавливает выполнение программы //пока объект не сигнализирован
EnterCriticalSection(&data);
DoSomeComputationOnData(buffer); //сделать некоторые вычисления
//над данными // удаляем из буфера обработанные данные
nBytes -= NUM_BYTES_REQUIRED; LeaveCriticalSection(&data);
}
}
Основная трудность в программировании блокирующих сокетов состоит в поддержке передачи-приёма данных для более чем одного сокета. Используя предыдущую реализацию, приложение (данный пример) должно быть изменено для того, чтобы иметь по одной паре читающего и обрабатывающего потока на каждый сокет. Это добавляет некоторую рутинную работу для програмиста и осложнение кода. Единственный недостаток состоит в том, что приложение плохо маштабируется при большом количестве сокетов.
7.3.POLLING (ПОСЛЕДОВАТЕЛЬНЫЙ ОПРОС)
Вдействительности последовательный опрос – очень плохая модель ввода/вывода.
Опрос используется для неблокирующих socket’ов, значит socket сначала должен быть установлен в неблокирующий режим. Polling повторяет некоторые действия, пока не будет достигнут желаемый результат, в данном случае будет повторяться WinSock-функция, пока она не завершится успешно [12] (рис. 13):
60
Рис. 13. Polling
Так как socket неблокирующий, WinSock-функция не заблокирует основной процесс. Если функция WinSock не может выполнить свою задачу
– она завершиться неудачно. Данная модель ввода/вывода будет циклически вызывать функцию, пока она не будет выполнена:
Программа: «Отправь-ка эти данные» WinSock: «Не могу сделать это сейчас» Программа: «Отправь-ка эти данные» WinSock: «Не могу сделать это сейчас» Программа: «Отправь-ка эти данные» WinSock: «Не могу сделать это сейчас» Программа: «Отправь-ка эти данные» WinSock: «Не могу сделать это сейчас» Программа: «Отправь-ка эти данные»
WinSock: «Сделано!»
Эта модель плоха в использовании, т. к. ее эффект такой же как и эффект блокирующих socket'ов, за исключением того, что можно хоть както контролировать цикл вызова функций. Такой стиль синхронизации называется «активное ожидание» (busy waiting), это значит, что программа «занята» ожиданием, затрачивая драгоценные ресурсы процессора. Блокирующие socket’ы более эффективны, т. к. у них другой режим ожидания: они не используют цикл и тем самым не сильно загружают процессор.
Неблокирующий режим [14] Ранее были рассмотрены функции, которые могут надолго приоста-
новить работу вызвавшего их потока (или нити), если действие не может быть выполнено немедленно. Это функции accept, recv, recvfrom, send, sendto и connect (в дальнейшем в этом разделе не будут упоминаться функции recvfrom и sendto, потому что они в смысле блокирования эквивалентны функциям recv и send соответственно, и все, что будет здесь сказано о recv и send, применимо к recvfrom и sendto). Такое поведение не все-
