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



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

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

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


Шаг 1.


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

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

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


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

Для инициализации Winsock вызываем функцию WSAStartup
Код:
int WSAStartup( WORD wVersionRequested, (in) LPWSADATA lpWSAData (out) );

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

Так это выглядит на практике:
Код:
   WSADATA ws;
   //...
   if (FAILED (WSAStartup (MAKEWORD( 1, 1 ), &ws) ) )
   {
      // Error...
      error = WSAGetLastError();
      //...
   }

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

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


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

Для начала объявим его:
Код:
   SOCKET s;

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

Пример:
Код:
   if (INVALID_SOCKET == (s = socket (AF_INET, SOCK_STREAM, 0) ) )
   {
      // Error...
      error = WSAGetLastError();
      // ...
   }

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

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


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

Для установки соединения используем функцию connect.
Код:
int connect(SOCKET s,                             // сокет (наш сокет)
            const struct sockaddr FAR *name,  // адрес
               int namelen                    // длинна адреса
           );

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

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

   // Дальше выполняем соединение:
   if (SOCKET_ERROR == ( connect (s, (sockaddr *) &s_addr, sizeof (s_addr) ) ) )
   {
      // Error...
      error = WSAGetLastError();
      // ...
   }

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

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


Для того что бы послать данные используем функцию send
Код:
int send(SOCKET s,              // сокет- отправитель
         const char FAR *buf,   // указатель на буффер с данными
         int len,               // длинна данных
         int flags              // флаги (может быть 0)
        );

Пример использования данной функции:
Код:
   if (SOCKET_ERROR == ( send (s, (char* ) & buff), 512, 0 ) )
   {
      // Error...
      error = WSAGetLastError();
      // ...
   }

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

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


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

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

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

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

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


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

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

Пример:
Код:
   closesocket (s);

Итоги


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