KnigaRead.com/
KnigaRead.com » Компьютеры и Интернет » Программное обеспечение » Уильям Стивенс - UNIX: разработка сетевых приложений

Уильям Стивенс - UNIX: разработка сетевых приложений

На нашем сайте KnigaRead.com Вы можете абсолютно бесплатно читать книгу онлайн Уильям Стивенс, "UNIX: разработка сетевых приложений" бесплатно, без регистрации.
Перейти на страницу:

3. Предположим, что на параллельном сервере после вызова функции fork запускается дочерний процесс, который завершает обслуживание клиента перед тем, как результат выполнения функции fork возвращается родительскому процессу. Что происходит при этих двух вызовах функции close в листинге 4.3?

4. В листинге 4.2 сначала измените порт сервера с 13 на 9999 (так, чтобы для запуска программы вам не потребовались права привилегированного пользователя). Удалите вызов функции listen. Что происходит?

5. Продолжайте предыдущее упражнение. Удалите вызов функции bind, но оставьте вызов функции listen. Что происходит?

Глава 5

Пример TCP-соединения клиент-сервер

5.1. Введение

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

1. Клиент считывает строку текста из стандартного потока ввода и отправляет ее серверу.

2. Сервер считывает строку из сети и отсылает эту строку обратно клиенту.

3. Клиент считывает отраженную строку и помещает ее в свой стандартный поток вывода.

На рис. 5.1 изображена пара клиент-сервер вместе с функциями, используемыми для ввода и вывода.

Рис. 5.1. Простой эхо-клиент и эхо-сервер

Между клиентом и сервером мы показали две стрелки, но на самом деле это одно двустороннее соединение TCP. Функции fgets и fputs имеются в стандартной библиотеке ввода-вывода, а функции writen и readline приведены в разделе 3.9.

Мы разрабатываем нашу собственную реализацию эхо-сервера, однако большинство реализаций TCP/IP предоставляют готовый эхо-сервер, работающий как с TCP, так и с UDP (см. раздел 2.12). С нашим собственным клиентом мы также будем использовать и готовый сервер.

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

С помощью этого примера мы можем не только проанализировать запуск нашего клиента и сервера в нормальном режиме (ввести строку и посмотреть, как она отражается), но и исследовать множество «граничных условий»: выяснить, что происходит в момент запуска клиента и сервера; что происходит, когда клиент нормальным образом завершает работу; что происходит с клиентом, если процесс сервера завершается до завершения клиента или если возникает сбой на узле сервера, и т.д. Рассмотрев эти сценарии мы сможем понять, что происходит на уровне сети и как это представляется для API сокетов, и научиться писать приложения так, чтобы они умели обрабатывать подобные ситуации.

Во всех рассматриваемых далее примерах присутствуют зависящие от протоколов жестко заданные (hard coded) константы, такие как адреса и порты. Это обусловлено двумя причинами. Во-первых, нам необходимо точно понимать, что нужно хранить в структурах адресов, относящихся к конкретным протоколам. Во-вторых, мы еще не рассмотрели библиотечные функции, которые сделали бы наши программы более переносимыми. Эти функции рассматриваются в главе 11.

В последующих главах код клиента и сервера будет претерпевать многочисленные изменения, по мере того как вы будете больше узнавать о сетевом программировании (см. табл. 1.3 и 1.4).

5.2. Эхо-сервер TCP: функция main

Наши клиент и сервер TCP используют функции, показанные на рис. 4.1. Программа параллельного сервера представлена в листинге 5.1[1].

Листинг 5.1. Эхо-сервер TCP (улучшенный в листинге 5.9)

//tcpcliserv/tcpserv01.с

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int listenfd, connfd;

 6  pid_t childpid;

 7  socklen_t clilen;

 8  struct sockaddr_in cliaddr, servaddr;


 9  listenfd = Socket(AF_INET, SOCK_STREAM, 0);


10  bzero(&servaddr, sizeof(servaddr));

11  servaddr.sin_family = AF_INET;

12  servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

13  servaddr.sin_port = htons(SERV_PORT);


14  Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));


15  Listen(listenfd, LISTENQ);


16  for (;;) {

17   clilen = sizeof(cliaddr);

18   connfd = Accept(listenfd, (SA*)&cliadd, &clilen);


19   if ((childpid = Fork()) == 0) { /* дочерний процесс */

20    Close(listenfd); /* закрываем прослушиваемый сокет */

21    str_echo(connfd); /* обрабатываем запрос */

22    exit(0);

23   }

24   Close(connfd); /* родительский процесс закрывает

                     присоединенный сокет */

25  }

26 }

Создание сокета, связывание с известным портом сервера

9-15 Создается сокет TCP. В структуру адреса сокета Интернета записывается универсальный адрес (INADDR_ANY) и номер заранее известного порта сервера (SERV_PORT, который определен как 9877 в нашем заголовочном файле unp.h). В результате связывания с универсальным адресом системе сообщается, что мы примем соединение, предназначенное для любого локального интерфейса в том случае, если система имеет несколько сетевых интерфейсов. Наш выбор номера порта TCP основан на рис. 2.10. Он должен быть больше 1023 (нам не нужен зарезервированный порт), больше 5000 (чтобы не допустить конфликта с динамически назначаемыми портами, которые выделяются многими реализациями, происходящими от Беркли), меньше 49 152 (чтобы избежать конфликта с «правильным» диапазоном динамически назначаемых портов) и не должен конфликтовать ни с одним зарегистрированным портом. Сокет преобразуется в прослушиваемый при помощи функции listen.

Ожидание завершения клиентского соединения

17-18 Сервер блокируется в вызове функции accept, ожидая подключения клиента.

Параллельный сервер

19-24 Для каждого клиента функция fork порождает дочерний процесс, и дочерний процесс обслуживает запрос этого клиента. Как мы говорили в разделе 4.8, дочерний процесс закрывает прослушиваемый сокет, а родительский процесс закрывает присоединенный сокет. Затем дочерний процесс вызывает функцию str_echo (см. листинг 5.2) для обработки запроса клиента.

5.3. Эхо-сервер TCP: функция str_echo

Функция str_echo, показанная в листинге 5.2, выполняет серверную обработку запроса клиента: считывание строк от клиента и отражение их обратно клиенту.

Листинг 5.2. Функция str_echo: отраженные строки на сокете

//lib/str_echo.c

 1 #include "unp.h"


 2 void

 3 str_echo(int sockfd)

 4 {

 5  ssize_t n;

 6  char buf[MAXLINE];


 7  for (;;) {

 8   if ((n = read(sockfd, buf, MAXLINE)) > 0)

 9    return; /* соединение закрыто с другого конца */


10   Writen(sockfd, line, n);

11  }

12 }

Чтение строки и ее отражение

7-11 Функция read считывает очередную строку из сокета, после чего строка отражается обратно клиенту с помощью функции writen. Если клиент закрывает соединение (нормальный сценарий), то при получении клиентского сегмента FIN функция дочернего процесса read возвращает нуль. После этого происходит возврат из функции str_echo и далее завершается дочерний процесс, приведенный в листинге 5.1.

5.4. Эхо-клиент TCP: функция main

В листинге 5.3 показана функция main TCP-клиента.

Листинг 5.3. Эхо-клиент TCP

//tcpcliserv/tcpcli01.c

 1 #include "unp.h"


 2 int

 3 main(int argc, char **argv)

 4 {

 5  int sockfd;

 6  struct sockaddr_in servaddr;


 7  if (argc != 2)

 8   err_quit("usage: tcpcli <Ipaddress>");


 9  sockfd = Socket(AF_INET, SOCK_STREAM, 0);


10  bzero(&servaddr. sizeof(servaddr));

11  servaddr.sin_family = AF_INET;

12  servaddr.sin_port = htons(SERV_PORT);

13  Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);


14  Connect(sockfd, (SA*)&servaddr, sizeof(servaddr));


15  str_cli(stdin, sockfd); /* эта функция выполняет все необходимые

                               действия со стороны клиента */

16  exit(0);

17 }

Создание сокета, заполнение структуры его адреса

9-13 Создается сокет TCP и структура адреса сокета заполняется IP-адресом сервера и номером порта. IP-адрес сервера мы берем из командной строки, а известный номер порта сервера (SERV_PORT) — из нашего заголовочного файла unp.h.

Перейти на страницу:
Прокомментировать
Подтвердите что вы не робот:*