.::WinSockets на блюдечке::.

 

Мы - главное зло рунета >:|

Winsockets на блюдечке.

Особенности создания сетевых приложений в Windows.

Автор: xh4ck (vol_e@mail.ru, 619327)

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Часто приходится писать программы, работающие с сетью. Сделать простенького клиента и сервера не составляет труда - можно воспользоваться компонентами. Другое дело, когда программа должна иметь минимальный размер и код ее не должен сильно зависеть от языка. Здесь нам на выручку приходит АПИ для работы с сетью, именуемый в среде Windows - WINSOCKETS API.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
А есть ли смысл?
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Давайте определимся: стоит ли овчинка выделки? Стоит ли API трудовременных затрат на его изучение, освоение и применение? Для этого выделим основные плюсы и минусы использования API. Начнем с положительного, то есть с плюсов. Первый плюс – независимость. Зависеть от сторонних библиотек не очень-то приятно, конечно, можно прилинковать весь необходимый код к исполняемому файлу, но тогда на выходе мы получим неповоротливого монстра. Так сразу вырисовывается и второй плюс - размер. Третий пункт в списке достоинств использования API - это переносимость между языками. Например, код, написанный на С и использующий WINSOCKETS API, можно без труда переписать на Pascal. При этом изменится только синтаксис - реализация останется той же. Конечно, не всегда все так просто, но незначительные мелочи я не учитываю. Ну и, наконец, почти полная свобода действий, можно делать, так как захочется, а не так как заложено в компоненте. Недостаток при использовании WINSOCKETS API только один - время разработки приложения. И то, при должном знании этого АПИ и при некотором опыте работы с ним, этот недостаток уже не становится таким уж большим. Итак, решено: изучаем WINSOCKETS API, а заодно и принципы создания сетевых приложений в Windows.

Здесь мы разберемся с основными функциями WINSOCKETS API, с методами работы с ними, подкрепляя все это примерами. Лучшего примера я и придумать не мог, чем собственный сокетный класс. В этом классе используются лишь функции API, это поможет тебе лучше разобраться в WINSOCK API и понять суть.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Getting started
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

WINSOCKETS API (полное имя - Windows Sockets 2 API), что же это такое? WINSOCKETS API - грубо говоря, набор функций для работы с виндовыми сокетами. Windows Sockets использует идею, впервые появившуюся в UNIX, где сокет - это конечная точка сетевых коммуникаций. Каждый использующийся сокет имеет тип и ассоциированный с ним процесс. Два сокета, один для хоста-получателя, другой для хоста-отправителя, определяют соединение для протоколов, обеспечивающих последовательный, надежный, ориентированный на установление двухсторонней связи поток байтов, либо для протоколов, неориентированных на установление связи, при этом не гарантируется, что поток будет последовательным и надежным, и что данные не будут дублироваться. Хорошо, думаю, теории уже хватит, перейдем ближе к делу. Как вы уже, наверное, догадались, писать мы все будем на С++, но выбор языка особого значения в данном случае не имеет, так как АПИ - он и в бейсике АПИ ;). Для создания полноценного сетевого приложения (пока без интерфейса) нам потребуется лишь один заголовочный файл - winsock2.h, также нам потребуется подключить к проекту lib-файл для использования WINSOCK 2 API функций. Если у тебя по каким-то причинам нет файла ws2_32.lib, то ты можешь изготовить его самостоятельно из ws2_32.dll. Начало программы должно выглядеть примерно так: 

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

#include <winsock2.h>
#pragma comment(lib, "ws2_32.lib")

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Перед тем как что-либо начать делать с WinSockets, вызовем функцию инициализации WSAStartup. Функция принимает два параметра: версию сокетов и указатель на структуру WSADATA. Версия сокетов имеет тип WORD и разделена на 2 части: minor и major version, которые находятся, соответственно, в нижней и верхней части слова (WORD). Чтобы было удобнее заполнять эти поля, рекомендую использовать макрос MAKEWORD(x,y). По окончании работы с WinSockets нужно вызвать WSACleanup(), которая освободит все ресурсы, занятые WinSockets. В целом программа, использующая WINSOCK API, имеет такой вид:

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

WSADATA wdata;
WSAStartup(MAKEWORD(2,2), &wdata);
/* Тут работаем с сетью, организовываем прием/передачу и прочее */
WSACleanup();

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
In action
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Для того, чтобы начать работу с сокетами нужно их сначала создать, делается это функцией socket. Она создает сокет для выбранного протокола. Выбрать какого типа будет сокет - ориентированный или нет на установление двухсторонней связи, можно меняя второй параметр на SOCK_STREAM и SOCK_DGRAM соответственно. Здесь я рассматриваю работу только с протоколами TCP и UDP, про остальные ищи инфу сам ;).

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); // TCP-Сокет
socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); // UDP-Сокет

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Функция socket создает блокирующий сокет. Созданный сокет, после того как тот отработал, нужно закрывать функцией closesocket, при этом закрывается соединение и освобождается хендл сокета. Всего существует два типа сокетов: блокирующие и неблокирующие. Блокирующие сокеты полностью блокируют поток, в котором они работают: скажем, вызвал ты функцию приема входящих подключений accept (здесь я немного забегаю вперед) и пока кто-нибудь не попытается подключиться ты ничего не сможешь сделать в этом потоке. В неблокирующих сокетах ты можешь поставить прослушку порта и, не растрачивая драгоценное время продолжить работу, а узнать о том, что к серверу ломятся клиенты можно, получив от системы сообщение, либо, обработав Event. Сервер, основанный на блокирующих сокетах, и обрабатывающий несколько клиентов одновременно, создает на каждого клиента свою нить (thread. так уж повелось, что нити у нас называют еще и потоками). Сервер на неблокирующих сокетах обрабатывает всех клиентов в одной нитке (одном потоке). Какой тип сокета выбрать - дело вкуса и обстоятельств. Выбор абсолютно никак не скажется на надежности и скорости работы (это при условии, что руки растут из того места, из которого нужно). Все различия реализации двух типов сокетов я постараюсь объяснить.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Клиент или сервер?
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Только что, созданный connection-based сокет, можно использовать либо для приема входящих подключений (сервер), либо, для соединения с удаленным хостом (клиент). У connectionless сокетов нет такого понятия как сервер или клиент, им не нужно устанавливать двухсторонне соединение. Отправил данные и все, а уж дошли они или нет, это не его проблема. Начнем с connection-based сокетов. 

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Клиент
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Сделать клиента не так уж сложно - вызываем функцию connect, не забыв указать адрес сервера, к которому хотим подключиться, и все - можно организовывать прием/передачу. Функция принимает три параметра: хендл сокета, структуру с адресом типа SOCKADDR и размер этой структуры. Структура SOCKADDR используется не только в функции connect, но и во многих других функциях WinSockets, поэтому важно уметь ее заполнять, а заполнять ее очень неудобно, поэтому заполнять мы будем структуру SOCKADDR_IN и затем приведем ее к типу SOCKADDR. Поле sin_family структуры SOCKADDR_IN содержит тип семейства протокола, мы будем использовать PF_INET. Поле sin_port должно содержать порт. Но прежде чем заполнять это поле, значение номера порта необходимо перевести в такое значение, байты которого расставлены в прямом порядке (network byte order), юзай функцию htons. Поле sin_addr предназначено для хранения ip-адреса. Предположим, у тебя уже есть IP-адрес сервера в виде строчки, тогда тебе осталось лишь заюзать функцию inet_addr, которая переведет ip-шник из строкового формата в числовой. Полученное число забивай в поле sin_addr.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Сервер
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Перед тем как начать принимать входящие подключения, необходимо забиндить для сокета порт и установить режим прослушки порта, а уж при обнаружении попытки подключения, выбирать: принимаем или нет. Чтобы назначить сокету локальный порт, вызовем функцию bind. IP у нас будет локальный, поэтому в поле sin_addr структуры SOCKADDR_IN можно забить INADDR_ANY, чтобы указать, что биндимся мы на локальный комп ;). Не думай, что функция bind предназначена лишь для серверов, ее можно использовать и на клиенте. Все дело в том, что сокет имеет четыре параметра: локальный порт, локальный адрес, удаленный порт, удаленный адрес. Так вот, вызывая connect, мы задаем лишь удаленный адрес и порт, локальный порт будет иметь значение от 1025 до 65535 и будет выбираться из свободных в этом диапазоне. Функция bind позволяет жестко застолбить за собой определенный локальный порт, чтобы его значение не было случайным, а определенным заранее. Это может пригодиться в некоторых случаях. 

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
listen
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Порт указали, теперь вызываем функцию listen и "слушаем" его. Первый параметр - хендл сокета, второй параметр указывает на размер очереди ожидающих подключения клиентов, эта очередь называется backlog. Допустим, ты установил backlog = 3, тогда при подключении одновременно четырех клиентов три станут в очередь ожидания, а четвертый получит ошибку WSAECONNREFUSED. Можешь установить backlog равным SOMAXCONN, чтобы обеспечить поддержку максимально большой очереди ожидания.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
accept
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

После начала прослушки начинаются различия между блокирующими и неблокирующими сокетами. Для блокирующих сокетов можно сразу вызывать функцию accept, для приема входящих подключений, при этом нить, в которой была вызвана эта функция, заблокируется пока клиент не попытается подключиться. Первый параметр функции accept - как обычно, хендл сокета, второй и третий параметры для получения адреса клиента (как на конверте "От кого"). При этом значение порта будет в "network byte order" и если ты его будешь использовать, то изволь ее сконвертить в нормальный вид функцией ntohs. Функция accept возвращает хендл свежесозданного сокета. Сокет имеет те же настройки протокола, тот же тип синхронизации (блокирующий или неблокирующий), что и сокет, от которого он был создан. Но не стоит вызывать accept сразу после listen, если мы хотим неблокирующий тип синхронизации. accept нужно вызывать только после получения уведомления о том, что к серверу пытаются подключиться. Давай определимся, как мы хотим получать уведомления о событиях происходящих с неблокирующим сокетом. Тут есть два варианта: с помощью оконных сообщений (WSAAsyncSelect) и с помощью объектов синхронизации (WSAEventSelect). Настраивать сокет на мессаги целесообразно, если у твоего сервера есть интерфейс и обработчик очереди сообщений, в противном случае тебе придется использовать события. Если ты еще ни разу не работал с событиями в системе - ни чего страшного, я подробно остановлюсь на каждом шаге.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
MSG
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Процесс передачи, приема и обработки оконных сообщений уже стал привычен для Windows-программистов, к тому же такой способ нотификации еще и очень удобен. Для того чтобы работать с оконными сообщениями нам понадобится окно, если программа абсолютно без интерфейса, то можно создать скрытое окно (правда, такой способ мне не нравится), а в случае программы с интерфейсом, пренебрегать этим методом нотификации не стоит. Функция WSAAsyncSelect позволяет связать сокет с выбранным тобой оконным сообщением. Оконные сообщения желательно делать с числом большим WM_USER, т.е. твое сообщение = WM_USER + некое число, сообщение можно зарегистрировать в системе, чтобы им не воспользовалась другая программа (RegisterWindowMessage). В качестве четвертого параметра указываем события, при которых система будет отсылать мессадж твоей программе. Обрабатывать сообщения следует, как и все остальные мессаги, не относящиеся к WinSockets. А информация о том, какое именно событие произошло, из выбранных тобой, записывается в lParam. Причем в верхнем слове lParam находится код ошибки, а в нижнем код события. Используй предопределенные макросы WSAGETSELECTERROR(lParam) и WSAGETSELECTEVENT(lParam) для доступа к верхнему и нижнему слову. Если ты больше не хочешь принимать сообщения нотификации состояния сокета, то вызывай WSAAsyncSelect с нулевыми последними параметрами.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Synchronization objects
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

OK, если тебя по каким-то причинам не устраивает использование оконных сообщений, то у тебя, как всегда, есть выбор - юзай объекты синхронизации. Для начала создадим этот самый объект, вызвав функцию WSACreateEvent. Связать объект и сокет поможет функция WSAEventSelect, которая переведет сокет в неблокирующий режим и назначит события, указанные в третьем параметре. В отличие от оконных сообщений, Event'ы являются объектами ядра, и при возникновении события система устанавливает объект в активный режим. Как мы узнаем, что объект выставился в сигнализированный режим? Да просто подождем. Функция WSAWaitForMultipleEvents как раз для этого предназначена. Она может ожидать не один объект, а сразу несколько, сделано это для того чтобы ты мог отслеживать события на разных сокетах (поддержка нескольких клиентов одновременно). Можно выбрать, чтобы ожидание не прерывалось, пока не установится все объекты. Ждать можно не бесконечно, указать величину своего терпения можно в четвертом параметре (WSA_INFINITE или -1 для бесконечности). Последний параметр оставляем false. Если WSAWaitForMultipleEvents не вернула ошибку, то значит, самое время определить какое событие сработало из тех, что мы указывали в WSAEventSelect. Вызываем WSAEnumNetworkEvents, не забыв указать третьим параметром, указатель на структуру WSANETWORKEVENTS, она нам еще пригодится. После вызова функции, в поле lNetworkEvents структуры WSANETWORKEVENTS будет содержаться номер свершившегося события, если тебе будет не лень сделать проверку ошибок (читай, тебе придется ее сделать), то тебе поможет поле iErrorCodes. Если ты вдоволь наигрался с сокетами, не забудь вызвать WSACloseEvent, дабы освободить занятые ресурсы.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Прием
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Для приема данных в WinSockets есть функция recv. Про первый параметр я уже молчу, второй - указатель на буфер, в который будут читаться данные. Третий - размер данных, которые мы хотим принять, на четвертый забиваем болт, он нам не нужен (пока). Вызов функции recv отличается при использовании блокирующих и неблокирующих сокетов. С блокирующими для приема данных просто вызываем функцию recv и ждем прихода ;). Гланое тут не наступить на очень больно бьющие грабли, связанные с тем, что ожидание прерывается, если придет хоть один байт, а не столько, сколько ты запросил в recv. Поэтому прием лучше организовать в виде цикла. Если это текст, то принимаем его до контрольного символа (обычно нуль-символ), если просто данные то тут уже необходимо позаботиться о том, чтобы нам сначала прислали размер этих данных, чтобы знать, сколько точно предстоит принять. Есть, конечно, функция ioctlsocket, которая позволяет узнать количество данных, находящихся в буфере сокета, но полученная величина является неточной и полагаться на нее не стоит. 

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

// Принимаем текстовую информацию
// И помещаем ее в динамический строковый буфер
char * buff = (char *) LocalAlloc(LPTR, 1);
for (int i = 0;;)
{
res = recv(sck, &buff[i], 1, 0);

// тут желательно сделать проверку возвращаемого значения WSAGetLastError
if (!res || res == SOCKET_ERROR)
{
Close();
buff[i] = '\0';
break;
}

if (buff[i] == 0) break;

buff = (char *) LocalReAlloc(buff, ++i + 1, LMEM_MOVEABLE);
}

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

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

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Передача
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

В нашем нелегком деле придется не только принимать данные, но и отправлять их, для этого в WinSockets предусмотрена функция send. Параметры у этой функции те же, что и у recv, различие лишь в том, что буфер должен быть уже заполнен данными, которые ты собираешься отослать. Опять же, в реализации отправки для блокирующих и неблокирующих сокетов существуют значительные отличия. В блокирующих, как всегда, все просто: вызываем send, она сразу же возвращает количество "отправленных" данных, если очередь отправки забита до отказа, то произойдет блокировка, пока не освободится внутренний буфер отправки. На самом деле данные могут еще не дойти до клиента, а функция уже успешно завершится. Данные просто копируются во внутренний буфер, и только потом начинается отправка. send лишь дает нам знать, что данные поставлены в очередь отправки. Это нужно учитывать. В неблокирующих сокетах отправку делать несколько сложнее, дело в том, что, если буфер отправки занят, то функция вернет нам ошибку WSAEWOULDBLOCK, поэтому придется ловить момент, когда этот самый буфер освободиться. Для этого подождем события FD_WRITE (способы ожидания событий описаны выше) и попытаемся отправить данные снова. Замечу, что события FD_WRITE может и не быть вовсе (конечно оно есть всегда - появляется один раз, сразу после установки соединения), если ты не попытаешься отправить большой объем данных. Поэтому вызывай сначала send, а затем, если произошла ошибка, жди FD_WRITE и в обработчике события отправляй данные.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

// Обработчик события FD_WRITE
if (NEvents.lNetworkEvents == FD_WRITE)
{
// Если нам есть что отправлять - отправляем
if (dwDataLen != 0)
{
int res = send(sck, (char *) pDataToSend, dwDataLen, 0);

if (res == dwDataLen)
{
// Данные были скопированы во временный буффер,
// Чтобы не получилось так: буфер уже уничтожен, 
// а данные еще не ушли.
LocalFree(pDataToSend);
dwDataLen = 0;
}
}
}

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
UDP
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Как я и обещал в начале статьи, мы будем работать не только с TCP сокетами, но и с UDP. Работа с этим протоколом значительно отличается от работы с TCP, здесь тебе самому придется проверять: дошли данные или нет - никаких гарантий этот протокол не предоставляет. Я лишь опишу возможности WinSockets API, предоставляемые для протоколов такого типа, а о проблемах конкретной реализации заботься сам, здесь нет единого универсального метода. Сокеты этого типа называются дейтаграммными, и еще connectionless сокетами. Как я уже и сказал, у connectionless сокетов нет ни серверов, ни клиентов, им не нужно ждать входящих подключений или к кому-то подключаться, следовательно, юзать функции bind и connect смысла мало (на таких сокетах эти функции ведут себя несколько по иному, хотя в определенных случаях это даже удобно). Для таких сокетов созданы функции sendto и recvfrom. Первая отправляет данные на указанный адрес, а вторая принимает их. Данные здесь называются сообщениями и размер одного сообщения не должен превышать SO_MAX_MSG_SIZE (обычно 8кб), если ты все же решишь отправить за один присест несколько мегов, то функция вернет ошибку WSAEMSGSIZE, и данные не отправятся, обом-с ;). С приемом аналогично: если размер данных превышает размер буфера для данных, то в UDP лишние данные просто отбрасываются, и устанавливается ошибка WSAEMSGSIZE. Функции sendto и recvfrom можно использовать и на SOCK_STREAM сокетах, при этом последние два параметра просто игнорируются и функции работают почти так же как send и recv. С синхронизацией у SOCK_DGRAM дела обстоят точно также как и у SOCK_STREAM сокетов, т.е. они, также могут быть блокирующими и неблокирующими, также можно вызывать для сокетов функции WSAEventSelect и WSAAsyncSelect.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Disconnected
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

Мое повествование подходит к концу, здесь я изложил основные приемы работы с WinSockets и методы создания несложных сетевых приложений. Рассказать обо всех тонкостях я не смог бы даже при великом желании. За подробностями - в MSDN. Базовые знания я дал, а дальше сам. Глядишь, сегодня ты еще не разбираешься в параметрах функции setsockopt, а уже завтра напишешь собственный Apache ;) Чтобы помочь тебе полнее понять все, о чем я написал, я сделал простенький клиент-серверный сокетный класс с поддержкой блокирующих и неблокирующих сокетов. Работа возможна только с одним клиентом одновременно (это не трудно исправить). Нотификация состояния неблокирующих сокетов у меня реализована посредством объектов синхронизации (Event'ы). Реализацию на оконных сообщениях я оставляю на тебя как домашнее задание. По любым вопросам, касающимся данной темы, ты запросто можешь обратиться ко мне по мылу или аське, я постараюсь помочь.

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Самую свежую информацию о WinSockets ты можешь узнать из MSDN (msdn.microsoft.com/library)
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Всегда проверяй значение, возвращаемое функцией, чтобы предусмотреть максимум возможных ошибок
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ
Почти все, что я описал, я реализовал в простеньком классе, исходники прилагаются.
ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ

ХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХХ


Скачать исходняки к статье


                                  (c) Hell  Knights Crew. Использование материалов hellknights.void.ru запрещено! Design by _1nf3ct0r_