UNIX: разработка сетевых приложений - Уильям Стивенс
Шрифт:
Интервал:
Закладка:
Если аргумент from функции recvfrom является пустым указателем, то соответствующий аргумент длины (addrlen) также должен быть пустым указателем, и это означает, что нас не интересует адрес отправителя данных.
И функция recvfrom, и функция sendto могут использоваться с TCP, хотя обычно в этом нет необходимости.
8.3. Эхо-сервер UDP: функция main
Теперь мы переделаем нашу простую модель клиент-сервер из главы 5, используя UDP. Диаграмма вызовов функций в программах наших клиента и сервера UDP показана на рис. 8.1. На рис. 8.2 представлены используемые функции. В листинге 8.1[1] показана функция сервера main.
Рис. 8.2. Простая модель клиент-сервер, использующая UDP
Листинг 8.1. Эхо-сервер UDP
//udpcliserv/udpserv01.с
1 #include "unp.h"
2
3 intmain(int argc, char **argv)
4 {
5 int sockfd;
6 struct sockaddr_in servaddr, cliaddr;
7 sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
8 bzero(&servaddr, sizeof(servaddr));
9 servaddr.sin_family = AF_INET;
10 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
11 servaddr.sin_port = htons(SERV_PORT);
12 Bind(sockfd, (SA*)&servaddr, sizeof(servaddr));
13 dg_echo(sodkfd, (SA*)&cliaddr, sizeof(cliaddr));
14 }
Создание сокета UDP, связывание с заранее известным портом при помощи функции bind7-12 Мы создаем сокет UDP, задавая в качестве второго аргумента функции socket значение SOCK_DGRAM (сокет дейтаграмм в протоколе IPv4). Как и в примере сервера TCP, адрес IPv4 для функции bind задается как INADDR_ANY, а заранее известный номер порта сервера — это константа SERV_PORT из заголовка unp.h.
13 Затем вызывается функция dg_echo для обработки клиентского запроса сервером.
8.4. Эхо-сервер UDP: функция dg_echo
В листинге 8.2 показана функция dg_echo.
Листинг 8.2. Функция dg_echo: отражение строк на сокете дейтаграмм
//lib/dg_echo.c
1 #include "unp.h"
2 void
3 dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen)
4 {
5 int n;
6 socklen_t len;
7 char mesg[MAXLINE];
8 for (;;) {
9 len = clilen;
10 n = Recvfrom(sockfd, mesg, MAXLINE, 0, pcliaddr, &len);
11 Sendto(sockfd, mesg, n, 0, pcliaddr, len);
12 }
13 }
Чтение дейтаграммы, отражение отправителю8-12 Эта функция является простым циклом, в котором очередная дейтаграмма, приходящая на порт сервера, читается функцией recvfrom и с помощью функции sendto отправляется обратно.
Несмотря на простоту этой функции, нужно учесть ряд важных деталей. Во- первых, эта функция никогда не завершается. Поскольку UDP — это протокол, не ориентированный на установление соединения, в нем не существует никаких аналогов признака конца файла, используемого в TCP.
Во-вторых, эта функция позволяет создать последовательный сервер, а не параллельный, который мы получали в случае TCP. Поскольку нет вызова функции fork, один процесс сервера выполняет обработку всех клиентов. В общем случае большинство серверов TCP являются параллельными, а большинство серверов UDP — последовательными.
Для сокета на уровне UDP происходит неявная буферизация дейтаграмм в виде очереди. Действительно, у каждого сокета UDP имеется буфер приема, и каждая дейтаграмма, приходящая на этот сокет, помещается в его буфер приема. Когда процесс вызывает функцию recvfrom, очередная дейтаграмма из буфера возвращается процессу в порядке FIFO (First In, First Out — первым пришел, первым обслужен). Таким образом, если множество дейтаграмм приходит на сокет до того, как процесс может прочитать данные, уже установленные в очередь для сокета, то приходящие дейтаграммы просто добавляются в буфер приема сокета. Но этот буфер имеет ограниченный размер. Мы обсуждали этот размер и способы его увеличения с помощью параметра сокета SO_RCVBUF в разделе 7.5.
На рис. 8.3 показано обобщение нашей модели TCP клиент-сервер из главы 5, когда два клиента устанавливают соединения с сервером.
Рис. 8.3. Обобщение модели TCP клиент-сервер с двумя клиентами
Здесь имеется два присоединенных сокета, и каждый из присоединенных сокетов на узле сервера имеет свой собственный буфер приема. На рис. 8.4 показан случай, когда два клиента отправляют дейтаграммы серверу UDP.
Рис. 8.4. Обобщение модели UDP клиент-сервер с двумя клиентами
Существует только один процесс сервера, и у него имеется один сокет, на который сервер получает все приходящие дейтаграммы и с которого отправляет все ответы. У этого сокета имеется буфер приема, в который помещаются все приходящие дейтаграммы.
Функция main в листинге 8.1 является зависящей от протокола (она создает сокет семейства AF_INET, а затем выделяет и инициализирует структуру адреса сокета IPv4), но функция dg_echo от протокола не зависит. Причина, по которой функция dg_echo не зависит от протокола, заключается в том, что вызывающий процесс (в нашем случае функция main) должен разместить в памяти структуру адреса сокета корректного размера, и указатель на эту структуру вместе с ее размером передаются в качестве аргументов функции dg_echo. Функция dg_echo никогда не углубляется в эту структуру: она просто передает указатель на нее функциям recvfrom и sendto. Функция recvfrom заполняет эту структуру, вписывая в нее IP-адрес и номер порта клиента, и поскольку тот же указатель (pcliaddr) затем передается функции sendto в качестве адреса получателя, таким образом дейтаграмма отражается обратно клиенту, отправившему дейтаграмму.
8.5. Эхо-клиент UDP: функция main
Функция main клиента UDP показана в листинге 8.3.
Листинг 8.3. Эхо-клиент UDP
//udpcliserv/udpcli01.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: udpcli <Ipaddress>");
9 bzero(&servaddr, sizeof(servaddr));
10 servaddr.sin_family = AF_INET;
11 servaddr.sin_port = htons(SERV_PORT);
12 Inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
13 sockfd = Socket(AF_INET, SOCK_DGRAM, 0);
14 dg_cli(stdin, sockfd, (SA*)&servaddr, sizeof(servaddr));
15 exit(0);
16 }
Заполнение структуры адреса сокета адресом сервера9-12 Структура адреса сокета IPv4 заполняется IP-адресом и номером порта сервера. Эта структура будет передана функции dg_cli. Она определяет, куда отправлять дейтаграммы.
13-14 Создается сокет UDP и вызывается функция dg_cli.
8.6. Эхо-клиент UDP: функция dg_cli
В листинге 8.4 показана функция dg_cli, которая выполняет большую часть работы на стороне клиента.
Листинг 8.4. Функция dg_cli: цикл обработки клиента
//lib/dg_cli.c
1 #include "unp.h"
2 void
3 dg_cli(FILE *fp, int sockfd, const SA *pservaddr, socklen_t servlen)
4 {
5 int n;
6 char sendline[MAXLINE], recvline[MAXLINE + 1];
7 while (Fgets(sendline, MAXLINE, fp) != NULL) {
8 Sendto(sockfd, sendline, strlen(sendline), 0, pservaddr, servlen);
9 n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
10 recvline[n] = 0; /* завершающий нуль */
11 Fputs(recvline, stdout);
12 }
13 }
7-12 В цикле обработки на стороне клиента имеется четыре шага: чтение строки из стандартного потока ввода при помощи функции fgets, отправка строки серверу с помощью функции sendto, чтение отраженного ответа сервера с помощью функции recvfrom и помещение отраженной строки в стандартный поток вывода с помощью функции fputs.
Наш клиент не запрашивал у ядра присваивания динамически назначаемого порта своему сокету (тогда как для клиента TCP это имело место при вызове функции connect). В случае сокета UDP при первом вызове функции sendto ядро выбирает динамически назначаемый порт, если с этим сокетом еще не был связан никакой локальный порт. Как и в случае TCP, клиент может вызвать функцию bind явно, но это делается редко.
Обратите внимание, что при вызове функции recvfrom в качестве пятого и шестого аргументов задаются пустые указатели. Таким образом мы сообщаем ядру, что мы не заинтересованы в том, чтобы знать, кто отправил ответ. Существует риск, что любой процесс, находящийся как на том же узле, так и на любом другом, может отправить на IP-адрес и порт клиента дейтаграмму, которая будет прочитана клиентом, предполагающим, что это ответ сервера. Эту ситуацию мы рассмотрим в разделе 8.8.