UNIX: разработка сетевых приложений
27 Close(listenfd); /* закрываем прослушиваемый сокет */28 str_echo(connfd); /* обрабатываем запрос */29 exit(0);30 }31 Close(connfd); /* родитель закрывает присоединенный сокет */32 }33 }Целью этого раздела было продемонстрировать три сценария, которые могут встретиться в сетевом программировании.
1. При выполнении функции
, порождающей дочерние процессы, следует перехватывать сигналfork.SIGCHLD2. При перехватывании сигналов мы должны обрабатывать прерванные системные вызовы.
3. Обработчик сигналов
должен быть создан корректно с использованием функцииSIGCHLD, чтобы не допустить появления зомби.waitpidОкончательная версия нашего сервера TCP (см. листинг 5.9) вместе с обработчиком сигналов
в листинге 5.8 обрабатывает все три сценария.SIGCHLD5.11. Прерывание соединения перед завершением функции accept
Существует другое условие, аналогичное прерванному системному вызову, пример которого был описан в предыдущем разделе. Оно может привести к возвращению функцией
нефатальной ошибки, в случае чего следует заново вызвать функциюaccept. Последовательность пакетов, показанная на рис. 5.4, встречается на загруженных серверах (эта последовательность типична для загруженных веб-серверов).acceptРис. 5.4. Получение сегмента RST для состояния соединения ESTABLISHED перед вызовом функции accept
Трехэтапное рукопожатие TCP завершается, устанавливается соединение, а затем TCP клиента посылает сегмент RST. На стороне сервера соединение ставится в очередь в ожидании вызова функции
, и в это время сервер получает сегмент RST. Спустя некоторое время процесс сервера вызывает функциюaccept.acceptК сожалению, принцип обработки прерванного соединения зависит от реализации. Реализации, происходящие от Беркли, обрабатывают прерванное соединение полностью внутри ядра, и сервер никогда не узнает об этом. Большинство реализаций SVR4, однако, возвращают процессу ошибку, и эта ошибка зависит от реализации. При этом переменная errno принимает значение
(ошибка протокола), хотя в POSIX указано, что должна возвращаться ошибкаEPROTO(прерывание соединения). POSIX определяет эту ошибку иначе, так как ошибкаECONNABORTEDвозвращается еще и в том случае, когда в подсистеме потоков происходят какие-либо фатальные события, имеющие отношение к протоколу. Возвращение той же ошибки для нефатального прерывания установленного соединения клиентом приводит к тому, что сервер не знает, вызывать снова функциюEPROTOили нет. В случае ошибкиacceptсервер может игнорировать ошибку и снова вызывать функцию accept.ECONNABORTEDПРИМЕЧАНИЕЭтот сценарий очень просто имитировать. Запустите сервер, который должен вызвать функции 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 и покажем, какие проблемы они могут порождать совместно с функцией
и прослушиваемым сокетом в нормальном режиме блокирования.select5.12. Завершение процесса сервера
Теперь мы запустим соединение клиент-сервер и уничтожим дочерний процесс сервера. Это симулирует сбой процесса сервера, благодаря чему мы сможем выяснить, что происходит с клиентом в подобных ситуациях. (Следует точно различать сбой процесса сервера, который мы рассмотрим здесь, и сбой на самом узле сервера, о котором речь пойдет в разделе 5.14.) События развиваются так:
1. Мы запускаем сервер и клиент на разных узлах и вводим на стороне клиента одну строку, чтобы проверить, все ли в порядке. Строка отражается дочерним процессом сервера.
2. Мы находим идентификатор дочернего процесса сервера и уничтожаем его с помощью программы
. Одним из этапов завершения процесса является закрытие всех открытых дескрипторов в дочернем процессе. Это вызывает отправку сегмента FIN клиенту, и TCP клиента отвечает сегментом ACK. Это первая половина завершения соединения TCP.kill3. Родительскому процессу сервера посылается сигнал
, и он корректно обрабатывается (см. листинг 5.9).SIGCHLD4. С клиентом ничего не происходит. TCP клиента получает от TCP сервера сегмент FIN и отвечает сегментом ACK, но проблема состоит в том, что клиентский процесс блокирован в вызове функции
в ожидании строки от терминала.fgets5. Запуск программы
на этом шаге из другого окна на стороне клиента показывает состояние клиентского сокета:netstatlinux % <b>netstat -a | grep 9877</b>tcp 0 0 *:9877 *:* LISTENtcp 0 0 localhost:9877 localhost:9877 FIN_WAIT2tcp 1 0 localhost.43604 localhost:9877 CLOSE_WAITКак видите, согласно рис. 2.4, осуществилась половина последовательности завершения соединения TCP.
6. Мы можем снова ввести строку на стороне клиента. Вот что происходит на стороне клиента (начиная с шага 1):
linux % <b>tcpcli01 127.0.0.1</b> <i>запускаем клиент</i><b>hello</b> <i>первая строка, которую мы ввели</i>hello <i>она корректно отражается</i><i> теперь мы уничтожаем (</i>kill<i>) дочерний процесс</i><i> сервера на узле сервера</i><b>another line</b> <i>затем мы вводим следующую строку на стороне клиента</i>
