Статья
Версия для печати
Обсудить на форуме
Winsock для всех (часть 3)



В последней статье мы написали простейшее Winsock-приложение, получающее данные с удалённого Web-сервера. Исходный код программы прост и прозрачен, и с виду никаких "подводных" камней в этой программе нет. Однако, это далеко не так. Ведь в предыдущих статьях нашей целью было понять только принцип работы и организации простейшего Winsock приложения, работающего с TCP сокетами.

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

1. В примере программы из статьи 2, есть такой фрагмент кода:
Код:
   // ...
   // Ждём ответа
   int len = recv (s, (char *) &buff, MAX_PACKET_SIZE, 0);
   // ...

На первый взгляд вроде бы всё нормально. Но это не так! В вызове функции recv присутствует параметр MAX_PACKET_SIZE, который определяет длину буфера приёма данных. То есть, Winsock протоколы (TCP/UDP) могут "отправить" пакет размером и 65535 байт. Однако, реальный размер IP пакета, может быть меньше передаваемых данных для конкретного типа сети ( величина MTU - размер наибольшего допустимого кадра в локальной сети или глобальном канале) и поэтому данные фрагментируются (кто хочет - смотрит описание TCP/IP протокола, мы не будем пока углубляться в описание "железного" уровня сети). При этом, фрагментированные данные могут идти с задержками. Какое отношение имеет данная особенность к нашей программе? Всё очень просто. Если мы запросим у удалённого Web-сервера не такой маленький кусочек HTML-кода, как в нашем примере, а немного побольше, то мы получим только первую порцию данных от сервера. Остальная часть данных будет, скорее всего, утеряна. Выход из данного положения очень прост. Он основан на знании принципа работы HTTP и TCP/IP протоколов, плюс некоторых особенностей Winsock. В нашем случае Web-сервер, передав последнюю порцию данных, закроет соединение. А функция recv в таком случае, после получения последнего фрагмента данных, возвращает ноль. Вывод напрашивается сам: мы должны читать входящие данные до тех пор, пока функция recv не вернет ноль.

Вот новый вариант кода:
Код:
   int len;
   do
   {
      if (SOCKET_ERROR == (len = recv (s, (char *) &buff, MAX_PACKET_SIZE, 0) ) )
      return -1;
      for (int i = 0; i < len; i++)
      printf ("%c", buff [i]);
   } while (len!=0);

При такой организации считывания данных, потерь не будет.

Полный исходный код программы можно взять здесь: ws3_1.zip

2. При рассмотрении функций send/recv мы упустили интерпретацию поля flags, заполняя его нулём, что значило отсутствие каких-либо флагов при вызове send/recv. Рассмотрим, какие значения может принимать поле flags.

Функция send.


MSG_DONTROUTE - указывает на то, что в отправляемое сообщение, не включатся информация о маршрутизации. Однако Winsock service provider может игнорировать этот флаг при доставке сообщения. Используется для отладки. Адрес назначения - локальный. То есть данные могут быть доставлены только на машины, соединенные напрямую.

MSG_OOB - Сообщение является OOB данными. (Out Of Band) То есть, такое сообщение передаётся вне потока. Это значит, что при отправке сообщения, транспортный протокол не ждёт полного заполнения буфера, а отсылает сообщение немедленно. Данный флаг можно использовать при передаче приоритетных данных. При использовании MSG_OOB, Winsock-приложения поддерживающие связь, должны заранее "договориться" об использовании этого флага.

Функция recv.


MSG_PEEK - Данные копируются в принимающий буфер, но из очереди сообщений не изымаются. Функция возвращает количество принятых на данный момент байт данных.

MSG_OOB - Сообщение является OOB данными. (Out Of Band) То есть, такое сообщение передаётся вне потока. Это значит, что при отправке такого сообщения, транспортный протокол не ждёт полного заполнения TCP-буфера, а отсылает сообщение немедленно. Данный флаг можно использовать при передаче приоритетных данных. При использовании MSG_OOB, Winsock-приложения поддерживающие связь, должны заранее "договориться" о использовании этого флага.

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

Блокировка сокета. Решение проблемы.


Если вы выполняли программу из статьи 2 пошагово, то могли заметить, что некоторые Winsock-функции ожидают завершения выполняемых ими операций. Особенно надолго "подвисает" функция recv. Другими словами, выход из функции не происходит до момента завершения текущей операции. Эта особенность не очень хорошо подходит для программ, которые кроме получения/отправки данных должны выполнять еще множество других действий (отслеживание состояния системы меню, вывод информации, опрос других устройств ввода/вывода) Избежать этого можно многими способами. Можно обойтись средствами мультизадачности, и процедуру обмена данными "повесить" на отдельную ветвь. А можно решить проблему средствами Winsock. В любом случае, выбор конкретного метода остаётся за программистом. Наша же задача, разобраться с механизмом блокировки TCP-сокетов в Winsock. Итак, я хочу предложить вам два метода устранения проблемы блокировки:

1. Функция ioctlsocket.


Функция ioctlsocket позволяет менять/получать режим ввода/вывода конкретного сокета.
Код:
int ioctlsocket(SOCKET s,              // Сокет [in]
                long cmd,              // Комманда [in]
                u_long FAR *argp       // Параметр/значение [in/out]
               );

Для перевода сокета в не блокируемое состояние (nonblocking mode) используется команда FIONBIO. Argp должно указывать на ненулевое значение.

Пример:
Код:
   BOOL l = TRUE;
   if (SOCKET_ERROR == ioctlsocket (s, FIONBIO, (unsigned long* ) &l) )
   {
   // Error
   int res = WSAGetLastError ();
   return -1;
   }

Кроме команды FIONBIO существуют команды FIONREAD и SIOCATMARK. Если коротко, то FIONREAD позволяет получить количество байт информации, поступившей в буфер на данный момент операции чтения, а SIOCATMARK - используется при работе с OOB данными. На данный момент нас интересует только команда FIONBIO. Остальные команды будем рассматривать более подробно по мере надобности в следующих статьях.

Итак, вернёмся к нашему примеру. После вызова ioctlsocket сокет s стал не блокируемым, то есть, Winsock-функции для этого сокета не дожидаются окончания операций ввода/вывода, что в свою очередь не вызывает нежелательных пауз в работе программы. Однако, не всё так просто. Например, возврат из функции recv может произойти до момента получения данных. Как определить, что текущая операция ввода/вывода полностью завершена? Способ есть. Имя ему - WSAEWOULDBLOCK. Что это такое? WSAEWOULDBLOCK - это код ошибки, которую возвращают Winsock-функции для nonblocked сокета, если текущая операция не завершена. То есть, если функция revc вернула этот код ошибки, значит, данные еще не готовы для чтения, и операцию придется повторить позже. В таком случае, ваша программа может выполнять другие действия, попутно проверяя, не завершена ли текущая операция ввода/вывода.

Полный исходный код программы можно взять здесь: ws3_2.zip

2. Функция select


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

Код:
int select(int nfds,                         // Не используется (оставлен для совместимости)
           fd_set FAR *readfds,              // множество сокетов, проверяемых на готовность к чтению
           fd_set FAR *writefds,             // множество сокетов, проверяемых на готовность к отсылке
           fd_set FAR *exceptfds,            // множество сокетов, проверяемых на ошибку/OOB данные
           const struct timeval FAR *timeout // Таймаут проверки
);

Каждый из параметров readfds, writefds, exceptfds, timeout есть необязательным, и может быть проигнорирован (установлен в NULL). В случае readfds/writefds/exceptfds == NULL проверка на опущенные типы сокетов просто не будет производиться. В случае timeout ==NULL, функция select вызовет блокировку (до первого готового к вводу/выводу сокета). Функция возвращает общее количество сокетов (во всех заданных множествах readfds/writefds/exceptfds), готовых к операциям ввода/вывода.

Параметры readfds/writefds/exceptfds - указатели на тип fd_set (представляющий собою множество сокетов). Для работы с этим типом данных объявлены такие макросы:
Код:
   FD_CLR (s, *set) -Удаляет дескриптор s из set.
   FD_ISSET(s, *set) - Возвращает ненулевое значение, если s присутствует в set. Иначе, возвращает ноль.
   FD_SET(s, *set) - добавляет s к set.
   FD_ZERO(*set) - Очищает множество set

Параметр timeout - указатель на структуру timeval, позволяет задать таймаут, в течении которого сокеты будут проверяться на готовность.
Код:
struct timeval {
   long tv_sec;  // секунды
   long tv_usec; // микросекунды
};

С помощью функции select и этого набора макросов, мы можем проверять конечное множество сокетов на готовность к считыванию/отсылке данных, выполнения connect, на предмет входящих соединений, наличия OOB сообщений и т.п. На данном этапе нас интересует проверка сокета на возможность считывания данных, поэтому пока ограничимся самым простым вызовом select. Для этого нам необходимо поместить наш сокет в множество на которое будет указывать readfds (в примере это read_s), задать timeout и выполнить select.

Пример:
Код:
   // ...

   fd_set read_s; // Множество
   timeval time_out; // Таймаут

   FD_ZERO (&read_s); // Обнуляем мнодество
   FD_SET (s, &read_s); // Заносим в него наш сокет
   time_out.tv_sec = 0;time_out.tv_usec = 500000; //Таймаут 0.5 секунды.
   if (SOCKET_ERROR == (res = select (0, &read_s, NULL, NULL, &time_out) ) ) return -1;

   if ((res!=0) && (FD_ISSET (s, &read_s)) ) // Использую FD_ISSET только для примера! :)
   {
      // Получаю данные
   }
   // ...

Полный исходный код программы можно взять здесь: ws3_3.zip

Итак, мы рассмотрели некоторые нюансы работы с TCP-сокетами и простейшие способы решения проблемы блокировки. Конечно, каждый пример носит условный характер, но ведь это же всё-таки примеры! 

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

В следующем выпуске читайте:
  • Приём входящих соединений
  • Учим новые функции.
Версия для печати
Обсудить на форуме