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(SERV_PORT);
15 Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
16 Listen(listenfd, LISTENQ);
17 Signal(SIGCHLD, sig_chld); /* нужно вызвать waitpid() */
18 for (;;) {
19 clilen = sizeof(cliaddr);
20 if ((connfd = accept(listenfd, (SA*)&cliaddr, &clilen)) < 0) {
21 if (errno == EINTR)
22 continue; /* назад к for() */
23 else
24 err_sys("accept error");
25 }
26 if ((childpid = Fork()) == 0) { /* дочерний процесс */
27 Close(listenfd); /* закрываем прослушиваемый сокет */
28 str_echo(connfd); /* обрабатываем запрос */
29 exit(0);
30 }
31 Close(connfd); /* родитель закрывает присоединенный сокет */
32 }
33 }
Целью этого раздела было продемонстрировать три сценария, которые могут встретиться в сетевом программировании.
1. При выполнении функции fork, порождающей дочерние процессы, следует перехватывать сигнал SIGCHLD.
2. При перехватывании сигналов мы должны обрабатывать прерванные системные вызовы.
3. Обработчик сигналов SIGCHLD должен быть создан корректно с использованием функции waitpid, чтобы не допустить появления зомби.
Окончательная версия нашего сервера TCP (см. листинг 5.9) вместе с обработчиком сигналов SIGCHLD в листинге 5.8 обрабатывает все три сценария.
5.11. Прерывание соединения перед завершением функции accept
Существует другое условие, аналогичное прерванному системному вызову, пример которого был описан в предыдущем разделе. Оно может привести к возвращению функцией accept нефатальной ошибки, в случае чего следует заново вызвать функцию accept. Последовательность пакетов, показанная на рис. 5.4, встречается на загруженных серверах (эта последовательность типична для загруженных веб-серверов).
Рис. 5.4. Получение сегмента RST для состояния соединения ESTABLISHED перед вызовом функции accept
Трехэтапное рукопожатие TCP завершается, устанавливается соединение, а затем TCP клиента посылает сегмент RST. На стороне сервера соединение ставится в очередь в ожидании вызова функции accept, и в это время сервер получает сегмент RST. Спустя некоторое время процесс сервера вызывает функцию accept.
К сожалению, принцип обработки прерванного соединения зависит от реализации. Реализации, происходящие от Беркли, обрабатывают прерванное соединение полностью внутри ядра, и сервер никогда не узнает об этом. Большинство реализаций SVR4, однако, возвращают процессу ошибку, и эта ошибка зависит от реализации. При этом переменная errno принимает значение EPROTO (ошибка протокола), хотя в POSIX указано, что должна возвращаться ошибка ECONNABORTED (прерывание соединения). POSIX определяет эту ошибку иначе, так как ошибка EPROTO возвращается еще и в том случае, когда в подсистеме потоков происходят какие-либо фатальные события, имеющие отношение к протоколу. Возвращение той же ошибки для нефатального прерывания установленного соединения клиентом приводит к тому, что сервер не знает, вызывать снова функцию accept или нет. В случае ошибки ECONNABORTED сервер может игнорировать ошибку и снова вызывать функцию accept.
Этот сценарий очень просто имитировать. Запустите сервер, который должен вызвать функции socket, bind и listen, а затем перед вызовом функции accept переведите сервер на короткое время в состояние ожидания. Пока процесс сервера находится в состоянии ожидания, запустите клиент, который вызовет функции socket и connect. Как только функция connect завершится, установите параметр сокета SO_LINGER, чтобы сгенерировать сегмент RST (который мы описываем в разделе 7.5 и демонстрируем в листинге 16.14), и завершите процессы.
В [128] описана обработка этой ошибки в Беркли-ядрах (Berkeley-derived kernels), которые никогда не передают ее процессу. Обработка RST с вызовом функции tcp_close представлена в [128, с. 964]. Эта функция вызывает функцию in_pcbdetach [128, с. 897], которая, в свою очередь, вызывает функцию sofree [128, с. 719]. Функция sofree [128, с. 473] обнаруживает, что сокет все еще находится в очереди полностью установленных соединений прослушиваемого сокета. Она удаляет этот сокет из очереди и освобождает сокет. Когда сервер, наконец, вызовет функцию accept, он не сможет узнать, что установленное соединение было удалено из очереди.
Мы вернемся к подобным прерванным соединениям в разделе 16.6 и покажем, какие проблемы они могут порождать совместно с функцией select и прослушиваемым сокетом в нормальном режиме блокирования.
5.12. Завершение процесса сервера
Теперь мы запустим соединение клиент-сервер и уничтожим дочерний процесс сервера. Это симулирует сбой процесса сервера, благодаря чему мы сможем выяснить, что происходит с клиентом в подобных ситуациях. (Следует точно различать сбой
1. Мы запускаем сервер и клиент на разных узлах и вводим на стороне клиента одну строку, чтобы проверить, все ли в порядке. Строка отражается дочерним процессом сервера.
2. Мы находим идентификатор дочернего процесса сервера и уничтожаем его с помощью программы kill. Одним из этапов завершения процесса является закрытие всех открытых дескрипторов в дочернем процессе. Это вызывает отправку сегмента FIN клиенту, и TCP клиента отвечает сегментом ACK. Это первая половина завершения соединения TCP.
3. Родительскому процессу сервера посылается сигнал SIGCHLD, и он корректно обрабатывается (см. листинг 5.9).
4. С клиентом ничего не происходит. TCP клиента получает от TCP сервера сегмент FIN и отвечает сегментом ACK, но проблема состоит в том, что клиентский процесс блокирован в вызове функции fgets в ожидании строки от терминала.
5. Запуск программы netstat на этом шаге из другого окна на стороне клиента показывает состояние клиентского сокета:
linux % netstat -a | grep 9877
tcp 0 0 *:9877 *:* LISTEN
tcp 0 0 localhost:9877 localhost:9877 FIN_WAIT2
tcp 1 0 localhost.43604 localhost:9877 CLOSE_WAIT
Как видите, согласно рис. 2.4, осуществилась половина последовательности завершения соединения TCP.
6. Мы можем снова ввести строку на стороне клиента. Вот что происходит на стороне клиента (начиная с шага 1):
linux % tcpcli01 127.0.0.1
hello
hello
another line
str_cli: server terminated prematurely
Когда мы вводим следующую строку, функция str_cli вызывает функцию writen, и TCP клиента отправляет данные серверу. TCP это допускает, поскольку получение сегмента FIN протоколом TCP клиента указывает только на то, что процесс сервера закрыл свой конец соединения и больше не будет отправлять данные. Получение сегмента FIN не сообщает протоколу TCP клиента, что процесс сервера завершился (хотя в данном случае он завершился). Мы вернемся к этому вопросу в разделе 6.6, когда будем говорить о половинном закрытии TCP.
Когда TCP сервера получает данные от клиента, он отвечает сегментом RST, поскольку процесс, у которого был открытый сокет, завершился. Мы можем проверить, что этот сегмент RST отправлен, просмотрев пакеты с помощью программы tcpdump.
7. Однако процесс клиента не увидит сегмента RST, поскольку он вызывает функцию readline сразу же после вызова функции writen, и readline сразу же возвращает 0 (признак конца файла) по причине того, что на шаге 2 был получен сегмент FIN. Наш клиент не предполагает получать признак конца файла на этом этапе (см. листинг 5.3), поэтому он завершает работу, сообщая об ошибке Server terminated prematurely (Сервер завершил работу преждевременно).
Этапы описанной последовательности также зависят от синхронизации времени. Вызов readline на стороне клиента может произойти до получения им пакета RST от сервера, но может произойти и после. Если readline вызывается до получения RST, происходит то, что мы описали выше (клиент считывает символ конца файла). Если же первым будет получен пакет RST, функция readline возвратит ошибку ECONNRESET (соединение сброшено собеседником).
8. Когда клиент завершает работу (вызывая функцию err_quit в листинге 5.4), все его открытые дескрипторы закрываются.
Проблема заключается в том, что клиент блокируется в вызове функции fgets, когда сегмент FIN приходит на сокет. Клиент в действительности работает с двумя дескрипторами — дескриптором сокета и дескриптором ввода пользователя, и поэтому он должен блокироваться при вводе из любого источника (сейчас в функции str_cli он блокируется при вводе только из одного источника). Обеспечить подобное блокирование — это одно из назначений функций select и poll, о которых рассказывается в главе 6. Когда в разделе 6.4 мы перепишем функцию str_cli, то как только мы уничтожим с помощью программы kill дочерний процесс сервера, клиенту будет отправлено уведомление о полученном сегменте FIN.
5.13. Сигнал SIGPIPE
Что происходит, если клиент игнорирует возвращение ошибки из функции readline и отсылает следующие данные серверу? Это может произойти, если, например, клиенту нужно выполнить две операции по отправке данных серверу перед считыванием данных от него, причем первая операция отправки данных вызывает RST.
Применяется следующее правило: когда процесс производит запись в сокет, получивший сегмент RST, процессу посылается сигнал SIGPIPE. По умолчанию действием этого сигнала является завершение процесса, так что процесс должен перехватить сигнал, чтобы не произошло непроизвольного завершения.
Если процесс либо перехватывает сигнал и возвращается из обработчика сигнала, либо игнорирует сигнал, то операция записи возвращает ошибку EPIPE.
Часто задаваемым вопросом (FAQ) в Usenet является такой: как получить этот сигнал при первой, а не при второй операции записи? Это невозможно. Как следует из приведенных выше рассуждений, первая операция записи выявляет сегмент RST, а вторая — сигнал. Если запись в сокет, получивший сегмент FIN, допускается, то запись в сокет, получивший сегмент RST, является ошибочной.
Чтобы увидеть, что происходит с сигналом SIGPIPE, изменим код нашего клиента так, как показано в листинге 5.10.
Листинг 5.10. Функция str_cli, дважды вызывающая функцию writen
//tcpcliserv/str_cli11.c
1 #include "unp.h"
2 void
3 str_cli(FILE *fp, int sockfd)
4 {
5 char sendline[MAXLINE], recvline[MAXLINE];
6 while (Fgets(sendline, MAXLINE, fp) != NULL) {
7 Writen(sockfd, sendline, 1);
8 sleep(1);
9 Writen(sockfd, sendline + 1, strlen(sendline) - 1);
10 if (Readline(sockfd, recvline, MAXLINE) == 0)
11 err_quit("str_cli, server terminated prematurely");
12 Fputs(recvline, stdout);
13 }
14 }
7-9 Все изменения, которые мы внесли, — это повторный вызов функции writen: сначала в сокет записывается первый байт данных, за этим следует пауза в 1 с и далее идет запись остатка строки. Наша цель — выявить сегмент RST при первом вызове функции writen и генерировать сигнал SIGPIPE при втором вызове.
Если мы запустим клиент на нашем узле Linux, мы получим:
linux % tcpcli11 127.0.0.1
hi there
hi there