UNIX: разработка сетевых приложений
Поток copytoЭтот поток осуществляет копирование из стандартного потока ввода в сокет. Когда он считывает признак конца файла из стандартного потока ввода, на сокете вызывается функция16-25и отсылается сегмент FIN, после чего поток возвращает управление. При выполнении оператораshutdown(то есть когда функция, запустившая поток, возвращает управление) поток также завершается.returnВ конце раздела 16.2 мы привели результаты измерений времени выполнения для пяти различных реализаций функции
. Мы отметили, что многопоточная версия выполняется всего 8,5 с — немногим быстрее, чем версия, использующая функциюstr_cli(как мы и ожидали), но медленнее, чем версия с неблокируемым вводом-выводом. Тем не менее, сравнивая устройство версии с неблокируемым вводом-выводом (см. раздел 16.2) и версии с использованием потоков, мы заметили, что первая гораздо сложнее. Поэтому мы рекомендуем использовать именно версию с потоками, а не с неблокируемым вводом-выводом.fork26.4. Использование потоков в эхо-сервере TCP
Теперь мы перепишем эхо-сервер TCP, приведенный в листинге 5.1, используя для каждого клиента по одному потоку вместо одного процесса. Кроме того, с помощью нашей функции
мы сделаем эту версию не зависящей от протокола. В листинге 26.2 показан код сервера.tcp_listenЛистинг 26.2. Эхо-сервер TCP, использующий потоки
//threads/tcpserv01.с1 #include "unpthread.h"2 static void *doit(void*); /* каждый поток выполняет эту функцию */3 int4 main(int argc, char **argv)5 {6 int listenfd, connfd;7 pthread_t tid;8 socklen_t addrlen, len;9 struct sockaddr *cliaddr;10 if (argc == 2)11 listenfd = Tcp_listen(NULL, argv[1], &addrlen);12 else if (argc == 3)13 listenfd = Tcp_listen(argv[1], argv[2], &addrlen);14 else15 err_quit("usage: tcpserv01 [ <host> ] <service or port>");16 cliaddr = Malloc(addrlen);17 for (;;) {18 len = addrlen;19 connfd = Accept(listenfd, cliaddr, &len);20 Pthread_create(&tid, NULL, &doit, (void*)connfd);21 }22 }23 static void*24 doit(void *arg)25 {26 Pthread_detach(pthread_self());27 str_echo((int)arg); /* та же функция, что и раньше */28 Close((int)arg); /* мы закончили с присоединенным сокетом */29 return (NULL);30 }Создание потокаКогда функция17-21возвращает управление, мы вызываем функциюacceptвместо функцииpthread_create. Мы передаем функцииforkединственный аргумент — дескриптор присоединенного сокетаdoit.connfdПРИМЕЧАНИЕМы преобразуем целочисленный дескриптор сокета к универсальному указателю (void). В ANSI С не гарантируется, что такое преобразование будет выполнено корректно, — мы можем быть уверены лишь в том, что оно сработает в тех системах, в которых размер целого числа не превышает размера указателя. К счастью, большинство реализаций Unix обладают этим свойством (см. табл. 1.5). Далее мы поговорим об этом подробнее.
Функция потока23-30— это функция, выполняемая потоком. Поток отделяет себя с помощью функцииdoit, так как нет причины, по которой главному потоку имело бы смысл ждать завершения каждого созданного им потока. Функцияpthread_detachне изменилась и осталась такой же, как в листинге 5.2. Когда эта функция завершается, следует вызвать функциюstr_echoдля того, чтобы закрыть присоединенный сокет, поскольку этот поток использует все дескрипторы совместно с главным потоком. При использовании функцииcloseдочерний процесс не должен специально закрывать присоединенный сокет, так как при завершении дочернего процесса все открытые дескрипторы закрываются (см. упражнение 26.2).forkОбратите также внимание на то, что главный поток не закрывает присоединенный сокет, что всегда происходило, когда параллельный сервер вызывал функцию
. Это объясняется тем, что все потоки внутри процесса совместно используют все дескрипторы, поэтому если главному потоку потребуется вызвать функциюfork, это приведет к закрытию соединения. Создание нового потока не влияет на счетчики ссылок для открытых дескрипторов, в отличие от того, что происходит при вызове функцииclose.forkВ этой программе имеется одна неявная ошибка, о которой рассказывается в разделе 26.5. Можете ли вы ее обнаружить? (См. упражнение 26.5.)
Передача аргументов новым потокам
Мы уже упомянули, что в листинге 26.2 мы преобразуем целочисленную переменную
к указателю на неопределенный тип (connfd), но этот способ не работает в некоторых системах. Для корректной обработки данной ситуации требуются дополнительные усилия.voidВ первую очередь, заметим, что мы не можем просто передать адрес
нового потока, то есть следующий код не будет работать:connfdint main(int argc, char **argv) {int listenfd, connfd;...for (;;) {len = addrlen;