Текст книги "UNIX: разработка сетевых приложений"
Автор книги: Уильям Ричард Стивенс
Соавторы: Эндрю М. Рудофф,Билл Феннер
Жанр:
ОС и Сети
сообщить о нарушении
Текущая страница: 2 (всего у книги 88 страниц) [доступный отрывок для чтения: 32 страниц]
Наша программа, представленная в листинге 1.1, является зависимой от протокола( protocol dependent) IPv4. Мы выделяем и инициализируем структуру sockaddr_in
, определяем адрес как относящийся к семейству AF_INET и устанавливаем первый аргумент функции socket
равным AF_INET
.
Если мы хотим изменить программу так, чтобы она работала по протоколу IPv6, мы должны изменить код. В листинге 1.2 показана новая версия программы с соответствующими изменениями, отмеченными полужирным шрифтом.
Листинг 1.2. Версия листинга 1.1 для IPv6
//intro/daytimetcpcliv6.с
1 #include "unp.h"
2 int
3 main(int argc, char **argv)
4 {
5 int sockfd, n;
6 char recvline[MAXLINE + 1];
7 struct sockaddr_in6servaddr;
8 if (argc != 2)
9 err_quit("usage: a.out
10 if ((sockfd = socket( AF_INET6, SOCK_STREAM, 0)) < 0)
11 err_sys("socket error");
12 bzero(&servaddr, sizeof(servaddr));
13 servaddr. sin6_family= AF_INET6;
14 servaddr. sin6_port= htons(13); /* сервер времени и даты */
15 if (inet_pton( AF_INET6, argv[1], &servaddr. sin6_addr) <= 0)
16 err_quit("inet_pton error for %s", argv[1]);
17 if (connect(sockfd, (SA*)&servaddr, sizeof(servaddr)) < 0)
18 err_sys("connect error");
19 while ((n = read(sockfd, recvline, MAXLINE)) > 0) {
20 recvline[n] = 0; /* символ конца строки */
21 if (fputs(recvline, stdout) == EOF)
22 err_sys("fputs error");
23 }
24 if (n < 0)
25 err_sys("read error");
26 exit(0);
27 }
Изменились только пять строк, но в результате мы все равно получили программу, зависимую от протокола, в данном случае – от протокола IPv6. Лучше сделать программу независимой от протокола( protocol independent). В листинге 11.3 представлена независимая от протокола версия этого клиента, основанная на вызове getaddrinfo
из tcp_connect
.
Другим недостатком наших программ является то, что пользователь должен вводить IP-адрес сервера в точечно-десятичной записи (например, 206.168.112.219 для версии IPv4). Людям проще работать с именами, чем с числами (например, www.unpbook.com
). В главе 11 мы обсудим функции, обеспечивающие преобразование имен узлов в IP-адреса и имен служб в порты. Мы специально откладываем описание этих функций, продолжая использовать IP-адреса и номера портов, чтобы иметь ясное представление о том, что именно входит в структуры адресов сокетов, которые мы должны заполнить и проверить. Это также упрощает наши объяснения сетевого программирования, снимая необходимость описывать в подробностях еще один набор функций.
В любой реальной программе существенным моментом является проверка каждоговызова функции на предмет возвращаемой ошибки. В листинге 1.1 мы проводим поиск ошибок в вызовах функций socket
, inet_pton
, connect
, read
и fputs
, и когда ошибка случается, мы вызываем свои собственные функции err_quit
и err_sys
для печати сообщения об ошибке и для прерывания выполнения программы. В отдельных случаях, когда функция возвращает ошибку, бывает нужно сделать еще что-либо помимо прерывания программы, как показано в листинге 5.9, когда мы должны проверить прерванный системный вызов.
Поскольку прерывание программы из-за ошибки – типичное явление, мы сократим наши программы, определив функции-обертки, которые будут вызывать соответствующие рабочие функции, проверять возвращаемые значения и прерывать программу при возникновении ошибки. Соглашение, используемое нами, заключается в том, что название функции-обертки пишется с заглавной буквы, например:
sockfd = Socket(AF_INET, SOCK_STREAM, 0);
Наша функция-обертка для функции socket показана в листинге 1.3.
Листинг 1.3. Наша функция-обертка для функции socket
//lib/wrapsock.c
172 int
173 Socket(int family, int type, int protocol)
174 {
175 int n;
176 if ((n = socket(family, type, protocol)) < 0)
177 err_sys("socket error");
178 return (n);
179 }
Хотя вы можете решить, что использование этих функций-оберток не обеспечивает большой экономии, на самом деле это не так. Обсуждая потоки (threads) в главе 26, мы обнаружим, что, когда происходит какая-либо ошибка, функции потоков не устанавливают значение стандартной переменной Unix errno
равным определенной константе, специфической для произошедшей ошибки. Вместо этого значение переменной errno
просто возвращается функцией. Это значит, что каждый раз, когда мы вызываем одну из функций pthread
, мы должны разместить в памяти переменную, сохранить возвращаемое значение в этой переменной и установить errno
равной этому значению перед вызовом err_sys
. Чтобы избежать загромождения кода скобками, мы можем использовать оператор языка С запятаядля объединения присваивания значения переменной errno
и вызова err_sys
в отдельное выражение следующим образом:
int n;
if ((n = pthread_mutex_lock(&ndone_mutex)) != 0)
errno = n, err_sys("pthread_mutex_lock error");
ВНИМАНИЕ
В тексте книги вам будут встречаться функции, имена которые начинаются с заглавной буквы. Это наши собственные функции-обертки. Функция-обертка вызывает функцию, имеющую такое же имя, но начинающееся со строчной буквы.
При описании исходного кода, представленного в тексте книги, мы всегда ссылаемся на вызываемую функцию низшего уровня (например, socket), но не на функцию-обертку (например, Socket).
В качестве альтернативы мы можем определить новую функцию выдачи сообщений об ошибках, которая в качестве аргумента получает системный код ошибки. Однако проще всего текст будет выглядеть с использованием функции-обертки, определенной в листинге 1.4:
Pthread_mutex_lock(&ndone_mutex);
Листинг 1.4. Наша собственная функция-обертка для функции pthread_mutex_lock
//lib/wrappthread.c
72 void
73 Pthread_mutex_lock(pthread_mutex_t *mptr)
74 {
75 int n;
76 if ((n = pthread_mutex_lock(mptr)) == 0)
77 return;
78 errno = n;
79 err_sys("pthread_mutex_lock error");
80 }
ПРИМЕЧАНИЕ
Если аккуратно программировать на С, можно использовать макросы вместо функций, что обеспечивает небольшой выигрыш в производительности, однако функции– обертки редко, если вообще когда-нибудь бывают причиной недостаточной производительности программ.
Наш выбор – первая заглавная буква в названии функции – является компромиссом. Было предложено множество других стилей: подстановка префикса e перед названием функции (как сделано в [67, с. 182]), добавление _е к имени функции и т.д. Наш вариант кажется наименее отвлекающим внимание и одновременно дающим визуальное указание на то, что вызывается какая-то другая функция.
Эта технология имеет, кроме того, полезный побочный эффект: она позволяет проверять возникновение ошибок при выполнении таких функций, ошибки в которых часто остаются незамеченными, например close и listen.
На протяжении всей книги мы будем использовать эти функции-обертки, кроме тех случаев, когда нам нужно проверить ошибку явно и обрабатывать ее другим, отличным от прерывания программы, способом. Мы не приводим исходный код для всех наших собственных функций-оберток, но он свободно доступен в Интернете (см. предисловие).
Когда при выполнении функции Unix (например, одной из функций сокетов) происходит ошибка, глобальной переменной errno
присваивается положительное значение, указывающее на тип ошибки, а возвращаемое значение функции обычно равно -1. Наша функция err_sys
проверяет значение переменной errno
и печатает строку с соответствующим сообщением об ошибке (например, «Время соединения истекло», если значение переменной errno равно ETIMEDOUT
).
Переменная errno устанавливается равной определенному значению, только если при выполнении функции произошла какая-либо ошибка. Ее значение не определено, если функция не возвращает ошибки. Все положительные значения ошибок являются константами с именами в верхнем регистре, начинающимися на «E», и обычно определяются в заголовке
. Ни одна ошибка не имеет кода 0.
Переменную errno нельзя хранить как глобальную переменную в случае множества потоков, у которых все глобальные переменные являются общими. О решении этой проблемы мы расскажем в главе 23.
На протяжении всего текста книги мы использовали фразы типа «функция connect возвращает ECONNREFUSED
» для сокращенного обозначения того, что при выполнении функции произошла ошибка (обычно при этом возвращаемое значение функции равно -1), и значение переменной errno
стало равным указанной константе.
Мы можем написать простую версию сервера TCP для определения времени и даты, который будет работать с клиентом, описанным в разделе 1.2. Мы используем функции-обертки, описанные в предыдущем разделе. Код сервера приведен в листинге 1.5.
Листинг 1.5. TCP-сервер времени и даты
//intro/daytimetcpsrv.c
1 #include "unp.h"
2 #include
3 int
4 main(int argc, char **argv)
5 {
6 int listenfd, connfd;
7 struct sockaddr_in servaddr;
8 char buff[MAXLINE];
9 time_t ticks;
10 listenfd = Socket(AF_INET, SOCK_STREAM, 0);
11 bzero(&servaddr, sizeof(servaddr));
12 servaddr.sin_family = AF_INET;
13 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
14 servaddr.sin_port = htons(13); /* сервер времени и даты */
15 Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
16 Listen(listenfd, LISTENQ);
17 for (;;) {
18 connfd = Accept(listenfd, (SA*)NULL, NULL);
19 ticks = time(NULL);
20 snprintf(buff, sizeof(buff), "%.24srn", ctime(&ticks));
21 Write(connfd. buff, strlen(buff));
22 Close(connfd);
23 }
24 }
Создание сокета TCP
10
Создание сокета TCP выполняется так же, как и в клиентском коде.
Связывание заранее известного порта сервера с сокетом
11-15
Заранее известный порт сервера (13 в случае сервера времени и даты) связывается с сокетом путем заполнения структуры адреса интернет-сокета и вызова функции bind
. Мы задаем IP-адрес как INADDR_ANY
, что позволяет серверу принимать соединение клиента на любом интерфейсе в том случае, если узел сервера имеет несколько интерфейсов. Далее мы рассмотрим, как можно ограничить прием соединений одним-единственным интерфейсом.
Преобразование сокета в прослушиваемый сокет
16
С помощью вызова функции listen
сокет преобразуется в прослушиваемый, то есть такой, на котором ядро принимает входящие соединения от клиентов. Эти три этапа, socket
, bind
и listen
, обычны для любого сервера TCP при создании того, что мы называем прослушиваемым дескриптором( listening descriptor) (в нашем примере это переменная listenfd
).
Константа LISTENQ
взята из нашего заголовочного файла unp.h
. Она задает максимальное количество клиентских соединений, которые ядро ставит в очередь на прослушиваемом сокете. Более подробно мы расскажем о таких очередях в разделе 4.5.
Прием клиентского соединения, отправка ответа
17-21
Обычно процесс сервера блокируется при вызове функции accept
, ожидая принятия подключения клиента. Для установки TCP-соединения используется трехэтапное рукопожатие( three-way handshake). Когда рукопожатие состоялось, функция accept возвращает значение, и это значение является новым дескриптором ( connfd
), который называется присоединенным дескриптором( connected descriptor). Этот новый дескриптор используется для связи с новым клиентом. Новый дескриптор возвращается функцией accept
для каждого клиента, соединяющегося с нашим сервером.
ПРИМЕЧАНИЕ
Стиль, используемый в книге для обозначения бесконечного цикла, выглядит так:
for (;;) {
...
}
Библиотечная функция time
возвращает количество секунд с начала эпохи Unix: 00:00:00 1 января 1970 года UTC (Universal Time Coordinated – универсальное синхронизированное время, среднее время по Гринвичу). Следующая библиотечная функция, ctime
, преобразует целочисленное значение секунд в строку следующего формата, удобного для человеческого восприятия:
Fri Jan 12 14:27:52 1996
Возврат каретки и пустая строка добавляются к строке функцией snprintf
, а результат передается клиенту функцией write
.
ПРИМЕЧАНИЕ
Если вы еще не выработали у себя привычку пользоваться функцией snprintf вместо устаревшей sprintf, сейчас самое время заняться этим. Функция sprintf не в состоянии обеспечить проверку переполнения буфера получателя. Функция snprintf, наоборот, требует, чтобы в качестве второго аргумента указывался размер буфера получателя, переполнение которого таким образом предотвращается.
Функция snprintf была добавлена в стандарт ANSI С относительно нравно, в версии ISO C99. Практически все поставщики программного обеспечения уже сейчас включают эту функцию в стандартную библиотеку языка С. Существуют и свободно распространяемые реализации. В нашей книге мы используем функцию snprintf и рекомендуем вам пользоваться ею в своих программах для повышения их надежности.
Удивительно много сетевых атак было реализовано хакерами с использованием незащищенности sprintf от переполнения буфера. Есть еще несколько функций, с которыми нужно быть аккуратными: gets, strcat и strcpy. Вместо них лучше использовать fgets, strncat и strncpy. Еще лучше работают более современные функции strlcat и strlcpy, возвращающие в качестве результата правильно завершенную строку. Полезные советы, касающиеся написания надежных сетевых программ, можно найти в главе 23 книги [32].
Завершение соединения
22
Сервер закрывает соединение с клиентом, вызывая функцию close
. Это инициирует обычную последовательность прерывания соединения TCP: пакет FIN посылается в обоих направлениях, и каждый пакет FIN распознается на другом конце соединения. Более подробно трехэтапное рукопожатие и четыре пакета TCP, используемые для прерывания соединения, будут описаны в разделе 2.6.
Сервер времени и даты был рассмотрен нами достаточно кратко, как и клиент из предыдущего раздела. Запомните следующие моменты.
■ Сервер, как и клиент, зависим от протокола IPv4. В листинге 11.7 мы покажем версию, не зависящую от протокола, которая использует функцию getaddrinfo
.
■ Наш сервер обрабатывает только один запрос клиента за один раз. Если приблизительно в одно время происходит множество клиентских соединений, ядро ставит их в очередь, максимальная длина которой регламентирована, и передает эти соединения функции accept по одному за один раз. Наш сервер времени и даты, который требует вызова двух библиотечных функций, time и ctime, является достаточно быстрым. Но если у сервера обслуживание каждого клиента занимает больше времени (допустим, несколько секунд или минуту), нам будет необходимо некоторым образом организовать одновременное обслуживание нескольких клиентов.
Сервер, показанный в листинге 1.5, называется последовательным сервером( iterative server), поскольку он обслуживает клиентов последовательно, по одному клиенту за один раз. Существует несколько технологий написания параллельного сервера( concurrent server), который обслуживает множество клиентов одновременно. Самой простой технологией является вызов функции Unix fork
(раздел 4.7), когда создается по одному дочернему процессу для каждого клиента. Другой способ – использование программных потоков (threads) вместо функции fork
(раздел 26.4) или предварительное порождение фиксированного количества дочерних процессов с помощью функции fork в начале работы (раздел 30.6).
■ Запуская такой сервер из командной строки, мы обычно рассчитываем, что он будет работать достаточно долго, поскольку часто серверы работают, пока работает система. Поэтому мы должны модифицировать код сервера таким образом, чтобы он корректно работал как демон( daemon) Unix, то есть процесс, функционирующий в фоновом режиме без подключения к терминалу. Это решение подробно описано в разделе 13.4.
1.6. Таблица соответствия примеров технологии клиент-серверТехнологии сетевого программирования иллюстрируются в этой книге на двух основных примерах:
■ клиент-сервер времени и даты (описание которого мы начали в листингах 1.1, 1.2 и 1.5), и
■ эхо-клиент-сервер (который появится в главе 5).
Чтобы обеспечить удобный поиск различных тем, которых мы касаемся в этой книге, мы объединили разработанные нами программы и сопроводили их номерами листингов, в которых приведен исходный код. В табл. 1.1 перечислены версии клиента времени и даты (две из них мы уже видели). В табл. 1.2 перечисляются версии сервера времени и даты. В табл. 1.3 представлены версии эхо-клиента, а в табл. 1.4 – версии эхо-сервера.
Таблица 1.1. Различные версии клиента времени и даты
1.1 | TCP/Ipv4, зависимый от протокола |
1.2 | TCP/Ipv6, зависимый от протокола |
11.2 | TCP/Ipv4, зависимый от протокола, вызывает функции gethostbyname и getservbyname |
11.5 | TCP, независимый от протокола, вызывает функции getaddrinfo и tcp_connect |
11.10 | UDP, независимый от протокола, вызывает функции getaddrinfo и udp_connect |
16.7 | TCP, использует неблокирующую функцию connect |
31.2 | TCP/IPv4, зависимый от протокола |
Д.1 | TCP, зависимый от протокола, генерирует SIGPIPE |
Д.2 | TCP, зависимый от протокола, печатает размер буфера сокета и MSS |
Д.5 | TCP, зависимый от протокола, допускает использование имени узла (функция gethostbyname) или IP-адреса |
Д.6 | TCP, независимый от протокола, допускает использование имени узла (функция gethostbyname). |
Таблица 1.2. Различные версии сервера времени и даты, рассматриваемые в данной книге
1.5 | TCP/IPv4, зависимый от протокола |
11.7 | TCP, независимый от протокола, вызывает getaddrinfo и tcp_listen |
11.8 | TCP, независимый от протокола, вызывает getaddrinfo и tcp_listen |
11.13 | UDP, независимый от протокола, вызывает getaddrinfo и udp_server |
13.2 | TCP, независимый от протокола, выполняется как автономный демон |
13.4 | TCP, независимый от протокола, порожденный демоном inetd |
Таблица 1.3. Различные версии эхо-клиента, рассматриваемые в данной книге
5.3 | TCP/IPv4, зависимый от протокола |
6.1 | TCP, использует функцию select |
6.2 | TCP, использует функцию select и работает в пакетном режиме |
8.3 | UDP/IPv4, зависимый от протокола |
8.5 | UDP, проверяет адрес сервера |
8.7 | UDP, вызывает функцию connect для получения асинхронных ошибок |
14.2 | UDP, тайм-аут при чтении ответа сервера с использованием сигнала SIGALRM |
14.4 | UDP, тайм-аут при чтении ответа сервера с использованием функции select |
14.5 | UDP, тайм-аут при чтении ответа сервера с использованием опции сокета SO_RCVTIMEO |
14.7 | TCP, использует интерфейс /dev/poll |
14.8 | TCP, использует интерфейс kqueue |
15.4 | Поток домена Unix, зависит от протокола |
15.6 | Дейтаграмма домена Unix, зависит от протокола |
16.1 | TCP, использует неблокируемый ввод-вывод |
16.6 | TCP, использует два процесса (функцию fork) |
16.14 | TCP, устанавливает соединение, затем посылает пакет RST |
20.1 | UDP, широковещательный, ситуация гонок |
20.2 | UDP, широковещательный, ситуация гонок |
20.3 | UDP, широковещательный, для устранения ситуации гонок используется функция pselect |
20.5 | UDP, широковещательный, для устранения ситуации гонок используются функции sigsetjmp и siglongmp |
20.6 | UDP, широковещательный, для устранения ситуации гонок в обработчике сигнала используется IPC |
22.4 | UDP, увеличение надежности протокола за счет применения повторной передачи, тайм-аутов и порядковых номеров |
26.1 | TCP, использование двух потоков |
27.4 | TCP/IPv4, задание маршрута от отправителя |
27.5 | UDP/IPv6, задание маршрута от отправителя |
Таблица 1.4. Различные версии эхо-сервера, рассматриваемые в данной книге
5.1 | TCP/IPv4, зависимый от протокола |
5.9 | TCP/IPv4, зависимый от протокола, корректно обрабатывает завершение всех дочерних процессов |
6.3 | TCP/IPv4, зависимый от протокола, использует функцию select, один процесс обрабатывает всех клиентов |
6.5 | TCP/IPv4, зависимый от протокола, использует функцию poll, один процесс обрабатывает всех клиентов |
8.1 | UDP/IPv4, зависимый от протокола |
8.14 | TCP и UDP/IPv4, зависимый от протокола, использует функцию select |
14.6 | TCP, использует стандартный ввод-вывод |
15.3 | Доменный сокет Unix, зависимый от протокола |
15.5 | Дейтаграмма домена Unix, зависит от протокола |
15.13 | Доменный сокет Unix, с передачей данных, идентифицирующих клиента |
22.3 | UDP, печатает полученный IP-адрес назначения и имя полученного интерфейса, обрезает дейтаграммы |
22.13 | UDP, связывает все адреса интерфейсов |
25.2 | UDP, использование модели ввода-вывода, управляемого сигналом |
26.2 | TCP, один поток на каждого клиента |
26.3 | TCP, один поток на каждого клиента, машинонезависимая (переносимая) передача аргумента |
27.4 | TCP/IPv4, печатает полученный маршрут от отправителя |
27.6 | UDP/IPv4, печатает полученный маршрут от отправителя и обращает его |
28.21 | UDP, использует функцию icmpd для получения асинхронных ошибок |
Д.9 | UDP, связывает все адреса интерфейсов |