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

Сетевые средства

Разбить на страницы
Показывать лекцию целиком

Основные понятия и объекты

Стандарт POSIX-2001 определяет сеть как совокупность взаимосвязанных хостов. Тем самым предполагается, что сетевая инфраструктура остается скрытой от приложений, которым предоставляются высокоуровневые средства взаимодействия в распределенной среде.

Под сетевым адресом понимается видимый в пределах сети идентификатор, используемый для обозначения оконечных точек сети . Адреса есть у определенных оконечных точек хостов, могут они быть и у хостов в целом.

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

Процесс присвоения сетевого адреса оконечной точке называется связыванием, или привязкой, а обратное действие - освобождением, или отменой привязки.

Обычно оконечной точкой служит аппаратный сетевой интерфейс, посредством которого данные передаются и принимаются, однако с таким интерфейсом, как шлейфовый (loopback), никакой аппаратуры не ассоциировано.

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

Данные передаются по сети в виде последовательности октетов (восьмибитных беззнаковых величин). Если некоторый элемент данных (например, адрес или номер порта ) состоит более чем из восьми бит, для его передачи и хранения требуется несколько октетов. Сетевым называется порядок октетов (байт), при котором первый (с наименьшим адресом) октет содержит старшие (наиболее значимые) биты.

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

Для преобразования значений типов uint16_t и uint32_t из хостового порядка байт в сетевой служат функции htons() и htonl() ; функции ntohs() и ntohl() осуществляют обратную операцию.

При взаимодействии процессов оконечными точками служат сокеты, они трактуются стандартом POSIX-2001 как отдельный тип файлов.

Под адресом сокета как (удаленной) оконечной точки понимается структура, включающая идентификатор адресного семейства и адресную информацию, специфичную для данного семейства. Последняя может состоять из нескольких компонентов, в том числе сетевого адреса хоста и идентификатора конкретной оконечной точки.

Основные описания, относящиеся к сокетам, сосредоточены в заголовочном файле <sys/socket.h>. Фигурирует в нем и упомянутая выше структура sockaddr для адреса сокета, которая должна содержать по крайней мере следующие поля.

sa_family_t sa_family;  
/* Адресное семейство */
char        sa_data []; 
/* Адрес сокета (данные переменной длины) */

Адресное семейство соответствует конкретной среде взаимодействия. Стандарт POSIX-2001 определяет три таких семейства.

AF_UNIX

Адресное семейство UNIX поддерживает межпроцессное взаимодействие в пределах одной системы. Формально это можно считать вырожденным случаем сетевого взаимодействия. Описания, специфичные для данного семейства, содержатся в заголовочном файле <sys/un.h>.

AF_INET

Адресное семейство, поддерживающее взаимодействие по протоколам   IPv4. Специфичные для него описания располагаются в заголовочном файле <netinet/in.h>.

AF_INET6

Взаимодействие по протоколам IPv6 (необязательная возможность). За счет поддержки адресов IPv6, отображенных на IPv4, обеспечивается совместимость с приложениями, использующими IPv4. Применяемые эти адресным семейством описания распределены по заголовочным файлам <netinet/in.h>, <arpa/inet.h> и <netdb.h>.

В пределах каждого адресного семейства могут существовать сокеты нескольких типов. В стандарте POSIX-2001 их четыре.

SOCK_STREAM

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

SOCK_SEQPACKET

Аналог SOCK_STREAM с дополнительным сохранением границ между записями.

SOCK_DGRAM

Передача данных в виде датаграмм в режиме без установления соединения.

SOCK_RAW

(Необязательная возможность). Аналог SOCK_DGRAM с дополнительной возможностью доступа к протокольным заголовкам и другой информации нижнего уровня. Создавать сокеты этого типа могут лишь процессы с соответствующими привилегиями.

Для каждого адресного семейства каждый тип сокета может поддерживаться одним или несколькими протоколами. В частности, в адресном семействе   AF_INET для сокетов   типа   SOCK_STREAM подразумеваемым является протокол с именем IPPROTO_TCP, а для типа   SOCK_DGRAM - IPPROTO_UDP ; посредством "прозрачных" сокетов ( SOCK_RAW ) можно воспользоваться протоколом   ICMP, задав имя IPPROTO_ICMP, и т.д.

Общая логика работы с сокетами состоит в следующем. Сокеты создаются с помощью функции socket(), которой в качестве аргументов передают адресное семейство, тип сокета и протокол, а в результате получают открытый файловый дескриптор. Затем посредством функции bind()   сокету присваивают локальный адрес. Если сокет ориентирован на режим с установлением соединения, то, прибегнув к функции listen(), его следует пометить как готового принимать соединения. Реальный прием соединений выполняет функция accept(), создающая для каждого из них новый сокет по образу и подобию " слушающего ". В свою очередь, потенциальный партнер по взаимодействию инициирует соединение, применяя функцию connect(). (В режиме без установления соединения функция connect() позволяет специфицировать адрес отправляемых через сокет   датаграмм.)

Для приема поступивших в сокет данных можно воспользоваться универсальной функцией низкоуровневого ввода/вывода read() или специализированным семейством функций recv*(), а для передачи - функцией write() или семейством send*(). Кроме того, функции select() и/или poll() помогут проверить наличие данных для приема или возможность отправки очередной порции данных.

Обращение к функции shutdown() завершает взаимодействие между партнерами.

Опрос данных о сети

Данные о хостах как узлах сети хранятся в сетевой базе, последовательный доступ к которой обслуживается функциями sethostent(), gethostent() и endhostent() (см. листинг 11.1).

#include <netdb.h>
void sethostent (int stayopen);
struct hostent *gethostent (void);
void endhostent (void);

Функция sethostent() устанавливает соединение с базой, остающееся открытым после вызова gethostent(), если значение аргумента stayopen отлично от нуля. Функция gethostent() последовательно читает элементы базы, возвращая результат в структуре типа hostent, содержащей по крайней мере следующие поля.

char  *h_name;          
/* Официальное имя хоста                */
char **h_aliases;       
/* Массив указателей на альтернативные  */
/* имена хоста, завершаемый пустым      */
/* указателем                           */
int    h_addrtype;      
/* Тип адреса хоста                     */
int    h_length;        
/* Длина в байтах адреса данного типа   */
char **h_addr_list;     
/* Массив указателей на сетевые адреса  */
/* хоста, завершаемый пустым указателем */

Функция endhostent() закрывает соединение с базой.

В листинге 11.2 показана программа, осуществляющая последовательный просмотр сетевой базы данных о хостах - узлах сети, а в листинге 11.3 приведен фрагмент ее возможной выдачи.

#include <stdio.h>
#include <netdb.h>

int main (void) {
 struct hostent *pht;
 char *pct;
 int i, j;

 sethostent (1);

 while ((pht = gethostent ()) != NULL) {
   printf ("Официальное имя хоста: %s\n", pht->h_name);
   printf ("Альтернативные имена:\n");
   for (i = 0; (pct = pht->h_aliases [i]) != NULL; i++) {
     printf ("  %s\n", pct);
   }
   printf ("Тип адреса хоста: %d\n", pht->h_addrtype);
   printf ("Длина адреса хоста: %d\n", pht->h_length);
   printf ("Сетевые адреса хоста:\n");
   for (i = 0; (pct = pht->h_addr_list [i]) != NULL; i++) {
     for (j = 0; j < pht->h_length; j++) {
	printf (" %d", (unsigned char) pct [j]);
     }
     printf ("\n");
   }
 }

 endhostent ();

 return 0;
}
Официальное имя хоста: localhost
Альтернативные имена:
Тип адреса хоста: 2
Длина адреса хоста: 4
Сетевые адреса хоста:
127 0 0 1

  . . .
Официальное имя хоста: t01
Альтернативные имена:
 niisi.msk.ru
 t01.niisi.msk.ru
 mail
 mailhost
 loghost
 server
 server3
 server3.systud.msk.su
 www
 www.systud.msk.su
 t01.systud.msk.su
Тип адреса хоста: 2
Длина адреса хоста: 4
Сетевые адреса хоста:
193 232 173 1

  . . .

Официальное имя хоста: t17
Альтернативные имена:
 galatenko
Тип адреса хоста: 2
Длина адреса хоста: 4
Сетевые адреса хоста:
193 232 173 17

  . . .

К рассматриваемой базе возможен и случайный доступ по ключам - именам и адресам хостов с помощью функций gethostbyname() и gethostbyaddr(), однако они считаются устаревшими и из новой версии стандарта POSIX могут быть исключены. Вместо них предлагается использовать функции getnameinfo() и getaddrinfo() (см. листинг 11.4).

#include <sys/socket.h>
#include <netdb.h>

void freeaddrinfo (struct addrinfo *ai);

int getaddrinfo 
    (const char *restrict nodename, 
     const char *restrict servname,
     const struct addrinfo *restrict hints,
     struct addrinfo **restrict res);

int getnameinfo 
    (const struct sockaddr *restrict sa, 
     socklen_t salen, char *restrict node, 
     socklen_t nodelen, char *restrict service,
     socklen_t servicelen, int flags);

Функция getaddrinfo() позволяет по имени узла сети (хоста) (аргумент nodename ) и/или имени сетевого сервиса ( servname ) получить набор адресов сокетов и ассоциированную информацию, что дает возможность создать сокет для обращения к заданному сервису.

Если аргумент nodename отличен от пустого указателя, он способен задавать описательное имя или адресную цепочку. Для адресных семейств   AF_INET и AF_UNSPEC (см. ниже описание аргумента hints ) именем может служить имя хоста, а адресной цепочкой - стандартные для Internet адреса в точечных обозначениях (например, 193.232.173.17 ).

При пустом значении nodename подразумевается хост, локальный для вызывающего процесса.

Аргумент servname может задавать имя сервиса или (для адресных семейств   AF_INET и AF_UNSPEC ) десятичный номер порта. Пустое значение servname означает запрос сетевого адреса.

Аргумент hints позволяет передать дополнительную информацию об опрашиваемом сервисе - адресное семейство, тип сокета, протокол, флаги. Согласно стандарту, структура addrinfo, описанная в заголовочном файле <netdb.h>, должна содержать по крайней мере следующие поля.

int              ai_flags;      
/* Входные флаги */

int              ai_family;     
/* Адресное семейство сокета */

int              ai_socktype;   
/* Тип сокета */

int              ai_protocol;   
/* Протокол сокета */

socklen_t        ai_addrlen;    
/* Длина адреса сокета */

struct sockaddr *ai_addr;       
/* Адрес сокета */

char            *ai_canonname;  
/* Официальное имя узла сети */

struct addrinfo *ai_next;       
/* Указатель на следующий элемент списка */

При обращении к функции getaddrinfo() все поля структуры addrinfo, на которую указывает аргумент hints, кроме первых четырех ( ai_flags, ai_family, ai_socktype, ai_protocol ), должны быть нулевыми или равными NULL. Значение AF_UNSPEC в поле ai_family подразумевает, что вызывающего устроит любое адресное семейство. Аналогичный смысл имеют нулевые значения полей ai_socktype и ai_protocol. При hints, равном NULL, подразумевается AF_UNSPEC для ai_family и нулевые значения для других полей.

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

AI_PASSIVE

Если значение аргумента nodename равно NULL, этот флаг игнорируется. В противном случае, если он указан, будет возвращен адрес сокета, предназначенного для принятия входящих соединений.

AI_CANONNAME

Данный флаг предписывает выяснить официальное имя узла сети.

AI_NUMERICHOST

Флаг означает, что хост задан адресной цепочкой, и не допускает использования какого-либо сервиса имен.

AI_NUMERICSERV

Флаг помечает, что сервис (аргумент servname ) задан номером порта, и налагает запрет на обращение к какому-либо сервису имен.

Признаком успешного завершения функции getaddrinfo() является нулевой результат. В таком случае выходной аргумент res будет ссылаться на указатель на список структур типа addrinfo (связанных полем ai_next ) - принадлежащие им значения полей ai_family, ai_socktype, ai_protocol пригодны для создания подходящих сокетов с помощью функции socket(), а значения ai_addr и ai_addrlen, в зависимости от флага AI_PASSIVE, могут служить аргументами функций connect() или bind(), применяемых к созданному сокету.

Функция freeaddrinfo() позволяет освободить память, занятую списком структур типа addrinfo и ассоциированными данными.

Функцию getnameinfo() можно считать обратной по отношению к getaddrinfo(). Аргумент sa - входной, он задает транслируемый адрес сокета ( salen - размер структуры sockaddr ). Аргументы node и service - выходные, задающие адреса областей памяти, куда помещаются, соответственно, имя узла и сервиса ; размеры этих областей ограничены значениями nodelen и servicelen.

По умолчанию предполагается, что сокет имеет тип SOCK_STREAM, а кроме того, возвращается полное доменное имя хоста. Аргумент flags позволяет изменить подразумеваемое поведение. Если задан флаг NI_DGRAM, сокет считается датаграммным. При установленном флаге NI_NOFQDN возвращается короткое имя узла. Флаги NI_NUMERICHOST и NI_NUMERICSERV предписывают возвращать числовые цепочки для адресов хоста и сервиса, соответственно.

Обратим внимание на несколько технических деталей. При работе с адресами сокетов вместо родовой структуры типа sockaddr обычно используются более специализированные (описанные в заголовочном файле <netinet/in.h> ) - sockaddr_in для адресного семейства   AF_INET ( IPv4 ) и sockaddr_in6 для AF_INET6 ( IPv6 ). Первая из них, согласно стандарту POSIX-2001, должна содержать по крайней мере следующие поля (все с сетевым порядком байт ).

sa_family_t    sin_family; /* AF_INET */
in_port_t      sin_port;   /* Номер порта */
struct in_addr sin_addr;   /* IP-адрес */

Структура типа sockaddr_in6 устроена несколько сложнее; мы не будем ее рассматривать.

Структуры типов sockaddr и sockaddr_in (или sockaddr_in6 ) мысленно накладываются друг на друга, а преобразование типов между ними выполняется по мере необходимости. Отметим также, что тип in_port_t эквивалентен uint16_t, структура типа in_addr содержит по крайней мере одно поле:

in_addr_t s_addr;
тип in_addr_t эквивалентен uint32_t.

Техническую роль играют и функции преобразования IP-адресов из текстового представления в числовое и наоборот (см. листинг 11.5).

#include <arpa/inet.h>
in_addr_t inet_addr (const char *cp);
char *inet_ntoa (struct in_addr in);
int inet_pton (int af, const char 
     *restrict src, void *restrict dst);
const char *inet_ntop (int af, const void 
     *restrict src, char *restrict dst, 
     socklen_t size);

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

Вторая пара функций по сути аналогична первой, но имеет чуть более общий характер, так как способна преобразовывать адреса в формате IPv6. Первый аргумент этих функций, af, задает адресное семейство: AF_INET для IPv4 и AF_INET6 для IPv6. Буфер, на который указывает аргумент dst функции inet_pton() (в него помещается результат преобразования - IP-адрес в числовой двоичной форме с сетевым порядком байт ), должен иметь длину не менее 32 бит для адресов IPv4 и 128 бит для IPv6.

Аргумент src функции inet_ntop(), возвращающей текстовое представление, указывает на буфер с IP-адресом в числовой форме с сетевым порядком байт. Аргумент size задает длину выходного буфера, на него указывает аргумент dst. Подходящими значениями, в зависимости от адресного семейства, могут служить INET_ADDRSTRLEN или INET6_ADDRSTRLEN.

Выше было отмечено, что преобразование значений типов uint16_t и uint32_t из хостового порядка байт в сетевой выполняется посредством функций htons() и htonl() ; функции ntohs() и ntohl() осуществляют обратную операцию (см. листинг 11.6).

#include <arpa/inet.h>
uint32_t htonl (uint32_t hostlong);
uint16_t htons (uint16_t hostshort);
uint32_t ntohl (uint32_t netlong);
uint16_t ntohs (uint16_t netshort);

Использование функции getaddrinfo() вместе с сопутствующими техническими деталями проиллюстрируем программой, показанной в листинге 11.7. Возможные результаты ее выполнения приведены в листинге 11.8.

#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>

int main (void) {
 struct addrinfo hints = {AI_CANONNAME, AF_INET, SOCK_STREAM, IPPROTO_TCP,
			   0, NULL, NULL, NULL};
 struct addrinfo *addr_res;

 if (getaddrinfo ("www", "http", hints, addr_res) != 0) {
   perror ("GETADDRINFO");
 } else {
   printf ("Результаты для сервиса http\n"); 
   /* Пройдем по списку возвращенных структур */
   do {
     printf ("Адрес сокета: Порт: %d IP-адрес: %s\n",
             ntohs (((struct sockaddr_in *) addr_res->ai_addr)->sin_port),
             inet_ntoa (((struct sockaddr_in *) addr_res->ai_addr)->sin_addr));
     printf ("Официальное имя хоста: %s\n", addr_res->ai_canonname);
   } while ((addr_res = addr_res->ai_next) != NULL);
 }

 return 0;
}
Результаты для сервиса http
Адрес сокета: Порт: 80 IP-адрес: 193.232.173.1
Официальное имя хоста: t01

Завершая изложение серии технических моментов, укажем, что полезным дополнением к функциям getaddrinfo() и getnameinfo() является функция gai_strerror() (см. листинг 11.9). Она возвращает текстовую цепочку, расшифровывающую коды ошибок, перечисленные в заголовочном файле <netdb.h>. Стандарт POSIX-2001 специфицирует следующие коды ошибок, имена которых говорят сами за себя: EAI_AGAIN, EAI_BADFLAGS, EAI_FAIL, EAI_FAMILY, EAI_MEMORY, EAI_NONAME, EAI_OVERFLOW, EAI_SERVICE, EAI_SOCKTYPE, EAI_SYSTEM.

#include <netdb.h>
const char *gai_strerror (int ecode);

Если немного модифицировать приведенную выше программу (в листинге 11.10 показан измененный фрагмент, где имя сервиса - "HTTP" - задано большими буквами), то в стандартный протокол с помощью функции gai_strerror() будет выдано содержательное диагностическое сообщение (см. листинг 11.11). Отметим, что выдача функции perror() в данном случае невразумительна.

. . .
 int res;

 if ((res = getaddrinfo ("www", "HTTP", hints, addr_res)) != 0) {
   perror ("GETADDRINFO");
   fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
 } else {
   printf ("Результаты для сервиса HTTP\n");
    . . .
GETADDRINFO: No such file or directory
GETADDRINFO: Servname not supported for ai_socktype

Наряду с базой данных хостов ( узлов сети ) поддерживается база данных сетей с аналогичной логикой работы и набором функций (см. листинг 11.12).

#include <netdb.h>
void setnetent (int stayopen);
struct netent *getnetent (void);
struct netent *getnetbyaddr (uint32_t net, 
                             int type);
struct netent *getnetbyname (const char *name);
void endnetent (void);

Функция getnetent() обслуживает последовательный доступ к базе, getnetbyaddr() осуществляет поиск по адресному семейству (аргумент type ) и номеру net сети, а getnetbyname() выбирает сеть с заданным (официальным) именем. Структура типа netent, указатель на которую возвращается в качестве результата этих функций, согласно стандарту POSIX-2001, должна содержать по крайней мере следующие поля.

char    *n_name;        
/* Официальное имя сети                      */

char   **n_aliases;     
/* Массив указателей на альтернативные       */
/* имена сети, завершаемый пустым указателем */

int      n_addrtype;    
/* Адресное семейство (тип адресов) сети     */

uint32_t n_net;         
/* Номер сети (в хостовом порядке байт)      */

Точно такой же программный интерфейс предоставляет база данных сетевых протоколов (см. листинг 11.13).

#include <netdb.h>
void setprotoent (int stayopen);
struct protoent *getprotoent (void);
struct protoent *getprotobyname 
                           (const char *name);
struct protoent *getprotobynumber (int proto);
void endprotoent (void);

Структура типа protoent содержит по крайней мере следующие поля.

char  *p_name;    
/* Официальное имя протокола           */

char **p_aliases; 
/* Массив указателей на альтернативные */
/* имена протокола, завершаемый пустым */
/* указателем                          */

int    p_proto;   
/* Номер протокола                     */

В листинге 11.14 показан пример программы, осуществляющей последовательный и случайный доступ к базе данных сетевых протоколов. Листинг 11.15 содержит фрагмент возможных результатов работы этой программы.

#include <stdio.h>
#include <netdb.h>

int main (void) {
 struct protoent *pht;
 char *pct;
 int i;

 setprotoent (1);

 while ((pht = getprotoent ()) != NULL) {
   printf ("Официальное имя протокола: %s\n", pht->p_name);
   printf ("Альтернативные имена:\n");
   for (i = 0; (pct = pht->p_aliases [i]) != NULL; i++) {
     printf ("  %s\n", pct);
   }
   printf ("Номер протокола: %d\n\n", pht->p_proto);
 }

 if ((pht = getprotobyname ("ipv6")) != NULL) {
   printf ("Номер протокола ipv6: %d\n\n", pht->p_proto);
 } else {
   fprintf (stderr, "Протокол ip в базе не найден\n");
 }

 if ((pht = getprotobyname ("IPV6")) != NULL) {
   printf ("Номер протокола IPV6: %d\n\n", pht->p_proto);
 } else {
   fprintf (stderr, "Протокол IPV6 в базе не найден\n");
 }

 endprotoent ();

 return 0;
}
Официальное имя протокола: ip
Альтернативные имена:
 IP
Номер протокола: 0

Официальное имя протокола: icmp
Альтернативные имена:
 ICMP
Номер протокола: 1

  . . .

Официальное имя протокола: tcp
Альтернативные имена:
 TCP
Номер протокола: 6

  . . .

Официальное имя протокола: udp
Альтернативные имена:
 UDP
Номер протокола: 17

  . . .

Официальное имя протокола: ipv6
Альтернативные имена:
 IPv6
Номер протокола: 41

  . . .

Официальное имя протокола: ipv6-crypt
Альтернативные имена:
 IPv6-Crypt
Номер протокола: 50

  . . .

Официальное имя протокола: visa
Альтернативные имена:
 VISA
Номер протокола: 70

  . . .

Официальное имя протокола: iso-ip
Альтернативные имена:
 ISO-IP
Номер протокола: 80

  . . .

Официальное имя протокола: sprite-rpc
Альтернативные имена:
 Sprite-RPC
Номер протокола: 90

  . . .

Официальное имя протокола: ipx-in-ip
Альтернативные имена:
 IPX-in-IP
Номер протокола: 111

  . . .

Официальное имя протокола: fc
Альтернативные имена:
 FC
Номер протокола: 133

Номер протокола ipv6: 41

Протокол IPV6 в базе не найден

Еще одно проявление той же логики работы - база данных сетевых сервисов (см. листинг 11.16).

#include <netdb.h>
void setservent (int stayopen);
struct servent *getservent (void);
struct servent *getservbyname 
     (const char *name, const char *proto);
struct servent *getservbyport 
     (int port, const char *proto);
void endservent (void);

Обратим внимание на то, что в данном случае можно указывать второй аргумент поиска - имя протокола. Впрочем, значение аргумента proto может быть пустым указателем, и тогда поиск производится только по имени сервиса (функция getservbyname() ) или номеру порта ( getservbyport() ), который должен быть задан с сетевым порядком байт.

Структура типа servent содержит по крайней мере следующие поля.

char  *s_name;          
/* Официальное имя сервиса              */

char **s_aliases;       
/* Массив указателей на альтернативные  */
/* имена сервиса, завершаемый пустым    */
/* указателем                           */

int    s_port;          
/* Номер порта, соответствующий сервису */
/* (в сетевом порядке байт)             */

char  *s_proto;         
/* Имя протокола для взаимодействия с   */
/* сервисом                             */

В листинге 11.17 приведен пример программы, использующей функции доступа к базе данных сервисов, а также функции преобразования между хостовым и сетевым порядками байт. В листинге 11.18 показан фрагмент возможных результатов работы этой программы.

#include <stdio.h>
#include <netdb.h>

int main (void) {
 struct servent *pht;
 char *pct;
 int i;

 setservent (1);

 while ((pht = getservent ()) != NULL) {
   printf ("Официальное имя сервиса: %s\n", pht->s_name);
   printf ("Альтернативные имена:\n");
   for (i = 0; (pct = pht->s_aliases [i]) != NULL; i++) {
     printf ("  %s\n", pct);
   }
   printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
   printf ("Имя протокола: %s\n\n", pht->s_proto);
 }

 if ((pht = getservbyport (htons ((in_port_t) 21), "udp")) != NULL) {
   printf ("Официальное имя сервиса: %s\n", pht->s_name);
   printf ("Альтернативные имена:\n");
   for (i = 0; (pct = pht->s_aliases [i]) != NULL; i++) {
     printf ("  %s\n", pct);
   }
   printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
   printf ("Имя протокола: %s\n\n", pht->s_proto);
 } else {
   perror ("GETSERVBYPORT");
 }

 if ((pht = getservbyport (htons ((in_port_t) 21), (char *) NULL)) != NULL) {
   printf ("Официальное имя сервиса: %s\n", pht->s_name);
   printf ("Альтернативные имена:\n");
   for (i = 0; (pct = pht->s_aliases [i]) != NULL; i++) {
     printf ("  %s\n", pct);
   }
   printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
   printf ("Имя протокола: %s\n\n", pht->s_proto);
 } else {
   perror ("GETSERVBYPORT");
 }

 endservent ();

 return 0;
}
. . .

Официальное имя сервиса: ftp-data
Альтернативные имена:
Номер порта: 20
Имя протокола: tcp

Официальное имя сервиса: ftp-data
Альтернативные имена:
Номер порта: 20
Имя протокола: udp

Официальное имя сервиса: ftp
Альтернативные имена:
Номер порта: 21
Имя протокола: tcp

Официальное имя сервиса: ftp

Альтернативные имена:
 fsp
 fspd
Номер порта: 21
Имя протокола: udp

  . . .

Официальное имя сервиса: kerberos
Альтернативные имена:
 kerberos5
 krb5
Номер порта: 88
Имя протокола: tcp

Официальное имя сервиса: kerberos
Альтернативные имена:
 kerberos5
 krb5
Номер порта: 88
Имя протокола: udp

  . . .

Официальное имя сервиса: auth
Альтернативные имена:
 authentication
 tap
 ident
Номер порта: 113
Имя протокола: tcp

Официальное имя сервиса: auth
Альтернативные имена:
 authentication
 tap
 ident
Номер порта: 113
Имя протокола: udp

  . . .


Официальное имя сервиса: printer
Альтернативные имена:
 spooler
Номер порта: 515
Имя протокола: tcp

Официальное имя сервиса: printer
Альтернативные имена:
 spooler
Номер порта: 515
Имя протокола: udp

  . . .

Официальное имя сервиса: fido
Альтернативные имена:
Номер порта: 60179
Имя протокола: tcp

Официальное имя сервиса: fido
Альтернативные имена:
Номер порта: 60179
Имя протокола: udp

Официальное имя сервиса: ftp
Альтернативные имена:
 fsp
 fspd
Номер порта: 21
Имя протокола: udp

Официальное имя сервиса: ftp
Альтернативные имена:
Номер порта: 21
Имя протокола: tcp

Отметим, что при поиске по ключу возвращается первый подходящий элемент базы данных. По этой причине, когда не был задан протокол (второе обращение к функции getservbyport() ), в качестве результата был возвращен элемент с протоколом tcp.

Функции для работы с сокетами

Работа с сокетами как оконечными точками при взаимодействии процессов начинается с их ( сокетов ) создания посредством функции socket() (см. листинг 11.19). Она возвращает открытый файловый дескриптор, который может быть использован в последующих вызовах функций, оперирующих с сокетами. В том же листинге показано описание функции socketpair(), создающей пару сокетов с установленным между ними соединением.

#include <sys/socket.h>
int socket (int af, int type, int protocol);
int socketpair (int af, int type, 
                int protocol, int sds [2]);

Аргумент af задает адресное семейство создаваемого сокета, аргумент type - тип, аргумент protocol - конкретный протокол ( 0 обозначает неспецифицированный подразумеваемый протокол, пригодный для запрошенного типа). Напомним, что подходящие значения для аргументов af, type и protocol можно получить с помощью описанной ранее функции getaddrinfo().

Функция socketpair() по назначению аналогична pipe(), только организуется не безымянный канал, а пара соединенных, безымянных (не привязанных к каким-либо адресам), идентичных сокетов , открытые файловые дескрипторы которых помещаются в массив sds. Обычно она используется для адресного семейства AF_UNIX ; поддержка для других семейств не гарантируется.

После того как посредством функции bind() (см. листинг 11.20) создан сокет, идентифицируемый дескриптором sd, ему присваивают локальный адрес, заданный аргументом address ( address_len - длина структуры sockaddr, на которую указывает address ). Источником локальных адресов для сокетов может служить вышеупомянутая функция getaddrinfo().

#include <sys/socket.h>
int bind (int sd, const struct sockaddr 
          *address, socklen_t address_len);

Опросить присвоенный локальный адрес (его иногда называют именем сокета ) можно с помощью функции getsockname() (см. листинг 11.21): она помещает его в структуру sockaddr, на которую указывает аргумент address, а длину адреса записывает по указателю address_len.

#include <sys/socket.h>
int getsockname 
   (int sd, struct sockaddr *restrict address,
    socklen_t *restrict address_len);

Если сокет ориентирован на режим с установлением соединения (имеет тип SOCK_STREAM ), то, воспользовавшись функцией listen() (см. листинг 11.22), его следует пометить как готового принимать соединения (" слушающего ").

#include <sys/socket.h>
int listen (int sd, int backlog);

Аргумент backlog сообщает операционной системе рекомендуемую длину очереди соединений, ожидающих обработки слушающим сокетом. Реализация должна поддерживать значения аргумента backlog вплоть до конфигурационной константы SOMAXCONN, определенной в заголовочном файле <sys/socket.h>. ОС имеет право установить длину очереди меньше рекомендуемой. При неположительном значении backlog очередь будет иметь зависящую от реализации минимальную длину.

Прием соединений выполняет функция accept() (см. листинг 11.23). Она выбирает первое соединение из очереди, ассоциированной с заданным дескриптором sd слушающим сокетом, создает новый сокет с теми же адресным семейством, типом и протоколом и возвращает в качестве результата его файловый дескриптор.

#include <sys/socket.h>
int accept 
   (int sd, struct sockaddr *restrict address,
    socklen_t *restrict address_len);

Если значение аргумента address отлично от пустого указателя, то в структуру sockaddr, на которую указывает address, помещается адрес сокета, пытающегося установить соединение. По указателю address_len при обращении к функции accept() должна быть задана длина переданной структуры sockaddr, а на выходе туда записывается длина адреса партнера по взаимодействию.

Если очередь соединений, ожидающих обработки слушающим сокетом, пуста и для дескриптора sd не установлен флаг O_NONBLOCK, то вызвавший функцию accept() процесс (поток управления) блокируется до появления подобного соединения. При непустой очереди функция select() сообщит о готовности дескриптора sd к чтению.

Другая сторона, т. е. потенциальный партнер по взаимодействию, инициирует соединение с помощью функции connect() (см. листинг 11.24). Аргументы address и address_len стандартным образом задают адрес сокета (как правило, слушающего ), с которым необходимо установить соединение.

#include <sys/socket.h>
int connect (int sd, const struct sockaddr 
     *address, socklen_t address_len);

Если для сокета, заданного аргументом sd (запрашивающего установление соединения ), еще не выполнена привязка к локальному адресу, функция connect() сама осуществит связывание со свободным локальным адресом (правда, лишь при условии, что адресное семейство сокета отлично от AF_UNIX ).

Функция connect() ограничится фиксацией адреса сокета, взаимодействующего с заданным аргументом sd, если тип сокета не требует установления соединения. В частности, для сокетов типа SOCK_DGRAM таким способом можно специфицировать адреса отправляемых (с помощью функции send() ) и принимаемых (посредством обращения к функции recv() ) датаграмм.

Когда в качестве аргумента address передается пустой указатель, адрес взаимодействующего сокета сбрасывается.

Попытка установить соединение блокирует вызывающий процесс на неспецифицируемый промежуток времени (в случае наличия флага O_NONBLOCK ). Если по истечении этого промежутка соединение установить не удалось, вызов connect(), равно как и попытка установления соединения, завершаются неудачей. Если ожидание прерывается обрабатываемым сигналом, вызов connect() завершается неудачей (переменной errno присваивается значение EINTR ), но установление соединения продолжается и будет завершено асинхронно.

Если для дескриптора sd задан флаг O_NONBLOCK, а соединение не может быть установлено немедленно, то вызов connect() завершается неудачей со значением errno, равным EINPROGRESS, но установление соединения продолжается и будет завершено асинхронно. Последующие обращения к функции connect() с тем же сокетом, выполненные до установления соединения, завершаются неудачей ( EALREADY ).

Сокет оказывается в неспецифицированном состоянии, если функция connect() завершается неудачей по другим причинам. Приложения, соответствующие стандарту POSIX-2001, должны закрыть файловый дескриптор sd и создать новый сокет для продолжения попыток установить соединение.

После асинхронного установления соединения функции select() и poll() сообщат, что файловый дескриптор sd готов к записи.

Функция poll(), позволяющая мультиплексировать ввод/вывод в пределах набора файловых дескрипторов, была описана нами выше. Имеющая сходную направленность (но входящая в обязательную часть стандарта POSIX-2001) функция select() и ее недавно введенный аналог pselect() представлены в листинге 11.25.

#include <sys/select.h>
int pselect (int nfds, fd_set *restrict readfds, fd_set *restrict writefds, 
             fd_set *restrict errorfds, const struct timespec *restrict timeout, 
			 const sigset_t *restrict sigmask);
int select (int nfds, fd_set *restrict readfds, fd_set *restrict writefds, 
            fd_set *restrict errorfds, struct timeval *restrict timeout);
void FD_CLR (int fd, fd_set *fdset);
int FD_ISSET (int fd, fd_set *fdset);
void FD_SET (int fd, fd_set *fdset);
void FD_ZERO (fd_set *fdset);

Если значение аргумента readfds ( writefds, errorfds ) функции pselect() отлично от NULL, оно ссылается на объект типа fd_set, который на входе специфицирует набор файловых дескрипторов, проверяемых на готовность к чтению (или к записи, или к обработке исключительных ситуаций), а на выходе указывает, какие из них успешно прошли проверку. Аргумент nfds задает границу проверяемых дескрипторов (они являются небольшими целыми числами): дескриптор подлежит проверке, если его значение находится в пределах от 0 до (nfds - 1) включительно.

Стандарт POSIX-2001 определяет тип fd_set как абстрактный. Для работы со значениями этого типа служат макросы FD_ZERO() (сделать набор пустым), FD_SET() (добавить дескриптор к набору), FD_CLR() (удалить дескриптор из набора), FD_ISSET() (проверить принадлежность дескриптора набору). Значение именованной константы FD_SETSIZE равно максимально допустимому числу дескрипторов в наборе.

В качестве результата pselect() возвращается общее количество дескрипторов во всех трех наборах, успешно прошедших проверку.

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

Если значение аргумента sigmask отлично от пустого указателя, функция pselect() на входе заменяет маску сигналов процесса на заданную, а на выходе восстанавливает прежнюю маску.

Функция pselect() поддерживает обычные файлы, терминалы и псевдотерминалы, каналы и сокеты.

Функция select() эквивалентна pselect() со следующими оговорками.

  • Для функции select() время ожидания задается в секундах и микросекундах в виде структуры типа timeval, а для pselect() - в секундах и наносекундах как аргумент типа struct timespec.
  • У функции select() нет аргумента - маски сигналов, что эквивалентно пустому указателю в качестве значения аргумента sigmask функции pselect().
  • В случае успешного завершения функция select() может модифицировать структуру, на которую указывает аргумент timeout.
  • С сокетами могут быть ассоциированы опции, влияющие на их функционирование. Значения этих опций можно опросить или изменить с помощью функций getsockopt() и setsockopt() (см. листинг 11.26).

    #include <sys/socket.h>
    int getsockopt (int sd, int level, 
          int option_name, 
          void *restrict option_value, 
          socklen_t *restrict option_len);
    int setsockopt (int sd, int level, 
          int option_name, 
          const void *option_value, 
          socklen_t option_len);

    Опции задаются именованными константами (аргумент option_name ), которые определены в заголовочном файле <sys/socket.h>. Выделим среди них наиболее употребительные и разделим на несколько групп. К первой отнесем опции с целочисленными значениями, описывающими характеристики или состояние сокета.

    SO_ERROR

    Статус ошибок (после опроса очищается).

    SO_TYPE

    Тип сокета.

    Ко второй группе отнесем булевы опции, представленные целочисленными значениями (0 означает ложь).

    SO_DEBUG

    Сообщает, записывается ли отладочная информация о работе сокета.

    SO_ACCEPTCONN

    Указывает, является ли сокет слушающим.

    В третью группу включены опции с целочисленными значениями, определяющими количественные характеристики объектов, ассоциированных с сокетом.

    SO_SNDBUF

    Размер буфера для передаваемых данных (выходного буфера).

    SO_RCVBUF

    Размер входного буфера.

    SO_RCVLOWATM

    Минимальное число байт, обрабатываемых при вводе. Подразумеваемое значение равно единице. Большие значения могут вызвать блокировку принимающего процесса до поступления в сокет необходимого объема данных.

    SO_SNDLOWAT

    Минимальное число байт, обрабатываемых при выводе.

    В четвертую группу входят опции со структурными значениями.

    SO_LINGER

    Определяет, блокировать ли процесс при закрытии дескриптора sd до передачи буферизованных данных, и если блокировать, то на какой срок. Значением опции является структура linger, определенная в заголовочном файле <sys/socket.h> и содержащая, согласно стандарту POSIX-2001, по крайней мере следующие поля.

    int l_onoff;    
    /* Признак, включена ли опция блокирования */
    /* при закрытии */
    
    int l_linger;   
    /* Длительность блокирования в секундах */
    SO_RCVTIMEO

    Длительность ожидания поступления данных при вводе. Значение - упомянутая выше структура типа timeval. Подразумевая длительность равна нулю. Если в течение специфицированного промежутка времени новых данных не поступило, операция ввода вернет число байт, меньше запрошенного, или завершится ошибкой EAGAIN.

    SO_SNDTIMEO

    Длительность ожидания отправки данных при выводе.

    Не все из перечисленных опций могут быть переустановлены функцией setsockopt(). Исключение по понятным причинам составляют SO_ERROR, SO_TYPE, SO_ACCEPTCONN.

    Аргумент level задает протокольный уровень опции. Уровню сокетов соответствует значение SOL_SOCKET, уровню TCP - IPPROTO_TCP.

    Функция getpeername() (см. листинг 11.27), во многом аналогичная рассмотренной выше функции getsockname(), позволяет опросить еще одну характеристику - адрес ( имя ) сокета, с которым установлено соединение.

    #include <sys/socket.h>
    int getpeername (int sd, 
          struct sockaddr *restrict address, 
          socklen_t *restrict address_len);

    После привязки сокета к локальному адресу, а также установления, при необходимости, соединения и задания значений опций, можно приступать к отправке и/или приему данных через сокет. Для этого служат функции, описания которых показаны в листинге 11.28.

    #include <sys/socket.h>
    ssize_t recvfrom 
            (int sd, void *restrict buffer, 
             size_t length, int flags, 
             struct sockaddr *restrict address, 
             socklen_t *restrict address_len);
    ssize_t recv (int sd, void *buffer, 
                  size_t length, int flags);  
    ssize_t recvmsg (int sd, struct msghdr 
                     *message, int flags);
    ssize_t sendto 
            (int sd, const void *message,
             size_t length, int flags, 
             const struct sockaddr *dest_addr, 
             socklen_t dest_len);
    ssize_t send (int sd, const void *buffer,
                  size_t length, int flags);
    ssize_t sendmsg (int sd, const struct msghdr
                     *message, int flags);

    Функция recvfrom() позволяет прочитать данные (в рассматриваемом контексте называемые также сообщением ) из сокета с дескриптором sd и поместить их в буфер buffer длины length. Обычно функцию recvfrom() применяют к сокетам, ориентированным на режим без установления соединения, поскольку она выдает исходный адрес в структуре типа sockaddr.

    В число возможных составляющих значения аргумента flags входят следующие флаги.

    MSG_PEEK

    Не удалять прочитанные данные. Следующий вызов recvfrom() или другой функции ввода снова вернет их.

    MSG_WAITALL

    Для сокетов типа SOCK_STREAM флаг означает, что вызывающий процесс блокируется до получения всего запрошенного объема данных (а не до прихода первого сообщения ).

    MSG_OOB

    Запрашиваются экстренные данные. Трактовка этого понятия зависит от протокола.

    Как и положено функции ввода, в результате recvfrom() возвращает количество прочитанных и помещенных в буфер данных. Для сокетов, учитывающих границы сообщений ( SOCK_RAW, SOCK_DGRAM, SOCK_SEQPACKET ), за одно обращение читается все сообщение ; если оно не помещается в буфер, лишние байты (в отсутствие флага MSG_PEEK ) отбрасываются.

    Формально можно считать, что функция recv() эквивалентна recvfrom() с нулевым значением аргумента address_len. Поскольку она не позволяет узнать исходный адрес, ее обычно используют для сокетов, установивших соединение. (Можно провести и еще одну аналогию: если пренебречь флагами, функция recv() эквивалентна read().)

    По сравнению с recv(), более содержательным аналогом функции recvfrom() является recvmsg(). Отличия в данном случае носят скорее синтаксический характер и по сути сводятся к способу передачи входных значений и возврата результатов: для минимизации числа аргументов функция recvmsg() использует структуру типа msghdr, которая, согласно стандарту POSIX-2001, должна содержать по крайней мере следующие поля.

    void         *msg_name;         
    /* Указатель на буфер для адреса            */
    /* (исходного или целевого)                 */
    
    socklen_t     msg_namelen;      
    /* Размер буфера для адреса                 */
    
    struct iovec *msg_iov;          
    /* Массив для разнесения/сборки сообщений   */
    
    int           msg_iovlen;       
    /* Число элементов в массиве                */
    /* для разнесения/сборки сообщений          */
    
    void         *msg_control;      
    /* Указатель на буфер                       */
    /* для вспомогательных данных               */
    
    socklen_t     msg_controllen;   
    /* Размер буфера для вспомогательных данных */  
    
    int           msg_flags;        
    /* Флаги принятого сообщения                */

    Если для сокета, специфицированного дескриптором sd, соединение не установлено, в заданный указателем msg_name буфер длины msg_namelen помещается исходный адрес сообщения ; если этого не нужно, указатель msg_name можно сделать пустым. При установлении соединения оба поля игнорируются.

    Массив msg_iov совместно с полем msg_iovlen задает набор буферов для размещения принимаемых данных. Структура типа iovec определяется в заголовочном файле <sys/uio.h> и содержит по крайней мере два поля.

    void  *iov_base;        
    /* Адрес области памяти (буфера)    */
    
    size_t iov_len;         
    /* Размер области памяти (в байтах) */

    Заданные полем msg_iov области памяти по очереди заполняются поступающими данными, пока не будут размещены все принятые данные или не заполнятся все буфера.

    Трактовка вспомогательных данных (поля msg_control и msg_controllen структуры типа msghdr ) в стандарте POSIX-2001 весьма туманна. Описаны лишь средства доступа к ним. Мы, однако, не будем на этом останавливаться.

    В случае успешного завершения функции recvmsg() в поле msg_flags могут быть установлены следующие флаги.

    MSG_EOR

    Достигнута граница записи (если это понятие поддерживается протоколом ).

    MSG_OOB

    Получены экстренные данные.

    MSG_TRUNC

    Пришлось урезать обычные данные.

    MSG_CTRUNC

    Пришлось урезать управляющие данные.

    Подобно тому, как write() составляет пару read(), функцию sendto() можно считать парной по отношению к recvfrom(). Она предназначена для отправки через сокет sd сообщения, заданного аргументами message и length, по адресу dest_addr длины dest_len. Если для сокета установлено соединение, целевой адрес dest_addr игнорируется.

    В число возможных составляющих значения аргумента flags входят следующие флаги.

    MSG_EOR

    Обозначает границу записи (если это понятие поддерживается протоколом ).

    MSG_OOB

    Отправляются экстренные данные.

    Разумеется, успешное завершение функции sendto() не гарантирует доставку сообщения. Результат, равный -1, указывает только на локально выявляемые ошибки.

    Функция send() - пара для recv() со всеми следующими из этого обстоятельства эквивалентностями и аналогиями.

    Для функции sendmsg() структура типа msghdr, на которую указывает аргумент message, является входной (что показывает спецификатор const ). В поле msg_name задается целевой адрес. Поле msg_flags игнорируется.

    Для завершения операций приема и/или отправки данных через сокет служит функция shutdown() (см. листинг 11.29).

    #include <sys/socket.h>
    int shutdown (int sd, int how);

    Значение аргумента how показывает, что именно завершается: SHUT_RD прекращает прием, SHUT_WR - отправку, SHUT_RDWR - и то, и другое.

    Примеры программ работы с сокетами

    Приводимые далее примеры строятся вокруг задачи копирования данных в распределенной программной конфигурации со стандартного ввода одного процесса на стандартный вывод другого, удаленного. Однако начнем мы с локального случая, обслуживаемого сокетами адресного семейства AF_UNIX (см. листинг 11.30).

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа копирует строки со стандартного ввода на стандартный вывод, */
    /* "прокачивая" их через пару сокетов.                                   */
    /* Используются функции ввода/вывода нижнего уровня                      */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    
    #define MY_PROMPT       "Вводите строки\n"
    #define MY_MSG          "Вы ввели: "
    
    int main (void) {
     int sds [2];
     char buf [1];
     int new_line = 1;     /* Признак того, что надо выдать сообщение MY_MSG */
                           /* перед отображением очередной строки            */
    
     /* Создадим пару соединенных безымянных сокетов */
     if (socketpair (AF_UNIX, SOCK_STREAM, 0, sds) < 0) {
       perror ("SOCKETPAIR");
       exit (1);
     }
    
     switch (fork ()) {
       case -1:
         perror ("FORK");
         exit (2);
       case 0:
         /* Чтение из сокета sds [0] и выдачу на стандартный вывод */
         /* реализуем в порожденном процессе                       */
         while (read (sds [0], buf, 1) == 1) {
    	if (write (STDOUT_FILENO, buf, 1) != 1) {
             perror ("WRITE TO STDOUT");
    	  break;
    	}
         }
         exit (0);
     }
    
     /* Чтение со стандартного ввода и запись в сокет sds [1] */
     /* возложим на родительский процесс                      */
     if (write (sds [1], MY_PROMPT, sizeof (MY_PROMPT) - 1) !=
                                    sizeof (MY_PROMPT) - 1) {
       perror ("WRITE TO SOCKET-1");
     }
    
     while (read (STDIN_FILENO, buf, 1) == 1) {
       /* Перед отображением очередной строки */
       /* нужно выдать сообщение MY_MSG       */
       if (new_line) {
         if (write (sds [1], MY_MSG, sizeof (MY_MSG) - 1) != sizeof (MY_MSG) - 1) {
           perror ("WRITE TO SOCKET-2");
           break;
         }
       }
       if (write (sds [1], buf, 1) != 1) {
         perror ("WRITE TO SOCKET-3");
         break;
       }
       new_line = (buf [0] == '\n');
     }
     shutdown (sds [1], SHUT_WR);
    
     (void) wait (NULL);
     return (0);
    }

    Обратим внимание на употребление в процессе, осуществляющем запись в сокет, функции shutdown(), без чего читающий процесс не определит конец файла и не выйдет из цикла.

    Решим теперь ту же задачу для действительно распределенной конфигурации. Наше приложение будет состоять из двух процессов, выполняющихся на разных хостах. Первый процесс (см. листинг 11.31) читает строки со стандартного ввода и отправляет их в виде датаграмм другому, который принимает датаграммы и выдает их содержимое на стандартный вывод (см. листинг 11.32).

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса, читающего строки со стандартного ввода  */
    /* и посылающего их в виде датаграмм другому процессу          */
    /* (будем называть его сервером),                              */
    /* который должно выдать их на свой стандартный вывод.         */
    /* Имя серверного хоста - аргумент командной строки            */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <string.h>
    
    #define MY_PROMPT       "Вводите строки\n"
    
    int main (int argc, char *argv []) {
     int sd;               /* Дескриптор передающего сокета */
     char line [LINE_MAX]; /* Буфер для копируемых строк    */
                           /* Структура - аргумент sendmsg  */
     struct msghdr msg = {NULL, 0, NULL, 0, NULL, 0, 0};
     struct iovec iovbuf;  /* Структура для сборки отправляемых данных  */
                           /* Структура - входной аргумент getaddrinfo */
     struct addrinfo hints = {0, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
    			   0, NULL, NULL, NULL};
                           /* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo             */
     int msg_len;          /* Длина очередной введенной строки, */
                           /* включая завершающий нулевой байт  */
    
     if (argc != 2) {
       fprintf (stderr, "Использование: %s имя_серверного_хоста\n", argv [0]);
       return (1);
     }
    
     /* Создадим сокет, через который будем отправлять */
     /* прочитанные строки                             */
     if ((sd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
       perror ("SOCKET");
       return (2);
     }
    
     /* Выясним целевой адрес для датаграмм      */
     /* Воспользуемся портом для сервиса spooler */
     if ((res = getaddrinfo (argv [1], "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (3);
     }
    
     /* Заполним структуру msghdr */
     msg.msg_name = addr_res->ai_addr;
     msg.msg_namelen = addr_res->ai_addrlen;
     msg.msg_iov = iovbuf;
     msg.msg_iovlen = 1;
     iovbuf.iov_base = line;
    
     /* Цикл чтения строк со стандартного ввода    */
     /* и отправки их через сокет в виде датаграмм */
     fputs (MY_PROMPT, stdout);
     while (fgets (line, sizeof (line), stdin) != NULL) {
       msg_len = strlen (line) + 1;
       iovbuf.iov_len = msg_len;
       if (sendmsg (sd, msg, 0) != msg_len) {
         perror ("SENDMSG");
         break;
       }
     }
    
     return (0);
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его сервером),     */
    /* читающего сообщения (строки) из датаграммного сокета  */
    /* и выдающего их на стандартный вывод                   */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    
    #define MY_MSG          "Вы ввели: "
    
    int main (void) {
     int sd;               /* Дескриптор приемного сокета  */
     char line [LINE_MAX]; /* Буфер для копируемых строк   */
                           /* Структура - аргумент recvmsg */
     struct msghdr msg = {NULL, 0, NULL, 0, NULL, 0, 0};
     struct iovec iovbuf;  /* Структура для разнесения принимаемых данных */
                           /* Структура - входной аргумент getaddrinfo    */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
    			   0, NULL, NULL, NULL};
                           /* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
    
     /* Создадим сокет, через который будем принимать */
     /* прочитанные строки                            */
     if ((sd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (2);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (3);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Заполним структуру msghdr */
     msg.msg_iov = iovbuf;
     msg.msg_iovlen = 1;
     iovbuf.iov_base = line;
     iovbuf.iov_len = sizeof (line);
    
     /* Цикл приема и выдачи строк */
     while (1) {
       if (recvmsg (sd, msg, 0) < 0) {
         perror ("RECVMSG");
         break;
       }
       fputs (MY_MSG, stdout);
       fputs (line, stdout);
     }
    
     return (0);
    }

    Обратим внимание на несколько моментов. Адреса сокетов (целевого и приемного) получены при помощи функции getaddrinfo() с сервисом "spooler" в качестве аргумента. (Если на хосте этот сервис реально используется, для данного примера придется подыскать другой, свободный порт.) С практической точки зрения более правильным было бы пополнить базу данных сетевых сервисов новым элементом, специально предназначенным для представленного приложения, однако подобные административные действия находятся вне рамок стандарта POSIX и, следовательно, нами не рассматриваются. (Менее правильно, на наш взгляд, формировать адрес сокета покомпонентно, выбирая порт, по сути, случайным образом, поскольку это ведет к неявному, бессистемному, неконтролируемому пополнению базы сервисов.)

    Для успешного вызова функции bind() процесс должен обладать соответствующими привилегиями. Это может оказаться существенным для запуска программы, показанной в листинге 11.32.

    Чтобы получить от getaddrinfo() адрес, пригодный для использования в качестве аргумента ориентированных на прием функций ( listen(), bind() для приемного сокета ), необходимо указать флаг AI_PASSIVE во входной для getaddrinfo() структуре addrinfo (аргумент hints в описании функции getaddrinfo() ). В таком случае для IP-адреса будет выдано значение INADDR_ANY, которое трактуется в адресном семействе AF_INET как адрес локального хоста, успешно сопоставляющийся как со шлейфовым ( 127.0.0.1 ), так и с реальным сетевыми адресами. В результате через этот сокет можно будет принимать датаграммы, посланные и с локального, и с удаленного хостов.

    Цикл приема данных сервером сделан бесконечным, поскольку у последовательности датаграмм нет естественного признака конца.

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

    В программе, посылающей датаграммы, выполним привязку сокета к свободному локальному адресу, воспользовавшись для этого функцией connect() и, для оправдания затраченных усилий, изменим способ отправки: вместо универсальной sendmsg() задействуем даже не более простую функцию send(), а ее вполне привычный аналог write(), скрытый под личиной fputs().

    Какой режим буферизации выбрать для потока, сформированного по открытому файловому дескриптору сокета, - дело вкуса. Читателю рекомендуется попробовать разные варианты, каждый из которых имеет свои "за" и "против". Второй вид модификаций касается серверного процесса. Поскольку теперь у датаграмм появился исходный адрес, его можно выяснить и выдать, а заодно опросить выходной флаг MSG_TRUNC, чтобы убедиться, что при пересылке в виде датаграмм копируемые строки не были урезаны. Кроме того, изменение способа формирования отправляемых датаграмм привело к тому, что теперь их содержимое не завершается нулевым байтом; следовательно, способ вывода принимаемых данных также нуждается в модификации (в нашем случае - в дописывании нулевого байта).

    Модифицированные варианты программ представлены в листингах 11.33 и 11.34.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса, читающего строки со стандартного ввода  */
    /* и посылающего их в виде датаграмм другому процессу          */
    /* (будем называть его сервером),                              */
    /* который должно выдать их на свой стандартный вывод.         */
    /* Имя серверного хоста - аргумент командной строки            */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    
    #define MY_PROMPT       "Вводите строки\n"
    
    int main (int argc, char *argv []) {
     int sd;               /* Дескриптор передающего сокета             */
     FILE *ss;             /* Поток, соответствующий передающему сокету */
     char line [LINE_MAX]; /* Буфер для копируемых строк                */
    			/* Структура - входной аргумент getaddrinfo  */ 
     struct addrinfo hints = {0, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
    
     if (argc != 2) {
       fprintf (stderr, "Использование: %s имя_серверного_хоста\n", argv [0]);
       return (1);
     }
    
     /* Создадим сокет, через который будем отправлять */
     /* прочитанные строки                             */
     if ((sd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
       perror ("SOCKET");
       return (2);
     }
    
     /* Выясним целевой адрес для датаграмм      */
     /* Воспользуемся портом для сервиса spooler */
     if ((res = getaddrinfo (argv [1], "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (3);
     }
    
     /* Воспользуемся функцией connect() для достижения двух целей: */
     /* фиксации целевого адреса и привязки к некоему локальному    */
     if (connect (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("CONNECT");
       return (4);
     }
    
     /* Сформирует поток по дескриптору сокета */
     if ((ss = fdopen (sd, "w")) == NULL) {
       perror ("FDOPEN");
       return (5);
     }
     /* Отменим буферизацию для этого потока */
     setbuf (ss, NULL);
    
     /* Цикл чтения строк со стандартного ввода    */
     /* и отправки их через сокет в виде датаграмм */
     fputs (MY_PROMPT, stdout);
     while (fgets (line, sizeof (line), stdin) != NULL) {
       fputs (line, ss);
     }
    
     return (0);
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его сервером), */
    /* читающего сообщения из датаграммного сокета       */
    /* и копирующего их на стандартный вывод             */
    /* с указанием адреса, откуда они поступили          */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main (void) {
     int sd;               /* Дескриптор приемного сокета                */
                           /* Буфер для принимаемых сообщений            */
                           /* Оставлено место для вставки нулевого байта */
     char lbuf [BUFSIZ + 1];
    			/* Структура - аргумент recvmsg */
     struct msghdr msg = {NULL, 0, NULL, 0, NULL, 0, 0};
     struct iovec iovbuf;  /* Структура для разнесения принимаемых данных */
    			/* Структура - входной аргумент getaddrinfo    */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo                    */
                           /* Структура для исходного адреса датаграмм */
     struct sockaddr_in sai;
     ssize_t lmsg;         /* Длина принятой датаграммы */
    
     /* Создадим сокет, через который будем принимать */
     /* прочитанные строки                            */
     if ((sd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Заполним структуру msghdr */
     msg.msg_name = sai;
     msg.msg_namelen = sizeof (struct sockaddr_in);
     msg.msg_iov = iovbuf;
     msg.msg_iovlen = 1;
     iovbuf.iov_base = lbuf;
                                   /* Оставим место для вставки нулевого байта */
     iovbuf.iov_len = sizeof (lbuf) - 1;
    
     /* Цикл приема и выдачи строк */
     while (1) {
       if ((lmsg = recvmsg (sd, msg, 0)) < 0) {
         perror ("RECVMSG");
         break;
       }
       printf ("Вы ввели и отправили с адреса %s, порт %d :",
               inet_ntoa (((struct sockaddr_in *) msg.msg_name)->sin_addr),
               ntohs (((struct sockaddr_in *) msg.msg_name)->sin_port));
       if (msg.msg_flags  MSG_TRUNC) {
         printf ("Датаграмма была урезана\n");
       }
       lbuf [lmsg] = 0;
       fputs (lbuf, stdout);
     }
    
     return (0);
    }

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

    Сделаем теперь более существенные изменения, прежде всего на серверной стороне. Перейдем на использование надежного режима с установлением соединения (и, соответственно, сокетов типа SOCK_STREAM ). Сервер будет получать запросы на установление соединения через порт сервиса "spooler" и порождать для каждого принятого запроса свой обслуживающий процесс.

    Новый вариант программы процесса, читающего строки со стандартного ввода и отправляющего их через потоковый сокет, показан в листинге 11.35; две программы серверной части - в листингах 11.36 и 11.37. Предполагается, что выполнимый файл с образом обслуживающего процесса находится в текущем каталоге и имеет имя gsce.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса, читающего строки со стандартного ввода  */
    /* и посылающего их в виде потока другому процессу             */
    /* (будем называть его сервером),                              */
    /* который должно выдать строки на свой стандартный вывод.     */
    /* Имя серверного хоста - аргумент командной строки            */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h> 
    #include <sys/socket.h>
    
    #define MY_PROMPT       "Вводите строки\n"
    
    int main (int argc, char *argv []) {
     int sd;               /* Дескриптор передающего сокета             */
     FILE *ss;             /* Поток, соответствующий передающему сокету */
     char line [LINE_MAX]; /* Буфер для копируемых строк                */
    			/* Структура - входной аргумент getaddrinfo  */
     struct addrinfo hints = {0, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
    
     if (argc != 2) {
       fprintf (stderr, "Использование: %s имя_серверного_хоста\n", argv [0]);
       return (1);
     }
    
     /* Создадим сокет, через который будем отправлять */
     /* прочитанные строки                             */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (2);
     }
    
     /* Выясним целевой адрес для соединения     */
     /* Воспользуемся портом для сервиса spooler */
     if ((res = getaddrinfo (argv [1], "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (3);
     }
    
     /* Воспользуемся функцией connect() для достижения двух целей:    */
     /* установления соединения и привязки к некоему локальному адресу */
     if (connect (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("CONNECT");
       return (4);
     }
    
     /* Сформируем поток по дескриптору сокета */
     if ((ss = fdopen (sd, "w")) == NULL) {
       perror ("FDOPEN");
       return (5);
     }
     /* Отменим буферизацию для этого потока */
     setbuf (ss, NULL);
    
     /* Цикл чтения строк со стандартного ввода */
     /* и отправки их через сокет в виде потока */
     fputs (MY_PROMPT, stdout);
     while (fgets (line, sizeof (line), stdin) != NULL) {
       fputs (line, ss);
     }
     shutdown (sd, SHUT_WR);
    
     return (0);
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его демоном),  */
    /* принимающего запросы на установления соединения   */
    /* и запускающего процессы для их обслуживания       */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    
    int main (void) {
     int sd;               /* Дескриптор слушающего сокета */
     int ad;               /* Дескриптор приемного сокета  */
    			/* Буфер для принимаемых строк  */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
    
     /* Создадим слушающий сокет */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Пометим сокет как слушающий */
     if (listen (sd, SOMAXCONN) < 0) {
       perror ("LISTEN");
       return (3);
     }
    
     /* Цикл приема соединений и запуска обслуживающих процессов */
     while (1) {
       /* Примем соединение.                                          */
       /* Адрес партнера по общению нас в данном случае не интересует */
       if ((ad = accept (sd, NULL, NULL)) < 0) {
         perror ("ACCEPT");
         return (4);
       }
    
       /* Запустим обслуживающий процесс */
       switch (fork ()) {
         case -1:
    	perror ("FORK");
    	return (5);
         case 0:
    	/* Сделаем сокет ad стандартным вводом */
    	(void) close (STDIN_FILENO);
    	(void) fcntl (ad, F_DUPFD, STDIN_FILENO);
    	(void) close (ad);
    	/* Сменим программу процесса на обслуживающую */
    	if (execl ("./gsce", "gsce", (char *) NULL) < 0) {
    	  perror ("EXECL");
    	  return (6);
    	}
       }
    
       /* В родительском процессе дескриптор принятого соединения нужно закрыть */
       (void) close (ad);
    
       /* Обслужим завершившиеся порожденные процессы */
       while (waitpid ((pid_t) (-1), NULL, WNOHANG) > 0) ;
     }
     
     return (0);
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа обслуживающего процесса,                  */
    /* выдающего принимаемые строки на стандартный вывод.  */
    /* Дескриптор приемного сокета передан                 */
    /* в качестве стандартного ввода                       */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main (void) {
     char line [LINE_MAX]; /* Буфер для принимаемых строк */
    			/* Структура для записи адреса */
     struct sockaddr_in sai;
    			/* Длина адреса */
     socklen_t sai_len = sizeof (struct sockaddr_in);
    
     /* Опросим адрес партнера по общению (передающего сокета) */
     if (getpeername (STDIN_FILENO, (struct sockaddr *) sai, sai_len) < 0) {
       perror ("GETPEERNAME");
       return (1);
     }
    
     /* Цикл чтения строк из сокета      */
     /* и выдачи их на стандартный вывод */
     while (fgets (line, sizeof (line), stdin) != NULL) {
       printf ("Вы ввели и отправили с адреса %s, порт %d :",
    	    inet_ntoa (sai.sin_addr), ntohs (sai.sin_port));
       fputs (line, stdout);
     }
    
     /* Закрытие соединения */
     shutdown (STDIN_FILENO, SHUT_RD);
    
     return (0);
    }

    Обратим внимание на два технических момента. Во-первых, дескриптор приемного сокета передается в обслуживающий процесс под видом стандартного ввода. В результате обслуживающая программа по сути свелась к циклу копирования строк со стандартного ввода на стандартный вывод. Мы вернулись к тому, с чего начинали рассмотрение средств буферизованного ввода/вывода, но на новом витке спирали. Во-вторых, в иллюстративных целях для обслуживания завершившихся порожденных процессов использована функция waitpid() со значением аргумента pid, равным -1 (выражающим готовность обслужить любой завершившийся процесс-потомок), и флагом WNOHANG, означающим отсутствие ожидания. В данном случае подобный прием не гарантирует отсутствия зомби-процессов, но препятствует их накоплению.

    Чтобы проиллюстрировать еще один типичный прием, связанный с обработкой запросов на установление соединения, изменим способ обслуживания завершившихся порожденных процессов. Определим функцию обработки сигнала SIGCHLD и поместим в нее вызов waitpid(). Модифицированный вариант программы процесса-демона, аккуратно ликвидирующий все зомби-процессы, показан в листинге 11.38.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его демоном),  */
    /* принимающего запросы на установления соединения   */
    /* и запускающего процессы для их обслуживания       */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    #include <signal.h>
    #include <errno.h>
    
    /* Функция обработки сигнала SIGCHLD */
    static void chldied (int dummy) {
     /* Вдруг число завершившихся потомков отлично от единицы? */
     while (waitpid ((pid_t) (-1), NULL, WNOHANG) > 0) ;
    }
    
    int main (void) {
    
     int sd;               /* Дескриптор слушающего сокета             */
     int ad;               /* Дескриптор приемного сокета              */
    			/* Структура - входной аргумент getaddrinfo */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo                           */
                           /* Структура для задания реакции на сигнал SIGCHLD */
     struct sigaction sact;
    
     /* Создадим слушающий сокет */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
     
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Пометим сокет как слушающий */
     if (listen (sd, SOMAXCONN) < 0) {
       perror ("LISTEN");
       return (3);
     }
    
     /* Установим обработку сигнала о завершении потомка */
     sact.sa_handler = chldied;
     (void) sigemptyset (sact.sa_mask);
     sact.sa_flags = 0;
     (void) sigaction (SIGCHLD, sact, (struct sigaction *) NULL);
    
     /* Цикл приема соединений и запуска обслуживающих процессов */
     while (1) {
       /* Примем соединение с учетом того, что ожидание может быть прервано */
       /* доставкой обрабатываемого сигнала.                                */
       /* Адрес партнера по общению в данном случае нас не интересует       */
       while ((ad = accept (sd, NULL, NULL)) < 0) {
         if (errno != EINTR) {
           perror ("ACCEPT");
           return (4);
         }
       }
    
       /* Запустим обслуживающий процесс */
       switch (fork ()) {
         case -1:
    	perror ("FORK");
    	return (5);
         case 0:
    	/* Сделаем сокет ad стандартным вводом */
    	(void) close (STDIN_FILENO);
    	(void) fcntl (ad, F_DUPFD, STDIN_FILENO);
    	(void) close (ad);
    	/* Сменим программу процесса на обслуживающую */
    	if (execl ("./gsce", "gsce", (char *) NULL) < 0) {
    	  perror ("EXECL");
    	  return (6);
    	}
       }
    
       /* В родительском процессе дескриптор принятого соединения нужно закрыть */
       (void) close (ad);
     }
    
     return (0);
    }

    Задание функции обработки сигнала о завершении потомка имеет нелокальный эффект. Оно повлияло на поведение функции accept(): ее вызов может быть прерван доставкой сигнала SIGCHLD. В такой ситуации прежний вариант процесса-демона завершился бы, выдав в стандартный протокол диагностическое сообщение вида "ACCEPT: Interrupted system call". Чтобы этого не случилось, вызов accept() пришлось заключить в цикл с проверкой (в случае неудачного завершения) значения переменной errno на совпадение с EINTR. Вообще говоря, по подобной схеме рекомендуется действовать для всех функций, выполнение которых может быть прервано доставкой сигнала, но допускающих и повторный вызов. Отметим в этой связи, что, например, функция connect() очень похожа, но все-таки отличается своими особенностями. После доставки сигнала ее вызов завершается неудачей, а установление соединения продолжается асинхронно, поэтому повторное обращение может завершиться неудачей со значением errno, равным EALREADY.

    Рассмотрим теперь еще один вариант сервера, имеющего однопроцессную организацию, но мультиплексирующего ввод с помощью функции select() (см. листинг 11.39).

    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его сервером), */
    /* принимающего запросы на установления соединения   */
    /* и обслуживающего их с использованием select()     */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <sys/select.h>
    
    /* Структура для хранения дескрипторов приемных сокетов */
    /* и ассоциированной информации                         */
    #define MY_MSG_LN       128
    struct sads {
     int ad;
     FILE *sd;
     char my_msg [MY_MSG_LN];
    };
    
    /* Максимальное число параллельно обслуживаемых запросов */
    #define MY_MAX_QS       FD_SETSIZE
    
    /* Функция для освобождения элемента массива */
    /* дескрипторов приемных сокетов             */
    static void free_elem (struct sads *sadsp) {
     shutdown (sadsp->ad, SHUT_RD);
     close (sadsp->ad);
     sadsp->ad = -1;
    }
    
    int main (void) {
     int sd;               /* Дескриптор слушающего сокета             */
    			/* Структура - входной аргумент getaddrinfo */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo         */
     fd_set rdset;         /* Набор дескрипторов для чтения */
    			/* Структура для записи адреса   */
     struct sockaddr_in sai;
     socklen_t sai_len;    /* Длина адреса                                      */
     char line [LINE_MAX]; /* Буфер для принимаемых строк                       */
                           /* Массов для хранения дескрипторов приемных сокетов */
                           /* и ассоциированной информации.                     */
                           /* Последний элемент - фиктивный,                    */
                           /* нужен для упрощения поиска свободного             */
     struct sads sadsarr [MY_MAX_QS + 1];
     int sads_max;         /* Верхняя граница дескрипторов, проверяемых select() */
     int i;
    
     /* Создадим слушающий сокет */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Пометим сокет как слушающий */
     if (listen (sd, SOMAXCONN) < 0) {
       perror ("LISTEN");
       return (3);
     }
    
     /* Инициализируем массив sadsarr.              */ 
     /* -1 в поле ad означает, что элемент свободен */
     for (i = 0; i < (MY_MAX_QS + 1); i++) {
       sadsarr [i].ad = -1;
     }
    
     /* Цикл приема соединений и обслуживания запросов */
     while (1) {
       /* Подготовим наборы дескрипторов для select() */
       FD_ZERO (rdset);
       FD_SET (sd, rdset);
       sads_max = sd + 1;
       for (i = 0; i < MY_MAX_QS; i++) {
         if (sadsarr [i].ad >= 0) {
           FD_SET (sadsarr [i].ad, rdset);
           if ((sadsarr [i].ad + 1) > sads_max) {
             sads_max = sadsarr [i].ad + 1;
           }
         }
       }
    
       /* Подождем запроса на установление соединения */
       /* или готовности данных для чтения.           */
       /* Время ожидания зададим как бесконечное      */
       if (select (sads_max, rdset, NULL, NULL, NULL) == -1) {
         perror ("PSELECT");
         return (4);
       }
    
       /* Посмотрим, есть ли запросы, ждущие установления соединения */
       if (FD_ISSET (sd, rdset)) {
         /* Примем запрос на установление соединения,               */
         /* если есть свободный элемент массива дескрипторов.       */
         /* Последний элемент считаем фиктивным; он всегда свободен */
         i = -1;
         while (sadsarr [++i].ad >= 0) ;
         if (i < MY_MAX_QS) {
           /* Свободный элемент нашелся */
           sai_len = sizeof (struct sockaddr_in);
           if ((sadsarr [i].ad = accept (sd, (struct sockaddr *) sai,
                                         sai_len)) == -1) {
             perror ("ACCEPT");
             continue;
           }
           /* Сформируем сообщение, выдаваемое перед принятой строкой */
           (void) sprintf (sadsarr [i].my_msg,
                           "Вы ввели и отправили с адреса %s, порт %d :",
               	        inet_ntoa (sai.sin_addr), ntohs (sai.sin_port));
           /* Сформируем поток по дескриптору сокета */
           /* и отменим буферизацию для этого потока */
           if ((sadsarr [i].sd = fdopen (sadsarr [i].ad, "r")) == NULL) {
             perror ("FDOPEN");
             free_elem (sadsarr [i]);
             continue;
           }
           setbuf (sadsarr [i].sd, NULL);
         }
       }
    
       /* Посмотрим, есть ли данные, готовые для чтения */
       for (i = 0; i < MY_MAX_QS; i++) {
         if ((sadsarr [i].ad >= 0)  FD_ISSET (sadsarr [i].ad, rdset)) {
           /* Есть данные, готовые для чтения,   */
           /* или установлен признак конца файла */
           if (fgets (line, sizeof (line), sadsarr [i].sd) != NULL) {
             /* Выведем полученную строку */
             fputs (sadsarr [i].my_msg, stdout);
             fputs (line, stdout);
           } else {
             /* Отправитель закрыл соединение. */
             /* Закроем его и мы               */
             fclose (sadsarr [i].sd);
             free_elem (sadsarr [i]);
           }
         }
       }
     }
    
     return (0);
    }

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

    В качестве следующего примера рассмотрим передачу целочисленных данных. Главная проблема здесь - аккуратное выполнение преобразований между хостовым и сетевым порядками байт. Для иллюстрации использовано вычисление и вывод строк треугольника Паскаля. Вычисляющая и передающая программа показана в листинге 11.40, принимающая и выводящая - в листинге 11.41.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа вычисляет несколько первых строк треугольника Паскаля */
    /* и отправляет их через сокет сервису spooler.                    */
    /* Имя целевого хоста - аргумент командной строки.                 */
    /* Передаваемые данные снабжаются однобайтными маркерами типа      */
    /* и кратности и преобразуются к сетевому порядку байт             */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    
    /* Количество вычисляемых строк треугольника Паскаля */
    #define T_SIZE  16
    
    /* Маркеры типов передаваемых данных */
    #define T_UCHAR         1
    #define T_UINT16        2
    #define T_UINT32        4
    
    #define T_HDR   "\nТреугольник Паскаля:"
    
    int main (int argc, char *argv []) {
     uint32_t tp [T_SIZE]; /* Массив для хранения текущей строки треугольника   */
     unsigned char mtl;    /* Переменная для хранения маркеров типа и кратности */
     uint32_t ntelem;      /* Текущий элемент строки в сетевом порядке байт     */
     int sd;               /* Дескриптор передающего сокета                     */ 
    			/* Структура - входной аргумент getaddrinfo          */
     struct addrinfo hints = {0, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
     int i, j;
    
     if (argc != 2) {
       fprintf (stderr, "Использование: %s имя_серверного_хоста\n", argv [0]);
       return (1);
     }
    
     /* Создадим передающий сокет и установим соединение */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (2);
     }
    
     if ((res = getaddrinfo (argv [1], "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (3);
     }
    
     if (connect (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("CONNECT");
       return (4);
     }
    
     /* Инициализируем массив для хранения текущей строки треугольника Паскаля, */
     /* чтобы далее все элементы можно было считать и передавать единообразно   */
     tp [0] = 1;
     for (i = 1; i < T_SIZE; i++) {
       tp [i] = 0;
     }
    
     /* Передадим заголовок */
     mtl = T_UCHAR;
     if (write (sd, mtl, 1) != 1) {
       perror ("WRITE-1");
       return (5);
     }
     mtl = sizeof (T_HDR) - 1;
     if (write (sd, mtl, 1) != 1) {
       perror ("WRITE-2");
       return (6);
     }
     if (write (sd, T_HDR, mtl) != mtl) {
       perror ("WRITE-3");
       return (7);
     }
    
     /* Вычислим и передадим строки треугольника Паскаля */
     for (i = 0; i < T_SIZE; i++) {
       /* Элементы очередной строки нужно считать от конца к началу */
       /* Элемент tp [0] пересчитывать не нужно                     */
       for (j = i; j > 0; j--) {
         tp [j] += tp [j - 1];
       }
       /* Вывод строки треугольника в сокет */
       mtl = T_UINT32;
       if (write (sd, mtl, 1) != 1) {
         perror ("WRITE-4");
         return (8);
       }
       mtl = i + 1;
       if (write (sd, mtl, 1) != 1) {
         perror ("WRITE-5");
         return (9);
       }
       /* Преобразование элементов строки к сетевому порядку байт и вывод */
       for (j = 0; j <= i; j++) {
         ntelem = htonl (tp [j]);
         if (write (sd, ntelem, sizeof (ntelem)) != sizeof (ntelem)) {
           perror ("WRITE-6");
           return (10);
         }
       }
     }
     shutdown (sd, SHUT_WR);
    
     return (close (sd));
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его наивным сервером), */
    /* принимающего запросы на установления соединения           */
    /* и осуществляющего вывод поступающих числовых данных       */
    /* без порождения процессов и мультиплексирования            */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    /* Маркеры типов передаваемых данных */
    #define T_UCHAR         1
    #define T_UINT16        2
    #define T_UINT32        4
    
    int main (void) {
     int sd;               /* Дескриптор слушающего сокета             */
     int ad;               /* Дескриптор приемного сокета              */
    			/* Структура - входной аргумент getaddrinfo */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo       */
    			/* Структура для записи адреса */
     struct sockaddr_in sai;
     socklen_t sai_len;    /* Длина адреса                        */
     char mt;              /* Маркер типа поступающих данных      */
     char ml;              /* Маркер кратности поступающих данных */
     char     bufc;        /* Буфер для приема символьных данных  */
     uint16_t buf16;       /* Буфер для приема 16-разрядных целых */
     uint32_t buf32;       /* Буфер для приема 16-разрядных целых */
    
     /* Создадим слушающий сокет */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Пометим сокет как слушающий */
     if (listen (sd, SOMAXCONN) < 0) {
       perror ("LISTEN");
       return (3);
     }
    
     /* Цикл приема соединений и обслуживания запросов на вывод */
     while (1) {
       /* Примем соединение */
       sai_len = sizeof (struct sockaddr_in);
       if ((ad = accept (sd, (struct sockaddr *) sai, sai_len)) < 0) {
         perror ("ACCEPT");
         continue;
       }
    
       /* Цикл приема поступающих данных и их вывода */
       printf ("Получено с адреса %s, порт %d :",
    	    inet_ntoa (sai.sin_addr), ntohs (sai.sin_port));
       while (read (ad, mt, 1) == 1) {
         /* Есть очередная порция данных, начинающаяся с типа. */
         /* Прочитаем кратность, потом сами данные             */
         if (read (ad, ml, 1) != 1) {
           perror ("READ-1");
           return (4);
         }
         while (ml-- > 0) {
           switch (mt) {
             case T_UCHAR:
               if (read (ad, bufc, sizeof (bufc)) != sizeof (bufc)) {
                 perror ("READ-2");
                 return (5);
               }
               printf ("%c", bufc);
               continue;
             case T_UINT16:
               if (read (ad, buf16, sizeof (buf16)) != sizeof (buf16)) {
                 perror ("READ-3");
                 return (6);
               }
               printf (" %d", ntohs (buf16));
               continue;
             case T_UINT32:
               if (read (ad, buf32, sizeof (buf32)) != sizeof (buf32)) {
                 perror ("READ-4");
                 return (7);
               }
               printf (" %d", ntohl (buf32));
               continue;
           }
         }
         /* Вывод порции завершим переводом строки */
         printf ("\n");
       }
    
       /* Конец обслуживания соединения */
       (void) close (ad);
     }
    
     return (0);
    }
    Страницы:

    Основные понятия и объекты

    Стандарт POSIX-2001 определяет сеть как совокупность взаимосвязанных хостов. Тем самым предполагается, что сетевая инфраструктура остается скрытой от приложений, которым предоставляются высокоуровневые средства взаимодействия в распределенной среде.

    Под сетевым адресом понимается видимый в пределах сети идентификатор, используемый для обозначения оконечных точек сети . Адреса есть у определенных оконечных точек хостов, могут они быть и у хостов в целом.

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

    Процесс присвоения сетевого адреса оконечной точке называется связыванием, или привязкой, а обратное действие - освобождением, или отменой привязки.

    Обычно оконечной точкой служит аппаратный сетевой интерфейс, посредством которого данные передаются и принимаются, однако с таким интерфейсом, как шлейфовый (loopback), никакой аппаратуры не ассоциировано.

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

    Данные передаются по сети в виде последовательности октетов (восьмибитных беззнаковых величин). Если некоторый элемент данных (например, адрес или номер порта ) состоит более чем из восьми бит, для его передачи и хранения требуется несколько октетов. Сетевым называется порядок октетов (байт), при котором первый (с наименьшим адресом) октет содержит старшие (наиболее значимые) биты.

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

    Для преобразования значений типов uint16_t и uint32_t из хостового порядка байт в сетевой служат функции htons() и htonl() ; функции ntohs() и ntohl() осуществляют обратную операцию.

    При взаимодействии процессов оконечными точками служат сокеты, они трактуются стандартом POSIX-2001 как отдельный тип файлов.

    Под адресом сокета как (удаленной) оконечной точки понимается структура, включающая идентификатор адресного семейства и адресную информацию, специфичную для данного семейства. Последняя может состоять из нескольких компонентов, в том числе сетевого адреса хоста и идентификатора конкретной оконечной точки.

    Основные описания, относящиеся к сокетам, сосредоточены в заголовочном файле <sys/socket.h>. Фигурирует в нем и упомянутая выше структура sockaddr для адреса сокета, которая должна содержать по крайней мере следующие поля.

    sa_family_t sa_family;  
    /* Адресное семейство */
    char        sa_data []; 
    /* Адрес сокета (данные переменной длины) */

    Адресное семейство соответствует конкретной среде взаимодействия. Стандарт POSIX-2001 определяет три таких семейства.

    AF_UNIX

    Адресное семейство UNIX поддерживает межпроцессное взаимодействие в пределах одной системы. Формально это можно считать вырожденным случаем сетевого взаимодействия. Описания, специфичные для данного семейства, содержатся в заголовочном файле <sys/un.h>.

    AF_INET

    Адресное семейство, поддерживающее взаимодействие по протоколам   IPv4. Специфичные для него описания располагаются в заголовочном файле <netinet/in.h>.

    AF_INET6

    Взаимодействие по протоколам IPv6 (необязательная возможность). За счет поддержки адресов IPv6, отображенных на IPv4, обеспечивается совместимость с приложениями, использующими IPv4. Применяемые эти адресным семейством описания распределены по заголовочным файлам <netinet/in.h>, <arpa/inet.h> и <netdb.h>.

    В пределах каждого адресного семейства могут существовать сокеты нескольких типов. В стандарте POSIX-2001 их четыре.

    SOCK_STREAM

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

    SOCK_SEQPACKET

    Аналог SOCK_STREAM с дополнительным сохранением границ между записями.

    SOCK_DGRAM

    Передача данных в виде датаграмм в режиме без установления соединения.

    SOCK_RAW

    (Необязательная возможность). Аналог SOCK_DGRAM с дополнительной возможностью доступа к протокольным заголовкам и другой информации нижнего уровня. Создавать сокеты этого типа могут лишь процессы с соответствующими привилегиями.

    Для каждого адресного семейства каждый тип сокета может поддерживаться одним или несколькими протоколами. В частности, в адресном семействе   AF_INET для сокетов   типа   SOCK_STREAM подразумеваемым является протокол с именем IPPROTO_TCP, а для типа   SOCK_DGRAM - IPPROTO_UDP ; посредством "прозрачных" сокетов ( SOCK_RAW ) можно воспользоваться протоколом   ICMP, задав имя IPPROTO_ICMP, и т.д.

    Общая логика работы с сокетами состоит в следующем. Сокеты создаются с помощью функции socket(), которой в качестве аргументов передают адресное семейство, тип сокета и протокол, а в результате получают открытый файловый дескриптор. Затем посредством функции bind()   сокету присваивают локальный адрес. Если сокет ориентирован на режим с установлением соединения, то, прибегнув к функции listen(), его следует пометить как готового принимать соединения. Реальный прием соединений выполняет функция accept(), создающая для каждого из них новый сокет по образу и подобию " слушающего ". В свою очередь, потенциальный партнер по взаимодействию инициирует соединение, применяя функцию connect(). (В режиме без установления соединения функция connect() позволяет специфицировать адрес отправляемых через сокет   датаграмм.)

    Для приема поступивших в сокет данных можно воспользоваться универсальной функцией низкоуровневого ввода/вывода read() или специализированным семейством функций recv*(), а для передачи - функцией write() или семейством send*(). Кроме того, функции select() и/или poll() помогут проверить наличие данных для приема или возможность отправки очередной порции данных.

    Обращение к функции shutdown() завершает взаимодействие между партнерами.

    Опрос данных о сети

    Данные о хостах как узлах сети хранятся в сетевой базе, последовательный доступ к которой обслуживается функциями sethostent(), gethostent() и endhostent() (см. листинг 11.1).

    #include <netdb.h>
    void sethostent (int stayopen);
    struct hostent *gethostent (void);
    void endhostent (void);

    Функция sethostent() устанавливает соединение с базой, остающееся открытым после вызова gethostent(), если значение аргумента stayopen отлично от нуля. Функция gethostent() последовательно читает элементы базы, возвращая результат в структуре типа hostent, содержащей по крайней мере следующие поля.

    char  *h_name;          
    /* Официальное имя хоста                */
    char **h_aliases;       
    /* Массив указателей на альтернативные  */
    /* имена хоста, завершаемый пустым      */
    /* указателем                           */
    int    h_addrtype;      
    /* Тип адреса хоста                     */
    int    h_length;        
    /* Длина в байтах адреса данного типа   */
    char **h_addr_list;     
    /* Массив указателей на сетевые адреса  */
    /* хоста, завершаемый пустым указателем */

    Функция endhostent() закрывает соединение с базой.

    В листинге 11.2 показана программа, осуществляющая последовательный просмотр сетевой базы данных о хостах - узлах сети, а в листинге 11.3 приведен фрагмент ее возможной выдачи.

    #include <stdio.h>
    #include <netdb.h>
    
    int main (void) {
     struct hostent *pht;
     char *pct;
     int i, j;
    
     sethostent (1);
    
     while ((pht = gethostent ()) != NULL) {
       printf ("Официальное имя хоста: %s\n", pht->h_name);
       printf ("Альтернативные имена:\n");
       for (i = 0; (pct = pht->h_aliases [i]) != NULL; i++) {
         printf ("  %s\n", pct);
       }
       printf ("Тип адреса хоста: %d\n", pht->h_addrtype);
       printf ("Длина адреса хоста: %d\n", pht->h_length);
       printf ("Сетевые адреса хоста:\n");
       for (i = 0; (pct = pht->h_addr_list [i]) != NULL; i++) {
         for (j = 0; j < pht->h_length; j++) {
    	printf (" %d", (unsigned char) pct [j]);
         }
         printf ("\n");
       }
     }
    
     endhostent ();
    
     return 0;
    }
    Официальное имя хоста: localhost
    Альтернативные имена:
    Тип адреса хоста: 2
    Длина адреса хоста: 4
    Сетевые адреса хоста:
    127 0 0 1
    
      . . .
    Официальное имя хоста: t01
    Альтернативные имена:
     niisi.msk.ru
     t01.niisi.msk.ru
     mail
     mailhost
     loghost
     server
     server3
     server3.systud.msk.su
     www
     www.systud.msk.su
     t01.systud.msk.su
    Тип адреса хоста: 2
    Длина адреса хоста: 4
    Сетевые адреса хоста:
    193 232 173 1
    
      . . .
    
    Официальное имя хоста: t17
    Альтернативные имена:
     galatenko
    Тип адреса хоста: 2
    Длина адреса хоста: 4
    Сетевые адреса хоста:
    193 232 173 17
    
      . . .

    К рассматриваемой базе возможен и случайный доступ по ключам - именам и адресам хостов с помощью функций gethostbyname() и gethostbyaddr(), однако они считаются устаревшими и из новой версии стандарта POSIX могут быть исключены. Вместо них предлагается использовать функции getnameinfo() и getaddrinfo() (см. листинг 11.4).

    #include <sys/socket.h>
    #include <netdb.h>
    
    void freeaddrinfo (struct addrinfo *ai);
    
    int getaddrinfo 
        (const char *restrict nodename, 
         const char *restrict servname,
         const struct addrinfo *restrict hints,
         struct addrinfo **restrict res);
    
    int getnameinfo 
        (const struct sockaddr *restrict sa, 
         socklen_t salen, char *restrict node, 
         socklen_t nodelen, char *restrict service,
         socklen_t servicelen, int flags);

    Функция getaddrinfo() позволяет по имени узла сети (хоста) (аргумент nodename ) и/или имени сетевого сервиса ( servname ) получить набор адресов сокетов и ассоциированную информацию, что дает возможность создать сокет для обращения к заданному сервису.

    Если аргумент nodename отличен от пустого указателя, он способен задавать описательное имя или адресную цепочку. Для адресных семейств   AF_INET и AF_UNSPEC (см. ниже описание аргумента hints ) именем может служить имя хоста, а адресной цепочкой - стандартные для Internet адреса в точечных обозначениях (например, 193.232.173.17 ).

    При пустом значении nodename подразумевается хост, локальный для вызывающего процесса.

    Аргумент servname может задавать имя сервиса или (для адресных семейств   AF_INET и AF_UNSPEC ) десятичный номер порта. Пустое значение servname означает запрос сетевого адреса.

    Аргумент hints позволяет передать дополнительную информацию об опрашиваемом сервисе - адресное семейство, тип сокета, протокол, флаги. Согласно стандарту, структура addrinfo, описанная в заголовочном файле <netdb.h>, должна содержать по крайней мере следующие поля.

    int              ai_flags;      
    /* Входные флаги */
    
    int              ai_family;     
    /* Адресное семейство сокета */
    
    int              ai_socktype;   
    /* Тип сокета */
    
    int              ai_protocol;   
    /* Протокол сокета */
    
    socklen_t        ai_addrlen;    
    /* Длина адреса сокета */
    
    struct sockaddr *ai_addr;       
    /* Адрес сокета */
    
    char            *ai_canonname;  
    /* Официальное имя узла сети */
    
    struct addrinfo *ai_next;       
    /* Указатель на следующий элемент списка */

    При обращении к функции getaddrinfo() все поля структуры addrinfo, на которую указывает аргумент hints, кроме первых четырех ( ai_flags, ai_family, ai_socktype, ai_protocol ), должны быть нулевыми или равными NULL. Значение AF_UNSPEC в поле ai_family подразумевает, что вызывающего устроит любое адресное семейство. Аналогичный смысл имеют нулевые значения полей ai_socktype и ai_protocol. При hints, равном NULL, подразумевается AF_UNSPEC для ai_family и нулевые значения для других полей.

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

    AI_PASSIVE

    Если значение аргумента nodename равно NULL, этот флаг игнорируется. В противном случае, если он указан, будет возвращен адрес сокета, предназначенного для принятия входящих соединений.

    AI_CANONNAME

    Данный флаг предписывает выяснить официальное имя узла сети.

    AI_NUMERICHOST

    Флаг означает, что хост задан адресной цепочкой, и не допускает использования какого-либо сервиса имен.

    AI_NUMERICSERV

    Флаг помечает, что сервис (аргумент servname ) задан номером порта, и налагает запрет на обращение к какому-либо сервису имен.

    Признаком успешного завершения функции getaddrinfo() является нулевой результат. В таком случае выходной аргумент res будет ссылаться на указатель на список структур типа addrinfo (связанных полем ai_next ) - принадлежащие им значения полей ai_family, ai_socktype, ai_protocol пригодны для создания подходящих сокетов с помощью функции socket(), а значения ai_addr и ai_addrlen, в зависимости от флага AI_PASSIVE, могут служить аргументами функций connect() или bind(), применяемых к созданному сокету.

    Функция freeaddrinfo() позволяет освободить память, занятую списком структур типа addrinfo и ассоциированными данными.

    Функцию getnameinfo() можно считать обратной по отношению к getaddrinfo(). Аргумент sa - входной, он задает транслируемый адрес сокета ( salen - размер структуры sockaddr ). Аргументы node и service - выходные, задающие адреса областей памяти, куда помещаются, соответственно, имя узла и сервиса ; размеры этих областей ограничены значениями nodelen и servicelen.

    По умолчанию предполагается, что сокет имеет тип SOCK_STREAM, а кроме того, возвращается полное доменное имя хоста. Аргумент flags позволяет изменить подразумеваемое поведение. Если задан флаг NI_DGRAM, сокет считается датаграммным. При установленном флаге NI_NOFQDN возвращается короткое имя узла. Флаги NI_NUMERICHOST и NI_NUMERICSERV предписывают возвращать числовые цепочки для адресов хоста и сервиса, соответственно.

    Обратим внимание на несколько технических деталей. При работе с адресами сокетов вместо родовой структуры типа sockaddr обычно используются более специализированные (описанные в заголовочном файле <netinet/in.h> ) - sockaddr_in для адресного семейства   AF_INET ( IPv4 ) и sockaddr_in6 для AF_INET6 ( IPv6 ). Первая из них, согласно стандарту POSIX-2001, должна содержать по крайней мере следующие поля (все с сетевым порядком байт ).

    sa_family_t    sin_family; /* AF_INET */
    in_port_t      sin_port;   /* Номер порта */
    struct in_addr sin_addr;   /* IP-адрес */

    Структура типа sockaddr_in6 устроена несколько сложнее; мы не будем ее рассматривать.

    Структуры типов sockaddr и sockaddr_in (или sockaddr_in6 ) мысленно накладываются друг на друга, а преобразование типов между ними выполняется по мере необходимости. Отметим также, что тип in_port_t эквивалентен uint16_t, структура типа in_addr содержит по крайней мере одно поле:

    in_addr_t s_addr;
    тип in_addr_t эквивалентен uint32_t.

    Техническую роль играют и функции преобразования IP-адресов из текстового представления в числовое и наоборот (см. листинг 11.5).

    #include <arpa/inet.h>
    in_addr_t inet_addr (const char *cp);
    char *inet_ntoa (struct in_addr in);
    int inet_pton (int af, const char 
         *restrict src, void *restrict dst);
    const char *inet_ntop (int af, const void 
         *restrict src, char *restrict dst, 
         socklen_t size);

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

    Вторая пара функций по сути аналогична первой, но имеет чуть более общий характер, так как способна преобразовывать адреса в формате IPv6. Первый аргумент этих функций, af, задает адресное семейство: AF_INET для IPv4 и AF_INET6 для IPv6. Буфер, на который указывает аргумент dst функции inet_pton() (в него помещается результат преобразования - IP-адрес в числовой двоичной форме с сетевым порядком байт ), должен иметь длину не менее 32 бит для адресов IPv4 и 128 бит для IPv6.

    Аргумент src функции inet_ntop(), возвращающей текстовое представление, указывает на буфер с IP-адресом в числовой форме с сетевым порядком байт. Аргумент size задает длину выходного буфера, на него указывает аргумент dst. Подходящими значениями, в зависимости от адресного семейства, могут служить INET_ADDRSTRLEN или INET6_ADDRSTRLEN.

    Выше было отмечено, что преобразование значений типов uint16_t и uint32_t из хостового порядка байт в сетевой выполняется посредством функций htons() и htonl() ; функции ntohs() и ntohl() осуществляют обратную операцию (см. листинг 11.6).

    #include <arpa/inet.h>
    uint32_t htonl (uint32_t hostlong);
    uint16_t htons (uint16_t hostshort);
    uint32_t ntohl (uint32_t netlong);
    uint16_t ntohs (uint16_t netshort);

    Использование функции getaddrinfo() вместе с сопутствующими техническими деталями проиллюстрируем программой, показанной в листинге 11.7. Возможные результаты ее выполнения приведены в листинге 11.8.

    #include <stdio.h>
    #include <netdb.h>
    #include <arpa/inet.h>
    
    int main (void) {
     struct addrinfo hints = {AI_CANONNAME, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
     struct addrinfo *addr_res;
    
     if (getaddrinfo ("www", "http", hints, addr_res) != 0) {
       perror ("GETADDRINFO");
     } else {
       printf ("Результаты для сервиса http\n"); 
       /* Пройдем по списку возвращенных структур */
       do {
         printf ("Адрес сокета: Порт: %d IP-адрес: %s\n",
                 ntohs (((struct sockaddr_in *) addr_res->ai_addr)->sin_port),
                 inet_ntoa (((struct sockaddr_in *) addr_res->ai_addr)->sin_addr));
         printf ("Официальное имя хоста: %s\n", addr_res->ai_canonname);
       } while ((addr_res = addr_res->ai_next) != NULL);
     }
    
     return 0;
    }
    Результаты для сервиса http
    Адрес сокета: Порт: 80 IP-адрес: 193.232.173.1
    Официальное имя хоста: t01

    Завершая изложение серии технических моментов, укажем, что полезным дополнением к функциям getaddrinfo() и getnameinfo() является функция gai_strerror() (см. листинг 11.9). Она возвращает текстовую цепочку, расшифровывающую коды ошибок, перечисленные в заголовочном файле <netdb.h>. Стандарт POSIX-2001 специфицирует следующие коды ошибок, имена которых говорят сами за себя: EAI_AGAIN, EAI_BADFLAGS, EAI_FAIL, EAI_FAMILY, EAI_MEMORY, EAI_NONAME, EAI_OVERFLOW, EAI_SERVICE, EAI_SOCKTYPE, EAI_SYSTEM.

    #include <netdb.h>
    const char *gai_strerror (int ecode);

    Если немного модифицировать приведенную выше программу (в листинге 11.10 показан измененный фрагмент, где имя сервиса - "HTTP" - задано большими буквами), то в стандартный протокол с помощью функции gai_strerror() будет выдано содержательное диагностическое сообщение (см. листинг 11.11). Отметим, что выдача функции perror() в данном случае невразумительна.

    . . .
     int res;
    
     if ((res = getaddrinfo ("www", "HTTP", hints, addr_res)) != 0) {
       perror ("GETADDRINFO");
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
     } else {
       printf ("Результаты для сервиса HTTP\n");
        . . .
    GETADDRINFO: No such file or directory
    GETADDRINFO: Servname not supported for ai_socktype

    Наряду с базой данных хостов ( узлов сети ) поддерживается база данных сетей с аналогичной логикой работы и набором функций (см. листинг 11.12).

    #include <netdb.h>
    void setnetent (int stayopen);
    struct netent *getnetent (void);
    struct netent *getnetbyaddr (uint32_t net, 
                                 int type);
    struct netent *getnetbyname (const char *name);
    void endnetent (void);

    Функция getnetent() обслуживает последовательный доступ к базе, getnetbyaddr() осуществляет поиск по адресному семейству (аргумент type ) и номеру net сети, а getnetbyname() выбирает сеть с заданным (официальным) именем. Структура типа netent, указатель на которую возвращается в качестве результата этих функций, согласно стандарту POSIX-2001, должна содержать по крайней мере следующие поля.

    char    *n_name;        
    /* Официальное имя сети                      */
    
    char   **n_aliases;     
    /* Массив указателей на альтернативные       */
    /* имена сети, завершаемый пустым указателем */
    
    int      n_addrtype;    
    /* Адресное семейство (тип адресов) сети     */
    
    uint32_t n_net;         
    /* Номер сети (в хостовом порядке байт)      */

    Точно такой же программный интерфейс предоставляет база данных сетевых протоколов (см. листинг 11.13).

    #include <netdb.h>
    void setprotoent (int stayopen);
    struct protoent *getprotoent (void);
    struct protoent *getprotobyname 
                               (const char *name);
    struct protoent *getprotobynumber (int proto);
    void endprotoent (void);

    Структура типа protoent содержит по крайней мере следующие поля.

    char  *p_name;    
    /* Официальное имя протокола           */
    
    char **p_aliases; 
    /* Массив указателей на альтернативные */
    /* имена протокола, завершаемый пустым */
    /* указателем                          */
    
    int    p_proto;   
    /* Номер протокола                     */

    В листинге 11.14 показан пример программы, осуществляющей последовательный и случайный доступ к базе данных сетевых протоколов. Листинг 11.15 содержит фрагмент возможных результатов работы этой программы.

    #include <stdio.h>
    #include <netdb.h>
    
    int main (void) {
     struct protoent *pht;
     char *pct;
     int i;
    
     setprotoent (1);
    
     while ((pht = getprotoent ()) != NULL) {
       printf ("Официальное имя протокола: %s\n", pht->p_name);
       printf ("Альтернативные имена:\n");
       for (i = 0; (pct = pht->p_aliases [i]) != NULL; i++) {
         printf ("  %s\n", pct);
       }
       printf ("Номер протокола: %d\n\n", pht->p_proto);
     }
    
     if ((pht = getprotobyname ("ipv6")) != NULL) {
       printf ("Номер протокола ipv6: %d\n\n", pht->p_proto);
     } else {
       fprintf (stderr, "Протокол ip в базе не найден\n");
     }
    
     if ((pht = getprotobyname ("IPV6")) != NULL) {
       printf ("Номер протокола IPV6: %d\n\n", pht->p_proto);
     } else {
       fprintf (stderr, "Протокол IPV6 в базе не найден\n");
     }
    
     endprotoent ();
    
     return 0;
    }
    Официальное имя протокола: ip
    Альтернативные имена:
     IP
    Номер протокола: 0
    
    Официальное имя протокола: icmp
    Альтернативные имена:
     ICMP
    Номер протокола: 1
    
      . . .
    
    Официальное имя протокола: tcp
    Альтернативные имена:
     TCP
    Номер протокола: 6
    
      . . .
    
    Официальное имя протокола: udp
    Альтернативные имена:
     UDP
    Номер протокола: 17
    
      . . .
    
    Официальное имя протокола: ipv6
    Альтернативные имена:
     IPv6
    Номер протокола: 41
    
      . . .
    
    Официальное имя протокола: ipv6-crypt
    Альтернативные имена:
     IPv6-Crypt
    Номер протокола: 50
    
      . . .
    
    Официальное имя протокола: visa
    Альтернативные имена:
     VISA
    Номер протокола: 70
    
      . . .
    
    Официальное имя протокола: iso-ip
    Альтернативные имена:
     ISO-IP
    Номер протокола: 80
    
      . . .
    
    Официальное имя протокола: sprite-rpc
    Альтернативные имена:
     Sprite-RPC
    Номер протокола: 90
    
      . . .
    
    Официальное имя протокола: ipx-in-ip
    Альтернативные имена:
     IPX-in-IP
    Номер протокола: 111
    
      . . .
    
    Официальное имя протокола: fc
    Альтернативные имена:
     FC
    Номер протокола: 133
    
    Номер протокола ipv6: 41
    
    Протокол IPV6 в базе не найден

    Еще одно проявление той же логики работы - база данных сетевых сервисов (см. листинг 11.16).

    #include <netdb.h>
    void setservent (int stayopen);
    struct servent *getservent (void);
    struct servent *getservbyname 
         (const char *name, const char *proto);
    struct servent *getservbyport 
         (int port, const char *proto);
    void endservent (void);

    Обратим внимание на то, что в данном случае можно указывать второй аргумент поиска - имя протокола. Впрочем, значение аргумента proto может быть пустым указателем, и тогда поиск производится только по имени сервиса (функция getservbyname() ) или номеру порта ( getservbyport() ), который должен быть задан с сетевым порядком байт.

    Структура типа servent содержит по крайней мере следующие поля.

    char  *s_name;          
    /* Официальное имя сервиса              */
    
    char **s_aliases;       
    /* Массив указателей на альтернативные  */
    /* имена сервиса, завершаемый пустым    */
    /* указателем                           */
    
    int    s_port;          
    /* Номер порта, соответствующий сервису */
    /* (в сетевом порядке байт)             */
    
    char  *s_proto;         
    /* Имя протокола для взаимодействия с   */
    /* сервисом                             */

    В листинге 11.17 приведен пример программы, использующей функции доступа к базе данных сервисов, а также функции преобразования между хостовым и сетевым порядками байт. В листинге 11.18 показан фрагмент возможных результатов работы этой программы.

    #include <stdio.h>
    #include <netdb.h>
    
    int main (void) {
     struct servent *pht;
     char *pct;
     int i;
    
     setservent (1);
    
     while ((pht = getservent ()) != NULL) {
       printf ("Официальное имя сервиса: %s\n", pht->s_name);
       printf ("Альтернативные имена:\n");
       for (i = 0; (pct = pht->s_aliases [i]) != NULL; i++) {
         printf ("  %s\n", pct);
       }
       printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
       printf ("Имя протокола: %s\n\n", pht->s_proto);
     }
    
     if ((pht = getservbyport (htons ((in_port_t) 21), "udp")) != NULL) {
       printf ("Официальное имя сервиса: %s\n", pht->s_name);
       printf ("Альтернативные имена:\n");
       for (i = 0; (pct = pht->s_aliases [i]) != NULL; i++) {
         printf ("  %s\n", pct);
       }
       printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
       printf ("Имя протокола: %s\n\n", pht->s_proto);
     } else {
       perror ("GETSERVBYPORT");
     }
    
     if ((pht = getservbyport (htons ((in_port_t) 21), (char *) NULL)) != NULL) {
       printf ("Официальное имя сервиса: %s\n", pht->s_name);
       printf ("Альтернативные имена:\n");
       for (i = 0; (pct = pht->s_aliases [i]) != NULL; i++) {
         printf ("  %s\n", pct);
       }
       printf ("Номер порта: %d\n", ntohs ((in_port_t) pht->s_port));
       printf ("Имя протокола: %s\n\n", pht->s_proto);
     } else {
       perror ("GETSERVBYPORT");
     }
    
     endservent ();
    
     return 0;
    }
    . . .
    
    Официальное имя сервиса: ftp-data
    Альтернативные имена:
    Номер порта: 20
    Имя протокола: tcp
    
    Официальное имя сервиса: ftp-data
    Альтернативные имена:
    Номер порта: 20
    Имя протокола: udp
    
    Официальное имя сервиса: ftp
    Альтернативные имена:
    Номер порта: 21
    Имя протокола: tcp
    
    Официальное имя сервиса: ftp
    
    Альтернативные имена:
     fsp
     fspd
    Номер порта: 21
    Имя протокола: udp
    
      . . .
    
    Официальное имя сервиса: kerberos
    Альтернативные имена:
     kerberos5
     krb5
    Номер порта: 88
    Имя протокола: tcp
    
    Официальное имя сервиса: kerberos
    Альтернативные имена:
     kerberos5
     krb5
    Номер порта: 88
    Имя протокола: udp
    
      . . .
    
    Официальное имя сервиса: auth
    Альтернативные имена:
     authentication
     tap
     ident
    Номер порта: 113
    Имя протокола: tcp
    
    Официальное имя сервиса: auth
    Альтернативные имена:
     authentication
     tap
     ident
    Номер порта: 113
    Имя протокола: udp
    
      . . .
    
    
    Официальное имя сервиса: printer
    Альтернативные имена:
     spooler
    Номер порта: 515
    Имя протокола: tcp
    
    Официальное имя сервиса: printer
    Альтернативные имена:
     spooler
    Номер порта: 515
    Имя протокола: udp
    
      . . .
    
    Официальное имя сервиса: fido
    Альтернативные имена:
    Номер порта: 60179
    Имя протокола: tcp
    
    Официальное имя сервиса: fido
    Альтернативные имена:
    Номер порта: 60179
    Имя протокола: udp
    
    Официальное имя сервиса: ftp
    Альтернативные имена:
     fsp
     fspd
    Номер порта: 21
    Имя протокола: udp
    
    Официальное имя сервиса: ftp
    Альтернативные имена:
    Номер порта: 21
    Имя протокола: tcp

    Отметим, что при поиске по ключу возвращается первый подходящий элемент базы данных. По этой причине, когда не был задан протокол (второе обращение к функции getservbyport() ), в качестве результата был возвращен элемент с протоколом tcp.

    Функции для работы с сокетами

    Работа с сокетами как оконечными точками при взаимодействии процессов начинается с их ( сокетов ) создания посредством функции socket() (см. листинг 11.19). Она возвращает открытый файловый дескриптор, который может быть использован в последующих вызовах функций, оперирующих с сокетами. В том же листинге показано описание функции socketpair(), создающей пару сокетов с установленным между ними соединением.

    #include <sys/socket.h>
    int socket (int af, int type, int protocol);
    int socketpair (int af, int type, 
                    int protocol, int sds [2]);

    Аргумент af задает адресное семейство создаваемого сокета, аргумент type - тип, аргумент protocol - конкретный протокол ( 0 обозначает неспецифицированный подразумеваемый протокол, пригодный для запрошенного типа). Напомним, что подходящие значения для аргументов af, type и protocol можно получить с помощью описанной ранее функции getaddrinfo().

    Функция socketpair() по назначению аналогична pipe(), только организуется не безымянный канал, а пара соединенных, безымянных (не привязанных к каким-либо адресам), идентичных сокетов , открытые файловые дескрипторы которых помещаются в массив sds. Обычно она используется для адресного семейства AF_UNIX ; поддержка для других семейств не гарантируется.

    После того как посредством функции bind() (см. листинг 11.20) создан сокет, идентифицируемый дескриптором sd, ему присваивают локальный адрес, заданный аргументом address ( address_len - длина структуры sockaddr, на которую указывает address ). Источником локальных адресов для сокетов может служить вышеупомянутая функция getaddrinfo().

    #include <sys/socket.h>
    int bind (int sd, const struct sockaddr 
              *address, socklen_t address_len);

    Опросить присвоенный локальный адрес (его иногда называют именем сокета ) можно с помощью функции getsockname() (см. листинг 11.21): она помещает его в структуру sockaddr, на которую указывает аргумент address, а длину адреса записывает по указателю address_len.

    #include <sys/socket.h>
    int getsockname 
       (int sd, struct sockaddr *restrict address,
        socklen_t *restrict address_len);

    Если сокет ориентирован на режим с установлением соединения (имеет тип SOCK_STREAM ), то, воспользовавшись функцией listen() (см. листинг 11.22), его следует пометить как готового принимать соединения (" слушающего ").

    #include <sys/socket.h>
    int listen (int sd, int backlog);

    Аргумент backlog сообщает операционной системе рекомендуемую длину очереди соединений, ожидающих обработки слушающим сокетом. Реализация должна поддерживать значения аргумента backlog вплоть до конфигурационной константы SOMAXCONN, определенной в заголовочном файле <sys/socket.h>. ОС имеет право установить длину очереди меньше рекомендуемой. При неположительном значении backlog очередь будет иметь зависящую от реализации минимальную длину.

    Прием соединений выполняет функция accept() (см. листинг 11.23). Она выбирает первое соединение из очереди, ассоциированной с заданным дескриптором sd слушающим сокетом, создает новый сокет с теми же адресным семейством, типом и протоколом и возвращает в качестве результата его файловый дескриптор.

    #include <sys/socket.h>
    int accept 
       (int sd, struct sockaddr *restrict address,
        socklen_t *restrict address_len);

    Если значение аргумента address отлично от пустого указателя, то в структуру sockaddr, на которую указывает address, помещается адрес сокета, пытающегося установить соединение. По указателю address_len при обращении к функции accept() должна быть задана длина переданной структуры sockaddr, а на выходе туда записывается длина адреса партнера по взаимодействию.

    Если очередь соединений, ожидающих обработки слушающим сокетом, пуста и для дескриптора sd не установлен флаг O_NONBLOCK, то вызвавший функцию accept() процесс (поток управления) блокируется до появления подобного соединения. При непустой очереди функция select() сообщит о готовности дескриптора sd к чтению.

    Другая сторона, т. е. потенциальный партнер по взаимодействию, инициирует соединение с помощью функции connect() (см. листинг 11.24). Аргументы address и address_len стандартным образом задают адрес сокета (как правило, слушающего ), с которым необходимо установить соединение.

    #include <sys/socket.h>
    int connect (int sd, const struct sockaddr 
         *address, socklen_t address_len);

    Если для сокета, заданного аргументом sd (запрашивающего установление соединения ), еще не выполнена привязка к локальному адресу, функция connect() сама осуществит связывание со свободным локальным адресом (правда, лишь при условии, что адресное семейство сокета отлично от AF_UNIX ).

    Функция connect() ограничится фиксацией адреса сокета, взаимодействующего с заданным аргументом sd, если тип сокета не требует установления соединения. В частности, для сокетов типа SOCK_DGRAM таким способом можно специфицировать адреса отправляемых (с помощью функции send() ) и принимаемых (посредством обращения к функции recv() ) датаграмм.

    Когда в качестве аргумента address передается пустой указатель, адрес взаимодействующего сокета сбрасывается.

    Попытка установить соединение блокирует вызывающий процесс на неспецифицируемый промежуток времени (в случае наличия флага O_NONBLOCK ). Если по истечении этого промежутка соединение установить не удалось, вызов connect(), равно как и попытка установления соединения, завершаются неудачей. Если ожидание прерывается обрабатываемым сигналом, вызов connect() завершается неудачей (переменной errno присваивается значение EINTR ), но установление соединения продолжается и будет завершено асинхронно.

    Если для дескриптора sd задан флаг O_NONBLOCK, а соединение не может быть установлено немедленно, то вызов connect() завершается неудачей со значением errno, равным EINPROGRESS, но установление соединения продолжается и будет завершено асинхронно. Последующие обращения к функции connect() с тем же сокетом, выполненные до установления соединения, завершаются неудачей ( EALREADY ).

    Сокет оказывается в неспецифицированном состоянии, если функция connect() завершается неудачей по другим причинам. Приложения, соответствующие стандарту POSIX-2001, должны закрыть файловый дескриптор sd и создать новый сокет для продолжения попыток установить соединение.

    После асинхронного установления соединения функции select() и poll() сообщат, что файловый дескриптор sd готов к записи.

    Функция poll(), позволяющая мультиплексировать ввод/вывод в пределах набора файловых дескрипторов, была описана нами выше. Имеющая сходную направленность (но входящая в обязательную часть стандарта POSIX-2001) функция select() и ее недавно введенный аналог pselect() представлены в листинге 11.25.

    #include <sys/select.h>
    int pselect (int nfds, fd_set *restrict readfds, fd_set *restrict writefds, 
                 fd_set *restrict errorfds, const struct timespec *restrict timeout, 
    			 const sigset_t *restrict sigmask);
    int select (int nfds, fd_set *restrict readfds, fd_set *restrict writefds, 
                fd_set *restrict errorfds, struct timeval *restrict timeout);
    void FD_CLR (int fd, fd_set *fdset);
    int FD_ISSET (int fd, fd_set *fdset);
    void FD_SET (int fd, fd_set *fdset);
    void FD_ZERO (fd_set *fdset);

    Если значение аргумента readfds ( writefds, errorfds ) функции pselect() отлично от NULL, оно ссылается на объект типа fd_set, который на входе специфицирует набор файловых дескрипторов, проверяемых на готовность к чтению (или к записи, или к обработке исключительных ситуаций), а на выходе указывает, какие из них успешно прошли проверку. Аргумент nfds задает границу проверяемых дескрипторов (они являются небольшими целыми числами): дескриптор подлежит проверке, если его значение находится в пределах от 0 до (nfds - 1) включительно.

    Стандарт POSIX-2001 определяет тип fd_set как абстрактный. Для работы со значениями этого типа служат макросы FD_ZERO() (сделать набор пустым), FD_SET() (добавить дескриптор к набору), FD_CLR() (удалить дескриптор из набора), FD_ISSET() (проверить принадлежность дескриптора набору). Значение именованной константы FD_SETSIZE равно максимально допустимому числу дескрипторов в наборе.

    В качестве результата pselect() возвращается общее количество дескрипторов во всех трех наборах, успешно прошедших проверку.

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

    Если значение аргумента sigmask отлично от пустого указателя, функция pselect() на входе заменяет маску сигналов процесса на заданную, а на выходе восстанавливает прежнюю маску.

    Функция pselect() поддерживает обычные файлы, терминалы и псевдотерминалы, каналы и сокеты.

    Функция select() эквивалентна pselect() со следующими оговорками.

  • Для функции select() время ожидания задается в секундах и микросекундах в виде структуры типа timeval, а для pselect() - в секундах и наносекундах как аргумент типа struct timespec.
  • У функции select() нет аргумента - маски сигналов, что эквивалентно пустому указателю в качестве значения аргумента sigmask функции pselect().
  • В случае успешного завершения функция select() может модифицировать структуру, на которую указывает аргумент timeout.
  • С сокетами могут быть ассоциированы опции, влияющие на их функционирование. Значения этих опций можно опросить или изменить с помощью функций getsockopt() и setsockopt() (см. листинг 11.26).

    #include <sys/socket.h>
    int getsockopt (int sd, int level, 
          int option_name, 
          void *restrict option_value, 
          socklen_t *restrict option_len);
    int setsockopt (int sd, int level, 
          int option_name, 
          const void *option_value, 
          socklen_t option_len);

    Опции задаются именованными константами (аргумент option_name ), которые определены в заголовочном файле <sys/socket.h>. Выделим среди них наиболее употребительные и разделим на несколько групп. К первой отнесем опции с целочисленными значениями, описывающими характеристики или состояние сокета.

    SO_ERROR

    Статус ошибок (после опроса очищается).

    SO_TYPE

    Тип сокета.

    Ко второй группе отнесем булевы опции, представленные целочисленными значениями (0 означает ложь).

    SO_DEBUG

    Сообщает, записывается ли отладочная информация о работе сокета.

    SO_ACCEPTCONN

    Указывает, является ли сокет слушающим.

    В третью группу включены опции с целочисленными значениями, определяющими количественные характеристики объектов, ассоциированных с сокетом.

    SO_SNDBUF

    Размер буфера для передаваемых данных (выходного буфера).

    SO_RCVBUF

    Размер входного буфера.

    SO_RCVLOWATM

    Минимальное число байт, обрабатываемых при вводе. Подразумеваемое значение равно единице. Большие значения могут вызвать блокировку принимающего процесса до поступления в сокет необходимого объема данных.

    SO_SNDLOWAT

    Минимальное число байт, обрабатываемых при выводе.

    В четвертую группу входят опции со структурными значениями.

    SO_LINGER

    Определяет, блокировать ли процесс при закрытии дескриптора sd до передачи буферизованных данных, и если блокировать, то на какой срок. Значением опции является структура linger, определенная в заголовочном файле <sys/socket.h> и содержащая, согласно стандарту POSIX-2001, по крайней мере следующие поля.

    int l_onoff;    
    /* Признак, включена ли опция блокирования */
    /* при закрытии */
    
    int l_linger;   
    /* Длительность блокирования в секундах */
    SO_RCVTIMEO

    Длительность ожидания поступления данных при вводе. Значение - упомянутая выше структура типа timeval. Подразумевая длительность равна нулю. Если в течение специфицированного промежутка времени новых данных не поступило, операция ввода вернет число байт, меньше запрошенного, или завершится ошибкой EAGAIN.

    SO_SNDTIMEO

    Длительность ожидания отправки данных при выводе.

    Не все из перечисленных опций могут быть переустановлены функцией setsockopt(). Исключение по понятным причинам составляют SO_ERROR, SO_TYPE, SO_ACCEPTCONN.

    Аргумент level задает протокольный уровень опции. Уровню сокетов соответствует значение SOL_SOCKET, уровню TCP - IPPROTO_TCP.

    Функция getpeername() (см. листинг 11.27), во многом аналогичная рассмотренной выше функции getsockname(), позволяет опросить еще одну характеристику - адрес ( имя ) сокета, с которым установлено соединение.

    #include <sys/socket.h>
    int getpeername (int sd, 
          struct sockaddr *restrict address, 
          socklen_t *restrict address_len);

    После привязки сокета к локальному адресу, а также установления, при необходимости, соединения и задания значений опций, можно приступать к отправке и/или приему данных через сокет. Для этого служат функции, описания которых показаны в листинге 11.28.

    #include <sys/socket.h>
    ssize_t recvfrom 
            (int sd, void *restrict buffer, 
             size_t length, int flags, 
             struct sockaddr *restrict address, 
             socklen_t *restrict address_len);
    ssize_t recv (int sd, void *buffer, 
                  size_t length, int flags);  
    ssize_t recvmsg (int sd, struct msghdr 
                     *message, int flags);
    ssize_t sendto 
            (int sd, const void *message,
             size_t length, int flags, 
             const struct sockaddr *dest_addr, 
             socklen_t dest_len);
    ssize_t send (int sd, const void *buffer,
                  size_t length, int flags);
    ssize_t sendmsg (int sd, const struct msghdr
                     *message, int flags);

    Функция recvfrom() позволяет прочитать данные (в рассматриваемом контексте называемые также сообщением ) из сокета с дескриптором sd и поместить их в буфер buffer длины length. Обычно функцию recvfrom() применяют к сокетам, ориентированным на режим без установления соединения, поскольку она выдает исходный адрес в структуре типа sockaddr.

    В число возможных составляющих значения аргумента flags входят следующие флаги.

    MSG_PEEK

    Не удалять прочитанные данные. Следующий вызов recvfrom() или другой функции ввода снова вернет их.

    MSG_WAITALL

    Для сокетов типа SOCK_STREAM флаг означает, что вызывающий процесс блокируется до получения всего запрошенного объема данных (а не до прихода первого сообщения ).

    MSG_OOB

    Запрашиваются экстренные данные. Трактовка этого понятия зависит от протокола.

    Как и положено функции ввода, в результате recvfrom() возвращает количество прочитанных и помещенных в буфер данных. Для сокетов, учитывающих границы сообщений ( SOCK_RAW, SOCK_DGRAM, SOCK_SEQPACKET ), за одно обращение читается все сообщение ; если оно не помещается в буфер, лишние байты (в отсутствие флага MSG_PEEK ) отбрасываются.

    Формально можно считать, что функция recv() эквивалентна recvfrom() с нулевым значением аргумента address_len. Поскольку она не позволяет узнать исходный адрес, ее обычно используют для сокетов, установивших соединение. (Можно провести и еще одну аналогию: если пренебречь флагами, функция recv() эквивалентна read().)

    По сравнению с recv(), более содержательным аналогом функции recvfrom() является recvmsg(). Отличия в данном случае носят скорее синтаксический характер и по сути сводятся к способу передачи входных значений и возврата результатов: для минимизации числа аргументов функция recvmsg() использует структуру типа msghdr, которая, согласно стандарту POSIX-2001, должна содержать по крайней мере следующие поля.

    void         *msg_name;         
    /* Указатель на буфер для адреса            */
    /* (исходного или целевого)                 */
    
    socklen_t     msg_namelen;      
    /* Размер буфера для адреса                 */
    
    struct iovec *msg_iov;          
    /* Массив для разнесения/сборки сообщений   */
    
    int           msg_iovlen;       
    /* Число элементов в массиве                */
    /* для разнесения/сборки сообщений          */
    
    void         *msg_control;      
    /* Указатель на буфер                       */
    /* для вспомогательных данных               */
    
    socklen_t     msg_controllen;   
    /* Размер буфера для вспомогательных данных */  
    
    int           msg_flags;        
    /* Флаги принятого сообщения                */

    Если для сокета, специфицированного дескриптором sd, соединение не установлено, в заданный указателем msg_name буфер длины msg_namelen помещается исходный адрес сообщения ; если этого не нужно, указатель msg_name можно сделать пустым. При установлении соединения оба поля игнорируются.

    Массив msg_iov совместно с полем msg_iovlen задает набор буферов для размещения принимаемых данных. Структура типа iovec определяется в заголовочном файле <sys/uio.h> и содержит по крайней мере два поля.

    void  *iov_base;        
    /* Адрес области памяти (буфера)    */
    
    size_t iov_len;         
    /* Размер области памяти (в байтах) */

    Заданные полем msg_iov области памяти по очереди заполняются поступающими данными, пока не будут размещены все принятые данные или не заполнятся все буфера.

    Трактовка вспомогательных данных (поля msg_control и msg_controllen структуры типа msghdr ) в стандарте POSIX-2001 весьма туманна. Описаны лишь средства доступа к ним. Мы, однако, не будем на этом останавливаться.

    В случае успешного завершения функции recvmsg() в поле msg_flags могут быть установлены следующие флаги.

    MSG_EOR

    Достигнута граница записи (если это понятие поддерживается протоколом ).

    MSG_OOB

    Получены экстренные данные.

    MSG_TRUNC

    Пришлось урезать обычные данные.

    MSG_CTRUNC

    Пришлось урезать управляющие данные.

    Подобно тому, как write() составляет пару read(), функцию sendto() можно считать парной по отношению к recvfrom(). Она предназначена для отправки через сокет sd сообщения, заданного аргументами message и length, по адресу dest_addr длины dest_len. Если для сокета установлено соединение, целевой адрес dest_addr игнорируется.

    В число возможных составляющих значения аргумента flags входят следующие флаги.

    MSG_EOR

    Обозначает границу записи (если это понятие поддерживается протоколом ).

    MSG_OOB

    Отправляются экстренные данные.

    Разумеется, успешное завершение функции sendto() не гарантирует доставку сообщения. Результат, равный -1, указывает только на локально выявляемые ошибки.

    Функция send() - пара для recv() со всеми следующими из этого обстоятельства эквивалентностями и аналогиями.

    Для функции sendmsg() структура типа msghdr, на которую указывает аргумент message, является входной (что показывает спецификатор const ). В поле msg_name задается целевой адрес. Поле msg_flags игнорируется.

    Для завершения операций приема и/или отправки данных через сокет служит функция shutdown() (см. листинг 11.29).

    #include <sys/socket.h>
    int shutdown (int sd, int how);

    Значение аргумента how показывает, что именно завершается: SHUT_RD прекращает прием, SHUT_WR - отправку, SHUT_RDWR - и то, и другое.

    Примеры программ работы с сокетами

    Приводимые далее примеры строятся вокруг задачи копирования данных в распределенной программной конфигурации со стандартного ввода одного процесса на стандартный вывод другого, удаленного. Однако начнем мы с локального случая, обслуживаемого сокетами адресного семейства AF_UNIX (см. листинг 11.30).

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа копирует строки со стандартного ввода на стандартный вывод, */
    /* "прокачивая" их через пару сокетов.                                   */
    /* Используются функции ввода/вывода нижнего уровня                      */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    
    #define MY_PROMPT       "Вводите строки\n"
    #define MY_MSG          "Вы ввели: "
    
    int main (void) {
     int sds [2];
     char buf [1];
     int new_line = 1;     /* Признак того, что надо выдать сообщение MY_MSG */
                           /* перед отображением очередной строки            */
    
     /* Создадим пару соединенных безымянных сокетов */
     if (socketpair (AF_UNIX, SOCK_STREAM, 0, sds) < 0) {
       perror ("SOCKETPAIR");
       exit (1);
     }
    
     switch (fork ()) {
       case -1:
         perror ("FORK");
         exit (2);
       case 0:
         /* Чтение из сокета sds [0] и выдачу на стандартный вывод */
         /* реализуем в порожденном процессе                       */
         while (read (sds [0], buf, 1) == 1) {
    	if (write (STDOUT_FILENO, buf, 1) != 1) {
             perror ("WRITE TO STDOUT");
    	  break;
    	}
         }
         exit (0);
     }
    
     /* Чтение со стандартного ввода и запись в сокет sds [1] */
     /* возложим на родительский процесс                      */
     if (write (sds [1], MY_PROMPT, sizeof (MY_PROMPT) - 1) !=
                                    sizeof (MY_PROMPT) - 1) {
       perror ("WRITE TO SOCKET-1");
     }
    
     while (read (STDIN_FILENO, buf, 1) == 1) {
       /* Перед отображением очередной строки */
       /* нужно выдать сообщение MY_MSG       */
       if (new_line) {
         if (write (sds [1], MY_MSG, sizeof (MY_MSG) - 1) != sizeof (MY_MSG) - 1) {
           perror ("WRITE TO SOCKET-2");
           break;
         }
       }
       if (write (sds [1], buf, 1) != 1) {
         perror ("WRITE TO SOCKET-3");
         break;
       }
       new_line = (buf [0] == '\n');
     }
     shutdown (sds [1], SHUT_WR);
    
     (void) wait (NULL);
     return (0);
    }

    Обратим внимание на употребление в процессе, осуществляющем запись в сокет, функции shutdown(), без чего читающий процесс не определит конец файла и не выйдет из цикла.

    Решим теперь ту же задачу для действительно распределенной конфигурации. Наше приложение будет состоять из двух процессов, выполняющихся на разных хостах. Первый процесс (см. листинг 11.31) читает строки со стандартного ввода и отправляет их в виде датаграмм другому, который принимает датаграммы и выдает их содержимое на стандартный вывод (см. листинг 11.32).

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса, читающего строки со стандартного ввода  */
    /* и посылающего их в виде датаграмм другому процессу          */
    /* (будем называть его сервером),                              */
    /* который должно выдать их на свой стандартный вывод.         */
    /* Имя серверного хоста - аргумент командной строки            */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <string.h>
    
    #define MY_PROMPT       "Вводите строки\n"
    
    int main (int argc, char *argv []) {
     int sd;               /* Дескриптор передающего сокета */
     char line [LINE_MAX]; /* Буфер для копируемых строк    */
                           /* Структура - аргумент sendmsg  */
     struct msghdr msg = {NULL, 0, NULL, 0, NULL, 0, 0};
     struct iovec iovbuf;  /* Структура для сборки отправляемых данных  */
                           /* Структура - входной аргумент getaddrinfo */
     struct addrinfo hints = {0, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
    			   0, NULL, NULL, NULL};
                           /* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo             */
     int msg_len;          /* Длина очередной введенной строки, */
                           /* включая завершающий нулевой байт  */
    
     if (argc != 2) {
       fprintf (stderr, "Использование: %s имя_серверного_хоста\n", argv [0]);
       return (1);
     }
    
     /* Создадим сокет, через который будем отправлять */
     /* прочитанные строки                             */
     if ((sd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
       perror ("SOCKET");
       return (2);
     }
    
     /* Выясним целевой адрес для датаграмм      */
     /* Воспользуемся портом для сервиса spooler */
     if ((res = getaddrinfo (argv [1], "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (3);
     }
    
     /* Заполним структуру msghdr */
     msg.msg_name = addr_res->ai_addr;
     msg.msg_namelen = addr_res->ai_addrlen;
     msg.msg_iov = iovbuf;
     msg.msg_iovlen = 1;
     iovbuf.iov_base = line;
    
     /* Цикл чтения строк со стандартного ввода    */
     /* и отправки их через сокет в виде датаграмм */
     fputs (MY_PROMPT, stdout);
     while (fgets (line, sizeof (line), stdin) != NULL) {
       msg_len = strlen (line) + 1;
       iovbuf.iov_len = msg_len;
       if (sendmsg (sd, msg, 0) != msg_len) {
         perror ("SENDMSG");
         break;
       }
     }
    
     return (0);
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его сервером),     */
    /* читающего сообщения (строки) из датаграммного сокета  */
    /* и выдающего их на стандартный вывод                   */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    
    #define MY_MSG          "Вы ввели: "
    
    int main (void) {
     int sd;               /* Дескриптор приемного сокета  */
     char line [LINE_MAX]; /* Буфер для копируемых строк   */
                           /* Структура - аргумент recvmsg */
     struct msghdr msg = {NULL, 0, NULL, 0, NULL, 0, 0};
     struct iovec iovbuf;  /* Структура для разнесения принимаемых данных */
                           /* Структура - входной аргумент getaddrinfo    */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
    			   0, NULL, NULL, NULL};
                           /* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
    
     /* Создадим сокет, через который будем принимать */
     /* прочитанные строки                            */
     if ((sd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (2);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (3);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Заполним структуру msghdr */
     msg.msg_iov = iovbuf;
     msg.msg_iovlen = 1;
     iovbuf.iov_base = line;
     iovbuf.iov_len = sizeof (line);
    
     /* Цикл приема и выдачи строк */
     while (1) {
       if (recvmsg (sd, msg, 0) < 0) {
         perror ("RECVMSG");
         break;
       }
       fputs (MY_MSG, stdout);
       fputs (line, stdout);
     }
    
     return (0);
    }

    Обратим внимание на несколько моментов. Адреса сокетов (целевого и приемного) получены при помощи функции getaddrinfo() с сервисом "spooler" в качестве аргумента. (Если на хосте этот сервис реально используется, для данного примера придется подыскать другой, свободный порт.) С практической точки зрения более правильным было бы пополнить базу данных сетевых сервисов новым элементом, специально предназначенным для представленного приложения, однако подобные административные действия находятся вне рамок стандарта POSIX и, следовательно, нами не рассматриваются. (Менее правильно, на наш взгляд, формировать адрес сокета покомпонентно, выбирая порт, по сути, случайным образом, поскольку это ведет к неявному, бессистемному, неконтролируемому пополнению базы сервисов.)

    Для успешного вызова функции bind() процесс должен обладать соответствующими привилегиями. Это может оказаться существенным для запуска программы, показанной в листинге 11.32.

    Чтобы получить от getaddrinfo() адрес, пригодный для использования в качестве аргумента ориентированных на прием функций ( listen(), bind() для приемного сокета ), необходимо указать флаг AI_PASSIVE во входной для getaddrinfo() структуре addrinfo (аргумент hints в описании функции getaddrinfo() ). В таком случае для IP-адреса будет выдано значение INADDR_ANY, которое трактуется в адресном семействе AF_INET как адрес локального хоста, успешно сопоставляющийся как со шлейфовым ( 127.0.0.1 ), так и с реальным сетевыми адресами. В результате через этот сокет можно будет принимать датаграммы, посланные и с локального, и с удаленного хостов.

    Цикл приема данных сервером сделан бесконечным, поскольку у последовательности датаграмм нет естественного признака конца.

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

    В программе, посылающей датаграммы, выполним привязку сокета к свободному локальному адресу, воспользовавшись для этого функцией connect() и, для оправдания затраченных усилий, изменим способ отправки: вместо универсальной sendmsg() задействуем даже не более простую функцию send(), а ее вполне привычный аналог write(), скрытый под личиной fputs().

    Какой режим буферизации выбрать для потока, сформированного по открытому файловому дескриптору сокета, - дело вкуса. Читателю рекомендуется попробовать разные варианты, каждый из которых имеет свои "за" и "против". Второй вид модификаций касается серверного процесса. Поскольку теперь у датаграмм появился исходный адрес, его можно выяснить и выдать, а заодно опросить выходной флаг MSG_TRUNC, чтобы убедиться, что при пересылке в виде датаграмм копируемые строки не были урезаны. Кроме того, изменение способа формирования отправляемых датаграмм привело к тому, что теперь их содержимое не завершается нулевым байтом; следовательно, способ вывода принимаемых данных также нуждается в модификации (в нашем случае - в дописывании нулевого байта).

    Модифицированные варианты программ представлены в листингах 11.33 и 11.34.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса, читающего строки со стандартного ввода  */
    /* и посылающего их в виде датаграмм другому процессу          */
    /* (будем называть его сервером),                              */
    /* который должно выдать их на свой стандартный вывод.         */
    /* Имя серверного хоста - аргумент командной строки            */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    
    #define MY_PROMPT       "Вводите строки\n"
    
    int main (int argc, char *argv []) {
     int sd;               /* Дескриптор передающего сокета             */
     FILE *ss;             /* Поток, соответствующий передающему сокету */
     char line [LINE_MAX]; /* Буфер для копируемых строк                */
    			/* Структура - входной аргумент getaddrinfo  */ 
     struct addrinfo hints = {0, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
    
     if (argc != 2) {
       fprintf (stderr, "Использование: %s имя_серверного_хоста\n", argv [0]);
       return (1);
     }
    
     /* Создадим сокет, через который будем отправлять */
     /* прочитанные строки                             */
     if ((sd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
       perror ("SOCKET");
       return (2);
     }
    
     /* Выясним целевой адрес для датаграмм      */
     /* Воспользуемся портом для сервиса spooler */
     if ((res = getaddrinfo (argv [1], "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (3);
     }
    
     /* Воспользуемся функцией connect() для достижения двух целей: */
     /* фиксации целевого адреса и привязки к некоему локальному    */
     if (connect (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("CONNECT");
       return (4);
     }
    
     /* Сформирует поток по дескриптору сокета */
     if ((ss = fdopen (sd, "w")) == NULL) {
       perror ("FDOPEN");
       return (5);
     }
     /* Отменим буферизацию для этого потока */
     setbuf (ss, NULL);
    
     /* Цикл чтения строк со стандартного ввода    */
     /* и отправки их через сокет в виде датаграмм */
     fputs (MY_PROMPT, stdout);
     while (fgets (line, sizeof (line), stdin) != NULL) {
       fputs (line, ss);
     }
    
     return (0);
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его сервером), */
    /* читающего сообщения из датаграммного сокета       */
    /* и копирующего их на стандартный вывод             */
    /* с указанием адреса, откуда они поступили          */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main (void) {
     int sd;               /* Дескриптор приемного сокета                */
                           /* Буфер для принимаемых сообщений            */
                           /* Оставлено место для вставки нулевого байта */
     char lbuf [BUFSIZ + 1];
    			/* Структура - аргумент recvmsg */
     struct msghdr msg = {NULL, 0, NULL, 0, NULL, 0, 0};
     struct iovec iovbuf;  /* Структура для разнесения принимаемых данных */
    			/* Структура - входной аргумент getaddrinfo    */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_DGRAM, IPPROTO_UDP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo                    */
                           /* Структура для исходного адреса датаграмм */
     struct sockaddr_in sai;
     ssize_t lmsg;         /* Длина принятой датаграммы */
    
     /* Создадим сокет, через который будем принимать */
     /* прочитанные строки                            */
     if ((sd = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Заполним структуру msghdr */
     msg.msg_name = sai;
     msg.msg_namelen = sizeof (struct sockaddr_in);
     msg.msg_iov = iovbuf;
     msg.msg_iovlen = 1;
     iovbuf.iov_base = lbuf;
                                   /* Оставим место для вставки нулевого байта */
     iovbuf.iov_len = sizeof (lbuf) - 1;
    
     /* Цикл приема и выдачи строк */
     while (1) {
       if ((lmsg = recvmsg (sd, msg, 0)) < 0) {
         perror ("RECVMSG");
         break;
       }
       printf ("Вы ввели и отправили с адреса %s, порт %d :",
               inet_ntoa (((struct sockaddr_in *) msg.msg_name)->sin_addr),
               ntohs (((struct sockaddr_in *) msg.msg_name)->sin_port));
       if (msg.msg_flags  MSG_TRUNC) {
         printf ("Датаграмма была урезана\n");
       }
       lbuf [lmsg] = 0;
       fputs (lbuf, stdout);
     }
    
     return (0);
    }

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

    Сделаем теперь более существенные изменения, прежде всего на серверной стороне. Перейдем на использование надежного режима с установлением соединения (и, соответственно, сокетов типа SOCK_STREAM ). Сервер будет получать запросы на установление соединения через порт сервиса "spooler" и порождать для каждого принятого запроса свой обслуживающий процесс.

    Новый вариант программы процесса, читающего строки со стандартного ввода и отправляющего их через потоковый сокет, показан в листинге 11.35; две программы серверной части - в листингах 11.36 и 11.37. Предполагается, что выполнимый файл с образом обслуживающего процесса находится в текущем каталоге и имеет имя gsce.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса, читающего строки со стандартного ввода  */
    /* и посылающего их в виде потока другому процессу             */
    /* (будем называть его сервером),                              */
    /* который должно выдать строки на свой стандартный вывод.     */
    /* Имя серверного хоста - аргумент командной строки            */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <stdio.h>
    #include <netdb.h> 
    #include <sys/socket.h>
    
    #define MY_PROMPT       "Вводите строки\n"
    
    int main (int argc, char *argv []) {
     int sd;               /* Дескриптор передающего сокета             */
     FILE *ss;             /* Поток, соответствующий передающему сокету */
     char line [LINE_MAX]; /* Буфер для копируемых строк                */
    			/* Структура - входной аргумент getaddrinfo  */
     struct addrinfo hints = {0, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
    
     if (argc != 2) {
       fprintf (stderr, "Использование: %s имя_серверного_хоста\n", argv [0]);
       return (1);
     }
    
     /* Создадим сокет, через который будем отправлять */
     /* прочитанные строки                             */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (2);
     }
    
     /* Выясним целевой адрес для соединения     */
     /* Воспользуемся портом для сервиса spooler */
     if ((res = getaddrinfo (argv [1], "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (3);
     }
    
     /* Воспользуемся функцией connect() для достижения двух целей:    */
     /* установления соединения и привязки к некоему локальному адресу */
     if (connect (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("CONNECT");
       return (4);
     }
    
     /* Сформируем поток по дескриптору сокета */
     if ((ss = fdopen (sd, "w")) == NULL) {
       perror ("FDOPEN");
       return (5);
     }
     /* Отменим буферизацию для этого потока */
     setbuf (ss, NULL);
    
     /* Цикл чтения строк со стандартного ввода */
     /* и отправки их через сокет в виде потока */
     fputs (MY_PROMPT, stdout);
     while (fgets (line, sizeof (line), stdin) != NULL) {
       fputs (line, ss);
     }
     shutdown (sd, SHUT_WR);
    
     return (0);
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его демоном),  */
    /* принимающего запросы на установления соединения   */
    /* и запускающего процессы для их обслуживания       */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    
    int main (void) {
     int sd;               /* Дескриптор слушающего сокета */
     int ad;               /* Дескриптор приемного сокета  */
    			/* Буфер для принимаемых строк  */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
    
     /* Создадим слушающий сокет */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Пометим сокет как слушающий */
     if (listen (sd, SOMAXCONN) < 0) {
       perror ("LISTEN");
       return (3);
     }
    
     /* Цикл приема соединений и запуска обслуживающих процессов */
     while (1) {
       /* Примем соединение.                                          */
       /* Адрес партнера по общению нас в данном случае не интересует */
       if ((ad = accept (sd, NULL, NULL)) < 0) {
         perror ("ACCEPT");
         return (4);
       }
    
       /* Запустим обслуживающий процесс */
       switch (fork ()) {
         case -1:
    	perror ("FORK");
    	return (5);
         case 0:
    	/* Сделаем сокет ad стандартным вводом */
    	(void) close (STDIN_FILENO);
    	(void) fcntl (ad, F_DUPFD, STDIN_FILENO);
    	(void) close (ad);
    	/* Сменим программу процесса на обслуживающую */
    	if (execl ("./gsce", "gsce", (char *) NULL) < 0) {
    	  perror ("EXECL");
    	  return (6);
    	}
       }
    
       /* В родительском процессе дескриптор принятого соединения нужно закрыть */
       (void) close (ad);
    
       /* Обслужим завершившиеся порожденные процессы */
       while (waitpid ((pid_t) (-1), NULL, WNOHANG) > 0) ;
     }
     
     return (0);
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа обслуживающего процесса,                  */
    /* выдающего принимаемые строки на стандартный вывод.  */
    /* Дескриптор приемного сокета передан                 */
    /* в качестве стандартного ввода                       */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    int main (void) {
     char line [LINE_MAX]; /* Буфер для принимаемых строк */
    			/* Структура для записи адреса */
     struct sockaddr_in sai;
    			/* Длина адреса */
     socklen_t sai_len = sizeof (struct sockaddr_in);
    
     /* Опросим адрес партнера по общению (передающего сокета) */
     if (getpeername (STDIN_FILENO, (struct sockaddr *) sai, sai_len) < 0) {
       perror ("GETPEERNAME");
       return (1);
     }
    
     /* Цикл чтения строк из сокета      */
     /* и выдачи их на стандартный вывод */
     while (fgets (line, sizeof (line), stdin) != NULL) {
       printf ("Вы ввели и отправили с адреса %s, порт %d :",
    	    inet_ntoa (sai.sin_addr), ntohs (sai.sin_port));
       fputs (line, stdout);
     }
    
     /* Закрытие соединения */
     shutdown (STDIN_FILENO, SHUT_RD);
    
     return (0);
    }

    Обратим внимание на два технических момента. Во-первых, дескриптор приемного сокета передается в обслуживающий процесс под видом стандартного ввода. В результате обслуживающая программа по сути свелась к циклу копирования строк со стандартного ввода на стандартный вывод. Мы вернулись к тому, с чего начинали рассмотрение средств буферизованного ввода/вывода, но на новом витке спирали. Во-вторых, в иллюстративных целях для обслуживания завершившихся порожденных процессов использована функция waitpid() со значением аргумента pid, равным -1 (выражающим готовность обслужить любой завершившийся процесс-потомок), и флагом WNOHANG, означающим отсутствие ожидания. В данном случае подобный прием не гарантирует отсутствия зомби-процессов, но препятствует их накоплению.

    Чтобы проиллюстрировать еще один типичный прием, связанный с обработкой запросов на установление соединения, изменим способ обслуживания завершившихся порожденных процессов. Определим функцию обработки сигнала SIGCHLD и поместим в нее вызов waitpid(). Модифицированный вариант программы процесса-демона, аккуратно ликвидирующий все зомби-процессы, показан в листинге 11.38.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его демоном),  */
    /* принимающего запросы на установления соединения   */
    /* и запускающего процессы для их обслуживания       */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <sys/wait.h>
    #include <signal.h>
    #include <errno.h>
    
    /* Функция обработки сигнала SIGCHLD */
    static void chldied (int dummy) {
     /* Вдруг число завершившихся потомков отлично от единицы? */
     while (waitpid ((pid_t) (-1), NULL, WNOHANG) > 0) ;
    }
    
    int main (void) {
    
     int sd;               /* Дескриптор слушающего сокета             */
     int ad;               /* Дескриптор приемного сокета              */
    			/* Структура - входной аргумент getaddrinfo */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo                           */
                           /* Структура для задания реакции на сигнал SIGCHLD */
     struct sigaction sact;
    
     /* Создадим слушающий сокет */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
     
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Пометим сокет как слушающий */
     if (listen (sd, SOMAXCONN) < 0) {
       perror ("LISTEN");
       return (3);
     }
    
     /* Установим обработку сигнала о завершении потомка */
     sact.sa_handler = chldied;
     (void) sigemptyset (sact.sa_mask);
     sact.sa_flags = 0;
     (void) sigaction (SIGCHLD, sact, (struct sigaction *) NULL);
    
     /* Цикл приема соединений и запуска обслуживающих процессов */
     while (1) {
       /* Примем соединение с учетом того, что ожидание может быть прервано */
       /* доставкой обрабатываемого сигнала.                                */
       /* Адрес партнера по общению в данном случае нас не интересует       */
       while ((ad = accept (sd, NULL, NULL)) < 0) {
         if (errno != EINTR) {
           perror ("ACCEPT");
           return (4);
         }
       }
    
       /* Запустим обслуживающий процесс */
       switch (fork ()) {
         case -1:
    	perror ("FORK");
    	return (5);
         case 0:
    	/* Сделаем сокет ad стандартным вводом */
    	(void) close (STDIN_FILENO);
    	(void) fcntl (ad, F_DUPFD, STDIN_FILENO);
    	(void) close (ad);
    	/* Сменим программу процесса на обслуживающую */
    	if (execl ("./gsce", "gsce", (char *) NULL) < 0) {
    	  perror ("EXECL");
    	  return (6);
    	}
       }
    
       /* В родительском процессе дескриптор принятого соединения нужно закрыть */
       (void) close (ad);
     }
    
     return (0);
    }

    Задание функции обработки сигнала о завершении потомка имеет нелокальный эффект. Оно повлияло на поведение функции accept(): ее вызов может быть прерван доставкой сигнала SIGCHLD. В такой ситуации прежний вариант процесса-демона завершился бы, выдав в стандартный протокол диагностическое сообщение вида "ACCEPT: Interrupted system call". Чтобы этого не случилось, вызов accept() пришлось заключить в цикл с проверкой (в случае неудачного завершения) значения переменной errno на совпадение с EINTR. Вообще говоря, по подобной схеме рекомендуется действовать для всех функций, выполнение которых может быть прервано доставкой сигнала, но допускающих и повторный вызов. Отметим в этой связи, что, например, функция connect() очень похожа, но все-таки отличается своими особенностями. После доставки сигнала ее вызов завершается неудачей, а установление соединения продолжается асинхронно, поэтому повторное обращение может завершиться неудачей со значением errno, равным EALREADY.

    Рассмотрим теперь еще один вариант сервера, имеющего однопроцессную организацию, но мультиплексирующего ввод с помощью функции select() (см. листинг 11.39).

    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его сервером), */
    /* принимающего запросы на установления соединения   */
    /* и обслуживающего их с использованием select()     */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <sys/select.h>
    
    /* Структура для хранения дескрипторов приемных сокетов */
    /* и ассоциированной информации                         */
    #define MY_MSG_LN       128
    struct sads {
     int ad;
     FILE *sd;
     char my_msg [MY_MSG_LN];
    };
    
    /* Максимальное число параллельно обслуживаемых запросов */
    #define MY_MAX_QS       FD_SETSIZE
    
    /* Функция для освобождения элемента массива */
    /* дескрипторов приемных сокетов             */
    static void free_elem (struct sads *sadsp) {
     shutdown (sadsp->ad, SHUT_RD);
     close (sadsp->ad);
     sadsp->ad = -1;
    }
    
    int main (void) {
     int sd;               /* Дескриптор слушающего сокета             */
    			/* Структура - входной аргумент getaddrinfo */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo         */
     fd_set rdset;         /* Набор дескрипторов для чтения */
    			/* Структура для записи адреса   */
     struct sockaddr_in sai;
     socklen_t sai_len;    /* Длина адреса                                      */
     char line [LINE_MAX]; /* Буфер для принимаемых строк                       */
                           /* Массов для хранения дескрипторов приемных сокетов */
                           /* и ассоциированной информации.                     */
                           /* Последний элемент - фиктивный,                    */
                           /* нужен для упрощения поиска свободного             */
     struct sads sadsarr [MY_MAX_QS + 1];
     int sads_max;         /* Верхняя граница дескрипторов, проверяемых select() */
     int i;
    
     /* Создадим слушающий сокет */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Пометим сокет как слушающий */
     if (listen (sd, SOMAXCONN) < 0) {
       perror ("LISTEN");
       return (3);
     }
    
     /* Инициализируем массив sadsarr.              */ 
     /* -1 в поле ad означает, что элемент свободен */
     for (i = 0; i < (MY_MAX_QS + 1); i++) {
       sadsarr [i].ad = -1;
     }
    
     /* Цикл приема соединений и обслуживания запросов */
     while (1) {
       /* Подготовим наборы дескрипторов для select() */
       FD_ZERO (rdset);
       FD_SET (sd, rdset);
       sads_max = sd + 1;
       for (i = 0; i < MY_MAX_QS; i++) {
         if (sadsarr [i].ad >= 0) {
           FD_SET (sadsarr [i].ad, rdset);
           if ((sadsarr [i].ad + 1) > sads_max) {
             sads_max = sadsarr [i].ad + 1;
           }
         }
       }
    
       /* Подождем запроса на установление соединения */
       /* или готовности данных для чтения.           */
       /* Время ожидания зададим как бесконечное      */
       if (select (sads_max, rdset, NULL, NULL, NULL) == -1) {
         perror ("PSELECT");
         return (4);
       }
    
       /* Посмотрим, есть ли запросы, ждущие установления соединения */
       if (FD_ISSET (sd, rdset)) {
         /* Примем запрос на установление соединения,               */
         /* если есть свободный элемент массива дескрипторов.       */
         /* Последний элемент считаем фиктивным; он всегда свободен */
         i = -1;
         while (sadsarr [++i].ad >= 0) ;
         if (i < MY_MAX_QS) {
           /* Свободный элемент нашелся */
           sai_len = sizeof (struct sockaddr_in);
           if ((sadsarr [i].ad = accept (sd, (struct sockaddr *) sai,
                                         sai_len)) == -1) {
             perror ("ACCEPT");
             continue;
           }
           /* Сформируем сообщение, выдаваемое перед принятой строкой */
           (void) sprintf (sadsarr [i].my_msg,
                           "Вы ввели и отправили с адреса %s, порт %d :",
               	        inet_ntoa (sai.sin_addr), ntohs (sai.sin_port));
           /* Сформируем поток по дескриптору сокета */
           /* и отменим буферизацию для этого потока */
           if ((sadsarr [i].sd = fdopen (sadsarr [i].ad, "r")) == NULL) {
             perror ("FDOPEN");
             free_elem (sadsarr [i]);
             continue;
           }
           setbuf (sadsarr [i].sd, NULL);
         }
       }
    
       /* Посмотрим, есть ли данные, готовые для чтения */
       for (i = 0; i < MY_MAX_QS; i++) {
         if ((sadsarr [i].ad >= 0)  FD_ISSET (sadsarr [i].ad, rdset)) {
           /* Есть данные, готовые для чтения,   */
           /* или установлен признак конца файла */
           if (fgets (line, sizeof (line), sadsarr [i].sd) != NULL) {
             /* Выведем полученную строку */
             fputs (sadsarr [i].my_msg, stdout);
             fputs (line, stdout);
           } else {
             /* Отправитель закрыл соединение. */
             /* Закроем его и мы               */
             fclose (sadsarr [i].sd);
             free_elem (sadsarr [i]);
           }
         }
       }
     }
    
     return (0);
    }

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

    В качестве следующего примера рассмотрим передачу целочисленных данных. Главная проблема здесь - аккуратное выполнение преобразований между хостовым и сетевым порядками байт. Для иллюстрации использовано вычисление и вывод строк треугольника Паскаля. Вычисляющая и передающая программа показана в листинге 11.40, принимающая и выводящая - в листинге 11.41.

    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа вычисляет несколько первых строк треугольника Паскаля */
    /* и отправляет их через сокет сервису spooler.                    */
    /* Имя целевого хоста - аргумент командной строки.                 */
    /* Передаваемые данные снабжаются однобайтными маркерами типа      */
    /* и кратности и преобразуются к сетевому порядку байт             */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    
    /* Количество вычисляемых строк треугольника Паскаля */
    #define T_SIZE  16
    
    /* Маркеры типов передаваемых данных */
    #define T_UCHAR         1
    #define T_UINT16        2
    #define T_UINT32        4
    
    #define T_HDR   "\nТреугольник Паскаля:"
    
    int main (int argc, char *argv []) {
     uint32_t tp [T_SIZE]; /* Массив для хранения текущей строки треугольника   */
     unsigned char mtl;    /* Переменная для хранения маркеров типа и кратности */
     uint32_t ntelem;      /* Текущий элемент строки в сетевом порядке байт     */
     int sd;               /* Дескриптор передающего сокета                     */ 
    			/* Структура - входной аргумент getaddrinfo          */
     struct addrinfo hints = {0, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo */
     int i, j;
    
     if (argc != 2) {
       fprintf (stderr, "Использование: %s имя_серверного_хоста\n", argv [0]);
       return (1);
     }
    
     /* Создадим передающий сокет и установим соединение */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (2);
     }
    
     if ((res = getaddrinfo (argv [1], "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (3);
     }
    
     if (connect (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("CONNECT");
       return (4);
     }
    
     /* Инициализируем массив для хранения текущей строки треугольника Паскаля, */
     /* чтобы далее все элементы можно было считать и передавать единообразно   */
     tp [0] = 1;
     for (i = 1; i < T_SIZE; i++) {
       tp [i] = 0;
     }
    
     /* Передадим заголовок */
     mtl = T_UCHAR;
     if (write (sd, mtl, 1) != 1) {
       perror ("WRITE-1");
       return (5);
     }
     mtl = sizeof (T_HDR) - 1;
     if (write (sd, mtl, 1) != 1) {
       perror ("WRITE-2");
       return (6);
     }
     if (write (sd, T_HDR, mtl) != mtl) {
       perror ("WRITE-3");
       return (7);
     }
    
     /* Вычислим и передадим строки треугольника Паскаля */
     for (i = 0; i < T_SIZE; i++) {
       /* Элементы очередной строки нужно считать от конца к началу */
       /* Элемент tp [0] пересчитывать не нужно                     */
       for (j = i; j > 0; j--) {
         tp [j] += tp [j - 1];
       }
       /* Вывод строки треугольника в сокет */
       mtl = T_UINT32;
       if (write (sd, mtl, 1) != 1) {
         perror ("WRITE-4");
         return (8);
       }
       mtl = i + 1;
       if (write (sd, mtl, 1) != 1) {
         perror ("WRITE-5");
         return (9);
       }
       /* Преобразование элементов строки к сетевому порядку байт и вывод */
       for (j = 0; j <= i; j++) {
         ntelem = htonl (tp [j]);
         if (write (sd, ntelem, sizeof (ntelem)) != sizeof (ntelem)) {
           perror ("WRITE-6");
           return (10);
         }
       }
     }
     shutdown (sd, SHUT_WR);
    
     return (close (sd));
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    /* Программа процесса (будем называть его наивным сервером), */
    /* принимающего запросы на установления соединения           */
    /* и осуществляющего вывод поступающих числовых данных       */
    /* без порождения процессов и мультиплексирования            */
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
    
    #include <unistd.h>
    #include <stdio.h>
    #include <netdb.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    
    /* Маркеры типов передаваемых данных */
    #define T_UCHAR         1
    #define T_UINT16        2
    #define T_UINT32        4
    
    int main (void) {
     int sd;               /* Дескриптор слушающего сокета             */
     int ad;               /* Дескриптор приемного сокета              */
    			/* Структура - входной аргумент getaddrinfo */
     struct addrinfo hints = {AI_PASSIVE, AF_INET, SOCK_STREAM, IPPROTO_TCP,
    			   0, NULL, NULL, NULL};
    			/* Указатель - выходной аргумент getaddrinfo */
     struct addrinfo *addr_res;
     int res;              /* Результат getaddrinfo       */
    			/* Структура для записи адреса */
     struct sockaddr_in sai;
     socklen_t sai_len;    /* Длина адреса                        */
     char mt;              /* Маркер типа поступающих данных      */
     char ml;              /* Маркер кратности поступающих данных */
     char     bufc;        /* Буфер для приема символьных данных  */
     uint16_t buf16;       /* Буфер для приема 16-разрядных целых */
     uint32_t buf32;       /* Буфер для приема 16-разрядных целых */
    
     /* Создадим слушающий сокет */
     if ((sd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) {
       perror ("SOCKET");
       return (1);
     }
    
     /* Привяжем этот сокет к адресу сервиса spooler на локальном хосте */
     if ((res = getaddrinfo (NULL, "spooler", hints, addr_res)) != 0) {
       fprintf (stderr, "GETADDRINFO: %s\n", gai_strerror (res));
       return (1);
     }
     if (bind (sd, addr_res->ai_addr, addr_res->ai_addrlen) < 0) {
       perror ("BIND");
       return (2);
     }
    
     /* Можно освободить память, которую запрашивала функция getaddrinfo() */
     freeaddrinfo (addr_res);
    
     /* Пометим сокет как слушающий */
     if (listen (sd, SOMAXCONN) < 0) {
       perror ("LISTEN");
       return (3);
     }
    
     /* Цикл приема соединений и обслуживания запросов на вывод */
     while (1) {
       /* Примем соединение */
       sai_len = sizeof (struct sockaddr_in);
       if ((ad = accept (sd, (struct sockaddr *) sai, sai_len)) < 0) {
         perror ("ACCEPT");
         continue;
       }
    
       /* Цикл приема поступающих данных и их вывода */
       printf ("Получено с адреса %s, порт %d :",
    	    inet_ntoa (sai.sin_addr), ntohs (sai.sin_port));
       while (read (ad, mt, 1) == 1) {
         /* Есть очередная порция данных, начинающаяся с типа. */
         /* Прочитаем кратность, потом сами данные             */
         if (read (ad, ml, 1) != 1) {
           perror ("READ-1");
           return (4);
         }
         while (ml-- > 0) {
           switch (mt) {
             case T_UCHAR:
               if (read (ad, bufc, sizeof (bufc)) != sizeof (bufc)) {
                 perror ("READ-2");
                 return (5);
               }
               printf ("%c", bufc);
               continue;
             case T_UINT16:
               if (read (ad, buf16, sizeof (buf16)) != sizeof (buf16)) {
                 perror ("READ-3");
                 return (6);
               }
               printf (" %d", ntohs (buf16));
               continue;
             case T_UINT32:
               if (read (ad, buf32, sizeof (buf32)) != sizeof (buf32)) {
                 perror ("READ-4");
                 return (7);
               }
               printf (" %d", ntohl (buf32));
               continue;
           }
         }
         /* Вывод порции завершим переводом строки */
         printf ("\n");
       }
    
       /* Конец обслуживания соединения */
       (void) close (ad);
     }
    
     return (0);
    }
    Вернуться к учебному плану