Что такое windows sockets. Сетевая библиотека Winsock. Клиентские функции сокета

Что такое windows sockets. Сетевая библиотека Winsock. Клиентские функции сокета

28.03.2019

Самоучитель игры на 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 Сетевеая библиотека 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. В качестве параметра нужно указать переменную-сокет.

Именованные каналы пригодны для организации межпроцессного взаимодействия как в случае процессов, выполняющихся на одной и той же системе, так и в случае процессов, выполняющихся на компьютерах, связанных друг с другом локальной или глобальной сетью. Эти возможности были продемонстрированы на примере клиент-серверной системы, разработанной в главе 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Отключение сервера до посылки запроса завершения");

Довольно теории, даешь WinSock!

Итак, сейчас в наличии имеются две версии WinSock: 1.1 и 2. Я предлагаю использовать вторую версию. Что же мы будем делать дальше? Мы напишем клиент-серверную версию игры "Камень-ножницы-бумага" Сервер будет мульти-потоковым консольным приложением, клиент будет написан на DirectX.

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

Для того чтобы использовать WinSock вы должны подключить соответствующий заголовочный файл и добавить ws2_32.lib в ваш проект. Теперь всё готово, чтобы программировать под WinSock. Но как он работает и с чего начать?

Существуют несколько способов программирования сокетов. Вы можете использовать базовые функции UNIX/Berkley, либо функции специализированные под Microsoft Windows, либо использовать объектно-ориентированную MFC версию сокетов. В начале я хотел использовать именно ОО версию сокетов, т.к. считаю, что классы делают API более удобоваримым. Но это не просто классы, это MFC. "Сделано так сложно, как только возможно" - это их лозунг. Нет, MFC это конечно здорово, но тот факт, что вы должны создать приложение Win32 и обрабатывать сокеты через Windows сообщения, посылаемые вашей программе, огорчает, особенно в случае сервера. Зачем нам делать сервер как приложение Win32? Это бессмысленно. Чтобы упростить себе задачу, мы будем использовать самые основные UNIX/Berkley-функции для сервера.

Типы данных.

Sockaddr_in(sockaddr)

Описание: тип sockaddr_in используется для описания соединения через сокеты. Он также содержит в себе IP-адрес и номер порта. Это TCP ориентированная версия sockaddr . Мы будем использовать sockaddr_in для создания сокетов.

struct sockaddr_in { short sin_family; // тип протокола (должен быть - AF_INET) u_short sin_port; // Номер порта сокета struct in_addr sin_addr; // IP-адрес char sin_zero[ 8 ] ; // не используется };

Описание: WSAData используется тогда, когда вы загружаете и инициализируете библиотеку ws2_32.dll . Этот тип используется в качестве возвращаемого значения функцией WSAStartup() . Используйте её для определения версии WinSock на компьютере.

Описание: Этот тип данных используется для хранения дескриптора сокета. Эти дескрипторы используются для идентификации сокета. По правде говоря, SOCKET это всего лишь unsigned int.

Ну, что же, начнем программировать

Для начала нужно загрузить ws2_32.dll:

// Этот код должен присутствовать в любой программе, использующей WinSock WSADATA w; // используется для хранения информации о версии сокета int error = WSAStartup (0x0202 , &w) ; // заполняем w if (error) { // какая-то ошибка return ; } if (w.wVersion != 0x0202 ) { // не та версия сокетов! WSACleanup () ; // выгружаем ws2_32.dll return ; }

У вас наверняка возник вопрос - что значит 0x0202? Это значит версия 2.2. Если понадобится версия 1.1, то это число нужно изменить на 0x0101. WSAStartup() заполняет переменную типа WSADATA и загружает динамическую библиотеку сокетов. WSACleanup(), соответственно, её выгружает.

Создаём сокет (create a socket)

SOCKET s = socket (AF_INET, SOCK_STREAM, 0); // Создаём сокет

В общем-то, это всё, что нужно для создания сокета, но необходимо ещё связать (bind ) его с некоторым портом, когда вы захотите непосредственно начать с ним работу. Константа AF_INET определена где-то в недрах winsock2.h. Если какая-нибудь функция потребует от вас что-то вроде семейства адреса (address family ) или int af , просто указывайте AF_INET. Константа SOCK_STREAM нужна для создания потокового (TCP/IP) сокета. Вы также можете создать UPD сокет, но как указывалось выше, он не так надёжен, как TCP. Значение последнего параметра будет нулём. Это всего лишь означает, что для вас будет автоматически выбран правильный протокол (это должен быть TCP/IP).

Назначаем нужный порт сокету (связываем (bind socket) порт и сокет):

// Запомните! Вам следует связывать только сокеты сервера, а не клиента // Функция WSAStartup вызвана sockaddr_in addr; // переменная для TCP сокета addr.sin_family = AF_INET; // Семейство адресов - Internet addr.sin_port = htons (5001 ) ; // Назначаем 5001 порт сокету addr.sin_addr.s_addr = htonl (INADDR_ANY) ; // Без конкретного адреса if (bind(s, (LPSOCKADDR) &addr, sizeof (addr) ) == SOCKET_ERROR) { // ошибка WSACleanup () ; // выгружаем WinSock return ; }

Этот код может показаться запутанным, но это не так. Addr описывает сокет, определяя порт. Но может возникнуть вопрос: "А как же насчёт IP-адреса?". Мы установили его как INADDR_ANY. Это позволит нам не заботиться о конкретном адресе. Нам нужно лишь обозначить номер порта, который мы хотим использовать для соединения. Почему мы используем htons() и htonl()? Эти функции преобразуют переменные типа short и long, соответственно, к формату понятному для сети. Например, если номер порта 7134 (число типа short), то необходимо вызвать функцию htons(7134). Для IP-адреса нам приходится использовать htonl(). Но что если мы захотим фактически задать IP-адрес? Мы должны использовать функцию inet_addr(). Например, inet_addr ("129.42.12.241"). Эта функция преобразует строчку адреса, удаляет из неё точки и приводит её к типу long.

Прослушивание связанного порта (listen port )

// WSAStartup () вызвана // SOCKET s уже указывает на созданный сокет if (listen(s,5 ) ==SOCKET_ERROR) { // ошибка! Прослушивание не возможно WSACleanup () ; return ; } // прослушиваем:

Здесь мы приняли соединение от клиента, который хочет вступить игру. Есть ещё кое-что интересное в строчке listen (SOCKET s, int backlog). Что такое backlog ? Backlog - это число клиентов, которые могут подсоединиться, пока мы используем сокет, т.е. этим клиентам придётся подождать, пока сервер договорится о соединении с другими клиентами. Например, если вы указываете в качестве backlog-а 5, а пытается присоединиться 7 человек, то последние 2 получат сообщения об ошибке и будут вынуждены устанавливать соединение позднее. Обычно, этот параметр лежит в интервале от 2 до 10, в зависимости от максимальной вместимости сервера.

Попытка соединения с сокетом (try & connect socket )

// WSAStartup () вызвана // SOCKET s уже указывает на созданный сокет // s связан с портом с помощью sockaddr_in. sockaddr_in target; target.sin_family = AF_INET; // семество адресов - Интернет target.sin_port = htons (5001 ) ; // порт сервера target.sin_addr.s_addr = inet_addr ("52 .123 .72 .251 ") ; // IP-адрес сервера if (connect(s, target, sizeof (target) ) == SOCKET_ERROR) { // ошибка соединения WSACleanup () ; return ; }

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

Приём соединения(accepting a connection )

// WSAStartup () вызвана // SOCKET s уже указывает на созданный сокет // s связан с портом с помощью sockaddr_in. // сокет s прослушивается #define MAX_CLIENTS 5 ; // только для ясности int number_of_clients = 0 ; SOCKET client[ MAX_CLIENTS] ; // сокеты клиентов sockaddr client_sock[ MAX_CLIENTS] ; // описание клиентских сокетов while (number_of_clients < MAX_CLIENTS) // подсоединилось ли MAX_CLIENTS клиентов? { client[ number_of_clients] = // принимаем запрос соединения accept (s, client_sock[ number_of_clients] , &addr_size) ; if (client[ number_of_clients] == INVALID_SOCKET) { // ошибка соединения WSACleanup () ; return ; } else { // клиент успешно присоединился // запускаем поток для связи с клиентом startThread (client[ number_of_clients] ) ; number_of_clients++; } }

В общем-то, здесь и так всё ясно. Количество клиентов не обязательно задавать выражением MAX_CLIENTS. Здесь оно используется только в целях повышения ясности и простоты восприятия этого кода. number_of_clients - переменная, которая содержит число подсоединившихся клиентов. client - массив типа SOCKET, который используется для хранения дескрипторов сокетов подсоединившихся клиентов. client_sock - массив типа sockaddr , содержащий информацию о типе соединения, номере порта и др. Обычно, он нам не очень нужен, хотя некоторые функции требуют описание соединения как параметр. Основной цикл всего лишь ожидает запроса на соединение, затем принимает его и запускает поток для общения с клиентом.

Отсылка данных (writing or sending )

// SOCKET s инициализирован char buffer[ 11 ] ; // буффер из 11-ти символов sprintf (buffer, "Whatever:") ; send (s, buffer, sizeof (buffer) , 0 ) ;

Второй параметр функции send() - это переменная типа char FAR *buf , которая является указателем на буфер с данными, который мы хотим отослать. Третий параметр - это размер отсылаемого буфера. Последний параметр для выставления различных флагов. Мы не будем его использовать и оставим нулём.

Приём данных (reading or receiving )

// SOCKET s инициализирован char buffer[ 80 ] ; // буфер из 80 символов recv (s, buffer, sizeof (buffer) , 0 ) ;

recv () , в значительной степени, похожа на send () , за исключением того, что мы не передаем данные, а принимаем.

Преобразования строкового адреса (IP-адреса или имени хоста) в числовой адрес, используемый при соединении (resolving IP-address ).

Сетевеая библиотека 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. В качестве параметра нужно указать переменную-сокет.



© 2024 beasthackerz.ru - Браузеры. Аудио. Жесткий диск. Программы. Локальная сеть. Windows