Библиотека сайта rus-linux.net
Интерфейс прикладного программирования Socket API, Часть 3: Параллельная обработка в серверах
Оригинал: "The Socket API, Part 3: Concurrent Servers"Автор: Pankaj Tanwar
Дата публикации: October 1, 2011
Перевод: Н.Ромоданов
Дата перевода: июль 2012 г.
Начало серии статей о Socket API
В этой части серии мы узнаем, как работать с несколькими клиентскими программами, подключенными к серверу.
Добро пожаловать за очередной дозой программирования сокетов! До сих пор мы создавали сервера, которые могут создавать соединения с несколькими клиентами (часть 1 и часть 2), но проблема в том, что сервер в любой момент времени будет общаться только с одной клиентской программой. Это связано с тем, что есть только один дескриптор сокета, cfd, создаваемый для общения с клиентской программой — и все соединения будут ожидать один и тот же дескриптор. Теперь, давайте воспользуемся системным вызовом fork() для того, чтобы можно создать копии сервера для каждой клиентской программы.
Вот код, взятый из предыдущей статьи. На этот раз для версии IPv4 ...
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <signal.h> int main() { int sfd, cfd; socklen_t len; char ch, buff[INET_ADDRSTRLEN]; struct sockaddr_in saddr, caddr; sfd= socket(AF_INET, SOCK_STREAM, 0); saddr.sin_family=AF_INET; saddr.sin_addr.s_addr=htonl(INADDR_ANY); saddr.sin_port=htons(1205); bind(sfd, (struct sockaddr *)&saddr, sizeof(saddr)); listen(sfd, 5); signal(SIGCHLD, SIG_IGN); while(1) { printf("Server waiting\n"); len=sizeof(caddr); cfd=accept(sfd, (struct sockaddr *)&caddr, &len); if( fork() == 0) { printf("Child Server Created Handling connection with %s\n", inet_ntop(AF_INET, &caddr.sin_addr, buff, sizeof(buff))); close(sfd); if(read(cfd, &ch, 1)<0) perror("read"); while( ch != EOF) { if((ch>='a' && ch<='z') || (ch>='A' && ch<='Z')) ch^=0x20; /* EXORing 6th bit will result in change in case */ if(write(cfd, &ch, 1)<0) perror("write"); if(read(cfd, &ch, 1)<0) perror("read"); } close(cfd); return 0; } close(cfd); } }
Давайте посмотрим, что мы здесь сделали. В строке 23, после получения дескриптора сокета cfd и после обращения к accept, мы создали копию сервера. Дочерний процесс (у которого pid==0), который закрывает дескриптор прослушивания с помощью операции close(sfd), выполняет работу, которую должен делать сервер, а когда закончит работу, он закроет дескриптор и возвратит управление (смотрите строки 27-39).
Сервер, с другой стороны, у которого pid>0, просто закрывает cfd (строка 39) и снова готов еще создавать соединения. Таким образом, для каждого поступающего соединения будет создаваться новый серверный процесс.
Другой способ сделать это состоит в использовании потоков, но мы не будем это делать прямо сейчас.
Теперь откомпилируем, запустим сервер и посмотрим, как он работает с несколькими клиентами. Смотрите рис.1 — сейчас я запускаю сервер в сети (;), а его клиентскими программами является VirtualBox, запускающий виртуальные машины в системах Backtrack (192.168.1.19) и Arch (192.168.1.4), а также телефон с системой Android, на котором для создания соединения TCP запущен пакет ConnectBot.
Рис.1: Экран с сообщениями сервера
Также запустим команду netstat -a | grep 1205 для того, чтобы проверить текущие сетевые подключения; я с помощью grep выбираю сообщения только с нашим портом 1205 для того, чтобы показать подключения только к нашему серверу (см. рис.2).
Рис.2: Данные, выдаваемые командой netstat
Мы также видим родительский процесс, слушающий (состояние LISTEN) и устанавливающий (состояние ESTABLISHED) соединения, с адресами IP и портами.
Мы добавили в код signal(SIGCHLD, SIG_IGN) для того, чтобы предотвратить превращение дочерних процессов в зомби процессы. Дочерний процесс, когда он завершается (нормальное завершение или завершение с помощью команды kill по какому-нибудь сигналу), возвращает в родительский процесс статус своего завершения, что происходит с помощью сигнала уведомления SIGCHLD, посылаемого системой. Если родительский процесс не обрабатывает этот сигнал, то дочерний процесс продолжает занимать часть памяти, и остается в состоянии зомби. Если родительский процесс завершается раньше дочернего процесса или не следит за состоянием дочерних процессов и не завершает их, то состояние будет обрабатываться самым главным корневым процессом, т. е. init с pid 1.
Давайте рассмотрим код, взятый со станицы man для команды signal():
#include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler);
С помощью команды signal() задается состояние сигнала signum для обработчика, которое будет либо SIG_IGN или SIG_DFL, либо адресом функции, определенной программистом ( "обработчик сигнала"). Здесь также указывается, что действия команды signal() различны в различных версиях Linux и UNIX и нам вместо этой команды рекомендуется пользоваться командой sigaction().
Проблема заключается в том, будет ли после запуска обработчика сигнала перезапущен заблокированный системный вызов или нет. Мы сможем разобраться с этим позже, когда мы напишем наш собственный обработчик сигналов - или просто посмотрим описание на страницах man для структуры sigaction.
В настоящий момент просто получаем сигнал и игнорируем его, устанавливая его состояние в SIG_IGN с помощью обработчика. Это не позволит дочернему процессу переходить в состояние зомби. Что делать, если родительский процесс заканчивается раньше дочернего процесса (хотя в данном случае это не так, поскольку сервер работает в бесконечном цикле)? В этом случае родительский процесс может ждать дочерний процесс с помощью вызовов wait() или waitpid(), причем вариант waitpid() будет более предпочтительным.
Эти системные вызовы ожидают изменения состояния в дочернем процессе, который может быть приостановлен или завершен по сигналу, либо возобновлен по сигналу (смотрите страницы man).
Немного теории
Теперь, прежде чем двигаться дальше, давайте поговорим здесь о протоколе TCP, поскольку понимание его основ необходимо для написания качественного кода.
На рис.3 прямоугольниками представлены состояния, а стрелками - переходы.
Рис.3: Диаграмма сотояний TCP
Мы должны представить себе состояния сервера и клиента в виде диаграммы. Из состояния CLOSED (ЗАКРЫТО) выходят две стрелки; одна из них помечена как Active Open (активное открытие). Этот переход происходит, когда клиентская программа посылает на сервер пакет SYN, а система инициирует соединение. Другим вариантом является стрелка Passive Open (пассивное открытие), когда сервер переходит в состояние прослушивания и ждет соединения.
Во-первых, после того, как клиентская программа пошлет сигнал SYN, она перейдет в состояние SYN_SENT (сигнал SYN послан). Затем после того, как сервер получит сигнал SYN и отправит сигналы SYN и ACK, он переходит в состояние ESTABLISHED (соединение установлено). Если из состояния ESTABLISHED (при котором происходит общение клиента и сервера), произойдет отправка сигнала FIN, то будет инициировано действие Active Close (активное закрытие соединения), которое завершит соединение и переведет сервер в состояние FIN_WAIT_1. Прием сигнала ACK переведет сервер в состояние FIN_WAIT_2.
Когда от сервера будет получен сигнал FIN, то это приведет в результате к отправке сигнала ACK и переходу в состояние TIME_WAIT. В этом состоянии система ожидает в течение времени, в двое большем, чем MSL (максимальное время жизни сегмента); значение, рекомендуемое в RFC 1337, равно 120 секундам, в Linux используется значение, равное 60 секундам. Это состояние помогает в случае, когда происходит потеря пакетов, надежно завершить соединение за счет того, что дает истечь времени, в течение которого в сети могут существовать старые дубликаты сегментов. Наконец, происходит переход в состояние CLOSED (закрыто).
На сервере пассивное открытие происходит в состоянии LISTEN (прослушивание). После того, как будет получен сигнал SYN, будут переданы сигналы SYN и ACK и произойдет переход в состояние SYN_RCVD (принят сигнал SYN). Прием сигнала ACK переведет сервер в состояние ESTABLISHED (соединение установлено), позволяющее обмениваться данными. Затем, когда будет получен сигнал FIN, сервер отправит сигнал ACK и проинициирует пассивное закрытие соединения и перейдет в состояние CLOSE_WAIT.
После того, как операция будет завершена, сервер пошлет сигнал FIN и перейдет в состояние LAST_ACK. При получении сигнала ACK, он завершит соединение и перейдет в состояние CLOSED (закрыто).
Здесь мы видим использование "ThreeHandshake" (тройного подтверждения) - обмен тремя пакетами для установления соединения TCP. Оно инициализируется вызовом connect(). Первым пакетом будет пакет SYN x, идущий от клиента к серверу, вторым пакетом будет пакет ACK x+1 и SYN y, идущий от сервера к клиенту, третьим пакетом будет пакет ACK y+1, идущий от клиента к серверу. Здесь х является порядковым номером со стороны клиента, а у - порядковым номером со стороны сервера.
Чтобы завершить соединение, нам нужно четыре пакета. Чтобы инициировать завершение, клиент вызывает функцию close(): первым пакетом является FIN, идущий от клиента к серверу, а вторым пакетом является ACK m+1, идущий от сервера к клиенту. Затем сервер завершает операцию, а затем вызывает функцию close() и передает FIN n. Клиент посылает ACK n+1 для завершения соединения.
Я закрываю соединение и завершаю статью. Следующий раз мы вернемся к программированию сокетов … вы можете послать сигнал ACK в ответ на мой сигнал FIN!
Продолжение серии статей о Socket API