Программирование сокетов::Журнал СА 1.2002
www.samag.ru
     
Поиск   
              
 www.samag.ru    Web  0 товаров , сумма 0 руб.
E-mail
Пароль  
 Запомнить меня
Регистрация | Забыли пароль?
Журнал "Системный администратор"
Журнал «БИТ»
Наука и технологии
Подписка
Где купить
Авторам
Рекламодателям
Архив номеров
Контакты
   

  Опросы
  Статьи

Электронный документооборот  

5 способов повысить безопасность электронной подписи

Область применения технологий электронной подписи с каждым годом расширяется. Все больше задач

 Читать далее...

Рынок труда  

Системные администраторы по-прежнему востребованы и незаменимы

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

 Читать далее...

Учебные центры  

Карьерные мечты нужно воплощать! А мы поможем

Школа Bell Integrator открывает свои двери для всех, кто хочет освоить перспективную

 Читать далее...

Гость номера  

Дмитрий Галов: «Нельзя сказать, что люди становятся доверчивее, скорее эволюционирует ландшафт киберугроз»

Использование мобильных устройств растет. А вместе с ними быстро растет количество мобильных

 Читать далее...

Прошу слова  

Твердая рука в бархатной перчатке: принципы soft skills

Лауреат Нобелевской премии, специалист по рынку труда, профессор Лондонской школы экономики Кристофер

 Читать далее...

1001 и 1 книга  
19.03.2018г.
Просмотров: 9941
Комментарии: 0
Потоковая обработка данных

 Читать далее...

19.03.2018г.
Просмотров: 8153
Комментарии: 0
Релевантный поиск с использованием Elasticsearch и Solr

 Читать далее...

19.03.2018г.
Просмотров: 8258
Комментарии: 0
Конкурентное программирование на SCALA

 Читать далее...

19.03.2018г.
Просмотров: 5226
Комментарии: 0
Машинное обучение с использованием библиотеки Н2О

 Читать далее...

12.03.2018г.
Просмотров: 5910
Комментарии: 0
Особенности киберпреступлений в России: инструменты нападения и защита информации

 Читать далее...

Друзья сайта  

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

Архив номеров / 2002 / Выпуск №1 (1) / Программирование сокетов

Рубрика: Сети /  Сети

ВСЕВОЛОД СТАХОВ

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

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

Таким образом, сетевые сокеты представляют собой парные структуры, жёстко между собой синхронизированные. Для создания сокетов в любой операционной системе, поддерживающей их, используется функция socket (к счастью, сокеты достаточно стандартизированы, поэтому их можно использовать для передачи данных между приложениями, работающими на разных платформах). Формат функции таков:

int socket(int domain, int type, int protocol);

 Параметр domain задаёт тип транспортного протокола, т.е. протокола доставки пакетов в сети. В настоящее время поддерживаются следующие протоколы (но учтите, что для разных типов протокола тип адресной структуры будет разный):

  •  PF_UNIX или PF_LOCAL – локальная коммуникация для ОС UNIX (и подобных).
  •  PF_INET – IPv4, IP-протокол Интернета, наиболее распространён сейчас (32-х битный адрес).
  •  PF_INET6  – IPv6, следующее поколение протокола IP (IPng) – 128 битный адрес.
  •  PF_IPX – IPX – протоколы Novell.

Поддерживаются и другие протоколы, но эти 4 являются самыми популярными.

Параметр type означает тип сокета, т.е. то, как будут передаваться данные: обычно применяется константа SOCK_STREAM, её использование означает безопасную передачу данных двунаправленным потоком с контролем ошибок. При таком способе передачи данных программисту не приходится заботиться об обработке ошибок сети, хотя это не уберегает от логических ошибок, что актуально для сетевого сервера.

Параметр protocol определяет конкретный тип протокола для данного domain, например IPPROTO_TCP или IPPROTO_UDP (параметр type должен в данном случае быть SOCK_DGRAM).

Функция socket просто создаёт конечную точку и возвращает дескриптор сокета; до того, как сокет не соединён с удалённым адресом функцией connect, данные через него пересылать нельзя! Если пакеты теряются в сети, т.е. произошло нарушение связи, то приложению, создавшему сокет, посылается сигнал Broken Pipe – SIGPIPE, поэтому целесообразно присвоить обработчик данному сигналу функцией signal. После того, как сокет соединён с другим функцией connect, по нему можно пересылать данные либо стандартными функциями read – write, либо специализированными recv – send. После окончания работы сокет надо закрыть функцией close. Для создания клиентского приложения достаточно связать локальный сокет с удалённым (серверным) функцией connect. Формат этой функции такой:

int connect(int sock_fd, const struct *sockaddr serv_addr, socketlen_t addr_len);

При ошибке функция возвращает -1, статус ошибки можно получить средствами операционной системы. При успешной работе возвращается 0. Сокет, однажды связанный, чаще всего не может быть связан снова, так, например, происходит в протоколе ip. Параметр sock_fd задаёт дескриптор сокета, структура serv_addr назначает удалённый адрес конечной точки, addr_len содержит длину serv_addr (тип socketlen_t имеет историческое происхождение, обычно он совпадает с типом int). Самый важный параметр в этой функции – адрес удалённого сокета. Он, естественно, неодинаков для разных протоколов, поэтому я опишу здесь структуру адреса только для IP (v4)-протокола. Для этого используется специализированная структура sockaddr_in (её необходимо прямо приводить к типу sockaddr при вызове connect). Поля данной структуры выглядят следующим образом:

struct sockaddr_in{

    sa_family_t    sin_family; – определяет семейство адресов, всегда должно быть AF_INET

    u_int16_t      sin_port;   – порт сокета в сетевом порядке байт

    struct in_addr  sin_addr;  – структура, содержащая IP-адрес

};

Структура, описывающая IP-адрес:

struct in_addr{

    u_int32_t      s_addr;     – IP-адрес сокета в сетевом порядке байт

};

Обратите внимание на особый порядок байт во всех целых полях. Для перевода номера порта в сетевой порядок байт можно воспользоваться макросом htons (unsigned short port). Очень важно использовать именно этот тип целого – беззнаковое короткое целое.

Адреса IPv4 делятся на одиночные, широковещательные (broadcast) и групповые (multicast). Каждый одиночный адрес указывает на один интерфейс хоста, широковещательные адреса указывают на все хосты в сети, а групповые адреса соответствуют всем хостам в группе (multicast group). В структуре in_addr можно назначать любой из этих адресов. Но для сокетных клиентов в подавляющем большинстве случаев присваивают одиночный адрес. Исключением является тот случай, когда необходимо просканировать всю локальную сеть в поисках сервера, тогда можно использовать в качестве адреса широковещательный. Затем, скорее всего, сервер должен сообщить свой реальный IP-адрес и сокет для дальнейшей передачи данных должен присоединяться именно к нему. Передача данных через широковещательные адреса не есть хорошая идея, так как неизвестно, какой именно сервер обрабатывает запрос. Поэтому в настоящее время сокеты, ориентированные на соединение, могут использовать лишь одиночные адреса. Для сокетных серверов, ориентированных на прослушивание адреса, наблюдается другая ситуация: здесь разрешено использовать широковещательные адреса, чтобы сразу же ответить клиенту на запрос о местоположении сервера. Но обо всём по порядку. Как вы заметили, в структуре sockaddr_in поле IP-адреса представлено как беззнаковое длинное целое, а мы привыкли к адресам либо в формате x.x.x.x (172.16.163.89) либо в символьном формате (myhost.com). Для преобразования первого служит функция inet_addr (const char *ip_addr), а для второго – функция gethostbyname (const char *host). Рассмотрим обе из них:

u_int32_t inet_addr(const char *ip_addr)

– возвращает сразу же целое, пригодное для использования в структуре sockaddr_in по IP-адресу, переданному ей в формате x.x.x.x. При возникновении ошибки возвращается значение INADDR_NONE.

struct HOSTENT* gethostbyname(const char *host_name)

– возвращает структуру информации о хосте, исходя из его имени. В случае неудачи возвращает NULL. Поиск имени происходит вначале в файле hosts, а затем в DNS. Структура HOSTENT предоставляет информацию о требуемом хосте. Из всех её полей наиболее значительным является поле char **h_addr_list, представляющее список IP-адресов данного хоста. Обычно используется h_addr_list[0], представляющая первый ip адрес хоста, для этого можно также использовать выражение h_addr. После выполнения функции gethostbyname в списке h_addr_list структуры HOSTENT оказываются простые символические ip адреса, поэтому необходимо воспользоваться дополнительно функцией inet_addr для преобразования в формат sockaddr_in.

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

Для передачи данных между сокетами существуют специальные функции, единые для всех ОС – это функции семейства recv и send. Формат их очень похож:

int send(int sockfd, void *data, size_t len, int flags);

– отправляет буфер data.

int recv(int sockfd, void *data, size_t len, int flags);

– принимает буфер data.

Первый аргумент – дескриптор сокета, второй – указатель на данные для передачи, третий – длина буфера и четвёртый – флаги. В случае успеха возвращается число переданных байт, в случае неудачи – отрицательный код ошибки. Флаги позволяют изменить параметры передачи (например, включить асинхронный режим работы), но для большинства задач достаточно оставить поле флагов нулевым для обычного режима передачи. При отсылке или приёме данных функции блокируют выполнение программы до того, как будет отослан весь буфер. А при использовании протокола tcp/ip от удалённого сокета должен прийти ответ об успешной отправке или приёме данных, иначе пакет пересылается ещё раз. При пересылке данных учитывайте MTU сети (максимальный размер передаваемого за один раз кадра). Для разных сетей он может быть разным, например, для сети Ethernet он равен 1500.

Итак, для полноты изложения приведу самый простенький пример программы на Си, реализующей сокетного клиента:

#include <sys/socket.h>  /*  Стандартные библиотеки сокетов для Linux        */

#include <net/netinet.h> /*  Для ОС Windows используйте #include <winsock.h> */

#include <stdio.h>

int main(){

    int sockfd = -1;                                   

    /* Дескриптор сокета */

    char buf[128];                                      

    /* Указатель на буфер для приёма */

    char s[] = "Client ready ";                       

    /* Строка для передачи серверу  */

    HOSTENT *h = NULL;                                

    /* Структура для получения ip адреса  */

    sockaddr_in addr;                             

    /* Cтруктура tcp/ip протокола  */

    unsigned short port = 80;

    /* Заполняем поля структуры: */

    addr.sin_family = AF_INET;

    addr.sin_port   = htons(port);

    sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

    /* Создаём сокет */

    if(sockfd == -1)                                   

    /* Создан ли сокет */

           return -1;

    h = gethostbyname("www.myhost.com");               

    /* Получаем адрес хоста */

    if(h == NULL)                                       

    /* А есть ли такой адрес? */

           return -1;

       addr.sin_addr.s_addr = inet_addr(h->h_addr_list[0]);

    /* Переводим ip адрес в число */

    if(connect(sockfd, (sockaddr*) &addr, sizeof(addr)))

    /* Пытаемся соединится с удалённым сокетом */

           return -1;

    /* Соединение прошло успешно - продолжаем */

    if(send(sockfd, s, sizeof(s), 0) < 0)              

    /* Посылаем удалённому сокету строку s */

           return -1;

    if(recv(sockfd, buf, sizeof(buf), 0) < 0)

    /* Получаем ответ от удалённого сервера */

           return -1;

      printf("Recieved string was: %s", buf);            

    /* Вывод буфера на стандартный вывод */

    close(sockfd);                                     

    /* Закрываем сокет */

/* Для Windows применяется функция closesocket(s) */

    return 0;

}

Вот видите, использовать сокеты не так трудно. В серверных приложениях используются совершенно другие принципы работы с сокетами. Вначале создается сокет, затем ему присваивается локальный адрес функцией bind, при этом можно присвоить сокету широковещательный адрес. Затем начинается прослушивание адреса функцией listen, запросы на соединение помещаются в очередь. То есть функция listen выполняет инициализацию сокета для приёма сообщений. После этого нужно применить функцию accept, которая возвращает новый, уже связанный с клиентом сокет. Обычно для серверов характерно принимать много соединений через небольшие промежутки времени. Поэтому нужно постоянно проверять очередь входящих соединений функцией accept. Для организации такого поведения чаще всего прибегают к возможностям операционной системы. Для ОС Windows чаще используется многопоточный вариант работы сервера (multi-threaded), после принятия соединения происходит создание нового потока в программе, который и обрабатывает сокет. В *nix-системах чаще используется порождение дочернего процесса функцией fork. При этом накладные расходы уменьшены за счёт того, что фактически происходит копия процесса в файловой системе proc. При этом все переменные дочернего процесса совпадают с родителем. И дочерний процесс может сразу же обрабатывать входящее соединение. Родительский же процесс продолжает прослушивание. Учтите, что порты с номерами от 1 до 1024 являются привилегированными и их прослушивание не всегда возможно. Ещё один момент: нельзя, чтобы два разных сокета прослушивали один и тот же порт по одному и тому же адресу! Для начала рассмотрим форматы вышеописанных функций для создания серверного сокета:

int bind(int sockfd, const struct *sockaddr, socklen_t addr_len);

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

int listen(int sockfd, int backlog);

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

int accept(int sockfd, struct *sockaddr, socklen_t addr_len)

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

Итак, приведу пример простого сокетного сервера, использующего функцию fork для создания дочернего процесса, обрабатывающего соединение:

int main(){

    pid_t pid;                             

    /* Идентификатор дочернего процесса              */

    int sockfd = -1;                                   

    /* Дескриптор сокета для прослушивания           */

    int s      = -1;                           

    /* Дескриптор сокета для приёма                  */

    char buf[128];                                     

    /* Указатель на буфер для приёма                 */

    char str[] = "Server ready ";                     

    /* Строка для передачи серверу                   */

    HOSTENT *h = NULL;                                 

    /* Структура для получения ip адреса             */

    sockaddr_in addr;                                  

    /* Cтруктура tcp/ip протокола                    */

    sockaddr_in raddr;

    unsigned short port = 80;

    /* Заполняем поля структуры: */

    addr.sin_family = AF_INET;

    addr.sin_port   = htons(port); 

    sockfd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);

    /* Создаём сокет                                 */

    if(sockfd == -1)                                   

    /* Создан ли сокет                               */

           return -1;

    addr.sin_addr.s_addr = INADDR_ANY;                 

    /* Слушаем на всех адресах                       */

    if(bind(sockfd, (sockaddr*) &addr, sizeof(addr)))  

    /* Присваиваем сокету локальный адрес            */

           return -1;

    if(listen(sockfd, 1))                              

    /* Начинаем прослушивание                        */

           return -1;

     s = accept(sockfd, (sockaddr *) &raddr, sizeof(raddr));

    /* Принимаем соединение                      */

    pid = fork();                                      

    /* порождаем дочерний процесс                    */

    if(pid == 0){                                      

    /* Это дочерний процесс                          */

           if(recv(s, buf, sizeof(buf), 0) < 0)       

           /* Посылаем удалённому сокету строку s           */

                 return -1;

           if(send(s, str, sizeof(str), 0) < 0)       

           /* Получаем ответ от удалённого сервера          */

                 return -1;

           printf("Recieved string was: %s", buf);    

           /* Вывод буфера на стандартный вывод             */

           close(s);                             

           /* Закрываем сокет                               */

           return 0;                            

           /* Выходим из дочернего процесса                 */

    }  

    close(sockfd);                                     

    /* Закрываем сокет для прослушивания             */

    return 0;

}

При создании потока (thread) для обработки сокета смотрите руководство к ОС, так как для разных систем вызов функции создания потока может существенно различаться. Но принципы обработки для потока остаются теми же. Функции обработки необходимо только передать в качестве аргумента указатель на сокет (обычно в функцию потока можно передавать данные любого типа в формате void *, что требует использования приведения типов).

Важное замечание для систем Windows. Мною было замечено, что система сокетов не работает без применения функции WSAStartup для инициализации библиотеки сокетов. Программа с сокетами в ОС Windows должна начинаться так:

WSADATA wsaData;

WSAStartup(0x0101, &wsaData);

И при выходе из программы пропишите следующее:

WSACleanup();

Так как в основном операции с сокетами являются блокирующими, приходится часто прерывать исполнение задачи ожиданием синхронизации. Поэтому часто в *nix-подобных системах избегают блокировки консоли созданием особого типа программы – демона. Демон не принадлежит виртуальным консолям и возникает, когда дочерний процесс вызывает fork, а родительский процесс завершается раньше, чем 2-й дочерний (а это всегда бывает именно таким образом). После этого 2-й дочерний процесс становится основным и не блокирует консоль. Приведу пример такого поведения программы:

pid = fork();                                                         

/* Создание первого дочернего процесса     */

if (pid <0){                                                          

/* Ошибка вызова fork                      */

    printf("Forking Error : ) ");              

    exit(-1);

}else if (pid !=0 ){                                           

/* Это первый родитель !                  */

    printf(" This is a Father 1 ");

}else{

    pid = fork();                                              

    /* Работа 1-го родителя завершается        */

/* И мы вызываем ещё один дочерний процесс */

    if (pid <0){

           printf("Forking error : ) ");

           exit(-1);

    }else if (pid !=0 ){                                       

    /* Это второй родитель                    */

           printf(" This is a father 2 ");

    }else{                                                            

    /* А вот это тот самый 2-й дочерний процесс*/

    /* Переход в "стандартный" режим демона */

           setsid();                                                  

           /* Выполняем демон в режиме superuser      */

           umask(0);                                             /* Стандартная маска файлов                */

           chdir("/");                                           /* Переход в корневой каталог              */

           daemoncode();                                         /* Собственно сам код демона               */

           /* При вызове fork демона появляется потомок-демон  */

    }

}

Вот и всё. Я думаю, для создания простенького сокетного сервера этого достаточно.


Комментарии отсутствуют

Добавить комментарий

Комментарии могут оставлять только зарегистрированные пользователи

               Copyright © Системный администратор

Яндекс.Метрика
Tel.: (499) 277-12-41
Fax: (499) 277-12-45
E-mail: sa@samag.ru