2 void
3 str_cli(FILE *fp, int sockfd)
4 {
5 pid_t pid;
6 char sendline[MAXLINE], recvline[MAXLINE];
7 if ((pid = Fork()) == 0) { /* дочерний процесс: сервер -> stdout */
8 while (Readline(sockfd, recvline, MAXLINE) > 0)
9 Fputs(recvline, stdout);
10 kill(getppid(), SIGTERM); /* в случае, если родительский процесс
все еще выполняется */
11 exit(0);
12 }
13 /* родитель: stdin -> сервер */
14 while (Fgets(sendline, MAXLINE, fp) != NULL)
15 Writen(sockfd, sendline, strlen(sendline));
16 Shutdown(sockfd, SHUT_WR); /* конец файла на stdin, посылаем FIN */
17 pause();
18 return;
19 }
Нам нужно снова вспомнить о последовательности завершения соединения. Обычное завершение происходит, когда в стандартном потоке ввода встречается конец файла. Родительский процесс считывает конец файла и вызывает функцию
shutdown
для отправки сегмента FIN. (Родительский процесс не может вызвать функцию
close
, см. упражнение 16.1.) Но когда это происходит, дочерний процесс должен продолжать копировать от сервера в стандартный поток вывода, пока он не получит признак конца файла на сокете.
Также возможно, что процесс сервера завершится преждевременно (см. раздел 5.12), и если это происходит, дочерний процесс считывает признак конца файла на сокете. В таком случае дочерний процесс должен сообщить родительскому, что нужно прекратить копирование из стандартного потока ввода в сокет (см. упражнение 16.2). В листинге 16.6 дочерний процесс отправляет родительскому процессу сигнал
SIGTERM
, в случае, если родительский процесс еще выполняется (см. упражнение 16.3). Другим способом обработки этой ситуации было бы завершение дочернего процесса, и если родительский процесс все еще выполнялся бы к этому моменту, он получил бы сигнал
SIGCHLD
.
Родительский процесс вызывает функцию
pause
, когда заканчивает копирование, что переводит его в состояние ожидания того момента, когда будет получен сигнал. Даже если родительский процесс не перехватывает никаких сигналов, он все равно переходит в состояние ожидания до получения сигнала
SIGTERM
от дочернего процесса. По умолчанию действие этого сигнала — завершение процесса, что вполне устраивает нас в этом примере. Родительский процесс ждет завершения дочернего процесса, чтобы измерить точное время для этой версии функции
str_cli
. Обычно дочерний процесс завершается после родительского, но поскольку мы измеряем время, используя команду оболочки
time
, измерение заканчивается, когда завершается родительский процесс.
Отметим простоту этой версии по сравнению с неблокируемым вводом-выводом, представленным ранее в этом разделе. Наша неблокируемая версия управляла четырьмя различными потоками ввода-вывода одновременно, и поскольку все четыре были неблокируемыми, нам пришлось иметь дело с частичным чтением и частичной записью для всех четырех потоков. Но в версии с функцией
fork
каждый процесс обрабатывает только два потока ввода-вывода, копируя из одного в другой. В применении неблокируемого ввода-вывода не возникает необходимости, поскольку если нет данных для чтения из потока ввода, то и в соответствующий поток вывода записывать нечего.
Сравнение времени выполнения различных версий функции str_cli
Итак, мы продемонстрировали четыре различных версии функции
str_cli
. Для каждой версии мы покажем время, которое потребовалось для ее выполнения, в том числе и для версии, использующей программные потоки (см. листинг 26.1). В каждом случае было скопировано 2000 строк от клиента Solaris к серверу с периодом RTT, равным 175 мс:
■ 354,0 с, режим остановки и ожидания (см. листинг 5.4);
■ 12,3 с, функция
select
и блокируемый ввод-вывод (см. листинг 6.2);
■ 6,9 с, неблокируемый ввод-вывод (см. листинг 16.1);
■ 8,7 с, функция
fork
(см. листинг 16.6);
■ 8,5 с, версия с потоками (см. листинг 26.1).
Наша версия с неблокируемым вводом-выводом почти вдвое быстрее версии, использующей блокируемый ввод-вывод с функцией
select
. Наша простая версия с применением функции
fork
медленнее версии с неблокируемым вводом- выводом. Тем не менее, учитывая сложность кода неблокируемого ввода-вывода по сравнению с кодом функции
fork
, мы рекомендуем более простой подход.
16.3. Неблокируемая функция connect
Когда сокет TCP устанавливается как неблокируемый, а затем вызывается функция
connect
, она немедленно возвращает ошибку
EINPROGRESS
, однако трехэтапное рукопожатие TCP продолжается. Далее мы с помощью функции
select
проверяем, успешно или нет завершилось установление соединения. Неблокируемая функция connect находит применение в трех случаях:
1. Трехэтапное рукопожатие может наложиться на какой-либо другой процесс. Для выполнения функции
connect
требуется один период обращения RTT (см. раздел 2.5), и это может занять от нескольких миллисекунд в локальной сети до сотен миллисекунд или нескольких секунд в глобальной сети. Это время мы можем провести с пользой, выполняя какой-либо другой процесс.
2. Мы можем установить множество соединений одновременно, используя эту технологию. Этот способ уже стал популярен в применении к веб-браузерам, и такой пример мы приводим в разделе 16.5.
3. Поскольку мы ждем завершения установления соединения с помощью функции
select
, мы можем задать предел времени для функции
select
, что позволит нам сократить тайм-аут для функции
connect
. Во многих реализациях тайм-аут функции connect лежит в пределах от 75 с до нескольких минут. Бывают случаи, когда приложению нужен более короткий тайм-аут, и одним из решений может стать использование неблокируемой функции
connect
. В разделе 14.2 рассматриваются другие способы помещения тайм-аута в операции с сокетами.
Как бы просто ни выглядела неблокируемая функция
connect
, есть ряд моментов, которые следует учитывать.
■ Даже если сокет является неблокируемым, то когда сервер, с которым мы соединяемся, находится на том же узле, обычно установление соединения происходит немедленно при вызове функции
connect
.