Опубликован: 15.06.2004 | Уровень: специалист | Доступ: платный
Лекция 11:

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

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

Приводимые далее примеры строятся вокруг задачи копирования данных в распределенной программной конфигурации со стандартного ввода одного процесса на стандартный вывод другого, удаленного. Однако начнем мы с локального случая, обслуживаемого сокетами адресного семейства 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);
}
Листинг 11.30. Пример программы, использующей сокеты адресного семейства AF_UNIX.

Обратим внимание на употребление в процессе, осуществляющем запись в сокет, функции 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);
}
Листинг 11.31. Пример программы, использующей сокеты адресного семейства AF_INET для отправки датаграмм.
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Программа процесса (будем называть его сервером),     */
/* читающего сообщения (строки) из датаграммного сокета  */
/* и выдающего их на стандартный вывод                   */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#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);
}
Листинг 11.32. Пример программы, использующей сокеты адресного семейства AF_INET для приема датаграмм.

Обратим внимание на несколько моментов. Адреса сокетов (целевого и приемного) получены при помощи функции 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);
}
Листинг 11.33. Модифицированный вариант программы, использующей сокеты адресного семейства AF_INET для отправки датаграмм.
/* * * * * * * * * * * * * * * * * * * * * * * * * * */
/* Программа процесса (будем называть его сервером), */
/* читающего сообщения из датаграммного сокета       */
/* и копирующего их на стандартный вывод             */
/* с указанием адреса, откуда они поступили          */
/* * * * * * * * * * * * * * * * * * * * * * * * * * */

#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);
}
Листинг 11.34. Модифицированный вариант программы, использующей сокеты адресного семейства AF_INET для приема датаграмм.

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

Антон Коновалов
Антон Коновалов

В настоящее время актуальный стандарт - это POSIX 2008 и его дополнение POSIX 1003.13
Планируется ли актуализация материалов данного очень полезного курса?