UNIX: разработка сетевых приложений
str_cli: server terminated prematurelyКогда мы вводим следующую строку, функция
вызывает функциюstr_cli, и TCP клиента отправляет данные серверу. TCP это допускает, поскольку получение сегмента FIN протоколом TCP клиента указывает только на то, что процесс сервера закрыл свой конец соединения и больше не будет отправлять данные. Получение сегмента FIN не сообщает протоколу TCP клиента, что процесс сервера завершился (хотя в данном случае он завершился). Мы вернемся к этому вопросу в разделе 6.6, когда будем говорить о половинном закрытии TCP.writenКогда TCP сервера получает данные от клиента, он отвечает сегментом RST, поскольку процесс, у которого был открытый сокет, завершился. Мы можем проверить, что этот сегмент RST отправлен, просмотрев пакеты с помощью программы
.tcpdump7. Однако процесс клиента не увидит сегмента RST, поскольку он вызывает функцию
сразу же после вызова функцииreadline, иwritenсразу же возвращает 0 (признак конца файла) по причине того, что на шаге 2 был получен сегмент FIN. Наш клиент не предполагает получать признак конца файла на этом этапе (см. листинг 5.3), поэтому он завершает работу, сообщая об ошибкеreadline(Сервер завершил работу преждевременно).Server terminated prematurelyПРИМЕЧАНИЕЭтапы описанной последовательности также зависят от синхронизации времени. Вызов readline на стороне клиента может произойти до получения им пакета RST от сервера, но может произойти и после. Если readline вызывается до получения RST, происходит то, что мы описали выше (клиент считывает символ конца файла). Если же первым будет получен пакет RST, функция readline возвратит ошибку ECONNRESET (соединение сброшено собеседником).
8. Когда клиент завершает работу (вызывая функцию
в листинге 5.4), все его открытые дескрипторы закрываются.err_quitПроблема заключается в том, что клиент блокируется в вызове функции
, когда сегмент FIN приходит на сокет. Клиент в действительности работает с двумя дескрипторами — дескриптором сокета и дескриптором ввода пользователя, и поэтому он должен блокироваться при вводе из любого источника (сейчас в функцииfgetsон блокируется при вводе только из одного источника). Обеспечить подобное блокирование — это одно из назначений функцийstr_cliиselect, о которых рассказывается в главе 6. Когда в разделе 6.4 мы перепишем функциюpoll, то как только мы уничтожим с помощью программыstr_cliдочерний процесс сервера, клиенту будет отправлено уведомление о полученном сегменте FIN.kill5.13. Сигнал SIGPIPE
Что происходит, если клиент игнорирует возвращение ошибки из функции
и отсылает следующие данные серверу? Это может произойти, если, например, клиенту нужно выполнить две операции по отправке данных серверу перед считыванием данных от него, причем первая операция отправки данных вызывает RST.readlineПрименяется следующее правило: когда процесс производит запись в сокет, получивший сегмент RST, процессу посылается сигнал
. По умолчанию действием этого сигнала является завершение процесса, так что процесс должен перехватить сигнал, чтобы не произошло непроизвольного завершения.SIGPIPEЕсли процесс либо перехватывает сигнал и возвращается из обработчика сигнала, либо игнорирует сигнал, то операция записи возвращает ошибку
.EPIPEПРИМЕЧАНИЕЧасто задаваемым вопросом (FAQ) в Usenet является такой: как получить этот сигнал при первой, а не при второй операции записи? Это невозможно. Как следует из приведенных выше рассуждений, первая операция записи выявляет сегмент RST, а вторая — сигнал. Если запись в сокет, получивший сегмент FIN, допускается, то запись в сокет, получивший сегмент RST, является ошибочной.
Чтобы увидеть, что происходит с сигналом
, изменим код нашего клиента так, как показано в листинге 5.10.SIGPIPEЛистинг 5.10. Функция str_cli, дважды вызывающая функцию writen
//tcpcliserv/str_cli11.c1 #include "unp.h"2 void3 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: сначала в сокет записывается первый байт данных, за этим следует пауза в 1 с и далее идет запись остатка строки. Наша цель — выявить сегмент RST при первом вызове функцииwritenи генерировать сигналwritenпри втором вызове.SIGPIPEЕсли мы запустим клиент на нашем узле Linux, мы получим:
linux % <b>tcpcli11 127.0.0.1</b><b>hi there</b> <i>мы вводим эту строку</i>hi there <i>и она отражается сервером</i><i>здесь мы завершаем дочерний процесс сервера</i><b>bye</b> <i>затем мы вводим эту строку</i>Broken pipe <i>это сообщение выводится интерпретатором</i>Мы запускаем клиент, вводим одну строку, видим, что строка отражена корректно, и затем завершаем дочерний процесс сервера на узле сервера. Затем мы вводим другую строку (
), но ничего не отражается, а интерпретатор сообщает нам о том, что процесс получил сигнал SIGPIPE. Некоторые интерпретаторы не выводят никаких сообщений, если процесс завершает работу без дампа памяти, но в нашем примере использовался интерпретаторbye, который берет на себя эту работу.bashРекомендуемый способ обработки сигнала
зависит от того, что приложение собирается делать, когда получает этот сигнал. Если ничего особенного делать не нужно, проще всего установить действиеSIGPIPE, предполагая, что последующие операции вывода перехватят ошибкуSIG_IGNи завершатся. Если при появлении сигнала необходимо проделать специальные действия (возможно, запись в системный журнал), то сигнал следует перехватить и выполнить требуемые действия в обработчике сигнала. Однако отдавайте себе отчет в том, что если используется множество сокетов, то при доставке сигнала мы не получаем информации о том, на каком сокете произошла ошибка. Если нам нужно знать, какая именно операцияEPIPEвызвала ошибку, следует либо игнорировать сигнал, либо вернуть управление из обработчика сигнала и обработать ошибкуwriteиз функцииEPIPE.write