Книга: Основы программирования в Linux

Множественные клиенты

До сих пор в этой главе вы видели, как применяются сокеты для реализации клиент-серверных систем, как локальных, так действующих, в сети. После установки соединения на базе сокетов они ведут себя как низкоуровневые открытые файловые дескрипторы и во многом как двунаправленные каналы.

Теперь необходимо рассмотреть случай множественных клиентов, одновременно подключающихся к серверу. Вы видели, что, когда серверная программа принимает от клиента запрос на соединение, создается новый сокет, а исходный сокет, ожидающий запросы на соединение, остается доступен для последующих запросов. Если сервер не сможет немедленно принять поступившие позже запросы на соединения, они сохранятся в очереди ожидания.

Тот факт, что исходный сокет все еще доступен, и что сокеты ведут себя как файловые дескрипторы, дает нам метод одновременного обслуживания многих клиентов. Если сервер вызовет функцию fork для создания своей второй копии, открытый сокет будет унаследован новым дочерним процессом. Далее он сможет обмениваться данными с подключившимся клиентом, в то время как основной сервер продолжит прием последующих запросов на соединение. В действительности в вашу программу сервера нужно внести очень простое изменение, показанное в упражнении 15.7.

Поскольку вы создаете дочерние процессы, но не ждете их завершения, следует сделать так, чтобы сервер игнорировал сигналы SIGCHLD, препятствуя возникновению процессов-зомби.

Упражнение 15.7. Сервер для многочисленных клиентов

1. Программа server4.c начинается так же, как последний рассмотренный сервер с важным добавлением директивы include для заголовочного файла signal.h. Переменные и процедуры создания и именования сокета остались прежними: 

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
 int server_sockfd, client_sockfd;
 int server_len, client_len;
 struct sockaddr_in server_address;
 struct sockaddr_in client_address;
 server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
 server_address.sin_family = AF_INET;
 server_address.sin_addr.s_addr = htonl(INADDR_ANY);
 server_address.sin_port = htons(9734);
 server_len = sizeof(server_address);
 bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

2. Создайте очередь соединений, игнорируйте подробности завершения дочернего процесса и ждите запросов клиентов:

 listen(server_sockfd, 5);
 signal(SIGCHLD, SIG_IGN);
 while(1) {
  char ch;
  printf("server waitingn");

3. Примите запрос на соединение:

  client_len = sizeof(client_address);
  client_sockfd = accept(server_sockfd,
   (struct_sockaddr*)&client_address, &client_len);

4. Вызовите fork с целью создания процесса для данного клиента и выполните проверку, чтобы определить, родитель вы или потомок:

  if (fork() == 0) {

5. Если вы потомок, то можете читать/писать в программе-клиенте на сокете client_sockfd. Пятисекундная задержка нужна для того, чтобы это продемонстрировать:

   read(client_sockfd, &ch, 1);
   sleep(5);
   ch++;
   write(client_sockfd, &ch, 1);
   close(client_sockfd);
   exit(0);
  }

6. В противном случае вы должны быть родителем и ваша работа с данным клиентом закончена:

  else {
   close(client_socket);
  }
 }
}

Код включает пятисекундную задержку при обработке запроса клиента для имитации вычислений сервера или обращения к базе данных. Если бы вы проделали это в предыдущем сервере, каждое выполнение программы client3 заняло бы пять секунд. С новым сервером вы сможете обрабатывать множественные клиентские программы client3 параллельно с общим затраченным временем, чуть превышающим пять секунд.

$ ./server4 &
[1] 26566 server waiting
$ ./client3 & ./client3 & ./client3 & ps x
[2] 26581
[3] 26582
[4] 26583
server waiting
server waiting
server waiting
PID   TTY   STAT TIME COMMAND
26566 pts/1 S    0:00 ./server4
26581 pts/1 S    0:00 ./client3
26582 pts/1 S    0:00 ./client3
26583 pts/1 S    0:00 ./client3
26584 pts/1 R+   0:00 ps x
26585 pts/1 S    0:00 ./server4
26586 pts/1 S    0:00 ./server4
26587 pts/1 S    0:00 ./server4
$ char from server = В
char from server = В
char from server = В
ps x
PID  TTY    STAT TIME COMMAND
26566 pts/1 S    0:00 ./server4
26590 pts/1 R+   0:00 ps x
[2] Done   ./client3
[3]- Done  ./client3
[4]+ Done  ./client3
$

Как это работает

Теперь серверная программа создает новый дочерний процесс для обработки каждого клиента, поэтому вы можете видеть несколько сообщений об ожидании сервера, поскольку основная программа продолжает ждать новые запросы на подключения. В выводе команды ps (отредактированном) показан главный процесс server4 с PID, равным 26 566, который ожидает новых клиентов, в то время, как три клиентских процесса client3 обслуживаются тремя потомками сервера. После пятисекундной паузы все клиенты получают свои результаты и завершаются. Дочерние серверные процессы тоже завершаются, оставляя только один главный серверный процесс.

Серверная программа применяет вызов fork для обработки множественных клиентов. В приложении для работы с базой данных это может быть не самым удачным решением, т.к. серверная программа может быть довольно большой, и, кроме того, существует проблема координации обращений к базе данных множественных копий сервера. На самом деле, все, что вам нужно, — это способ обработки множественных клиентов единственным сервером без блокировки и ожидания доставки клиентских запросов. Решение этой задачи включает одновременную обработку множественных открытых файловых дескрипторов и не ограничено только приложениями с применением сокетов. Рассмотрим функцию select.

Оглавление книги


Генерация: 0.062. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз