Подписаться  на наше издание быстро и дешевле чем где-либо Вы можете прямо сейчас! Подписаться! 

 


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


Вводная


Захотел я по осени одну тулзу написать, которая (в общем виде, разумеется) должна была уметь: а) подключиться к удаленной машине; б) послать/получить данные; в) вежливо отключиться. Как известно, такой процесс реализуется посредством стека протоколов TCP/IP. Самостоятельно писать провайдера TCP мне было лень, поэтому, порывшись в Сети, самым удобным для себя инструментом определил WinSock.


Почему именно его? Ну-с, во-первых, он достаточно прост в использовании, во-вторых — имеется в любой Windows-системе, в третьих — предоставляет достаточный уровень абстракции от физики и логики передачи данных. Наконец, в четвертых, программу под WinSock можно запросто портировать под UNIX с минимумом изменений (вернуть на родину, так сказать).


Общее описание


(каковое мы подсмотрим в Microsoft Windows Sockets 2 Reference из Win32 SDK)


Windows Sockets 2 использует парадигму сокетов, которая впервые была популяризирована в Berkeley Software Distribution (BSD) UNIX. Позже она была адаптирована к Microsoft Windows в Windows Sockets 1.1. Одной из главных целей Windows Sockets было обеспечение протокольно-независимого интерфейса, полностью способного поддерживать различные сетевые потребности, такие как мультимедийные соединения реального времени.


Существенно, что WinSock — это интерфейс, но не протокол. Как интерфейс он позволяет использовать коммуникационные возможности любого количества транспортных протоколов. А поскольку это не протокол, он не влияет на "биты в проводе" и не обязателен для использования на обоих концах соединения.


Неразлучная пара: клиент и сервер


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


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


Приступим-с...


Начало всех начал — директива #include... Сначала мы включаем windows.h, потом winsock2.h, долго нудимся над текстом — и в итоге... компилятор выдает многочисленные ошибки "редекларации типов". Происходит это по простой причине: в windows.h инклудится winsock.h, который во многом повторяет winsock2.h.


Путей решения два. Путь первый (ламерский): переставить местами windows.h и winsock2.h. Путь второй (для нормальных героев): поставить перед windows.h директиву #define _WINSOCKAPI_ , что автоматически отменит включение winsock.h.
Теперь инициализируем библиотеку. В WinSock 2 это делается вызовом функции из майкрософтовского расширения WSA. Оказывается, они ввели свое собственное расширение sockets, все функции которого начинаются на WSA (Windows Sockets Appendix, что ли?). Например, WSAAsyncSelect, или WSAEventSelect. Однако этот раздел я оставляю гостям из будущего, поскольку в данный момент нас интересуют только две функции: WSAStartup и WSACleanup.

 

#define _WINSOCKAPI_
#include <windows.h>
#include <winsock2.h>
WORD wVersionRequested;
WSADATA wsaData;
// ...
wVersionRequested = MAKEWORD( 2, 0 );
if ( WSAStartup(wVersionRequested, &wsaData) ) myError("can't startup WinSock DLL\n");

 

В качестве первого параметра для WSAStartup передается слово с номером версии (для WinSock 2 соответственно 2.0, также возможны значения 1.0 и 1.1), в качестве второго — ссылка на структуру типа WSADATA, которая заполняется в случае успешной инициализации библиотеки. Функция возвращает ненулевое значение при ошибке, информацию о которой можно получить через WSAGetLastError. Использование WSACleanup еще проще, мы увидим его ниже.


Следующий шаг — обработка информации от пользователя. Необходимы функции, которые определяли бы имя хоста по его IP и vice versa. Кстати, WinSock маскирует запросы к серверу DNS, что избавляет нас от части трудов. Вышеуказанные действия осуществляются такими функциями-соседками: gethostbyname и gethostbyaddr.


Первая получает информацию о хосте, используя его имя. Оная информация заносится в структуру hostent, ссылка на которую нам и возвращается. Соседка, gethostbyaddr, проделывает то же самое, но использует адрес хоста, причем "забивая" целых три параметра: указатель на адрес из байтов, выстроенных в сетевом порядке (справа налево, если не ошибаюсь); размер этого адреса; тип адреса — этих типов больше десятка, но нам нужен только AF_INET. Обе функции возвращают NULL pointer в случае неудачи.


Еще две полезных вспомогательных функции: inet_addr и inet_ntoa, производящие взаимообратное преобразование IP-адреса в двоичную и в строковую формы соответственно.

 

struct hostent FAR *he;
struct sockaddr_in sa;
struct in_addr ia;
char host[80]; //здесь должен содержаться либо адрес-строка (типа "
www.comizdat.com\0"), либо айпи ("127.0.0.1\0") в строковой форме, строки должны быть null-terminated
sa.sin_family = AF_INET;
if ( (sa.sin_addr.s_addr = inet_addr(host)) == INADDR_NONE) {
   if ( !(he = gethostbyname(host)) ) myError(bad_addr);
   sa.sin_addr.s_addr = * (long *) he->h_addr; //хе-хе, ссылка на ссылке сидит и ссылкой погоняет
}
else {
   if ( !(he = gethostbyaddr(&sa.sin_addr, sizeof(sa.sin_addr), AF_INET)) )
      lstrcpy(host, "unknown");
   else
      lstrcpy(host, he->h_name);
}

 

Использование inet_addr понятно из листинга, inet_ntoa юзаем примерно таким образом:

 

struct hostent FAR *he;
struct in_addr ia;
char host[80];
gethostname(host, 80); //как вы уже догадались, определяем имя локального хоста, второй параметр - размер буфера
he = gethostbyname(host);
ia.s_addr = * (long *) he->h_addr;
printf("Our host is %s!\r\n", inet_ntoa(ia)); //на stdout напечатает "Our host is localhost!"

 

Клиентская часть


Все приготовления окончены, последующие действия таковы:

 

struct sockaddr_in sa; //пример заполнения структуры показан в листинге 1
struct servent FAR *se;
SOCKET sckt;
unsigned int i, cnt_port; //cnt_port - номер порта
// ...
   if ( ( sckt = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) ) == INVALID_SOCKET)
      myError("can't create socket\r\n");
   else {
      sa.sin_port = htons(cnt_port);
      if ( !connect(sckt, &sa, sizeof(sa)) ) {
         if ( se = getservbyport(sa.sin_port, NULL) )
            sprintf(rbuf, "connection successful on %u (%s)\r\n", cnt_port, se->s_name);
         else
            sprintf(rbuf, "connection successful on %u\r\n", cnt_port);
         myWriteAll(rbuf);
         while (1) {
            if ( ( value = recv(sckt, &buf2, 256, 0) ) != SOCKET_ERROR ) {
               if ( !value )  break;
               buf2[value] = 0;
               myWriteAll(buf2);
            } else break;
            value = myReadConsole(buf2, 256);
            if ( buf2[0] == '!' ) break;
            if ( send(sckt, &buf2, value, 0) != SOCKET_ERROR )
               myWriteLog(buf2);
            else break;
         }
         shutdown(sckt, SD_SEND);
         while ( recv(sckt, &buf2, 256, 0) )
            myWriteAll(buf2);
      } else {
         sprintf(rbuf, "connection failed on %u\r\n", cnt_port);
         myWriteAll(rbuf);
      }
      closesocket(sckt);
   }
   WSACleanup();

 

Разберемся по порядку. За создание собственно сокета, очевидно (а "очевидно" значит "и ежу понятно"), отвечает функция socket. Первый параметр — тип адреса, о нем я уже упоминал. Второй — тип сокета, который в WinSock 1.1 принимал значения либо SOCK_STREAM либо SOCK_DGRAM для протоколов TCP и UDP соответственно, а в версии 2.0 расширился еще несколькими типами. Третий параметр определяет используемый протокол и изменяется в диапазоне 0…256. Значения, которые могут нас интересовать, таковы: IPPROTO_IP (0; "dummy IP"), IPPROTO_ICMP (1), IPPROTO_TCP (6), IPPROTO_UDP (17). В случае ошибки socket возвращает нам INVALID_SOCKET.


Функция htons (есть еще и htonl для long) конвертирует unsigned short, как оно есть на данном хосте, в то же самое, но с сетевым порядком байт.


С коннектом при помощи connect, думается мне, и так понятно...


Не столько необходимая, сколько информативная функция getservbyport по номеру порта и имени протокола (опционально) заполняет структуру servent. Я использую ее "для красоты", чтобы рядом с номером порта мне писали еще и название сервиса. В то же время, возможна и обратная операция — то есть по имени сервиса узнать номер порта, что проделывается с помощью getservbyname. Функция аналогична getservbyport, но в качестве первого параметра получаем указатель на имя сервиса, к примеру "smtp\0".


Сразу же после осуществления соединения мы читаем из сокета данные общего назначения, которые посылает сервер, после — читаем с консоли (хотя какая разница, откуда) строку и отправляем ее серверу. Мы заставим нашу MustDie выполнять эти действия бесконечно — пока не прервется соединение или пользователь не введет [!+Enter]. Параметры у функций recv и send совпадают:

  1. ID сокета;
  2. указатель на буфер;
  3. размер буфера (причем для send нет гарантии, что буфер будет отправлен полностью, если он превышает определенный размер для данного протокола);
  4. флаги, влияющие на поведение функции (например, флаг MSG_PEEK для recv означает "скопировать данные из сокета в буфер, но не удалять из очереди").

И recv и send в случае удачного завершения возвращают количество прочитанных/записанных байт, в случае ошибки — SOCKET_ERROR, а если соединение было корректно закрыто сервером, то recv возвратит 0.

 


Я общаюсь с "Яндексом" по SMTP

 


Эхо-сервис в работе


Функция shutdown отключает посылку и/или прием данных в сокете. Соответственно используются флаги SD_SEND, SD_RECEIVE и SD_BOTH. SD_SEND отключает последовательные посылки и, если используется TCP, посылает FIN на сервер. SD_RECEIVE для TCP-сокета сбрасывает соединение в исходное состояние — и все данные, ожидающие в очереди либо получаемые сокетом, теряются. Для UDP SD_RECEIVE не производит никакого эффекта. SD_BOTH является комбинацией указанных флагов. Функция shutdown не закрывает сокет и не освобождает ресурсы, связанные с ним, служа для корректного завершения сеанса связи. Последнее осуществляется так: на стороне клиента вызывается shutdown с флагом SD_SEND (короче, отсылаем FIN); производится последовательное чтение из сокета, пока recv не вернет 0; сокет закрывается посредством closesocket.


Серверная часть


У нас будет ооочень простенький эхо-сервер. Вот его примерный текст:

 

   SOCKET sckt, sckt0;
   WSADATA wsaData;
   WORD wVersionRequested;
   struct sockaddr_in sa;
   char buf[256] = "Hello world! This is our echo-service!\r\n\0";
   int value;
   wVersionRequested = MAKEWORD( 2, 0 );
   WSAStartup(wVersionRequested, &wsaData);
   sa.sin_family = AF_INET;
   sa.sin_addr.s_addr = htonl(INADDR_ANY); //принимать соединение с любого адреса
   sa.sin_port = htons(25); //номер порта
   sckt = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
   bind(sckt, &sa, sizeof(sa));
   listen(sckt, 1);
   sckt0 = accept(sckt, NULL, 0);
   value = lstrlen(&buf);
   while (1) {
      if ( ( send(sckt0, &buf, value, 0) ) == SOCKET_ERROR )
         break;
      value = recv(sckt0, &buf, 256, 0);
      if ( ( value == SOCKET_ERROR )  ||  ( !value ) )
         break;
   }
   closesocket(sckt0);
   closesocket(sckt);
   WSACleanup();

 

Происходит тут следующее. Вызов bind связывает локальный адрес с сокетом. Функция listen вводит программу в статус ожидания соединения, когда сервер "слушает" порт. Вторым параметром ей передается максимальное значение, до которого очередь входящих соединений может расти. Если параметр равен SOMAXCONN, то за их количество отвечает нижележащий провайдер сервиса. Эта фишка нужна для асинхронной работы со многими сокетами. Организовать ее помогает функция select — да только до нее, прошу прощения, у меня руки не дошли (а смысл такой, что она выбирает из массива сокеты, готовые к чтению и/или записи). Подтверждает соединение функция accept. Что интересно, возвращает она новый ID сокета, который во всем идентичен старому, только его нельзя "слушать". Старый сокет остается без изменений.


Дальше движемся по сценарию клиента, поменяв местами recv и send.

 

Вот и вся история. Конечно, следовало бы еще ввести продвинутую обработку ошибок и т.п. и т.д... Но кто, сказать по правде, этим занимается? :)

2005.11.30
19.03.2009
В IV квартале 2008 г. украинский рынок серверов по сравнению с аналогичным периодом прошлого года сократился в денежном выражении на 34% – до $30 млн (в ценах для конечных пользователей), а за весь календарный год – более чем на 5%, до 132 млн долл.


12.03.2009
4 марта в Киеве компания Telco провела конференцию "Инновационные телекоммуникации", посвященную новым эффективным телекоммуникационным технологиям для решения задач современного бизнеса.


05.03.2009
25 февраля в Киеве компания IBM, при информационной поддержке "1С" и Canonical, провела конференцию "Как сохранить деньги в условиях кризиса?"


26.02.2009
18-19 февраля в Киеве прошел юбилейный съезд ИТ-директоров Украины. Участниками данного мероприятия стали ИТ-директора, ИТ-менеджеры, поставщики ИТ-решений из Киева, Николаева, Днепропетровска, Чернигова и других городов Украины...


19.02.2009
10 февраля в Киеве состоялась пресс-конференция, посвященная итогам деятельности компании "DiaWest – Комп’ютерний світ" в 2008 году.


12.02.2009
С 5 февраля 2009 г. в Киеве начали работу учебные курсы по использованию услуг "электронного предприятия/ учреждения" на базе сети информационно-маркетинговых центров (ИМЦ).


04.02.2009
29 января 2009 года в редакции еженедельника "Computer World/Украина" состоялось награждение победителей акции "Оформи подписку – получи приз!".


29.01.2009
22 января в Киеве компания "МУК" и представительство компании Cisco в Украине провели семинар для партнеров "Обзор продуктов и решений Cisco Small Business"

 

 
 
Copyright © 1997-2008 ИД "Комиздат".