Winsock для всех (часть 1)

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

Не будем вдаваться в детали внутренней архитектуры, ведь нас интересует не то, как он устроен внутри, а то, как использовать функции, предоставляемые Winsock пользователю для работы. Наша задача - на конкретных примерах разобраться с механизмом действия WinsockAPI. "Для чего это можно использовать? Ведь существуют библиотеки, упрощающие работу с сетями и имеющие простой интерфейс?" - спросите вы. Я отчасти согласен с этим утверждением, но по-моему полностью универсальных библиотек, ориентированных под все задачи существовать не может. Да и к тому же, намного приятней разобраться во всём самому, не чувствуя неловкости перед "чёрным ящиком" принципа работы которого не понимаешь, а лишь используешь как инструмент:) Весь материал рассчитан на новичков. Я думаю с его освоением не будет никаких проблем. Если вопросы всё-таки возникнут, пишите на [email protected] . Отвечу всем. Для иллюстрации примеров будем использовать фрагменты кода Microsoft VC++. Итак, приступим!

Winsock - с чего начать?

Итак, первый вопрос - если есть Winsock, то как его использовать? На деле всё не так уж и сложно. Этап первый - подключение библиотек и заголовков.

#include "winsock.h" или #include "winsock2.h" - в зависимости от того, какую версию Winsock вы будете использовать
Так же в проект должны быть включены все соответствующие lib-файлы (Ws2_32.lib или Wsock32.lib)

Шаг 2 - инициализация.

Теперь мы можем спокойно использовать функции WinsockAPI. (полный список функций можно найти в соответствующих разделах MSDN).

Для инициализации Winsock вызываем функцию WSAStartup

int WSAStartup(WORD wVersionRequested, (in) LPWSADATA lpWSAData (out));


Параметр WORD wVersionRequested - младший байт - версия, старший байт - под.версия, интерфейса Winsock. Возможные версии - 1.0, 1.1, 2.0, 2.2... Для "сборки" этого параметра используем макрос MAKEWORD. Например: MAKEWORD (1, 1) - версия 1.1. Более поздние версии отличаются наличием новых функций и механизмов расширений. Параметр lpWSAData - указатель на структуру WSADATA. При возврате из функции данная структура содержит информацию о проинициализированной нами версии WinsockAPI. В принципе, ёё можно игнорировать, но если кому-то будет интересно что же там внутри - не поленитесь, откройте документацию;)

Так это выглядит на практике:

WSADATA ws;
//...
if (FAILED (WSAStartup (MAKEWORD(1, 1), &ws)))
{
// Error...
error = WSAGetLastError();
//...
}


В таком случае можно получить расширенную информацию об ошибке используя вызов WSAGetLastError(). Данная функция возвращает код ошибки (тип int)

Шаг 3 - создание сокета.

Итак, мы можем приступить к следующему этапу - создания основного средства коммуникации в Winsock- сокета (socket). С точки зрения WinsockAPI сокет - это дескриптор, который может получать или отправлять данные. На практике всё выглядит так: мы создаём сокет с определёнными свойствами и используем его для подключения, приёма/передачи данных и т.п. А теперь сделаем небольшое отступление... Итак, создавая сокет мы должны указать его параметры: сокет использует TCP/IP протокол или IPX (если TCP/IP, то какой тип и т.д.). Так как следующие разделы данной статьи будут ориентированы на TCP/IP протокол, то остановимся на особенностях сокетов использующих этот протокол. Мы можем создать два основных типа сокетов работающих по TCP/IP протоколу - SOCK_STREAM и SOCK_DGRAM (RAW socket пока оставим в покое:)). Разница в том, что для первого типа сокетов (их еще называют TCP или connection-based socket), для отправки данных сокет должен постоянно поддерживать соединение с адресатом, при этом доставка пакета адресату гарантирована. Во втором случае наличие постоянного соединения не нужно, но информацию о том, дошел ли пакет, или нет - получить невозможно (так называемые UDP или connectionless sockets). И первый и второй типы сокетов имеют своё практическое применение. Начнём наше знакомство с сокетами с TCP (connection-based) сокетов.

Для начала объявим его:

Создать сокет можно с помощью функции socket

SOCKET socket (int af (in), // протокол (TCP/IP, IPX...)
int type (in), // тип сокета (SOCK_STREAM/SOCK_DGRAM)
int protocol (in) // для Windows приложений может быть 0
);


Пример:

if (INVALID_SOCKET == (s = socket (AF_INET, SOCK_STREAM, 0)))
{
// Error...
error = WSAGetLastError();
// ...
}


При ошибке функция возвращает INVALID_SOCKET. В таком случае можно получить расширенную информацию об ошибке используя вызов WSAGetLastError().

Шаг 4 -устанавливаем соединение.

В предыдущем примере мы создали сокет. Что же теперь с ним делать? :) Теперь мы можем использовать этот сокет для обмена данными с другими клиентами winsock-клиентами и не только. Для того, что бы установить соединение с другой машиной необходимо знать ее IP адрес и порт. Удалённая машина должна "слушать" этот порт на предмет входящих соединений (т.е. она выступает в качестве сервера). В таком случае наше приложение это клиент.

Для установки соединения используем функцию connect.

int connect(SOCKET s, // сокет (наш сокет)
const struct sockaddr FAR *name, // адрес
int namelen // длинна адреса
);


Пример:

// Объявим переменную для хранения адреса
sockaddr_in s_addr;

// Заполним ее:
ZeorMemory (&s_addr, sizeof (s_addr));
// тип адреса (TCP/IP)
s_addr.sin_family = AF_INET;
//адрес сервера. Т.к. TCP/IP представляет адреса в числовом виде, то для перевода
// адреса используем функцию inet_addr.
s_addr.sin_addr.S_un.S_addr = inet_addr ("193.108.128.226");
// Порт. Используем функцию htons для перевода номера порта из обычного в //TCP/IP представление.
s_addr.sin_port = htons (1234);


При ошибке функция возвращает SOCKET_ERROR.
Теперь сокет s связан с удаленной машиной и может посылать/принимать данные только с нее.

Шаг 5 - посылаем данные.

Для того что бы послать данные используем функцию send

int send(SOCKET s, // сокет- отправитель
const char FAR *buf, // указатель на буффер с данными
int len, // длинна данных
);


Пример использования данной функции:

if (SOCKET_ERROR == (send (s, (char*) & buff), 512, 0))
{
// Error...
error = WSAGetLastError();
// ...
}


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

Шаг 6 -принимаем данные.

Принять данные от машины с которой мы предварительно установили соединение позволяет функция recv.

int recv(SOCKET s, // сокет- получатель
char FAR *buf, // адрес буфера для приёма данных
int len, // длинна буфера для приёма данных
int flags // флаги (может быть 0)
);


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

int actual_len = 0;

If (SOCKET_ERROR == (actual_len = recv (s, (char*) & buff), max_packet_size, 0))
{
// Error...
error = WSAGetLastError();
// ...
}


Если данные получены, то функция возвращает размер полученного пакета данных (а примере - actual_len) При ошибке функция возвращает SOCKET_ERROR. Заметьте, что функции send/recv будут ждать пока не выйдет тайм-аут или не отправится/придет пакет данных. Это соответственно вызывает задержку в работе программы. Как этого избежать читайте в следующих выпусках.

Шаг 6 -закрываем соединение.

Процедура закрытия активного соединения происходит с помощью функций shutdown и closesocket. Различают два типа закрытия соединений: abortive и graceful. Первый вид - это экстренное закрытие сокета (closesocket). В таком случае соединение разрывается моментально. Вызов closesocket имеет мгновенный еффект. После вызова closesocket сокет уже недоступен. Как закрыть сокет с помощью shutdown/closesocket читайте в следующих выпусках, так как эта тема требует более полного знания Winsock.

int shutdown(SOCKET s, // Закрываемый сокет
int how // Способ закрытия
);


int closesocket(SOCKET s // Закрываемый сокет
);


Пример:

closesocket (s);

Как видите, рассмотренный нами Winsock-механизм обмена данными очень прост. От программиста требуется лишь выработать свой "протокол" общения удалённых машин и реализовать его с помощью данных функций. Конечно, рассмотренные нами примеры не отражают всех возможностей Winsock. В наших статьях мы постараемся рассмотреть наиболее важные на наш взгляд особенности работы с Winsock. Stay tuned. :)
В следующем выпуске читайте:

  • Пишем простейшее winsock приложение.
  • UDP сокеты - приём/доставка негарантированных пакетов
  • Решаем проблему "блокировки" сокетов.

Самоучитель игры на WINSOCK

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

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

Автор постарался дать максимально целостное и связанное описание, затрагивающее не только основные моменты, но и некоторые тонкости не известные рядовым программистам. Ограниченный объем журнальной статьи не позволяет рассказать обо всем, поэтому, пришлось сосредоточиться только на одной реализации сокетов - библиотеке Winsock 2, одном языке программирования - Си/Си ++ (хотя сказанное большей частью приемлемо к Delphi, Perl и т. д.) и одном виде сокетов - блокируемых синхронных сокетах .

ALMA MATER

Основное подспорье в изучении сокетов - Windows Sockets 2 SDK. SDK - это документация, набор заголовочных файлов и инструментарий разработчика. Документация не то, чтобы очень хороша - но все же написана достаточна грамотно и позволяет, пускай, не без труда, освоить сокеты даже без помощи какой-либо другой литературы. Причем, большинство книг, имеющиеся на рынке, явно уступают Microsoft в полноте и продуманности описания. Единственный недостаток SDK - он полностью на английском (для некоторых это очень существенно).

Из инструментария, входящего в SDK, в первую очередь хотелось бы выделить утилиту sockeye.exe - это настоящий "тестовый стенд" разработчика. Она позволяет в интерактивном режиме вызывать различные сокет-функции и манипулировать ими по своему усмотрению.

Демонстрационные программы, к сожалению, не лишены ошибок, причем порой довольно грубых и наводящих на мысли - а тестировались ли эти примеры вообще? (Например, в исходном тексте программы simples.c в вызове функций send и sendto вместо strlen стоит sizeof) В то же время, все примеры содержат множество подробных комментариев и раскрывают довольно любопытные приемы нетрадиционного программирования, поэтому ознакомится с ними все-таки стоит.

Из WEB-ресуросов, посвященных программированию сокетов, и всему, что с ними связано, в первую очередь хотелось бы отметить следующие три: sockaddr.com; www.winsock.com и www.sockets.com.

Обзор сокетов

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

ОС Windows 3.x поддерживает только асинхронные сокеты, поскольку, в среде с корпоративной многозадачностью захват управления одной задачей "подвешивает" все остальные, включая и саму систему. ОС Windows 9x\NT поддерживают оба вида сокетов, однако, в силу того, что синхронные сокеты программируются более просто, чем асинхронные, последние не получили большого распространения. Эта статья посвящена исключительно синхронным сокетам (асинхронные - тема отдельного разговора).

Сокеты позволяют работать со множеством протоколов и являются удобным средством межпроцессорного взаимодействия, но в данной статье речь будет идти только о сокетах семейства протоколов TCP/IP, использующихся для обмена данными между узлами сети Интернет. Все остальные протоколы, такие как IPX/SPX, NetBIOS по причине ограниченности объема журнальной статьи рассматриваться не будут.

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

Выбор того или иного типа сокетов определяется транспортным протоколом на котором работает сервер, - клиент не может по своему желанию установить с дейтаграммным сервером потоковое соединение.

Замечание : дейтаграммные сокеты опираются на протокол UDP, а потоковые на TCP.

Первый шаг, второй, третий

Для работы с библиотекой Winsock 2.х в исходный тест программы необходимо включить директиву "#include ", а в командной строке линкера указать "ws2_32.lib". В среде разработки Microsoft Visual Studio для этого достаточно нажать <Alt-F7 >, перейти к закладке "Link" и к списку библиотек, перечисленных в строке "Object/Library modules", добавить "ws2_32.lib", отделив ее от остальных символом пробела.

Перед началом использования функций библиотеки Winsock ее необходимо подготовить к работе вызовом функции "int WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData)" передав в старшем байта слова wVersionRequested номер требуемой версии, а в младшем - номер подверсии.

Аргумент lpWSAData должен указывать на структуру WSADATA , в которую при успешной инициализации будет занесена информация о производителе библиотеки. Никакого особенного интереса она не представляет и прикладное приложение может ее игнорировать. Если инициализация проваливается, функция возвращает ненулевое значение.

Второй шаг – создание объекта "сокет". Это осуществляется функцией "SOCKET socket (int af, int type, int protocol) ". Первый слева аргумент указывает на семейство используемых протоколов. Для Интернет - приложений он должен иметь значение AF_INET.

Следующий аргумент задает тип создаваемого сокета - потоковый (SOCK_STREAM ) или дейтаграммный (SOCK_DGRAM ) (еще существуют и сырые сокеты, но они не поддерживаются Windows - см раздел "Сырые сокеты").

Последний аргумент уточняет какой транспортный протокол следует использовать. Нулевое значение соответствует выбору по умолчанию: TCP - для потоковых сокетов и UDP для дейтаграммных. В большинстве случаев нет никакого смысла задавать протокол вручную и обычно полагаются на автоматический выбор по умолчанию.

Если функция завершилась успешно она возвращает дескриптор сокета, в противном случае INVALID_SOCKET.

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

Клиент: шаг третий - для установки соединения с удаленным узлом потоковый сокет должен вызвать функцию "int connect (SOCKET s, const struct sockaddr FAR* name, int namelen)" . Датаграмные сокеты работают без установки соединения, поэтому, обычно не обращаются к функции connect.

Примечание : за словом "обычно" кроется один хитрый примем программирования - вызов connect позволяет дейтаграмному сокету обмениваться данными с узлом не только функциями sendto, recvfrom, но и более удобными и компактными send и recv. Эта тонкость описана в Winsocket SDK и широко используется как самой Microsoft, так и сторонними разработчикам. Поэтому, ее использование вполне безопасно.

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

После вызова connect система предпринимает попытку установить соединение с указанным узлом. Если по каким-то причинам это сделать не удастся (адрес задан неправильно, узел не существует или "висит", компьютер находится не в сети), функция возвратит ненулевое значение.

Сервер: шаг третий – прежде, чем сервер сможет использовать сокет, он должен связать его с локальным адресом. Локальный, как, впрочем, и любой другой адрес Интернета, состоит из IP-адреса узла и номера порта. Если сервер имеет несколько IP адресов, то сокет может быть связан как со вмести ними сразу (для этого вместо IP-адреса следует указать константу INADDR_ANY равную нулю), так и с каким-то конкретным одним.

Связывание осуществляется вызовом функции "int bind (SOCKET s, const struct sockaddr FAR* name, int namelen)". Первым слева аргументом передается дескриптор сокета, возращенный функций socket, за ним следуют указатель на структуру sockaddr и ее длина (см. раздел "Адрес раз, адрес два ").

Строго говоря, клиент также должен связывать сокет с локальным адресом перед его использованием, однако, за него это делает функция connect, ассоциируя сокет с одним из портов, наугад выбранных из диапазона 1024-5000. Сервер же должен "садиться" на заранее определенный порт, например, 21 для FTP, 23 для telnet, 25 для SMTP, 80 для WEB, 110 для POP3 и т.д. Поэтому ему приходится осуществлять связывание "вручную".

При успешном выполнении функция возвращает нулевое значение и ненулевое в противном случае.

Сервер: шаг четвертый - выполнив связывание, потоковый сервер переходит в режим ожидания подключений, вызывая функцию "int listen (SOCKET s, int backlog) ", где s – дескриптор сокета, а backlog – максимально допустимый размер очереди сообщений.

Размер очереди ограничивает количество одновременно обрабатываемых соединений, поэтому, к его выбору следует подходить "с умом". Если очередь полностью заполнена, очередной клиент при попытке установить соединение получит отказ (TCP пакет с установленным флагом RST). В то же время максимально разумное количество подключений определяются производительностью сервера, объемом оперативной памяти и т.д.

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

Сервер: шаг пятый – извлечение запросов на соединение из очереди осуществляется функцией "SOCKET accept (SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen) ", которая автоматически создает новый сокет, выполняет связывание и возвращает его дескриптор, а в структуру sockaddr заносит сведения о подключившемся клиенте (IP-адрес и порт). Если в момент вызова accept очередь пуста, функция не возвращает управление до тех пор, пока с сервером не будет установлено хотя бы одно соединение. В случае возникновения ошибки функция возвращает отрицательное значение.

Для параллельной работы с несколькими клиентами следует сразу же после извлечения запроса из очереди порождать новый поток (процесс), передавая ему дескриптор созданного функцией accept сокета, затем вновь извлекать из очереди очередной запрос и т.д. В противном случае, пока не завершит работу один клиент, север не сможет обслуживать всех остальных.

все вместе – после того как соединение установлено, потоковые сокеты могут обмениваться с удаленным узлом данными, вызывая функции "int send (SOCKET s, const char FAR * buf, int len,int flags) " и "int recv (SOCKET s, char FAR* buf, int len, int flags) " для посылки и приема данных соответственно.

Функция send возвращает управление сразу же после ее выполнения независимо от того, получила ли принимающая сторона наши данные или нет. При успешном завершении функция возвращает количество передаваемых (не переданных! ) данных - т. е. успешное завершение еще не свидетельствует от успешной доставке! В общем-то, протокол TCP (на который опираются потоковые сокеты) гарантирует успешную доставку данных получателю, но лишь при условии, что соединение не будет преждевременно разорвано. Если связь прервется до окончания пересылки, данные останутся не переданными, но вызывающий код не получит об этом никакого уведомления! А ошибка возвращается лишь в том случае, если соединение разорвано до вызова функции send!

Функция же recv возвращает управление только после того, как получит хотя бы один байт. Точнее говоря, она ожидает прихода целой дейтаграммы . Дейтаграмма - это совокупность одного или нескольких IP пакетов, посланных вызовом send. Упрощенно говоря, каждый вызов recv за один раз получает столько байтов, сколько их было послано функцией send. При этом подразумевается, что функции recv предоставлен буфер достаточных размеров, - в противном случае ее придется вызвать несколько раз. Однако, при всех последующих обращениях данные будет браться из локального буфера, а не приниматься из сети, т.к. TCP-провайдер не может получить "кусочек" дейтаграммы, а только ею всю целиком.

Работой обоих функций можно управлять с помощью флагов , передаваемых в одной переменной типа int третьим слева аргументом. Эта переменная может принимать одно из двух значений: MSG _PEEK и MSG _OOB .

Флаг MSG_PEEK заставляет функцию recv просматривать данные вместо их чтения. Просмотр, в отличие от чтения, не уничтожает просматриваемые данные. Некоторые источники утверждают, что при взведенном флаге MSG_PEEK функция recv не задерживает управления если в локальном буфере нет данных, доступных для немедленного получения. Это неверно! Аналогично, иногда приходится встречать откровенно ложное утверждение, якобы функция send со взведенным флагом MSG_PEEK возвращает количество уже переданных байт (вызов send не блокирует управления). На самом деле функция send игнорирует этот флаг!

Флаг MSG_OOB предназначен для передачи и приема срочных (Out Of Band ) данных. Срочные данные не имеют преимущества перед другими при пересылке по сети, а всего лишь позволяют оторвать клиента от нормальной обработки потока обычных данных и сообщить ему "срочную" информацию. Если данные передавались функцией send с установленным флагом MSG_OOB, для их чтения флаг MSG_OOB функции recv так же должен быть установлен.

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

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

Дейтаграммный сокет так же может пользоваться функциями send и recv, если предварительно вызовет connect (см. "Клиент : шаг третий "), но у него есть и свои, "персональные", функции: "int sendto (SOCKET s, const char FAR * buf, int len,int flags, const struct sockaddr FAR * to, int tolen) " и "int recvfrom (SOCKET s, char FAR* buf, int len, int flags, struct sockaddr FAR* from, int FAR* fromlen) ".

Они очень похожи на send и recv, - разница лишь в том, что sendto и recvfrom требуют явного указания адреса узла принимаемого или передаваемого данные. Вызов recvfrom не требует предварительного задания адреса передающего узла - функция принимает все пакеты, приходящие на заданный UDP-порт со всех IP адресов и портов. Напротив, отвечать отправителю следует на тот же самый порт откуда пришло сообщение. Поскольку, функция recvfrom заносит IP-адрес и номер порта клиента после получения от него сообщения, программисту фактически ничего не нужно делать - только передать sendto тот же самый указатель на структуру sockaddr, который был ранее передан функции recvfrem, получившей сообщение от клиента.

Еще одна деталь – транспортный протокол UDP, на который опираются дейтаграммные сокеты, не гарантирует успешной доставки сообщений и эта задача ложиться на плечи самого разработчика. Решить ее можно, например, посылкой клиентом подтверждения об успешности получения данных. Правда, клиент тоже не может быть уверен, что подтверждение дойдет до сервера, а не потеряется где-нибудь в дороге. Подтверждать же получение подтверждения - бессмысленно, т. к. это рекурсивно неразрешимо. Лучше вообще не использовать дейтаграммные сокеты на ненадежных каналах.

Во всем остальном обе пары функций полностью идентичны и работают с теми самыми флагами - MSG_PEEK и MSG_OOB.

Все четыре функции при возникновении ошибки возвращают значение SOCKET_ERROR (== -1).

Примечание: в UNIX с сокетами можно обращаться точно так, как с обычными файлами, в частности писать и читать в них функциями write и read. ОС Windows 3.1 не поддерживала такой возможности, поэтому, при переносе приложений их UNIX в Windows все вызовы write и read должны были быть заменены на send и recv соответственно. В Windows 95 с установленным Windows 2.x это упущение исправлено, - теперь дескрипторы сокетов можно передавать функциям ReadFil, WriteFile, DuplicateHandle и др.

Шаг шестой, последний – для закрытия соединения и уничтожения сокета предназначена функция "int closesocket (SOCKET s) ", которая в случае удачного завершения операции возвращает нулевое значение.

Перед выходом из программы, необходимо вызвать функцию "int WSACleanup (void) " для деинициализации библиотеки WINSOCK и освобождения используемых этим приложением ресурсов. Внимание : завершение процесса функцией ExitProcess автоматически не освобождает ресурсы сокетов!

Примечание: более сложные приемы закрытия соединения - протокол TCP позволяет выборочно закрывать соединение любой из сторон, оставляя другую сторону активной. Например, клиент может сообщить серверу, что не будет больше передавать ему никаких данных и закрывает соединение "клиент (сервер", однако, готов продолжать принимать от него данные, до тех пор, пока сервер будет их посылать, т.е. хочет оставить соединение "клиент (сервер" открытым.

Для этого необходимо вызвать функцию "int shutdown (SOCKET s ,int how)", передав в аргументе how одно из следующих значений: SD_RECEIVE для закрытия канала "сервер (клиент", SD_SEND для закрытия канала "клиент (сервер", и, наконец, SD_BOTH для закрытия обоих каналов.

Последний вариант выгодно отличается от closesocket "мягким" закрытием соединения - удаленному узлу будет послано уведомление о желании разорвать связь, но это желание не будет воплощено в действительность, пока тот узел не возвратит свое подтверждение. Таким образом, можно не волноваться, что соединение будет закрыто в самый неподходящий момент.

Внимание : вызов shutdown не освобождает от необходимости закрытия сокета функцией closesocket!

Дерево вызовов

Для большей наглядности демонстрации взаимосвязи socket-функций друг с другом, ниже приведено дерево вызовов, показывающее в каком порядке должны следовать вызовы функций в зависимости от типа сокетов (потоковый или дейтаграммный) и рода обработки запросв (клиент или сервер).

клиент сервер

connect |-sendto TCP UDP

| |-recvfrom | |

|-send listen |

|-send |-sendto

|-recv |-recvform

Листинг 29 Последовательность вызова функций сокетов при различных операциях

Адрес раз, адрес два

С адресами как раз и наблюдается наибольшая путаница, в которую не помешает внести немного ясности. Прежде всего структура sockaddr определенная так:

u_short sa_family; // семейство протоколов

// (как правило AF_INET)

char sa_data; // IP-адрес узла и порт

Листинг 30 Устаревшее определение структуры sockaddr

теперь уже считается устаревшей, и в Winsock 2.x на смену ей пришла структура sockaddr_in, определенная следующим образом:

struct sockaddr_in

short sin_family; // семейство протоколов

// (как правило AF_INET)

u_short sin_port; // порт

struct in_addr sin_addr; // IP – адрес

char sin_zero; // хвост

Листинг 31 Современное определение структуры sockaddr_in

В общем-то ничего не изменилось (и стоило огород городить?), замена безнакового короткого целого на знаковое короткое целое для представления семейства протоколов ничего не дает. Зато теперь адрес узла представлен в виде трех полей - sin_port (номера порта), sin_addr (IP-адреса узла) и "хвоста" из восьми нулевых байт, который остался от четырнадцати символьного массива sa_data . Для чего он нужен? Дело в том, что структура sockaddr не привязана именно к Интернету и может работать и с другими сетями. Адреса же некоторых сетей требуют для своего представления гораздо больше четырех байт, - вот и приходится брать с запасом!

Структура in_addr определяется следующим в образом:

struct in_addr {

struct { u_char s_b1,s_b2,s_b3,s_b4; } S_un_b;

// IP-адрес

struct { u_short s_w1,s_w2; } S_un_w;

// IP-адрес

u_long S_addr; // IP-алрес

Листинг 32 Определение структуры in_addr

Как видно, она состоит из одного IP-адреса, записанного в трех "ипостасях" - четырехбайтовой последовательности (S_un_b), пары двухбайтовых слов (S_un_W) и одного длинного целого (S_addr) - выбирай на вкус Но не все так просто! Во многих программах, технических руководствах и даже демонстрационных примерах, прилагающихся к Winsock SDK, встречается обращение к "таинственному" члену структуры s_addr, который явно не описан в SDK! Например, вот строка из файла "Simples.h": "local.sin_addr.s_addr = (!interface)?INADDR_ANY:inet_addr(interface);"

Это что такое?! Заглянув в файл "winsock2.h" можно обнаружить следующее: "#define s_addr S_un.S_addr". Ага, да ведь это эквивалент s_addr, т.е. IP-адресу, записанному в виде длинного целого!

На практике можно с одинаковым успехом пользоваться как "устаревшей" sockaddr, так и "новомодной" sockaddr_in. Однако, поскольку, прототипы остальных функций не изменились, при использовании sockaddr_in придется постоянно выполнять явные преобразования, например так: " sockaddr_in dest_addr; connect (mysocket, (struct sockaddr* ) &dest_addr, sizeof(dest_addr)" .

Для преобразования IP-адреса, записанного в виде символьной последовательности наподобие "127.0.0.1" в четырехбайтовую числовую последовательность предназначена функция "unsigned long inet_addr (const char FAR * cp) ". Она принимает указатель на символьную строку и в случае успешной операции преобразует ее в четырехбайтовый IP адрес или –1 если это невозможно. Возвращенный функцией результат можно присвоить элементу структуры sockaddr_in следующим образом: "struct sockaddr_in dest_addr; dest_addr.sin_addr.S_addr=inet_addr("195.161.42.222"); ". При использовании структуры sockaddr это будет выглядеть так: "struc sockaddr dest_addr; ((unsigned int *)(&dest_addr.sa_data+2)) = inet_addr("195.161.42.222"); "

Попытка передать inet_addr доменное имя узла приводит к провалу. Узнать IP-адрес такого-то домена можно с помощью функции "struct hostent FAR * gethostbyname (const char FAR * name); ". Функция обращается к DNS и возвращает свой ответ в структуре hostent или нуль если DNS сервер не смог определить IP-адрес данного домена.

Структура hostent выглядит следующим образом:

char FAR * h_name; // официальное имя узла

char FAR * FAR* h_aliases; // альтернативные имена

// узла (массив строк)

short h_addrtype; // тип адреса

short h_length; // длина адреса

// (как правило AF_INET)

char FAR * FAR * h_addr_list; // список указателей

//на IP-адреса

// ноль – конец списка

Листинг 33 Определение структуры hostent

Как и в случае с in_addr, во множестве программ и прилагаемых к Winsock SDK примерах активно используется недокументированное поле структуры h_addr. Например, вот строка из файла "simplec.c" "memcpy(&(server.sin_addr),hp-> h_addr ,hp->h_length);" Заглянув в "winsock2.h" можно найти, что оно обозначает: "#define h_addr h_addr_list ".

А вот это уже интересно! Дело в том, что с некоторыми доменными именами связано сразу несколько IP-адресов. В случае неработоспособности одного узла, клиент может попробовать подключится к другому или просто выбрать узел с наибольшей скоростью обмена. Но в приведенном примере клиент использует только первый IP-адрес в списке и игнорирует все остальные! Конечно, это не смертельно, но все же будет лучше, если в своих программах вы будете учитывать возможность подключения к остальным IP-адресам, при невозможности установить соединение с первым.

Функция gethostbyname ожидает на входе только доменные имена, но не цифровые IP-адреса. Между тем, правила "хорошего тона" требуют предоставления клиенту возможности как задания доменных имен, так и цифровых IP-адресов.

Решение заключается в следующем - необходимо проанализировать переданную клиентом строку - если это IP адрес, то передать его функции inet_addr в противном случае - gethostbyaddr, полагая, что это доменное имя. Для отличия IP-адресов от доменных имен многие программисты используют нехитрый трюк: если первый символ строки - цифра, это IP-адрес, иначе - имя домена. Однако, такой трюк не совсем честен - доменные имя могут начинаться с цифры, например, "666.ru", могут они и заканчиваться цифрой, например, к узлу "666.ru" члены cубдомена "666" могут так и обращаться - "666". Самое смешное, что (теоретически) могут существовать имена доменов, синтаксически неотличимые от IP-адресов! Поэтому, на взгляд автора данной статьи, лучше всего действовать так: передаем введенную пользователем строку функции inet_addr, если она возвращает ошибку, то вызываем gethostbyaddr.

Для решения обратной задачи – определении доменного имени по IP адресу предусмотрена функция "struct HOSTENT FAR * gethostbyaddr (const char FAR * addr, int len, int type) ", которая во всем аналогична gethostbyname, за тем исключением, что ее аргументом является не указатель на строку, содержащую имя, а указатель на четырехбайтовый IP-адрес. Еще два аргумента задают его длину и тип (соответственно, 4 и AF_INET).

Определение имени узла по его адресу бывает полезным для серверов, желающих "в лицо" знать своих клиентов.

Для преобразования IP-адреса, записанного в сетевом формате в символьную строку, предусмотрена функция "char FAR * inet _ ntoa (struct in_addr) ", которая принимает на вход структуру in_addr, а возвращает указатель на строку, если преобразование выполнено успешно и ноль в противном случае.

Сетевой порядок байт

Сетевеая библиотека Winsock

Работа с сетью через компоненты delphi очень удобна и достаточно проста, но слишком уж медленна. Это можно исправить, если напрямую обращаться к сетевой библиотеке окошек - winsock. Сегодня мы познакомимся с ее основами.

Что такое winsock

Библиотека winsock состоит из одного лишь файла winsock.dll. Она очень хорошо подходит для создания простых приложений, потому что в ней реализованы все необходимые функции для создания соединения и приема/передачи файлов. Зато сниффер создавать даже не пытайся. В winsock нет ничего для доступа к заголовкам пакетов. ms обещала встроить эти необходимые продвинутому челу вещи в winsock2, но, как всегда, прокатила нас задницей по наждачной бумаге и сказала, мол, обойдемся. Чем хороша эта библиотека, так это тем, что все ее функции одинаковы для многих платформ и языков программирования. Так, например, если мы напишем сканер портов, его легко можно будет перенести на язык С/С++ и даже написать что-то подобное в *nix, потому что там сетевые функции называются так же и имеют практически те же параметры. Разница между сетевой библиотекой windows и linux минимальна, хотя и есть. Но так и должно быть, ведь Билл не может по-человечески, и ему обязательно надо выпендриться. Сразу же предупрежу, что мы будем изучать winsock2, а delphi поддерживает только первую версию. Чтобы он смог увидеть вторую, нужно скачать заголовочные файлы для 2-й версии их можно найти в интернете. Вся работа сетевой библиотеки построена вокруг понятия socket - это как бы виртуальный сетевой канал. Для соединения с сервером ты должен подготовить такой канал к работе и потом можешь соединяться на любой порт серванта. Все это лучше всего увидеть на практике, но я попробую дать тебе сейчас общий алгоритм работы с сокетами:
1. Инициализируем библиотеку winsock.
2. Инициализируем socket (канал для связи). После инициализации у нас должна быть переменная, указывающая на новый канал. Созданный socket - это, можно сказать, что открытый порт на твоем компе. Порты есть не только на серванте, но и у тебя, и когда происходит передача данных между компами, то она происходит между сетевыми портами.
3. Можно присоединяться к серверу. В каждой функции для работы с сетью первым параметром обязательно указывается переменная, указывающая на созданный канал, через который будет происходить соединение.

Стартуем winsock

Самое первое, что надо сделать - стартануть библиотеку (для юниксоидов это не нужно делать). Для этого нужно вызвать функцию wsastartup. У нее есть два параметра:
- Версия winsock, которую мы хотим стартануть. Для версии 1.0 нужно указать makeword(1,0), но нам нужна вторая, значит, будем указывать makeword(2,0).
- Структура типа twsadata, в которой будет возвращена информация о найденном winsock.
Теперь узнаем, как нужно закрывать библиотеку. Для этого нужно вызвать функцию wsacleanup, у которой нет параметров. В принципе, если ты не закроешь winsock, то ничего критического не произойдет. После выхода из программы все само закроется, просто освобождение ненужного сразу после использования является хорошим тоном в кодинге.

Первый пример

Давай сразу напишем пример, который будет инициализировать winsock и выводить на экран информацию о нем. Создай в delphi новый проект. Теперь к нему надо подключить заголовочные файлы winsock 2-й версии. Для этого надо перейти в раздел uses и добавить туда модуль winsock2. Если ты попробуешь сейчас скомпилировать этот пустой проект, то delphi проругается на добавленный модуль. Это потому, что он не может найти сами файлы. Если ты скачал заголовочные файлы winsock2, то можно поступить двумя способами:
1. Сохранить новый проект в какую-нибудь диру и туда же забросить файлы winsock2.pas, ws2tcpip.inc, wsipx.inc, wsnwlink.inc и wsnetbs.inс. Неудобство этого способа - в каждый проект, использующий winsock2, надо забрасывать заголовочные файлы.
2. Можно забросить эти файлы в диру delphilib, и тогда уж точно любой проект найдет их.

Шкодим

Теперь создай форму с кнопкой и полем вывода. После этого создай обработчик события onclick для кнопки и напиши там следующий текст:
procedure tform1.button1click(sender: tobject);
var
info:twsadata;
begin
wsastartup(makeword(2,0), info);
versionedit.text:=inttostr(info.wversion);
descriptionedit.text:=info.szdescription;
systemstatusedit.text:=info.szsystemstatus;
wsacleanup;
end;

В самом начале я стартую winsock с помощью wsastartup. В нем я запрашиваю 2-ю версию, а информация о текущем состоянии мне будет возвращена в структуру info. После этого я вывожу полученную инфу для всеобщего просмотра. При выводе информации о версии у меня есть небольшая проблема, потому что свойство wversion структуры info имеет числовой тип, а для вывода мне надо преобразовать его в строку. Для этого я выполняю преобразование с помощью inttostr.

Подготовка разъема

Прежде чем производить коннект к серверу, надо еще подготовить socket к работе. Этим и займемся. Для подготовки нужно выполнить функцию socket, у которой есть три параметра:
1. Тип используемой адресации. Нас интересует Инет, поэтому мы будем указывать pf_inet или af_inet. Как видишь, оба значения очень похожи и показывают одну и ту же адресацию, только в первом случае работа будет синхронной, а во втором асинхронной.
2. Базовый протокол. Здесь мы должны указать, на основе какого протокола будет происходить работа. Ты должен знать, что существует два базовых протокола - tcp (с надежным соединением) и udp (не производящий соединений, а просто выплевывающий данные в порт). Для tcp в этом параметре надо указать sock_stream, а если нужен udp, то указывай sock_dgram.
3. Вот здесь мы можем указывать, какой конкретно протокол нас интересует. Возможных значений тут немерено (например, ipproto_ip, ipport_echo, ipport_ftp и т.д.). Если хочешь увидеть все, то открывай файл winsock2.pas и запускай поиск по ipport_, и все что ты найдешь - это и будут возможные протоколы.

Синхронность/асинхронность

Теперь я хочу тебя познакомить с синхронностью и асинхронностью работы порта. Разница в этих двух режимах следующая. Синхронная работа: когда ты вызываешь функцию, то программа останавливается и ждет полного ее выполнения. Допустим, что ты запросил коннект с сервером. Прога тут же тормозит и ждет, пока не произойдет коннект или ошибка. Асинхронная работа: при этом режиме программа не спотыкается о каждую сетевую функцию. Допустим, что ты запросил все тот же коннект с сервером. Твоя прога посылает запрос на соединение и тут же продолжает выполнять следующие действия, не дожидаясь физического контакта с сервантом. Это очень удобно (но тяжело в кодинге), потому что можно использовать время, пока произойдет контакт, в своих целях. Единственное, что ты не можешь делать - вызывать сетевые функции, пока не произойдет реального физического контакта. Недостаток в том, что самому приходится следить за тем, когда закончится выполнение функции и можно будет дальше работать с сетью.

Полный коннект

Сокет готов, а значит, можно произвести соединение с сервером. Для этого в библиотеки winsock есть функция connect. У этой функции есть три параметра:
1. Переменная-сокет, которую мы получили после вызова функции socket.
2. Структура типа tsockaddr.
3. Размер структуры, указанной во втором параметре. Для того чтобы узнать размер, можно воспользоваться функцией sizeof и указать в качестве параметра структуру.
Структура tsockaddr очень сложная, и описывать ее полностью нет смысла. Лучше мы познакомимся с нею на практике, а пока я покажу только основные поля, которые должны быть заполнены.
sin_family - семейство используемой адресации. Здесь нужно указывать то же, что указывали в первом параметре при создании сокета (для нас это Рf_inet или af_inet).
sin_addr - адрес сервера, куда мы хотим присоединиться.
sin_port - порт, на который мы хотим приконнектиться.
На деле это будет выглядеть так:

var
addr: tsockaddr;
begin
addr.sin_family:= af_inet;
addr.sin_addr:= servername;
addr.sin_port:= htons(21);
connect(fsocket, @addr, sizeof(addr));
end;

shutdown

Ну и напоследок - функция для закрытия соединения - closesocket. В качестве параметра нужно указать переменную-сокет.

WinSock или Windows socket - это интерфейс прикладного программи-рования (API ), созданный для реализации приложений в сети на основе прото-кола TCP / IP . Для работы используется WSOCK 32. DLL . Эта библиотека нахо-дится в папке \ System 32 системного каталога Windows .

Существуют две версии WinSock :

WinSock 1.1 - поддерживает только протокол TCP / IP ;

WinSock 2.0 - поддерживает дополнительное программное обеспечение.

WinSock 1.1 дал толчок к развитию World Wide Web и позволил получить доступ в Internet обычному пользователю ПК под Windows . Если цель версии 1.1 состояла в решении проблемы, то цель WinSock 2.0 - сделать сетевую среду лучше, быстрее и надежнее. В WinSock 2.0 добавлена поддержка других транс-портных протоколов и новые функциональные возможности обеспечения на-дежности сетевого обмена информацией. WinSock 2.0 позволяет создавать не-зависящие от транспортных протоколов приложения, работающие с TCP / IP (Transmission Control Protocol / Internet Protocol ), UDP (User Datagram Protocol ), IPX / SPX (Internetwork Packet Exchange / Sequenced Packet Exchange ), NetBEUI (NetBios Extended User Interface ). Большая эффективность таких приложений достигается за счет совмещенного ввода/вывода и разделяемых сокетов.

Спецификация WinSock разделяет функции на три типа:

Блокирующие и неблокирующие (функции Беркли);

Информационные (получение информации о наименовании доменов, службах, протоколах Internet );

Инициализации и деинициализации библиотеки.

Блокирующая – это функция, которая останавливает работу программы до своего завершения; неблокирующая – это функция, которая выполняется па-раллельно с программой. Список основных функций, необходимых для созда-ния приложения, приведен в таблицах 1, 2, 3. Все описания функций WinSock даны в формате языка С, а примеры их вызова – на Delphi .

Сетевое программирование с помощью сокетов Windows

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

Однако как именованные каналы, так и почтовые ящики (в отношении которых для простоты мы будем использовать далее общий термин — "именованные каналы", если различия между ними не будут играть существенной роли) обладают тем недостатком, что они не являются промышленным стандартом. Это обстоятельство усложняет перенос программ наподобие тех, которые рассматривались в главе 11, в системы, не принадлежащие семейству Windows , хотя именованные каналы не зависят от протоколов и могут выполняться поверх многих стандартных промышленных протоколов, например TCP / IP .

Возможность взаимодействия с другими системами обеспечивается в Windows поддержкой сокетов (sockets ) Windows Sockets — совместимого и почти точного аналога сокетов Berkeley Sockets , де-факто играющих роль промышленного стандарта. В этой главе использование API Windows Sockets (или " Winsock ") показано на примере модифицированной клиент-серверной системы из главы 11. Результирующая система способна функционировать в глобальных сетях, использующих протокол TCP / IP , что, например, позволяет серверу принимать запросы от клиентов UNIX или каких-либо других, отличных от Windows систем.

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

Привлекая средства обеспечения взаимодействия между разнородными системами, ориентированные на стандарты, интерфейс Winsock открывает перед программистами возможность доступа к высокоуровневым протоколам и приложениям, таким как ftp , http , RPC и СОМ, которые в совокупности предоставляют богатый набор высокоуровневых моделей, обеспечивающих поддержку межпроцессного сетевого взаимодействия для систем с различной архитектурой.

В данной главе указанная клиент-серверная система используется в качестве механизма демонстрации интерфейса Winsock , и в процессе того, как сервер будет модифицироваться, в него будут добавляться новые интересные возможности. В частности, нами будут впервые использованы точки входа DLL (глава 5) и внутрипроцессные серверы DLL . (Эти новые средства можно было включить уже в первоначальную версию программы в главе 11, однако это отвлекло бы ваше внимание от разработки основной архитектуры системы.) Наконец, дополнительные примеры покажут вам, как создаются безопасные реентерабельные многопоточные библиотеки.

Поскольку интерфейс Winsock должен соответствовать промышленным стандартам, принятые в нем соглашения о правилах присвоения имен и стилях программирования несколько отличаются от тех, с которыми мы сталкивались в процессе работы с описанными ранее функциями Windows . Строго говоря, Winsock API не является частью Win 32/64. Кроме того, Winsock предоставляет дополнительные функции, не подчиняющиеся стандартам; эти функции используются лишь в случае крайней необходимости. Среди других преимуществ, обеспечиваемых Winsock , следует отметить улучшенную переносимость результирующих программ на другие системы.

Сокеты Windows

Winsock API разрабатывался как расширение Berkley Sockets API для среды Windows и поэтому поддерживается всеми системами Windows . К преимуществам Winsock можно отнести следующее:

Перенос уже имеющегося кода, написанного для Berkeley Sockets API , осуществляется непосредственно.

Системы Windows легко встраиваются в сети, использующие как версию IPv 4 протокола TCP / IP , так и постепенно распространяющуюся версию IPv 6. Помимо всего остального, версия IPv 6 допускает использование более длинных IP -адресов, преодолевая существующий 4-байтовый адресный барьер версии IPv 4.

Сокеты могут использоваться совместно с перекрывающимся вводом/выводом Windows (глава 14), что, помимо всего прочего, обеспечивает возможность масштабирования серверов при увеличении количества активных клиентов.

Сокеты можно рассматривать как дескрипторы (типа HANDLE ) файлов при использовании функций ReadFile и WriteFile и, с некоторыми ограничениями, при использовании других функций, точно так же, как в качестве дескрипторов файлов сокеты применяются в UNIX . Эта возможность оказывается удобной в тех случаях, когда требуется использование асинхронного ввода/вывода и портов завершения ввода/вывода.

Существуют также дополнительные, непереносимые расширения.

WinSock – это сетевой интерфейс прикладного программирования, реализованный на всех платформах Win 32, основной интерфейс доступа к разным базовым сетевым протоколам. Интерфейс унаследовал многое от реализации Berkeley (BSD ) Sockets на платформах UNIX . В средах Win 32 он стал абсолютно независимым от протокола, особенно с выпуском WinSock 2.

Термин сокеты (sockets ) используется для обозначения описателей поставщиков транспорта. В Win 32 сокет отличается от описателя файла, а потому представлен отдельным типом — SOCKET . С позиций эталонной модели OSI интерфейс Winsock расположен м/у сеансовым и транспортным уровнями. Под управлением Windows прикладной, представительский и сеансовый уровни, в основном относятся к вашему приложению. C уществуют значительные отличия реализаций сокетов в UNIX и в Windows , что создает очевидные проблемы. Библиотека WinSock поддерживает два вида сокетов — синхронные (блокируемые) и асинхронные (неблокируемые). Синхронные сокеты задерживают управление на время выполнения операции, а асинхронные возвращают его немедленно, продолжая выполнение в фоновом режиме, и, закончив работу, уведомляют об этом вызывающий код.

Устаревшие ОС Windows 3. x поддерживали только асинхронные сокеты, поскольку в среде с корпоративной многозадачностью захват управления одной задачей «подвешивает» все остальные, включая и саму систему. ОС Windows 9 x и NT /2000/ XP поддерживают оба вида сокетов, однако в силу того, что синхронные сокеты программируются более просто, чем асинхронные, последние не получили большого распространения. Сокеты семейства протоколов TCP / IP используются для обмена данными между узлами сети Интернет.

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

Winsock и модель OSI

Поставщики транспорта из каталога Winsock , перечисленные WSAEnumProtocols , работают на транспортном уровне модели OSI , то есть каждый из них обеспечивает обмен данными. Впрочем, все они относятся к какому-то протоколу, а сетевой протокол работает на сетевом уровне, поскольку обусловливает способ адресации каждого узла в сети. Например, UDP и TCP — это транспорты, хотя оба относятся к протоколу IP . Интерфейс Winsock расположен между сеансовым и транспортным уровнями. Winsock позволяет открывать и закрывать сеанс связи и управлять им для любого данного транспорта. Под управлением Windows три верхних уровня: прикладной, представительский и сеансовый, — в основном относятся к приложению Winsock . Другими словами, приложение Winsock управляет всеми аспектами сеанса связи и при необходимости форматирует данные согласно целям программы.

Сокеты Windows

Целесообразно рассмотреть, как доступные протоколы используют средства Winsock. Этот интерфейс основан на понятии сокета. Сокет — это описатель поставщика транспорта. В Win32 сокет отличается от описателя файла, а потому представлен отдельным типом — SOCKET. Сокет создается одной из двух функций:

// Code 1.06

SOCKET WSASocket (

Int af,

Int type,

Int protocol,

LPWSAPROTOCOL_INFO lpProtocolInfo,

GROUP g,

DWORD dwFlags

SOCKET socket (

Int af,

Int type,

Int protocol

Первый параметр — af, определяет семейство адресов протокола. Например, если необходимо создать UDP- или ТСР-сокет, надо подставить константу AF_ INET, чтобы сослаться на протокол IP. Второй параметр — type, это тип сокета для данного протокола. Он может принимать одно из следующих значений: SOCK_STREAM, SOCK_DGRAM, SOCK_SEQPACKET, SOCK_RAW и SOCK_RDM. Третий параметр — protocol, указывает конкретный транспорт, если для данного семейства адресов и типа сокета существует несколько записей. В табл. 1.2 перечислены значения, используемые в полях семейства адресов, типа сокета и протокола для данного сетевого транспорта.

Начальные три параметра для создания сокета подразделены на три уровня. Первый и самый важный — семейство адресов. Он указывает используемый в настоящее время протокол и ограничивает применение второго и третьего параметров. Например, семейство адресов ATM (AF_АТМ) позволяет использовать только простые сокеты (SOCK_RAW). Аналогично, выбор семейства адресов и типа сокета ограничивает выбор протокола.

Впрочем, можно передать в параметре protocol значение 0. В этом случае система выбирает поставщика транспорта, исходя из других двух параметров — af и type. Перечисляя записи каталога для протоколов, следует проверить значение поля dwProviderFlags из структуры WSAPROTOCOL_INFO. Если оно равно PFL_ MATCHES_PROTOCOL_ZERO — это стандартный транспорт, применяемый, если в параметре протокола socket или WSASocket передано значение 0.

Перечислив все протоколы с помощью WSAEnumProtocols, следует передать структуру WSAPROTOCOL_INFO в функцию WSASocket как параметр lpProtocolInfo.

Затем необходимо указать константу FROM_PROTOCOL_INFO во всех трех параметрах (af, type и protocol) — для них будут использоваться значения из переданной структуры WSAPROTOCOL_INFO. Так указывается определенная запись протокола.

Теперь рассмотрим два последних флага из WSASocket. Параметр группы всегда равен 0, так как ни одна из версий Winsock не поддерживает группы сокетов. В параметре dwFlags указывают один или несколько следующих флагов:

WSA_FLAG_OVERLAPPED;

WSA_FLAG_MULTIPOINT_C_ROOT;

WSA_FLAG_MULTIPOINT_C_LEAF;

WSA_FLAG_MULTIPOINT_D_ROOT;

WSA_FLAG_MULTIPOINT_D_LEAF.

Первый флаг — WSA_FLAG_OVERLAPPED, указывает, что данный сокет допускает перекрытый ввод-вывод — это один из механизмов связи, предусмотренных в Winsock (см. в следующих главах). Если создается сокет функцией socket, флаг WSA_FLAG_OVERLAPPED задан по умолчанию. Рекомендуется всегда задавать этот флаг при использовании WSASocket. Последние четыре флага относятся к сокетам многоадресного вещания.

Серверные API-функции

Сервер — это процесс, который ожидает подключения клиентов для обслуживания их запросов. Сервер должен прослушивать соединения на стандартном имени. В TCP/IP таким именем является IP-адрес локального интерфейса и номер порта. У каждого протокола своя схема адресации, а потому и свои особенности именования. Первый шаг установления соединения — привязка сокета данного протокола к его стандартному имени функцией bind. Второй — перевод сокета в режим прослушивания функцией listen. И наконец, сервер должен принять соединение клиента функцией accept или WSAAccept.

Целесообразно рассмотреть каждый API-вызов, необходимый для привязки, прослушивания и установления соединения с клиентом. Базовые вызовы, которые клиент и сервер должны сделать для установления канала связи:

Функция bind

После создания сокета определенного протокола следует связать его со стандартным адресом, вызвав функцию bind:

// Code 3.04

Int bind(

SOCKET s,

int namelen

Параметр s задает сокет, на котором ожидаются соединения клиентов. Второй параметр с типом struct sockaddr — просто универсальный буфер. Фактически, в этот буфер надо поместить адрес, соответствующий стандартам используемого протокола, а затем при вызове bind привести его к типу struct sockaddr. В заголовочном файле Winsock определен тип SOCKADDR, соответствующий структуре struct sockaddr. Далее в главе этот тип будет использоваться для краткости. Последний параметр задает размер переданной структуры адреса, зависящей от протокола. Например, следующий код иллюстрирует привязку при TCP-соединении:

// Code 3.05

SOCKET s;

struct sockaddr_in tcpaddr;

Int port = 5150;

S = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

Tcpaddr.sin_family = AF_INET;

Tcpaddr.sin_port = htons(port);

Tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);

Bind(s, (SOCKADDR *)&tcpaddr, sizeof(tcpaddr));

Подробнее о структуре sockaddr_in было сказано в разделе, посвященном адресации TCP/IP, предыдущей главы. Там приведен пример создания потокового сокета и последующей настройки структуры адреса TCP/IP для приема соединений клиентов. В данном случае сокет указывает на IP-интерфейс по умолчанию с номером порта 5150. Формально вызов blind связывает сокет с IP-интерфейсом и портом.

При возникновении ошибки функция bind возвращает значение SOCKET_ERROR. Самая распространенная ошибка при вызове bind — WSAEADDRINUSE. В случае использования TCP/IP это означает, что с локальным IP-интерфейсом и номером порта уже связан другой процесс, или они находятся в состоянии TIME_WAIT. При повторном вызове bind для уже связанного сокета возвращается ошибка WSAEFAULT.

^ Функция listen

Теперь нужно перевести сокет в режим прослушивания. Функция bind только ассоциирует сокет с заданным адресом. Для перевода сокета в состояние ожидания входящих соединений используется API-функция listen:

// Code 3.06

Int listen(

SOCKET s,

Int backlog

Первый параметр — связанный сокет. Параметр backlog определяет максимальную длину очереди соединений, ожидающих обработки, что важно при запросе нескольких соединений сервера. Пусть значение этого параметра равно 2, тогда при одновременном приеме трех клиентских запросов первые два соединения будут помещены в очередь ожидания, и приложение сможет их обработать. Третий запрос вернет ошибку WSAECONNREFUSED. После того как сервер примет соединение, запрос удаляется из очереди, а другой — занимает его место. Значение backlog зависит от поставщика протокола. Недопустимое значение заменяется ближайшим разрешенным. Стандартного способа получить действительное значение backlog нет.

Ошибки, связанные с listen, довольно просты. Самая частая из них — WSAEINVAL, обычно означает, что перед listen не была вызвана функция bind. Иногда при вызове listen возникает ошибка WSAEADDRINUSE, но чаще она происходит при вызове bind.

^ Функции accept и WSAAccept

Все готово к приему соединений клиентов и можно вызвать функцию accept или WSAAccept. Прототип accept:

// Code 3.07

SOCKET accept(

SOCKET s,

Struct sockaddr FAR * addr,

Int FAR * addrlen

Параметр s — связанный сокет в состоянии прослушивания. Второй параметр — адрес действительной структуры SOCKADDR_IN, a addrlen — ссылка на длину структуры SOCKADDR_IN. Для сокета другого протокола можно заменить SOCKADDR_IN на структуру SOCKADDR, соответствующую этому протоколу. Вызов accept обслуживает первый находящийся в очереди запрос на соединение. По его завершении структура addr будет содержать сведения об IP-адресе клиента, отправившего запрос, а параметр addrlen — размер структуры.

Кроме того, accept возвращает новый дескриптор сокета, соответствующий принятому клиентскому соединению. Для всех последующих операций с этим клиентом должен применяться новый сокет. Исходный прослушивающий сокет используется для приема других клиентских соединений и продолжает находиться в режиме прослушивания.

В Winsock 2 есть функция WSAAccept, способная устанавливать соединения в зависимости от результата вычисления условия:

// Code 3.08

SOCKET WSAAccept(

SOCKET s,

Struct sockaddr FAR * addr,

LPINT addrlen,

LPCONDITIONPROC lpfnCondition,

DWORD dwCallbackData

Первые три параметра — те же , что и в accept для Winsock 1. Параметр lpfnCondition — указатель на функцию, вызываемую при запросе клиента. Она определяет возможность приема соединения и имеет следующий прототип:

// Code 3.09

int CALLBACK ConditionFunc(

LPWSABUF lpCallerId,

LPWSABUF lpCallerData,

LPQOS lpSQOS,

LPQOS lpGQOS,

LPWSABUF lpCalleeId,

LPWSABUF lpCalleeData,

GROUP FAR * g,

DWORD dwCallbackData

Передаваемый по значению параметр lpCallerId содержит адрес соединяющегося объекта. Структура WSABUF используется многими функциями Winsock 2 и определена так:

// Code 3.10

typedef struct _WSABUF

U_long len;

Char FAR * buf;

} WSABUF , FAR * LPWSABUF ;

В зависимости от ее использования, поле len определяет размер буфера, на который ссылается поле buf, или количество данных в буфере buf.

Для lpCallerId параметр buf указывает на структуру адреса протокола, по которому осуществляется соединение. Чтобы получить корректный доступ к информации, просто приведите указатель buf к соответствующему типу SOCKADDR. При использовании протокола TCP/IP это должна быть структура SOCKADDR_IN, содержащая IP-адрес подключающегося клиента. Большинство сетевых протоколов удаленного доступа поддерживают идентификацию абонента на этапе запроса.

Параметр lpCallerData содержит данные, отправленные клиентом в ходе запроса соединения. Если эти данные не указаны, он равен NULL. Большинство сетевых протоколов, таких как TCP/IP, не используют данные о соединении. Чтобы узнать, поддерживает ли протокол эту возможность, можно обратиться к соответствующей записи в каталоге Winsock путем вызова функции WSAEnumProtocols (см. первую главу).

Следующие два параметра — lpSQOS и lpGQOS, задают уровень качества обслуживания, запрашиваемый клиентом. Оба параметра ссылаются на структуру, содержащую сведения о требованиях пропускной способности для приема и передачи. Если клиент не запрашивает параметры качества обслуживания (quality of service, QoS), то они равны NULL. Разница между ними в том, что lpSQOS используется для единственного соединения, a IpGQOS — для групп сокетов. Группы сокетов не реализованы и не поддерживаются в Winsock 1 и 2. Подробнее о QoS — в последующих главах.

Параметр lpCalleeId — другая структура WSABUF, содержащая локальный адрес, к которому подключен клиент. Поле buf указывает на объект SOCKADDR соответствующего семейства адресов. Эта информация полезна, если сервер запущен на многоадресной машине. Если сервер связан с адресом INADDR_ANY, запросы соединения будут обслуживаться на любом сетевом интерфейсе, а параметр — содержать адрес интерфейса, принявшего соединение.

Параметр lpCalleeData дополняет lpCallerData. Он ссылается на структуру WSABUF, которую сервер может использовать для отправки данных клиенту в ходе установления соединения. Если поставщик услуг поддерживает эту возможность, поле len указывает максимальное число отправляемых байт. В этом случае сервер копирует некоторое, не превышающее это значение, количество байт, в блок buf структуры WSABUF и обновляет поле len, чтобы показать, сколько байт передается. Если сервер не должен возвращать данные о соединении, то перед возвращением условная функция приема соединения присвоит полю len значение 0. Если поставщик не поддерживает передачу данных о соединении, поле len будет равно 0. Большинство протоколов: фактически, все, поддерживаемые платформами Win32 — не поддерживают обмен данными при установлении соединения.

Обработав переданные в условную функцию параметры, сервер должен решить: принимать, отклонять или задержать запрос соединения. Если соединение принимается, условная функция вернет значение СF_АССЕРТ, если отклоняется — CF_REJECT. Если по каким-либо причинам на данный момент решение не может быть принято, возвращается CF_DEFER.

Как только сервер готов обработать запрос, он вызывает функцию WSAAccept. Условная функция выполняется в одном процессе с WSAAccept и должна работать как можно быстрее. В протоколах, поддерживаемых платформами Win32, клиентский запрос задерживается, пока не будет вычислено значение условной функции. В большинстве случаев базовый сетевой стек ко времени вызова условной функции уже может принять соединение. При возвращении значения CF_REJECT стек просто закрывает его (см. последующие главы).

При возникновении ошибки возвращается значение INVALID_SOCKET, чаще всего — WSAEWOULDBLOCK. Оно возникает, если сокет находится в асинхронном или неблокирующем режиме и нет соединения для приема. Если условная функция вернет CF_DEFER, WSAAccept генерирует ошибку WSATRY_AGAIN, если CF_REJECT — WSAECONNREFUSED.

API-функции клиента

Клиентская часть значительно проще и для установления соединения требуется всего три шага: создать сокет функцией socket или WSASocket; разрешить имя сервера (зависит от используемого протокола); инициировать соединение функцией connect или WSAConnect.

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

^ Состояния TCP

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

Начальное состояние любого сокета — CLOSED. Как только клиент инициирует соединение, серверу отправляется пакет SYN и клиентский сокет переходит в состояние SYN_SENT. Получив пакет SYN, сервер отправляет пакет SYN-and-ACK, а клиент отвечает на него пакетом АСК. С этого момента клиентский сокет переходит в состояние ESTABLISHED. Если сервер не отправляет пакет SYN-ACK, клиент по истечении времени ожидания возвращается в состояние CLOSED.

Если сокет сервера связан и прослушивает локальный интерфейс и порт, то он находится в состоянии LISTEN. При попытке клиента установить соединение сервер получает пакет SYN и отвечает пакетом SYN-ACK. Состояние сокета сервера меняется на SYN_RCVD. Наконец, после отправки клиентом пакета АСК сокет сервера переводится в состояние ESTABLISHED.

Существует два способа закрыть соединение. Если этот процесс начинает приложение, то закрытие называется активным, иначе — пассивным. На рис. 3.2 изображены оба вида закрытия. При активном закрытии соединения приложение отправляет пакет FIN. Если приложение вызывает closesocket или shutdown (со вторым аргументом SD_SEND), оно отправляет узлу пакет FIN, и состояние сокета меняется на FIN_WAIT_1. Обычно узел отвечает пакетом АСК и сокет переходит в состояние FIN_WAIT_2. Если узел тоже закрывает соединение, он отправляет пакет FIN, а компьютер отвечает пакетом АСК и переводит сокет в состояние TIME_WAIT.

Состояние TIME_WAIT также называется состоянием ожидания 2*MSL MSL — максимальное время жизни сегмента (Maximum Segment Lifetime), иными словами, время существования пакета в сети перед его отбрасыванием. У каждого IP-пакета есть поле времени жизни (time-to-live, TTL). Если оно равно 0, значит, пакет можно отбросить. Каждый маршрутизатор, обслуживающий пакет, уменьшает значение TTL на 1 и передает пакет дальше. Перейдя в состояние TIME_WAIT, приложение остается в нем на протяжении двух периодов времени, равных MSL. Это позволяет TCP в случае потери заключительного пакета АСК послать его заново, с последующей отправкой FIN. По истечении 2*MSL сокет переходит в состояние CLOSED.

Результат двух других способов активного закрытия — состояние TIME_WAIT. В предыдущем случае только одна сторона отправляла FIN и получала ответ АСК, а узел оставался свободным для передачи данных до момента своего закрытия. Здесь возможны и два других способа. В первом случае, при одновременном закрытии, компьютер и узел одновременно запрашивают закрытие: компьютер отправляет узлу пакет FIN и получает от него пакет FIN.

Затем в ответ на пакет FIN компьютер отправляет пакет АСК и изменяет состояние сокета на CLOSING. После получения компьютером пакета АСК от узла сокет переходит в состояние TIME_WAIT.

Второй случай активного закрытия является вариацией одновременного закрытия: сокет из состояния FIN_WAIT_1 сразу переходит в состояние TIME_WAIT. Это происходит, если приложение отправляет пакет FIN и тут же после этого получает от узла пакет FIN-ACK. В таком случае узел подтверждает пакет FIN приложения отправкой своего, на которое приложение отвечает пакетом АСК.

Основной смысл состояния TIME_WAIT заключается в том, что пока соединение ожидает истечения 2*MSL, сокетная пара, участвующая в соединении, не может быть использована повторно. Сокетная пара — это комбинация локального и удаленного IP-портов. Некоторые реализации TCP не позволяют повторно использовать любой из портов сокетной пары, находящейся в состоянии TIME_WAIT. В реализации Microsoft этого дефекта нет. При попытке соединения с сокетной парой, находящейся в состоянии TIME_WAIT, произойдет ошибка WSAEADDRINUSE. Одно из решений проблемы (кроме ожидания окончания состояния TIME_WAIT пары сокетов, использующей локальный порт) состоит в использовании параметра сокета SO_REUSEADDR. Более подробно SO_REUSEADDR рассматривается в последующих главах.

Наконец, целесообразно рассмотреть пассивное закрытие. По этому сценарию приложение получает от узла пакет FIN и отвечает пакетом АСК. В этом случае сокет приложения переходит в состояние CLOSE_WAIT. Так как узел закрыл свою сторону, он больше не может отправлять данные, но приложение вправе это делать, пока не закроет свою сторону соединения. Для закрытия своей стороны приложение отправляет пакет FIN, после чего ТСР-сокет приложения переводится в состояние LAST_ACK. После получения от узла пакета АСК сокет приложения возвращается в состояние CLOSED.

^ Функции connect и WSAConnect

Осталось обсудить собственно установление соединения. Оно осуществляется вызовом connect или WSAConnect. Сначала можно рассмотреть версию Winsock 1 этой функции:

// Code 3.11

Int connect(

SOCKET s ,

const struct sockaddr FAR * name,

int namelen

Параметры практически не требуют пояснений: s — действительный ТСР-сокет для установления соединения, name — структура адреса сокета (SOCKADDR_IN) для TCP, описывающая сервер к которому подключаются, namelen — длина переменной пате. Версия Winsock 2 этой функции определена так:

// Code 3.12

Int WSAConnect(

SOCKET s,

const struct sockaddr FAR * name,

Int namelen,

LPWSABUF lpCallerData,

LPWSABUF lpCalleeData,

LPQOS lpSQOS,

LPQOS lpGQOS

Первые три параметра такие же, как и в функции connect. Следующие два: lpCallerData и lpCalleeData, — это строковые буферы, используемые для приема и отправки данных в момент установления соединения. Параметр lpCallerData указывает на буфер, содержащий данные, отправляемые клиентом серверу вместе с запросом на соединение; lpCallerData — на буфер с данными, возвращаемыми сервером в ходе установления соединения. Обе переменные являются структурами WSABUF, и для lpCallerData поле len должно указывать длину данных передаваемого буфера buf. В случае lpCalleeData поле len определяет размер буфера buf, куда принимаются данные от сервера. Два последних параметра: lpSQOS и lpGQOS, — ссылаются на структуры QoS, определяющие требования пропускной способности отправки и приема данных устанавливаемого соединения. Параметр lpSQOS указывает требования к сокету s, a lpGQOS — к группе сокетов. На данный момент группы сокетов в полной мере не поддерживаются. Нулевое значение lpSQOS означает, что приложение не предъявляет требований к качеству обслуживания.

Если на компьютере, к которому выполнено подключение, не запущен процесс, прослушивающий данный порт, функция connect вернет ошибку WSAECONNREFUSED. Другая ошибка — WSAETIMEDOUT, происходит, когда вызываемый адресат недоступен, например, из-за отказа коммуникационного оборудования на пути к узлу или отсутствия узла в сети.

Передача данных

В сетевом программировании самое главное — уметь отправлять и принимать данные. Для пересылки данных по сокету используются функции send и WSASend. Аналогично, для приема данных существуют функции recv и WSARecv.

Все буферы, используемые при отправке и приеме данных, состоят из элементов типа char. To есть эти функции не предназначены для работы с кодировкой UNICODE. Это особенно важно для Windows СЕ, так как она использует UNICODE по умолчанию. Отправить строку символов UNICODE можно двумя способами: в исходном виде или привести к типу char. Нюанс в том, что при указании количества отправляемых или принимаемых символов результат функции, определяющей длину строки, нужно умножить на 2, так как каждый UNICODE-символ занимает 2 байта строкового массива. Другой способ: сначала перевести строку из UNICODE в ASCII функцией WideCharToMultiByte.

Все функции приема и отправки данных при возникновении ошибки возвращают код SOCKET_ERROR. Для получения более подробной информации об ошибке можно вызвать функцию WSAGetLastError. Самые распространенные ошибки — WSAECONNABORTED и WSAECONNRESET. Обе возникают при закрытии соединения: либо по истечении времени ожидания, либо при закрытии соединения партнерским узлом. Еще одна типичная ошибка — WSAEWOULDBLOCK, обычно происходит при использовании неблокирующих или асинхронных сокетов. По существу, она означает, что функция не может быть выполнена в данный момент. В следующейглаве будут описаны разные методы ввода-вывода Winsock, которые помогут избежать этих ошибок.

^ Функции send и WSASend

API-функция send для отправки данных по сокету определена так:

// Code 3.13

Int send(

SOCKET s,

Const char FAR * buf,

Int len,

int flags

Параметр s определяет сокет для отправки данных. Второй параметр — buf, указывает на символьный буфер, содержащий данные для отправки. Третий — len, задает число отправляемых из буфера символов. И последний параметр — flags, может принимать значения 0, MSG_DONTROUTE, MSG_OOB, или результат логического ИЛИ над любыми из этих параметров. При указании флага MSG_DONTROUTE транспорт не будет маршрутизировать отправляемые пакеты. Обработка этого запроса остается на усмотрение базового протокола (например, если транспорт не поддерживает этот параметр, запрос игнорируется). Флаг MSG_OOB указывает, что данные должны быть отправлены вне полосы (out of band), то есть срочно.

При успешном выполнении функция send вернет количество переданных байт, иначе — ошибку SOCKET_ERROR. Одна из типичных ошибок — WSAECONNABORTED, происходит при разрыве виртуального соединения из-за ошибки протокола или истечения времени ожидания. В этом случае сокет должен быть закрыт, так как он больше не может использоваться. Ошибка WSAECONNRESET происходит, если приложение на удаленном узле, выполнив аппаратное закрытие, сбрасывает виртуальное соединение, или неожиданно завершается, или происходит перезагрузка удаленного узла. В этой ситуации сокет также должен быть закрыт. Еще одна ошибка — WSAETIMEDOUT, часто происходит при обрыве соединения по причине сбоев сети или отказа удаленной системы без предупреждения.

Функция Winsock версии 2 WSASend — аналог send, определена так:

// Code 3.14

Int WSASend(

SOCKET s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

DWORD dwFlags,

Сокет является действительным описателем сеанса соединения. Второй параметр указывает на структуру WSABUF или на массив этих структур. Третий — определяет число передаваемых структур WSABUF. Структура WSABUF включает сам символьный буфер и его длину. Может возникнуть вопрос: зачем нужно отправлять более одного буфера за раз? Это называется комплексным вводом-выводом (scatter-gather I/O). Подробней он будет обсуждаться далее, сейчас можно отметить, что при использовании нескольких буферов для отправки данных по сокету соединения массив буферов отправляется, начиная с первой и заканчивая последней структурой WSABUF.

Параметр lpNumberOfBytesSent — указатель на тип DWORD, который после вызова WSASend содержит общее число переданных байт. Параметр флагов dwFlags такой же, что и в функции send. Последние два указателя — 1рOverlapped и lpCompletionROUTINE используются для перекрытого ввода-вывода (overlapped I/O) — одной из моделей асинхронного ввода-вывода, поддерживаемых Winsock (см. также следующую главу).

WSASend присваивает параметру lpNumberOfBytesSent количество записанных байт. При успешном выполнении функция возвращает 0, иначе — SOCKET_ERROR. Ошибки те же, что и у функции send.

^ Функция WSASendDisconnect

Это специализированная функция используется редко. Она определена так :

// Code 3.15

Int WSASendDisconnect (

SOCKET s,

LPWSABUF lpOUT boundDisconnectData

^ Срочные данные

Если приложению требуется отправить через потоковый сокет информацию более высокого приоритета, оно может обозначить эти сведения как срочные данные (out-of-band, OOB). Приложение с другой стороны соединения получает и обрабатывает ООВ-данные через отдельный логический канал, концептуально независимый от потока данных.

В TCP передача ООВ-данных реализована путем добавления 1-битового маркера (называемого URG) и 16-битного указателя в заголовке сегмента TCP, которые позволяют выделить важные байты в основном трафике. На данный момент для TCP существуют два способа выделения срочных данных. В RFC 793, описывающем TCP и концепцию срочных данных, говорится, что указатель срочности в заголовке TCP является положительным смещением байта, следующего за байтом срочных данных. Однако в RFC 1122 это смещение трактуется, как указатель на сам байт срочности.

В спецификации Winsock под термином ООВ понимают как независимые от протокола ООВ-данные, так и реализацию механизма передачи срочных данных в TCP. Для проверки, есть ли в очереди срочные данные, вызывается функция ioctlsocket с параметром SIOCATMARK. Подробнее об этой функции — в последующих главах.

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

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

Функция WSASendDisconnect начинает процесс закрытия сокета и отправляет соответствующие данные. Она доступна только для протоколов, поддерживающих постепенное закрытие и передачу данных при его осуществлении. Ни один из существующих поставщиков транспорта на данный момент не поддерживает передачу данных о закрытии соединения. Функция WSASendDisconnect действует аналогично shutdown с параметром SD_SEND, но также отправляет данные, содержащиеся в параметре boundDisconnectData. После ее вызова отправлять данные через сокет невозможно. В случае неудачного завершения WSASendDisconnect возвращает значение SOCKET_ERROR. Ошибки, встречающиеся при работе функции, аналогичны ошибкам send.

^ Функции recv и WSARecv

Функция recv — основной инструмент приема данных по сокету. Она определена так:

// Code 3.16

Int recv(

SOCKET s ,

char FAR * buf,

Int len,

int flags

Параметр s определяет сокет для приема данных. Второй параметр — buf, является символьным буфером и предназначен для полученных данных, a len указывает число принимаемых байт или размер буфера buf. Последний параметр — flags, может принимать значения 0, MSG_PEEK, MSG_OOB или результат логического ИЛИ над любыми из этих параметров. Разумеется, 0 означает отсутствие особых действий. Флаг MSG_PEEK указывает, что доступные данные должны копироваться в принимающий буфер и при этом оставаться в системном буфере. По завершении функция также возвращает количество ожидающих байт.

Считывать сообщения таким образом не рекомендуется. Мало того, что из-за двух системных вызовов (одного — для считывания данных, и другого, без флага MSG_PEEK — для удаления данных), снижается производительность. В ряде случаев этот способ просто не надежен. Объем возвращаемых данных может не соответствовать их суммарному доступному количеству. К тому же, сохраняя данные в системных буферах, система оставляет все меньше памяти для размещения входящих данных. В результате уменьшается размер окна TCP для всех отправителей, что не позволяет приложению достичь максимальной производительности. Лучше всего скопировать все данные в собственный буфер и обрабатывать их там. Флаг MSG_OOB уже обсуждался ранее при рассмотрении отправки данных.

Использование recv в сокетах, ориентированных на передачу сообщений или дейтаграмм, имеет несколько особенностей. Если при вызове recv размер ожидающих обработки данных больше предоставляемого буфера, то после его полного заполнения возникает ошибка WSAEMSGSIZE. Ошибка превышения размера сообщения происходит только при использовании протоколов, ориентированных на передачу сообщений. Потоковые протоколы буферизируют поступающие данные и при запросе приложением предоставляют их в полном объеме, даже если количество ожидающих обработки данных больше размера буфера. Таким образом, ошибка WSAEMSGSIZE не может произойти при работе с потоковыми протоколами.

Функция WSARecv обладает дополнительными по сравнению с recv возможностями: поддерживает перекрытый ввод-вывод и фрагментарные дейтаграммные уведомления.

// Code 3.17

int WSARecv(

SOCKET s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

LPOWORD lpNumberOfBytesRecvd,

LPDWORD lpFlags,

LPWSAOVERLAPPED lpOveflapped,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE

Параметр s — сокет соединения. Второй и третий параметры определяют буферы для приема данных. Указатель lpBuffers ссылается на массив структур WSABUF, a dwBufferCount — определяет количество таких структур в массиве. Параметр lpNumberOfBytesReceived в случае немедленного завершения операции получения данных указывает на количество принятых этим вызовом байт. Параметр lpFlags может принимать значения MSG_PEEK, MSG_OOB, MSG_PARTIAL или результат логического ИЛИ над любыми из этих параметров.

У флага MSG_PARTIAL в зависимости от способа использования могут быть разные значения и смысл. Для протоколов, ориентированных на передачу сообщений, этот флаг задается после вызова WSARecv (если все сообщение не может быть возвращено из-за нехватки места в буфере). В этом случае каждый последующий вызов WSARecv задает флаг MSG_PARTIAL, пока сообщение не будет прочитано целиком. Если этот флаг передается как входной параметр, операция приема данных должна завершиться, как только данные будут доступны, даже если это только часть сообщения. Флаг MSG_PARTIAL используется только с протоколами, ориентированными на передачу сообщений. Запись каждого протокола в каталоге Winsock содержит флаг, указывающий на поддержку этой возможности (см. также последующие главы). Параметры lpOverlapped и lpCompletionROUTINE применяются в операциях перекрытого ввода-вывода (обсуждаются в следующей главе).

^ Функция WSARecvDisconnect

Эта функция обратна WSASendDisconnect и определена так:

// Code 3.18

Int WSARecvDisconnect(

SOCKET s,

LPWSABUF lpInboundDisconnectData

Как и у WSASendDisconnect, ее параметрами являются описатель сокета соединения и действительная структура WSABUF для приема данных. Функция принимает только данные о закрытии соединения, отправленные с другой стороны функцией WSASendDisconnect, ее нельзя использовать для приема обычных данных. К тому же, сразу после принятия данных она прекращает прием с удаленной стороны, что эквивалентно вызову shutdown с параметром SD_RECV.

^ Функция WSARecvEx

Эта функция — специальное расширение Microsoft для Winsock 1. Она идентична recv во всем, кроме того, что параметр flags передается по ссылке. Это позволяет базовому поставщику задавать флаг MSG_PARTLAL.

// Code 3.19

Int PASCAL FAR WSARecvEx(

SOCKET s ,

char FAR * buf,

Int len,

int * flags

Если полученные данные не составляют полного сообщения, в параметре flags возвращается флаг MSG_PARTIAL. Он используется только с протоколами, ориентированными на передачу сообщений. Когда при принятии неполного сообщения флаг MSG_PARTIAL передается как часть параметра flags, функция завершается немедленно, вернув принятые данные. Если в буфере не хватает места, чтобы принять сообщение целиком, WSARecvEx вернет ошибку WSAEMSGSIZE, а оставшиеся данные будут отброшены. Есть разница между флагом MSG_PARTIAL и ошибкой WSAEMSGSIZE: в случае ошибки сообщение поступило целиком, однако соответствующий буфер слишком мал для его приема. Флаги MSG_PEEK и MSG_OOB также можно использовать в WSARecvEx.

Завершение сеанса

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

^ Функция shutdown

Правильно написанное приложение уведомляет получателя об окончании отправки данных. Так же должен поступить и узел. Такое поведение называется корректным завершением сеанса и осуществляется с помощью функции shutdoum:

// Code 3.23

Int shutdown(

SOCKET s,

Int how

Параметр bow может принимать значения SD_RECEIVE, SD_SEND или SD_ BOTH. Значение SD_RECEIVE запрещает все последующие вызовы любых функций приема данных, на протоколы нижнего уровня это не действует. Если в очереди ТСР-сокета есть данные, либо они поступают позже, то соединение сбрасывается. UDP-сокеты в аналогиной ситуации продолжают принимать данные и ставить их в очередь. SD_SEND запрещает все последующие вызовы функций отправки данных. В случае ТСР-сокетов после подтверждения получателем приема всех отправленных данных передается пакет FIN. Наконец, SD_BOTH запрещает как прием, так и отправлку.

^ Функция closesocket

Эта функция закрывает сокет. Она определена так:

// Code 3.24

int closesocket (SOCKET s );

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

Ожидающие асинхронные вызовы, исходящие от любого потока данного процесса, отменяются без уведомления. Ожидающие операции перекрытого ввода-вывода также аннулируются. Все выполняющиеся события, процедура и порт завершения, связанные с перекрытым вводом-выводом, завершатся ошибкой WSA_OPERATION_ABORTED. (Асинхронные и неблокирующие модели ввода-вывода более подробно обсуждаются в следующей главе.) Другой фактор, влияющий на поведение функции closesocket, — значение параметра сокета SO_LINGER (его полное описание — в последующих главах).

Протоколы, не требующие соединения

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

Приемник

Процесс получения данных на сокете, не требующем соединения, прост. Сначала создают сокет функцией socket или WSASocket. Затем выполняют привязку сокета к интерфейсу, на котором будут принимать данные, функцией bind (как и в случае протоколов, ориентированных на сеансы). Разница в том, что нельзя вызвать listen или accept: вместо этого нужно просто ожидать приема входящих данных. Поскольку в этом случае соединения нет, принимающий сокет может получать дейтаграммы от любой машины в сети. Простейшая функция приема — recvform.

// Code 3.28

Int recvfrom(

SOCKET s,

Char FAR* buf,

Int len,

Int flags,

Struct sockaddr FAR * from,

int FAR * fromlen

Первые четыре параметра такие же, как и для функции recv, включают допустимые значения для flags-. MSG_OOB и MSG_PEEK. Параметр from — структура SOCKADDR для данного протокола слушающего сокета, на размер структуры адреса ссылается fromlen. После возврата вызова структура SOCKADDR будет содержать адрес рабочей станции, которая отправляет данные.

В Winsock 2 применяется другая версия recvform — WSARecvForm:

// Code 3.29

Int WSARecvFrom(

SOCKET s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

LPDWORD lpNumberOfBytesRecvd,

LPDWORD lpFlags,

Struct sockaddr FAR * lpFrom,

LPINT lpFromlen,

LPWSAOVERLAPPED lpOverlapped,

LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionROUTINE

Разница между версиями — в использовании структуры WSABUF для получения данных. Можно предоставить один или несколько буферов WSABUF, указав их количество в divBufferCount — в этом случае возможен комплексный ввод-вывод. Суммарное количество считанных байт передается в lpNumberOfBytesRecvd. При вызове функции WSARecvFrom, lpFlags может принимать следующие значения: 0 (при отсутствии параметров), MSG_OOB, MSG_PEEK или MSG_PARTIAL. Данные флаги можно комбинировать логической операцией ИЛИ. Если при вызове функции задан флаг MSG_PARTlAL, поставщик перешлет данные даже в случае приема лишь части сообщения. По возвращении флаг задается в MSG_PARTIAL, только если сообщение принято частично. По возвращении WSARecvFrom присвоит параметру lpFrom (указатель на структуру SOCKADDR) адрес компьютера-отправителя. Опять же lpFromLen указывает на размер структуры SACKADDR, однако в данной функции он является указателем на DWORD. Два последних параметра — lpOverlapped и lpCompletionROUTINE, используются для перекрытого ввода-вывода (см. следующую главу).

Другой способ приема (отправки) данных в сокетах, не требующих соединения, — установление соединения (хоть это и звучит странно). После создания сокета можно вызвать connect или WSAConnect, присвоив параметру SOCKADDR адрес удаленного компьютера, с которым необходимо связаться. Фактически никакого соединения не происходит. Адрес сокета, переданный в функцию соединения, ассоциируется с сокетом, чтобы было можно использовать функции recv и WSARecv вместо recvfrom или WSARecvFrom (поскольку источник данных известен). Если приложению нужно одновременно связываться лишь с одной конечной точкой, можно задействовать возможность подключить сокет дейтаграмм.

Отправитель

Есть два способа отправки данных через сокет, не требующий соединения. Первый и самый простой — создать сокет и вызвать функцию sendto или WSASendTo. Целесообразно рассмотреть сначала функцию sendto:

// Code 3.30

Int sendto(

SOCKET s,

const char FAR * buf,

Int len,

Int flags,

Const struct sockaddr FAR * to,

int tolen

Параметры этой функции такие же, как и у recvfrom, за исключением buf— буфера данных для отправки, и len — показывающего, сколько байт отправлять. Параметр to — указатель на структуру SOCKADDR с адресом принимающей рабочей станции.

Также можно использовать функцию WSASendTo из Winsock 2:

// Code 3.31

Int WSASendTo(

SOCKET s,

LPWSABUF lpBuffers,

DWORD dwBufferCount,

LPDWORD lpNumberOfBytesSent,

DWORD dwFlags,

Const struct sockaddr FAR * lpTo,

Int iToLen,

LPWSAOVERLAPPED lpOverlapped,

LPWSAOVERLAPPED_COMPLETION ROUTINE lpCompletionROUTINE

Снова функция WSASendTo аналогична своей предшественнице. Она принимает указатель на одну или несколько структур WSABUF с данными для отправки получателю в виде параметра lpBuffers, a divBufferCount задает количество структур. Для комплексного ввода-вывода можно отправить несколько структур WSABUF. Перед выходом WSASendTo присваивает четвертому параметру — lpNumberOfBytesSent, количество реально отправленных получателю байт. Параметр lpTo — структура SOCKADDR для данного протокола с адресом приемника. Параметр iToLen — длина структуры SOCKADDR. Два последних параметра — lpOverlapped и lpCompletionROUTINE, применяются для перекрытого ввода-вывода (см. также следующую главу).

Как и при получении данных, сокет, не требующий соединения, можно подключать к адресу конечной точки и отправлять данные функциями send и WSASend. После создания этой привязки использовать для обмена данными функции sendto или WSASendTo с другим адресом нельзя — будет выдана ошибка WSAEISCONN. Отменить привязку сокета можно, лишь вызвав функцию closesocket с описателем этого сокета, после чего следует создать новый сокет.

Модель ввода - вывода в свою очередь определяет как приложение будет обрабатывать операции ввода - вывода для определённого сокета.

Winsock предостовляет два режима для сокетов: блокирующий и неблокирующий, а также несколько интересных моделей ввода - вывода которые помогают приложениям в управлении оерациями ввода - вывода нескольких сокетов одновременно асинхронным способом: блокирование, select, WSAAsyncSelect, WSAEventSelect, перекрытый ввод - вывод (overlapped I/O), и порт завершения (completion port). Все Windows платформы предоставляют блокирующий и неблокирующий режим работы для сокетов. И всеже не все модели ввода - вывода доступны на всех платформах. Следующая таблица показывает доступность моделей на разных Windows платформах.

Спецификация Winsock 2.0 позволяет использовать следующие основные модели выполнения операций ввода/вывода на сокете:

блокирующий ввод/вывод,

мультиплексирование ввода/вывода с помощью select() на блокирующем или неблокирующем сокете,

асинхронный неблокирующий ввод/вывод с использованием Windows-сообщений о сетевых событиях – WSAAsyncSelect(),

неблокирующий ввод/вывод с асинхронными сетевыми событиями – WSAEventSelect(),

совмещенный ввод/вывод (или ввод/вывод с перекрытием - overlapped I/O),

порт завершения (completion port).

Режимы сокетов

Как мы уже упомянали, Windows сокеты могут выполнять операции ввода - вывода в двух режимах: блокирующий и неблокирующий. В блокирующем режиме вызовы Winsock функций которые выполняют операции ввода - вывода, таких как send и recv - ждут пока операция завершится прежде чем отдать управление приложению. В неблокирующем режиме Winsock функции отдают управление приложению сразу. Приложения которые работают на Windows CE и Windows 95 (Winsock 1) платформах, которые потдерживают только некоторые модели ввода - вывода, вынуждены выполнять оперделенные дейвсвия с блокирующими и неблокирующими сокетами для коректной отработки разных ситуаций.

1.1. Блокирующий режим

Блокирующие сокеты создают некоторые неудобства, потомуч то вызов любой из Winsock API функций блокируют на некоторое время. Большинство Winsock приложений следуют модели "производитель - потребитель" в которой приложение считывает либо записывает определенное количество байт и выполняет их обработку. Следующий отрывок кода иллюстрирует эту модель:

SOCKET sock;

Char buffer;

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). Задача читающего потока состоит в считвыании поступающих данных из сети на сокет в общий буфер. Когда читающий поток считал минимальное необходимое количество данных преднозначенных для обрабатывающего потока, он переключает событие в отсигналенное состояние, таким образом давая знать обрабатывающему потоку о наличии в общем буфере данных для обработки. Обрабатывающий поток в свою очередь забирает из буфера данные и обрабатывает их.

Следующий кусок кода показывает реализацию данного метода, реализуя две функции: одна обеспечивая чтение данных из сети (ReadingThread), другая обработку данных(ProcessingThread):

#define MAX_BUFFER_SIZE 4096

// Инициализация critical section

// и события с автосбросом перед инициализацией потоков

CRITICAL_SECTION data;

HANDLE hEvent;

SOCKET sock;

CHAR buffer;

// создание читающего сокета

// читающий поток

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)

NRead = recv(sock, &(buffer), nLeft, 0);

If (nRead == -1)

Printf("errorn");

ExitThread();

NTotal += nRead;

NLeft -= nRead;

NBytes += nRead;

SetEvent(hEvent);

// обрабатывающий поток

Void ProcessingThread(void)

While (true)

// ждем данных

WaitForSingleObject(hEvent);

EnterCriticalSection(&data);

DoSomeComputationOnData(buffer);

// удаляем из буфера обработанные данные

nBytes -= NUM_BYTES_REQUIRED;

LeaveCriticalSection(&data);

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

1.2. Неблокирующий режим

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

SOCKET sock;

Unsigned long nb = 1;

Int err;

Sock = socket(AF_INET, SOCK_STREAM, 0);

Err = ioctlsocket(sock, FIONBIO, (unsigned long *) &nb);

if (err == SOCKET_ERROR)

//ошибка при переключении сокета в неблокирующий режим

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

Потому как большинство неблокирующих вызовов функции терпят неудачу с ошибкой WSAEWOULDBLOCK, вы должны проверять все коды возвратов и быть готовыми к неудачному вызову в любое время. Многие программисты совершают большую ошибку все время вызывая функцию пока она не вернет удачный код возврата. К примеру постоянный вызов recv в цикле в ожидании прочтения 100 байт данных ничем не лучше чем вызов recv в блокирующем режиме с параметром MSG_PEEK. Winsock модели ввода - вывода могут помоч приложению, определьть когда сокет готов к чтению, либо передаче данных.

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

Модель порта завершения

Последняя модель ввода/вывода, которую мы рассмотрим, это модель "порта завершения" – completion port. Порт завершения представляет собой специальный механизм в составе ОС, с помощью которого приложение использует объединение (пул) нескольких потоков, предназначенных единственно для цели обработки асинхронных операций ввода/вывода с перекрытием.

Приложения, которые вынуждены обрабатывать многочисленные асинхронные запросы (речь идет о сотнях и тысячах одновременно поступающих запросах – например, на поисковых серверах или популярных серверах типа www.microsoft.com), с помощью этого механизма могут обрабатывать I/O- запросы существенно быстрее и эффективнее, чем просто запускать новый поток для обработки поступившего запроса. Поддержка этого механизма включена в Windows NT, Windows 2000, Windows XP и Windows Server 2003 и особенно эффективна для мультипроцессорных систем. Так, демонстрационный программный код, который опубликован в MSDN, рассчитан на 16-ти процессорную аппаратную платформу.

Для функционирования этой модели необходимо создание специального программного объекта ядра системы, который и был назван "порт завершения". Это осуществляется с помощью функции CreateIoCompletionPort(), которая асссоциирует этот объект с одним или несколькими файловыми (сокетными) дескрипторами (см. ниже пример в разделе 4.5.1.1) и который будет управлять перекрывающимися I/O операциями, используя определенное количество потоков для обслуживания завершенных запросов.

Для начала нам необходимо создать программный объект - порт завершения I/O, который будет использоваться, чтобы управлять множественными I/O-запросами для любого количества сокетных дескрипторов. Это выполняется вызовом функции CreateIoCompletionPort(), которая определена как:

HANDLE CreateIoCompletionPort(

HANDLE FileHandle ,

HANDLE ExistingCompletionPort,

DWORD CompletionKey,

DWORD NumberOfConcurrentThreads

Прежде чем рассматривать параметры подробно, следует отметить, что эта функция фактически используется для двух различных целей:

Чтобы создать объект порта завершения

Связать дескриптор с портом завершения

Когда Вы первоначально создаете объект порта завершения, интерес представляет единственный параметр - NumberOfConcurrentThreads; первые три параметра не существенны. Параметр NumberOfConcurrentThreads специфичен, потому что он определяет число потоков, которым позволяется выполниться одновременно на порте завершения. По идее, нам нужен только один поток для каждого отдельного процессора, чтобы обслужить порт завершения и избежать переключения контекста потока. Значение для этого параметра равное 0 сообщает системе разрешить иметь столько потоков, сколько процессоров имеется в системе. Следующий вызов создает порт завершения I/O:

CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL, 0, 0);

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

Модели ввода / вывода

Select (Выбор)

Эта модель обеспечивает более контролируемый способ блокирования. Хотя она позволяет работать и с блокирующими socket’ами, я остановлюсь на неблокирующем режиме. Принцип этой модели станет понятным, если взглянуть на иллюстрацию:

Диалог программы и WinSock будет следующим:

Программа: «Хорошо, скажешь мне, когда будет наилучший момент, чтоб повторить попытку»

WinSock: «Конечно, повиси минутку»

«Пробуй снова!»

Программа: «Отправь-ка эти данные»

WinSock: «Сделано!»

Вы, возможно, заметили, что эта модель выглядит подобно блокирующему socket’у. Это, потому что select действительно блокирует. Первый вызов пытается выполнить WinSock операцию. В этом случае операция заблокировала бы выполнение основного процесса, но функция не может быть выполнена и она завершается неудачей. Тогда управление передается основному потоку программы, который, в свою очередь, вызывает select-метод (т.е. программа обращается к модели, что бы определить подходящее время для повторной попытки). Он будет ждать наилучшего момента, чтобы повторить WinSock функцию.

Но тут может возникнуть вполне справедливый вопрос: если данная модель блокирует, то почему мы используем ее для неблокирующих socket’ов? Дело в том, что этот способ может «ждать» при многократных событиях. Ниже приведен прототип функции select:

select (nfds:DWORD, readfds:DWORD, writefds:DWORD, exceptfds:DWORD, timeout:DWORD)

Select определяет статус одного или нескольких socket’ов, предоставляя синхронизацию ввода/вывода, если это необходимо. Первый параметр игнорируется, последний параметр используется для определения оптимального времени «ожидания» функции. Остальные параметры определяют набор socket’ов:

readfds – набор socket’ов, которые будут проверены на возможность чтения.

writefds - набор socket’ов, которые будут проверены на возможность записи.

exceptfds - набор socket’ов, которые будут проверены на наличие ошибок.

«Возможность чтения» значит, что данные прибыли на socket, и, что само чтение после select’а аналогично получению данных. «Возможность записи» значит, что сейчас подходящее время для передачи данных, т.к. получатель, возможно, готов принять их. Exceptfds используется, чтобы «словить» ошибки из неблокирующих соединений.

WSAASyncSelect

Большинство оконных программ используют специальные диалоговые окна, что бы получить информацию от пользователя или наоборот. WinSock обеспечивает способ взаимодействия уведомлений о сетевых событиях с обработкой сообщений Windows. Функция WSAAsyncSelect позволяет зарегистрировать уведомление для определенного сетевого события в виде привычного сообщения Windows.

WSAAsyncSelect (s:DWORD, hWnd:DWORD, wMsg:DWORD, lEvent:DWORD)

Эта функция требует специального сообщения (wMsg), которое выбирает пользователь. А оконная процедура должна обработать это самое сообщение. lEvent является битовой маской, которая определяет событие, о котором будет сообщено. Рисунок для данной модели можно сделать таким:

Допустим, что первое сообщение хочет отправить какие-то данные socket’у, используя send. Так как socket неблокирующий, функция будет завершена мгновенно. Вызов функции может завершиться успешно, но тут этого не происходит. Предполагая, что WSAAsyncSelect была настроена таким образом, что сообщит нам о событии FD_WRITE, в конечном итоге мы получим сообщение от WinSock, говорящее нам о том, что данное событие произошло. В данном случае это событие FD_WRITE, которое означает что-то типа «Я готово, попробуй переслать свои данные». Таким образом, в обработчике сообщения программа пытается переслать данные, и эта попытка завершается успехом.

Беседа между программой и WinSock подобна модели select, различие лишь в методе уведомления: оконное сообщение вместо синхронного вызова select’а. В то время как select блокирует основной процесс, ожидая пока произойдет событие, программа, использующая WSAAsyncSelect, может продолжить обработку сообщений Windows до тех пор, пока не происходит никаких событий:

Программа регистрируется для уведомления о сетевых событиях через оконные сообщения

Программа: «Отправь-ка эти данные»

WinSock: «Я не могу сделать это сейчас»

Программа обрабатывает некоторое сообщение

Программа обрабатывает другое сообщение

Программа получает уведомляющее сообщение от WinSock

Программа: «Отправь-ка эти данные»

WinSock: «Сделано! »

WSAAsyncSelect обеспечивает более «Windows’овский» способ уведомления и он довольно прост в использовании. Для серверов с низкой пропускной способностью (меньше 1000 соединений) этот способ вполне хорош. Недостатком является то, что оконные сообщения, сами по себе, не очень быстрые, а так же в том, что для использования этой модели требуются окна (т.е. программа должна быть GUI).

WSAEventSelect

Примечание: под «объектом события» далее будет пониматься какое-то определенное сетевое событие. Дело в том, что тут событие рассматривается, как класс =).

WSAEventSelect можно назвать родственником WSAAsyncSelect, который работает очень похожим способом, но вместо оконных сообщений использует объекты событий. В этом есть определенные преимущества, одним из которых является эффективность (объекты событий работают быстрее оконных сообщений). Графическая интерпретация этой модели выглядит немного сложнее, чем предыдущей, но на самом деле это не так:

Программа регистрируется для уведомления о сетевых событиях через объекты событий

Программа: «Отправь-ка эти данные»

WinSock: «Я не могу сделать это сейчас»

Программа ждет события, чтобы сигнализировать о нем

Программа: «Отправь-ка эти данные»

WinSock: «Сделано! »

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

Поначалу эта модель похожа на блокирующую: Вы ждете событие, о котором Вам будет сообщено. Это верно, но в тоже самое время Вы можете создать свой объект события. Все объекты события являются частью WinAPI, которую использует WinSock. В WinSock есть некоторые функции для создания объектов, но фактически это API функции в WinSock упаковке.

Все, что WinSock делает в этой модели, это сигнализирует объект события, когда это событие должно произойти.

Функция, с помощью которой регистрируется сетевое событие WSAEventSelect:

WSAEventSelect (s:DWORD, hEventObject:DWORD, lNetworkEvents:DWORD)

WSAAsyncSelect отправит Вам сообщение о произошедшем сетевом событии (FD_READ, FD_WRITE, и т.д.) В отличие от WSAAsyncSelect, у WSAEventSelect есть только один способ уведомления: сигнализирование объекта событий. Это позволяет использовать данную модель как в GUI приложениях, так и в консольных. Какие события произошли можно узнать с помощью WSAEnumNetworkEvents.

Введение в перекрытый ввод/вывод

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

В рассмотренных ранее моделях посылались некоторые уведомления (такие как «данные доступны» или «готов попробовать переслать данные» и т.д.) при возникновении определенных сетевых событий. Перекрывающие модели также посылают уведомления, но не о наступлении сетевых событий, а об их завершении. При вызове WinSock функции она может либо успешно завершиться, либо завершиться неудачей с кодом ошибки WSA_IO_PENDING. При использовании перекрывающих моделей Вы будете уведомлены о завершении операции. Это значит, что Вам просто надо подождать, пока операция не завершится.

Ценой этого эффективного подхода является трудная реализация. Если Вам не требуется действительно хорошая эффективность, то лучше воспользоваться ранее описанными моделями. Кроме того, операционные системы Windows 9x/ME не полностью поддерживают перекрытые модели ввода/вывода.

Как и модели с уведомлением о сетевых событиях, перекрытие модели так же могут быть реализованы по-разному. Они отличаются способом уведомления: блокирование, polling, процедуры завершения и порты завершения.

Перекрытый ввод/вывод: блокирование

Первая модель перекрытого ввода/вывода, о которой я расскажу, использует объект события для сигнализирования о завершении. Эта модель во многом похожа на WSAEventSelect, но отличием является то, что объект устанавливается в сигнализированное состояние при завершении WinSock операции, а не при наступлении какого-то сетевого события.

Программа: «Отправь-ка эти данные»

WinSock: «Окей, но я не могу отправить их прямо сейчас»

Программа ждет сигнала от объекта события, указывающего на то, что функция завершена

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

Перекрытый ввод/вывод: polling

Так же как и в ранее упомянутой модели polling, в этой модели так же можно запросить статус выполнения операции (хотя в ранее описанном polling’e мы не запрашивали статус выполнения, а просто получали данные о неудачном завершении функции. Но основной поток программы знал, когда функция завершилась неудачно, а когда наоборот). С помощью функции WSAGetOverlappedResult можно узнать статус выполняемой операции. Графическая интерпретация перекрытого polling’а очень похожа на интерпретацию обычного polling’а, за исключением того, что WinSock функция выполняется в то же время, что и опрос программы о выполнении функции.

Программа: «Отправь-ка эти данные»

Программа: «Уже отправил?»

WinSock: «Нет»

Программа: «Уже отправил?»

WinSock: «Нет»

Программа: «Уже отправил?»

WinSock: «Нет»

Программа: «Уже отправил?»

WinSock: «Да! »

И тут я повторюсь: эта модель не очень хороша, так как она приводит процессор в панику. Поэтому я не рекомендую использовать эту модель.

Перекрытый ввод/вывод: процедуры завершения

Процедуры завершения – процедуры обратного вызова (т.е. вызываются в ответ на определенное действие. Далее я буду называть эти процедуры процедурами отзыва), которые вызываются при завершении операции. Тут вроде бы все просто, но есть одна хитрость: эти процедуры вызываются в контексте потока, который начал операцию. Что это значит? Представьте себе поток, который запросил перекрытую операцию записи. WinSock выполняет эту операцию, в то время как Ваш поток тоже выполняется. Таким образом у WinSock есть свой собственный поток для этой операции. Когда операция закончится, WinSock должен вызвать процедуру отзыва. Если это произойдет, то вызванная процедура будет выполняться в контексте потока WinSock. Это означает, что поток, вызвавший операцию записи, будет выполняться одновременно с процедурой вызова. Проблема состоит в том, что синхронизация с вызывающим потоком отсутствует, и он не знает, завершена ли операция (только если ему не сообщат об этом из параллельного потока).

Что бы избежать этого, WinSock удостоверяется в том, что процедура отзыва протекает в том же потоке, из которого происходил запрос. Осуществляется это с помощью APC (Asynchronous Procedure Call или Асинхронный Вызов Процедуры), механизма, встроенного в Windows. Это можно представить как «внедрение» процедуры в основной поток выполнения программы. Таким образом, поток сначала выполнит процедуру, а потом будет делать, то, что делал до ее «внедрения». Естественно, что система не может приказать потоку: «Прекрати делать все, что делал и обработай сначала эту процедуру».

Для того, что бы обеспечить «внедрение» в нужное место механизм APC требует, что бы поток находился в, так называемом, извещающем состоянии ожидания. Каждый поток имеет свою собственную APC очередь, в которой процедуры ждут своего вызова. Когда поток входит в извещающее состояние ожидания, это указывает на то, что он готов обслуживать очередь APC.

Перекрытый ввод/вывод с процедурами завершения использует APC для уведомления о завершении операции.

Программа: «Отправь-ка эти данные»

WinSock: «Окей, но я не могу отправить их сейчас»

Программа входит в извещающее состояние ожидания

Функция завершилась

Состояние ожидания получает сигнал, о том, что функция завершена

Выполняется функция отзыва и управление переходит к программе

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

Перекрытый ввод/вывод: порты завершения

Наконец мы подошли к последней и, возможно, самой эффективной модели ввода/вывода: перекрытый ввод/вывод с портами завершения. Порт завершения – механизм, доступный в NT ядрах ОС (9x не поддерживают их), позволяющий эффективно управлять потоками. В отличие от всех рассмотренных моделей, «порты завершения» обладают собственным управлением потоков. Как Вы могли заметить, все предыдущие иллюстрации представляли собой что-то типа графиков зависимости от времени. Для этой модели я не делал такого графика и диалога программы с WinSock, т.к. это вряд ли поможет прояснить ситуацию. Вместо этого я изобразил образ самого механизма, который хорошо показывает, что происходит:

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

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

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

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

Однако как именованные каналы, так и почтовые ящики (в отношении которых для простоты мы будем использовать далее общий термин - "именованные каналы", если различия между ними не будут играть существенной роли) обладают тем недостатком, что они не являются промышленным стандартом. Это обстоятельство усложняет перенос программ наподобие тех, которые рассматривались в главе 11, в системы, не принадлежащие семейству Windows, хотя именованные каналы не зависят от протоколов и могут выполняться поверх многих стандартных промышленных протоколов, например TCP/IP.

Возможность взаимодействия с другими системами обеспечивается в Windows поддержкой сокетов (sockets) Windows Sockets - совместимого и почти точного аналога сокетов Berkeley Sockets, де-факто играющих роль промышленного стандарта. В этой главе использование API Windows Sockets (или "Winsock") показано на примере модифицированной клиент-серверной системы из главы 11. Результирующая система способна функционировать в глобальных сетях, использующих протокол TCP/IP, что, например, позволяет серверу принимать запросы от клиентов UNIX или каких-либо других, отличных от Windows систем.

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

Привлекая средства обеспечения взаимодействия между разнородными системами, ориентированные на стандарты, интерфейс Winsock открывает перед программистами возможность доступа к высокоуровневым протоколам и приложениям, таким как ftp, http, RPC и СОМ, которые в совокупности предоставляют богатый набор высокоуровневых моделей, обеспечивающих поддержку межпроцессного сетевого взаимодействия для систем с различной архитектурой.

В данной главе указанная клиент-серверная система используется в качестве механизма демонстрации интерфейса Winsock, и в процессе того, как сервер будет модифицироваться, в него будут добавляться новые интересные возможности. В частности, нами будут впервые использованы точки входа DLL (глава 5) и внутрипроцессные серверы DLL. (Эти новые средства можно было включить уже в первоначальную версию программы в главе 11, однако это отвлекло бы ваше внимание от разработки основной архитектуры системы.) Наконец, дополнительные примеры покажут вам, как создаются безопасные реентерабельные многопоточные библиотеки.

Поскольку интерфейс Winsock должен соответствовать промышленным стандартам, принятые в нем соглашения о правилах присвоения имен и стилях программирования несколько отличаются от тех, с которыми мы сталкивались в процессе работы с описанными ранее функциями Windows. Строго говоря, Winsock API не является частью Win32/64. Кроме того, Winsock предоставляет дополнительные функции, не подчиняющиеся стандартам; эти функции используются лишь в случае крайней необходимости. Среди других преимуществ, обеспечиваемых Winsock, следует отметить улучшенную переносимость результирующих программ на другие системы.

Сокеты Windows

Winsock API разрабатывался как расширение Berkley Sockets API для среды Windows и поэтому поддерживается всеми системами Windows. К преимуществам Winsock можно отнести следующее:

Перенос уже имеющегося кода, написанного для Berkeley Sockets API, осуществляется непосредственно.

Системы Windows легко встраиваются в сети, использующие как версию IPv4 протокола TCP/IP, так и постепенно распространяющуюся версию IPv6. Помимо всего остального, версия IPv6 допускает использование более длинных IP-адресов, преодолевая существующий 4-байтовый адресный барьер версии IPv4.

Сокеты могут использоваться совместно с перекрывающимся вводом/выводом Windows (глава 14), что, помимо всего прочего, обеспечивает возможность масштабирования серверов при увеличении количества активных клиентов.

Сокеты можно рассматривать как дескрипторы (типа HANDLE) файлов при использовании функций ReadFile и WriteFile и, с некоторыми ограничениями, при использовании других функций, точно так же, как в качестве дескрипторов файлов сокеты применяются в UNIX. Эта возможность оказывается удобной в тех случаях, когда требуется использование асинхронного ввода/вывода и портов завершения ввода/вывода.

Существуют также дополнительные, непереносимые расширения.

Инициализация Winsock

Winsock API поддерживается библиотекой DLL (WS2_32.DLL), для получения доступа к которой следует подключить к программе библиотеку WS_232.LIB. Эту DLL следует инициализировать с помощью нестандартной, специфической для Winsock функции WSAStartup, которая должна быть первой из функций Winsock, вызываемых программой. Когда необходимость в использовании функциональных возможностей Winsock отпадает, следует вызывать функцию WSACleanup. Примечание. Префикс WSA означает "Windows Sockets asynchronous …" ("Асинхронный Windows Sockets …"). Средства асинхронного режима Winsock нами здесь не используются, поскольку при возникновении необходимости в выполнении асинхронных операций мы можем и будем использовать потоки.

Хотя функции WSAStartup и WSACleanup необходимо вызывать в обязательном порядке, вполне возможно, что они будут единственными нестандартными функциями, с которыми вам придется иметь дело. Распространенной практикой является применение директив препроцессора #ifdef для проверки значения символической константы _WIN32 (обычно определяется Visual C++ на стадии компиляции), в результате чего функции WSA будут вызываться только тогда, когда вы работаете в Windows). Разумеется, такой подход предполагает, что остальная часть кода не зависит от платформы.

int WSAStartup(WORD wVersionRequired, LPWSADATA ipWSAData);
Параметры

wVersionRequired - указывает старший номер версии библиотеки DLL, который вам требуется и который вы можете использовать. Как правило, версии 1.1 вполне достаточно для того, чтобы обеспечить любое взаимодействие с другими системами, в котором у вас может возникнуть необходимость. Тем не менее, во всех системах Windows, включая Windows 9x, доступна версия Winsock 2.0, которая и используется в приведенных ниже примерах. Версия 1.1 считается устаревшей и постепенно выходит из употребления.

Функция возвращает ненулевое значение, если запрошенная вами версия данной DLL не поддерживается.

Младший байт параметра wVersionRequired указывает основной номер версии, а старший байт - дополнительный. Обычно используют макрос MAKEWORD; таким образом, выражение MAKEWORD (2,0) представляет версию 2.0.

ipWSAData - указатель на структуру WSADATA, которая возвращает информацию о конфигурации DLL, включая старший доступный номер версии. О том, как интерпретировать ее содержимое, вы можете прочитать в материалах оперативной справки Visual Studio.

Чтобы получить более подробную информацию об ошибках, можно воспользоваться функцией WSAGetLastError, но для этой цели подходит также функция GetLastError, а также функция ReportError, разработанная в главе 2.

По окончании работы программы, а также в тех случаях, когда необходимости в использовании сокетов больше нет, следует вызывать функцию WSACleanup, чтобы библиотека WS_32.DLL, обслуживающая сокеты, могла освободить ресурсы, распределенные для этого процесса.

Создание сокета

Инициализировав Winsock DLL, вы можете использовать стандартные (Berkeley Sockets) функции для создания сокетов и соединений, обеспечивающих взаимодействие серверов с клиентами или взаимодействие равноправных узлов сети между собой.

Используемый в Winsock тип данных SOCKET аналогичен типу данных HANDLE в Windows, и его даже можно применять совместно с функцией ReadFile и другими функциями Windows, требующими использования дескрипторов типа HANDLE. Для создания (или открытия) сокета служит функция socket.

SOCKET socket(int af, int type, int protocol);
Параметры

Тип данных SOCKET фактически определяется как тип данных int, потому код UNIX остается переносимым, не требуя привлечения типов данных Windows.

af - обозначает семейство адресов, или протокол; для указания протокола IP (компонент протокола TCP/IP, отвечающий за протокол Internet) следует использовать значение PF_INET (или AF_INET, которое имеет то же самое числовое значение, но обычно используется при вызове функции bind).

type - указывает тип взаимодействия: ориентированное на установку соединения (connection-oriented communication), или потоковое (SOCK_STREAM), и дейтаграммное (datagram communication) (SOCK_DGRAM), что в определенной степени сопоставимо соответственно с именованными каналами и почтовыми ящиками.

protocol - является излишним, если параметр af установлен равным AF_INET; используйте значение 0.

В случае неудачного завершения функция socket возвращает значение INVALID_SOCKET.

Winsock можно использовать совместно с протоколами, отличными от TCP/IP, указывая различные значения параметра protocol; мы же будем использовать только протокол TCP/IP.

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

Серверные функции сокета

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

Если не оговорено иное, типом сокетов в наших примерах всегда будет SOCK_STREAM. Сокеты типа SOCK_DGRAM рассматривается далее в этой главе.

Связывание сокета

Следующий шаг заключается в привязке сокета к его адресу и конечной точке (endpoint) (направление канала связи от приложения к службе). Вызов socket, за которым следует вызов bind, аналогичен созданию именованного канала. Однако не существует имен, используя которые можно было бы различать сокеты данного компьютера. Вместо этого в качестве конечной точки службы используется номер порта (port number). Любой заданный сервер может иметь несколько конечных точек. Прототип функции bind приводится ниже.

int bind(SOCKET s, const struct sockaddr *saddr, int namelen);
Параметры

s - несвязанный сокет, возвращенный функцией socket.

saddr - заполняется перед вызовом и задает протокол и специфическую для протокола информацию, как описано ниже. Кроме всего прочего, в этой структуре содержится номер порта.

namelen - присвойте значение sizeof (sockaddr).

В случае успешного выполнения функция возвращает значение 0, иначе SOCKET_ERROR. Структура sockaddr определяется следующим образом:

typedef struct sockaddr SOCKADDR, *PSOCKADDR;

Первый член этой структуры, sa_family, обозначает протокол. Второй член, sa_data, зависит от протокола. Internet-версией структуры sa_data является структура sockaddr_in:

short sin_family; /* AF_INET */
struct in_addr sin_addr; /* 4-байтовый IP-адрес */
typedef struct sockaddr_in SOCKADDR_IN, *PSOCKADDR IN;

Обратите внимание на использование типа данных short integer для номера порта. Кроме того, номер порта и иная информация должны храниться с соблюдением подходящего порядка следования байтов, при котором старший байт помещается в крайней позиции справа (big-endian), чтобы обеспечивалась двоичная совместимость с другими системами. В структуре sin_addr содержится подструктура s_addr, заполняемая уже знакомым нам 4-байтовым IP-адресом, например 127.0.0.1, указывающим систему, чей запрос на образование соединения должен быть принят. Обычно удовлетворяются запросы любых систем, в связи с чем следует использовать значение INADDR_ANY, хотя этот символический параметр должен быть преобразован к корректному формату, как показано в приведенном ниже фрагменте кода.

Для преобразования текстовой строки с IP-адресом к требуемому формату можно использовать функцию inet_addr, поэтому член sin_addr.s_addr переменной sockaddr_in инициализируется следующим образом:

sa.sin_addr.s_addr = inet_addr("192 .13.12.1");

О связанном сокете, для которого определены протокол, номер порта и IP-адрес, иногда говорят как об именованном сокете (named socket).

Перевод связанного сокета в состояние прослушивания

Функция listen делает сервер доступным для образования соединения с клиентом. Аналогичной функции для именованных каналов не существует.

int listen(SOCKET s, int nQueueSize);

Параметр nQueueSize указывает число запросов на соединение, которые вы намерены помещать в очередь сокета. В версии Winsock 2.0 значение этого параметра не имеет ограничения сверху, но в версии 1.1 оно ограничено предельным значением SOMAXCON (равным 5).

Прием клиентских запросов соединения

Наконец, сервер может ожидать соединения с клиентом, используя функцию accept, возвращающую новый подключенный сокет, который будет использоваться в операциях ввода/вывода. Заметьте, что исходный сокет, который теперь находится в состоянии прослушивания (listening state), используется исключительно в качестве параметра функции accept, а не для непосредственного участия в операциях ввода/вывода.

Функция accept блокируется до тех пор, пока от клиента не поступит запрос соединения, после чего она возвращает новый сокет ввода/вывода. Хотя рассмотрение этого и выходит за рамки данной книги, возможно создание неблокирующихся сокетов, а в сервере (программа 12.2) для приема запроса используется отдельный поток, что позволяет создавать также неблокирующиеся серверы.

SOCKET accept(SOCKET s, LPSOCKADDR lpAddr, LPINT lpAddrLen);
Параметры

s - прослушивающий сокет. Чтобы перевести сокет в состояние прослушивания, необходимо предварительно вызвать функции socket, bind и listen.

lpAddr - указатель на структуру sockaddr_in, предоставляющую адрес клиентской системы.

lpAddrLen - указатель на переменную, которая будет содержать размер возвращенной структуры sockaddr_in. Перед вызовом функции accept эта переменная должна быть инициализирована значением sizeof(struct sockaddr_in).

Отключение и закрытие сокетов

Для отключения сокетов применяется функция shutdown(s, how). Аргумент how может принимать одно из двух значений: 1, указывающее на то, что соединение может быть разорвано только для посылки сообщений, и 2, соответствующее разрыву соединения как для посылки, так и для приема сообщений. Функция shutdown не освобождает ресурсы, связанные с сокетом, но гарантирует завершение посылки и приема всех данных до закрытия сокета. Тем не менее, после вызова функции shutdown приложение уже не должно использовать этот сокет.

Когда работа с сокетом закончена, его следует закрыть, вызвав функцию closesocket(SOCKET s). Сначала сервер закрывает сокет, созданный функцией accept, а не прослушивающий сокет, созданный с помощью функции socket. Сервер должен закрывать прослушивающий сокет только тогда, когда завершает работу или прекращает принимать клиентские запросы соединения. Даже если вы работаете с сокетом как с дескриптором типа HANDLE и используете функции ReadFile и WriteFile, уничтожить сокет одним только вызовом функции CloseHandle вам не удастся; для этого следует использовать функцию closesocket.

Пример: подготовка и получение клиентских запросов соединения

Ниже приводится фрагмент кода, показывающий, как создать сокет и организовать прием клиентских запросов соединения.

В этом примере используются две стандартные функции: htons ("host to network short" - "ближняя связь") и htonl ("host to network long" - "дальняя связь"), которые преобразуют целые числа к форме с обратным порядком байтов, требуемой протоколом IP.

Номером порта сервера может быть любое число из диапазона, допустимого для целых чисел типа short integer, но для определенных пользователем служб обычно используются числа в диапазоне 1025-5000. Порты с меньшими номерами зарезервированы для таких известных служб, как telnet или ftp, в то время как порты с большими номерами предполагаются для использования других стандартных служб.

struct sockaddr_in SrvSAddr; /* Адресная структура сервера. */
struct sockaddr_in ConnectAddr;
AddrLen = sizeof(ConnectAddr);
sockio = accept(SrvSock, (struct sockaddr *) &ConnectAddr, &AddrLen);
… Получение запросов и отправка ответов …

Клиентские функции сокета

Клиентская станция, которая желает установить соединение с сервером, также должна создать сокет, вызвав функцию socket. Следующий шаг заключается в установке соединения сервером, а, кроме того, необходимо указать номер порта, адрес хоста и другую информацию. Имеется только одна дополнительная функция – connect.

Установление клиентского соединения с сервером

Если имеется сервер с сокетом в режиме прослушивания, клиент может соединиться с ним при помощи функции connect.

int connect(SOCKET s, LPSOCKADDR lpName, int nNameLen);
Параметры

s - сокет, созданный с использованием функции socket.

lpName - указатель на структуру sockaddr_in, инициализированную значениями номера порта и IP-адреса системы с сокетом, связанным с указанным портом, который находится в состоянии прослушивания.

Инициализируйте nNameLen значением sizeof (struct sockaddr_in).

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

Сокет s не обязательно должен быть связанным с портом до вызова функции connect, хотя это и может иметь место. При необходимости система распределяет порт и определяет протокол.

Пример: подключение клиента к серверу

Показанный ниже фрагмент кода обеспечивает соединение клиента с сервером. Для этого нужны только два вызова функций, но адресная структура должна быть инициализирована до вызова функции connect. Проверка возможных ошибок здесь отсутствует, но в реальные программы она должна включаться. В примере предполагается, что IP-адрес (текстовая строка наподобие "192.76.33.4") задается в аргументе argv командной строки.

ClientSAddr.sin_addr.s_addr = inet_addr(argv);
ConVal = connect(ClientSock, (struct sockaddr *)&ClientSAddr, sizeof(ClientSAddr));

Отправка и получение данных

Программы, использующие сокеты, обмениваются данными с помощью функций send и recv, прототипы которых почти совпадают (перед указателем буфера функции send помещается модификатор const). Ниже представлен только прототип функции send.

int send(SOCKET s, const char * lpBuffer, int nBufferLen, int nFlags);

Возвращаемым значением является число фактически переданных байтов. Значение SOCKET_ERROR указывает на ошибку.

nFlags - может использоваться для обозначения степени срочности сообщений (например, экстренных сообщений), а значение MSG_PEEK позволяет просматривать получаемые данные без их считывания.

Самое главное, что вы должны запомнить - это то, что функции send и recv не являются атомарными (atomic), и поэтому нет никакой гарантии, что затребованные данные будут действительно отправлены или получены. Передача "коротких" сообщений ("short sends") встречается крайне редко, хотя и возможна, что справедливо и по отношению к приему "коротких" сообщений ("short receives"). Понятие сообщения в том смысле, который оно имело в случае именованных каналов, здесь отсутствует, и поэтому вы должны проверять возвращаемое значение и повторно отправлять или принимать данные до тех пор, пока все они не будут переданы.

С сокетами могут использоваться также функции ReadFile и WriteFile, только в этом случае при вызове функции необходимо привести сокет к типу HANDLE.

Сравнение именованных каналов и сокетов

Именованные каналы, описанные в главе 11, очень похожи на сокеты, но в способах их использования имеются значительные различия.

Именованные каналы могут быть ориентированными на работу с сообщениями, что значительно упрощает программы.

Именованные каналы требуют использования функций ReadFile и WriteFile, в то время как сокеты могут обращаться также к функциям send и recv.

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

Сокеты основаны на промышленном стандарте, что обеспечивает их совместимость с системами, отличными от Windows.

Имеются также различия в моделях программирования сервера и клиента.

Сравнение серверов именованных каналов и сокетов

Установка соединения с несколькими клиентами при использовании сокетов требует выполнения повторных вызовов функции accept. Каждый из вызовов возвращает очередной подключенный сокет. По сравнению с именованными каналами имеются следующие отличия:

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

Допустимое количество клиентских сокетов ничем не ограничено (функция listen ограничивает лишь количество клиентов, помещаемых в очередь), в то время как количество экземпляров именованных каналов, в зависимости от того, что было указано при первом вызове функции CreateNamedPipe, может быть ограниченным.

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

Именованные каналы не имеют портов с явно заданными номерами и различаются по именам.

В случае сервера именованных каналов получение пригодного для работы дескриптора типа HANDLE требует вызова двух функций (CreateNamedPipe и ConnectNamedPipe), тогда как сервер сокета требует вызова четырех функций (socket, bind, listen и accept).

Сравнение клиентов именованных каналов и сокетов

В случае именованных каналов необходимо последовательно вызывать функции WaitNamedPipe и CreateFile. Если же используются сокеты, этот порядок вызовов обращается, поскольку можно считать, что функция socket создает сокет, а функция connect - блокирует.

Дополнительное отличие состоит в том, что функция connect является функцией клиента сокета, в то время как функция ConnectNamedPipe используется сервером именованного канала.

Пример: функция приема сообщений в случае сокета

Часто оказывается удобным отправлять и получать сообщения в виде единых блоков. Как было показано в главе 11, каналы позволяют это сделать. Однако в случае сокетов требуется создание заголовка, содержащего размер сообщения, за которым следует само сообщение. Для приема таких сообщений предназначена функция ReceiveMessage, которая будет использоваться в примерах. То же самое можно сказать и о функции SendMessage, предназначенной для передачи сообщений.

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

Примечание, относящееся к Win64

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

DWORD ReceiveMessage (MESSAGE *pMsg, SOCKET sd) {
/* Сообщение состоит из 4-байтового поля размера сообщения, за которым следует собственно содержимое. */
/* Считать сообщение. */
/* Сначала считывается заголовок, а затем содержимое. */
nRemainRecv = 4; /* Размер поля заголовка. */
pBuffer = (LPBYTE)pMsg; /* recv может не передать все запрошенные байты. */
while (nRemainRecv > 0 && !Disconnect) {
/* Считать содержимое сообщения. */
while (nRemainRecv > 0 && !Disconnect) {
nXfer = recv(sd, pBuffer, nRemainRecv, 0);

Пример: клиент на основе сокета

Программа 12.1 представляет собой переработанный вариант клиентской программы clientNP (программа 11.2), которая использовалась в случае именованных каналов. Преобразование программы осуществляется самым непосредственным образом и требует лишь некоторых пояснений.

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

Для отправки и приема сообщений применяются функции, например, ReceiveMessage, которые здесь не представлены.

Номер порта, SERVER_PORT, определен в заголовочном файле ClntSrvr.h.

Хотя код написан для выполнения под управлением Windows, единственная зависимость от Windows связана с использованием вызовов функций, имеющих префикс WSA.

Программа 12.1. clientSK: клиент на основе сокетов
/* Глава 12. clientSK.с */
/* Однопоточный клиент командной строки. */
/* ВЕРСИЯ НА ОСНОВЕ WINDOWS SOCKETS. */
/* Считывает последовательность команд для пересылки серверному процессу*/
/* через соединение с сокетом. Дожидается ответа и отображает его. */

#define _NOEXCLUSIONS /* Требуется для включения определений сокета. */
#include "ClntSrvr.h" /* Определяет структуры записей запроса и ответа. */

/* Функции сообщения для обслуживания запросов и ответов. */
/* Кроме того, ReceiveResponseMessage отображает полученные сообщения. */
static DWORD SendRequestMessage(REQUEST *, SOCKET);
static DWORD ReceiveResponseMessage(RESPONSE *, SOCKET);
struct sockaddr_in ClientSAddr; /* Адрес сокета клиента. */
int _tmain(DWORD argc, LPTSTR argv) {
SOCKET ClientSock = INVALID_SOCKET;
REQUEST Request; /* См. ClntSrvr.h. */
RESPONSE Response; /* См. ClntSrvr.h. */
TCHAR PromptMsg = _T("\nВведите команду> ");
TCHAR QuitMsg = _T("$Quit");
/* Запрос: завершить работу клиента. */
TCHAR ShutMsg = _T("$ShutDownServer"); /* Остановить все потоки. */
CHAR DefaultIPAddr = "127.0.0.1"; /* Локальная система. */
/* Подключиться к серверу. */
/* Следовать стандартной процедуре вызова последовательности функций socket/connect клиентом. */
ClientSock = socket(AF_INET, SOCK_STREAM, 0);
memset(&ClientSAddr, 0, sizeof(ClientSAddr));
ClientSAddr.sin_family = AF_INET;
if (argc >= 2) ClientSAddr.sin_addr.s_addr = inet_addr(argv );
else ClientSAddr.sin_addr.s_addr = inet_addr(DefaultIPAddr);
ClientSAddr.sin_port = htons(SERVER_PORT);
/* Номер порта определен равным 1070. */
connect(ClientSock, (struct sockaddr *)&ClientSAddr, sizeof(ClientSAddr));
/* Основной цикл для вывода приглашения на ввод команд, посылки запроса и получения ответа. */
_tprintf(_T("%s"), PromptMsg);
/* Ввод в формате обобщенных строк, но команда серверу должна указываться в формате ASCII. */
_fgetts(Req, MAX_RQRS_LEN-1, stdin);
for (j = 0; j <= _tcslen(Req) Request.Record[j] = Req[j];
/* Избавиться от символа новой строки в конце строки. */
Request.Record = "\0";
if (strcmp(Request.Record, QuitMsg) == 0 || strcmp(Request.Record, ShutMsg) == 0) Quit = TRUE;
SendRequestMessage(&Request, ClientSock);
ReceiveResponseMessage(&Response, ClientSock);
shutdown(ClientSock, 2); /* Запретить посылку и прием сообщений. */
_tprintf(_T("\n****Выход из клиентской программы\n"));

Пример: усовершенствованный сервер на основе сокетов

Программа serverSK (программа 12.2) аналогична программе serverNP (программа 11.3), являясь ее видоизмененным и усовершенствованным вариантом.

В усовершенствованном варианте программы серверные потоки создаются по требованию (on demand), а не в виде пула потоков фиксированного размера. Каждый раз, когда сервер принимает запрос клиента на соединение, создается серверный рабочий поток, и когда клиент прекращает работу, выполнение потока завершается.

Сервер создает отдельный поток приема (accept thread), что позволяет основному потоку опрашивать глобальный флаг завершения работы, пока вызов accept остается блокированным. Хотя сокеты и могут определяться как неблокирующиеся, потоки обеспечивают удобное универсальное решение. Следует отметить, что значительная часть расширенных функциональных возможностей Winsock призвана поддерживать асинхронные операции, тогда как потоки Windows дают возможность воспользоваться более простой и близкой к стандартам функциональностью синхронного режима работы сокетов.

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

Данный сервер поддерживает также внутрипроцессные серверы (in-process servers), что достигается путем загрузки библиотеки DLL во время инициализации. Имя библиотеки DLL задается в командной строке, и серверный поток сначала пытается определить точку входа этой DLL. В случае успеха серверный поток вызывает точку входа DLL; в противном случае сервер создает процесс аналогично тому, как это делалось в программе serverNP. Пример DLL приведен в программе 12.3. Поскольку генерация исключений библиотекой DLL будет приводить к уничтожению всего серверного процесса, вызов функции DLL защищен простым обработчиком исключений.

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

Поскольку в коде сервера использованы специфические для Windows возможности, в частности, возможности управления потоками и некоторые другие, он, в отличие от кода клиента, оказывается привязанным к Windows.

Программа 12.2. serverSK: сервер на основе сокета с внутрипроцессными серверами
/* Глава 12. Клиент-серверная система. ПРОГРАММА СЕРВЕРА. ВЕРСИЯ НА ОСНОВЕ СОКЕТА. */
/* Выполняет указанную в запросе команду и возвращает ответ. */
/* Если удается обнаружить точку входа разделяемой библиотеки, команды */
/* выполняются внутри процесса, в противном случае – вне процесса. */
/* ДОПОЛНИТЕЛЬНАЯ ВОЗМОЖНОСТЬ: argv может содержать имя библиотеки */
/* DLL, поддерживающей внутрипроцессные серверы. */

#include "ClntSrvr.h" /* Определяет структуру записей запроса и ответа. */

/* Адресная структура сокета сервера. */
struct sockaddr_in ConnectSAddr; /* Подключенный сокет. */
WSADATA WSStartData; /* Структура данных библиотеки сокета. */

typedef struct SERVER_ARG_TAG { /* Аргументы серверного потока. */
/* Пояснения содержатся в комментариях к основному потоку. */
HINSTANCE dlhandle; /* Дескриптор разделяемой библиотеки. */

volatile static ShutFlag = FALSE;
static SOCKET SrvSock, ConnectSock;
int _tmain(DWORD argc, LPCTSTR argv) {
/* Прослушивающий и подключенный сокеты сервера. */
SERVER_ARG srv_arg;
/* Инициализировать библиотеку WSA; задана версия 2.0, но будет работать и версия 1.1. */
WSAStartup(MAKEWORD(2, 0), &WSStartData);
/* Открыть динамическую библиотеку команд, если ее имя указано в командной строке. */
if (argc > 1) hDll = LoadLibrary(argv);
/* Инициализировать массив arg потока. */
for (ith = 0; ith < MAXCLIENTS; ith++) {
srv_arg.dlhandle = hDll;
/* Следовать стандартной процедуре вызова последовательности функций socket/bind/listen/accept клиентом. */
SrvSock = socket(AF_INET, SOCK_STREAM, 0);
SrvSAddr.sin_family = AF_INET;
SrvSAddr.sin_addr.s_addr = htonl(INADDR_ANY);
SrvSAddr.sin_port = htons(SERVER_PORT);
bind(SrvSock, (struct sockaddr *)&SrvSAddr, sizeof SrvSAddr);
listen(SrvSock, MAX_CLIENTS);

/* Основной поток становится потоком прослушивания/соединения/контроля.*/
/* Найти пустую ячейку в массиве arg потока сервера. */
/* параметр состояния: 0 – ячейка свободна; 1 – поток остановлен; 2 - поток выполняется; 3 – остановлена вся система. */
for (ith = 0; ith < MAX_CLIENTS && !ShutFlag;) {
if (srv_arg.status==1 || srv_arg.status==3) { /* Выполнение потока завершено либо обычным способом, либо по запросу останова. */
WaitForSingleObject(srv_arg.srv_thd INFINITE);
CloseHandle(srv_arg.srv_tnd);
if (srv_arg.status == 3) ShutFlag = TRUE;
else srv_arg.status = 0;
/* Освободить ячейку данного потока. */
if (srv_arg.status == 0 || ShutFlag) break;
ith = (ith + 1) % MAXCLIENTS;
/* Прервать цикл опроса. */
/* Альтернативный вариант: использовать событие для генерации сигнала, указывающего на освобождение ячейки. */
/* Ожидать попытки соединения через данный сокет. */
/* Отдельный поток для опроса флага завершения ShutFlag. */
hAcceptTh = (HANDLE)_beginthreadex(NULL, 0, AcceptTh, &srv_arg, 0, &ThId);
tstatus = WaitForSingleObject(hAcceptTh, CS_TIMEOUT);
if (tstatus == WAIT_OBJECT_0) break; /* Соединение установлено. */
hAcceptTh = NULL; /* Подготовиться к следующему соединению. */
_tprintf(_T("Остановка сервера. Ожидание завершения всех потоков сервера\n"));
/* Завершить принимающий поток, если он все еще выполняется. */
/* Более подробная информация об используемой логике завершения */
/* работы приведена на Web-сайте книги. */
if (hDll != NULL) FreeLibrary(hDll);
if (hAcceptTh != NULL) TerminateThread(hAcceptTh, 0);
/* Ожидать завершения всех активных потоков сервера. */
for (ith = 0; ith < MAXCLIENTS; ith++) if (srv_arg .status != 0) {
WaitForSingleObject(srv_arg.srv_thd, INFINITE);
CloseHandle(srv_arg.srv_thd);

static DWORD WINAPI AcceptTh(SERVER_ARG * pThArg) {
/* Принимающий поток, который предоставляет основному потоку возможность опроса флага завершения. Кроме того, этот поток создает серверный поток. */
AddrLen = sizeof(ConnectSAddr);
pThArg->sock = accept(SrvSock, /* Это блокирующий вызов. */
(struct sockaddr *)&ConnectSAddr, &AddrLen);
/* Новое соединение. Создать серверный поток. */
pThArg->srv_thd = (HANDLE)_beginthreadex (NULL, 0, Server, pThArg, 0, &ThId);
return 0; /* Серверный поток продолжает выполняться. */

static DWORD WINAPI Server(SERVER_ARG * pThArg)
/* Функция серверного потока. Поток создается по требованию. */
/* Каждый поток поддерживает в стеке собственные структуры данных запроса, ответа и регистрационных записей. */
/* … Стандартные объявления из serverNP опущены … */
int (*dl_addr)(char *, char *);
char *ws = " \0\t\n"; /* Пробелы. */
GetStartupInfo(&StartInfoCh);
/* Создать имя временного файла. */
sprintf(TempFile, "%s%d%s", "ServerTemp", pThArg->number, ".tmp");
while (!Done && !ShutFlag) { /* Основной командный цикл. */
Disconnect = ReceiveRequestMessage(&Request, ConnectSock);
Done = Disconnect || (strcmp(Request.Record, "$Quit") == 0) || (strcmp(Request.Record, "$ShutDownServer") == 0);
/* Остановить этот поток по получении команды "$Quit" или "$ShutDownServer". */
hTrapFile = CreateFile(TempFile, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &TempSA, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
/* Проверка наличия этой команды в DLL. Для упрощения команды */
/* разделяемой библиотеки имеют более высокий приоритет по сравнению */
/* с командами процесса. Прежде всего, необходимо извлечь имя команды.*/
i = strcspn(Request.Record, ws); /* Размер лексемы. */
memcpy(sys_command, Request.Record, i) ;
dl_addr = NULL; /* Будет установлен в случае успешного выполнения функции GetProcAddress. */
if (pThArg->dlhandle != NULL) {/* Проверка поддержки "внутрипроцессного" сервера. */
dl_addr = (int (*)(char *, char *))GetProcAddress(pThArg->dlhandle, sys_command);
/* Защитить серверный процесс от исключений, возникающих в DLL*/
(*dl_addr)(Request.Record, TempFile);
} __except (EXCEPTION_EXECUTE_HANDLER) {
ReportError(_T("Исключение в DLL"), 0, FALSE);
if (dl_addr == NULL) { /* Поддержка внутрипроцессного сервера отсутствует. */
/* Создать процесс для выполнения команды. */
/* … То же, что в serverNP … */
} /* Конец основного командного цикла. Получить следующую команду. */
/* Конец командного цикла. Освободить ресурсы; выйти из потока. */
_tprintf(_T("Завершение работы сервера# %d\n"), pThArg->number);
if (strcmp(Request.Record, "$ShutDownServer") == 0) {

Замечания по поводу безопасности

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

Полное обсуждение методов построения безопасных систем выходит за рамки данной книги. Тем не менее, в главе 15 показано, как обезопасить объекты Windows, а в упражнении 12.14 предлагается воспользоваться протоколом SSL.

Внутрипроцессные серверы

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

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

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

Аналогично, функции не должны перенаправлять стандартный ввод и вывод.

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

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

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

В программе 12.3 представлена небольшая библиотека DLL, включающая две функции.

Программа 12.3. command: пример внутри процессных серверов
/* Глава 12. commands.с. */
/* Команды внутрипроцессного сервера для использования в serverSK и так далее. */
/* Имеется несколько команд, реализованных в виде библиотек DLLs. */
/* Функция каждой команды принимает два параметра и обеспечивает */
/* безопасное выполнение в многопоточном режиме. Первым параметром */
/* является строка: команда arg1 arg2 … argn */
/* (то есть обычная командная строка), а вторым – имя выходного файла. … */

static void extract_token(int, char *, char *);

int wcip(char * command, char * output_file)
/* Счетчик слов; внутрипроцессный. */
/* ПРИМЕЧАНИЕ: упрощенная версия; результаты могут отличаться от тех, которые обеспечивает утилита wc. */
while ((c = fgetc(fin)) != EOF) {
/* … Стандартный код - для данного примера не является существенным … */
/* Записать результаты. */
fprintf(fout, " %9d %9d %9d %s\n", nl, nw, nc, input_file);

int toupperip(char * command, char * output_file)
/* Преобразует входные данные к верхнему регистру; выполняется внутри процесса. */
/* Вторая лексема задает входной файл (первая лексема – "toupperip"). */
extract_token(1, command, input_file);
fin = fopen(input_file, "r");
fout = fopen(output_file, "w");
while ((c = fgetc (fin)) != EOF) {
if (isalpha(c)) с = toupper(c);

static void extract_token(int it, char * command, char * token) {
/* Извлекает из "команды" лексему номер "it" (номером первой лексемы */
/* является "0"). Результат переходит в "лексему" (token) */
/* В качестве разделителей лексем используются пробелы. … */

Ориентированные на строки сообщения, точкив хода DLL и TLS

Программы serverSK и clientSK взаимодействуют между собой, обмениваясь сообщениями, каждое из которых состоит из 4-байтового заголовка, содержащего размер сообщения, и собственно содержимого. Обычной альтернативой такому подходу служат сообщения, отделяемые друг от друга символами конца строки (или перевода строки).

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

В более общей формулировке, мы сталкиваемся здесь с проблемой сохранения долговременных состояний в многопоточной среде (multithreaded persistent state problem). Эта проблема возникает всякий раз, когда безопасная в отношении многопоточного выполнения функция должна поддерживать сохранение некоторой информации от одного вызова функции к другому. Такая же проблема возникает при работе с функцией strtook, входящей в стандартную библиотеку С, которая предназначена для просмотра строки для последовательного нахождения экземпляров определенной лексемы.

Решение проблемы долговременных состояний в многопоточной среде

В искомом решении сочетаются несколько компонентов:

Библиотека DLL, в которой содержатся функции, обеспечивающие отправку и прием сообщений.

Функция, представляющая точку входа в DLL.

Локальная область хранения потока (TLS, глава 7). Подключение процесса к библиотеке сопровождается созданием индекса DLL, а отключение - уничтожением. Значение индекса хранится в статическом хранилище, доступ к которому имеют все потоки.

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

Таким образом, TLS играет роль статического хранилища, и у каждого потока имеется собственная уникальная копия этого хранилища.

Пример: безопасная многопоточная DLL для обмена сообщениями через сокет

Программа 12.4 представляет собой DLL, содержащую две функции для обработки символьных строк (в именах которых в данном случае присутствует "CS", от character string - строка символов), или потоковые функции сокета (socket streaming functions): SendCSMessage и ReceiveCSMessage, а также точку входа DllMain (см. главу 5). Указанные две функции играют ту же роль, что и функция ReceiveMessage, а также функции, использованные в программах 12.1 и 12.2, и фактически заменяют их.

Функция DllMain служит характерным примером решения проблемы долговременных состояний в многопоточной среде и объединяет TLS и библиотеки DLL.

Освобождать ресурсы при отсоединении потоков (случай DLL_THREAD_DETACH) особенно важно в случае серверной среды; если этого не делать, то ресурсы сервера, в конечном счете, исчерпаются, что может привести к сбоям в его работе или снижению производительности или к тому и другому одновременно.

Примечание

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

Использующие эту DLL коды клиента и сервера, незначительно измененные по сравнению с программами 12.1 и 12.2, доступны на Web-сайте книги.

Программа 12.4. SendReceiveSKST: безопасная многопоточная DLL
/* SendReceiveSKST.с - DLL многопоточного потокового сокета. */
/* В качестве разделителей сообщений используются символы конца */
/* строки ("\0"), так что размер сообщения заранее не известен. */
/* Поступающие данные буферизуются и сохраняются в промежутках между */
/* вызовами функций. */
/* Для этой цели используются локальные области хранения потоков */
/* (Thread Local Storage, TLS), обеспечивающие каждый из потоков */
/* собственным закрытым "статическим хранилищем". */

#include "ClntSrvr.h" /* Определяет записи запроса и ответа. */

/* "static_buf" содержит "static_buf_len" байтов остаточных данных. */
/* Символы конца строки (нулевые символы) могут присутствовать, а могут */
/* и не присутствовать. */
char static_buf ;

static DWORD TlsIx = 0; /* Индекс TLS – ДЛЯ КАЖДОГО ПРОЦЕССА СВОЙ ИНДЕКС.*/
/* Для однопоточной библиотеки использовались бы следующие определения:
static char static_buf ;
static LONG32 static_buf_len; */
/* Основная функция DLL. */

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
/* Для основного потока подключение отсутствует, поэтому во время подключения процесса необходимо выполнить также операции по подключению потока. */
/* Указать, что память не была распределена. */
return TRUE; /* В действительности это значение игнорируется. */
/* Отсоединить также основной поток. */

BOOL ReceiveCSMessage(REQUEST *pRequest, SOCKET sd) {
/* Возвращаемое значение TRUE указывает на ошибку или отсоединение. */
LONG32 nRemainRecv = 0, nXfer, k; /* Должны быть целыми со знаком. */
CHAR TempBuf;
p = (STATIC_BUF *)TlsGetValue(TlsIx);
if (p == NULL) { /* Инициализация при первом вызове. */
/* Распределять это хранилище будут только те потоки, которым оно */
/* необходимо. Другие типы потоков могут использовать TLS для иных целей. */
р = malloc(sizeof(STATIC_BUF));
if (p == NULL) return TRUE; /* Ошибка. */
p->static_buf_len = 0; /* Инициализировать состояние. */
/* Считать до символа новой строки, оставляя остаточные данные в статическом буфере. */
for (k = 0; k < p->static_buf_len && p->static_buf[k] != "\0"; k++) {
message[k] = p->static_buf[k];
} /* k – количество переданных символов. */
if (k < p->static_buf_len) { /* В статическом буфере обнаружен нулевой символ. */
p->static_buf_len –= (k + 1); /* Скорректировать состояние статического буфера. */
memcpy(p->static_buf, &(p->static_buf), p->static_buf_len);
return FALSE; /* Входные данные сокета не требуются. */

/* Передан весь статический буфер. Признак конца строки не обнаружен.*/
nRemainRecv = sizeof(TempBuf) – 1 – p->static_buf_len;
pBuffer = message + p->static_buf_len;
while (nRemainRecv > 0 && !Disconnect) {
nXfer = recv(sd, TempBuf, nRemainRecv, 0);
/* Передать в целевое сообщение все символы вплоть до нулевого, если таковой имеется. */
for (k =0; k < nXfer && TempBuf[k] != "\0"; k++) {
if (k >= nXfer) { /*Признак конца строки не обнаружен, читать дальше*/
} else { /* Обнаружен признак конца строки. */
memcpy(p->static_buf, &TempBuf, nXfer – k – 1);
p->static_buf_len = nXfer – k – 1;

BOOL SendCSMessage(RESPONSE *pResponse, SOCKET sd) {
/* Послать запрос серверу в сокет sd. */
nRemainSend = strlen(pBuffer) + 1;
while (nRemainSend > 0 && !Disconnect) {
/* Отправка еще не гарантирует, что будет отослано все сообщение. */
nXfer = send(sd, pBuffer, nRemainSend, 0);
fprintf(stderr, "\nОтключение сервера до посылки запроса завершения");