UNIX: разработка сетевых приложений - Уильям Стивенс
Шрифт:
Интервал:
Закладка:
В листинге 1.5 показаны эти моменты. Присоединенный сокет закрывается при каждом прохождении цикла, но прослушиваемый сокет остается открытым в течение времени жизни сервера. Мы также видим, что второй и третий аргументы функции accept являются пустыми указателями, поскольку нам не нужно идентифицировать клиент.
Пример: аргументы типа «значение-результат»
В листинге 4.2 представлен измененный код из листинга 1.5 (вывод IP-адреса и номера порта клиента), обрабатывающий аргумент типа «значение-результат» функции accept.
Листинг 4.2. Сервер определения времени и даты, сообщающий IP-адрес и номер порта клиента
//intro/daytimetcpsrv1.c
1 #include "unp.h"
2 #include <time.h>
3 int
4 main(int argc, char **argv)
5 {
6 int listenfd, connfd;
7 socklen_t len;
8 struct sockaddr_in servaddr, cliaddr;
9 char buff[MAXLINE];
10 time_t ticks;
11 listenfd = Socket(AF_INET, SOCK_STREAM, 0);
12 bzero(&servaddr, sizeof(servaddr));
13 servaddr.sin_family = AF_INET;
14 servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
15 servaddr.sin_port = htons(13); /* сервер времени и даты */
16 Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));
17 Listen(listenfd, LISTENQ);
18 for (;;) {
19 len = sizeof(cliaddr);
20 connfd = Accept(listenfd, (SA*)&cliaddr, &len);
21 printf("connection from %s, port %dn",
22 Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff));
23 ntohs(cliaddr.sin_port));
24 ticks = time(NULL);
25 snprintf(buff, sizeof(buff), "% 24srn", ctime(&ticks));
26 Write(connfd, buff, strlen(buff));
27 Close(connfd);
28 }
29 }
Новые объявления7-8 Мы определяем две новых переменных: len, которая будет переменной типа «значение-результат», и cliaddr, которая будет содержать адрес протокола клиента.
Принятие соединения и вывод адреса клиента19-23 Мы инициализируем переменную len, присвоив ей значение, равное размеру структуры адреса сокета, и передаем указатель на структуру cliaddr и указатель на len в качестве второго и третьего аргументов функции accept. Мы вызываем функцию inet_ntop (см. раздел 3.7) для преобразования 32-битового IP-адреса в структуре адреса сокета в строку ASCII (точечно-десятичную запись), а затем вызываем функцию ntohs (см. раздел 3.4) для преобразования сетевого порядка байтов в 16-битовом номере порта в порядок байтов узла.
ПРИМЕЧАНИЕПри вызове функции sock_ntop вместо inet_ntop наш сервер станет меньше зависеть от протокола, однако он все равно зависит от IPv4. Мы покажем версию этого сервера, не зависящего от протокола, в листинге 11.7.
Если мы запустим наш новый сервер, а затем запустим клиент на том же узле, то дважды соединившись с сервером, мы получим от клиента следующий вывод:
solaris % daytimetcpcli 127.0.0.1
Thu Sep 11 12:44:00 2003
solaris % daytimetcpcli 192.168.1.20
Thu Sep 11 12:44:09 2003
Сначала мы задаем IP-адрес сервера как адрес закольцовки на себя (loopback address) (127.0.0.1), а затем как его собственный IP-адрес (192.168.1.20). Вот соответствующий вывод сервера:
solaris # daytimetcpsrv1
connection from 127.0.0.1, port 43388
connection from 192.168.1.20, port 43389
Обратите внимание на то, что происходит с IP-адресом клиента. Поскольку наш клиент времени и даты (см. листинг 1.1) не вызывает функцию bind, как сказано в разделе 4.4, ядро выбирает IP-адрес отправителя, основанный на используемом исходящем интерфейсе. В первом случае ядро задает IP-адрес равным адресу закольцовки, во втором случае — равным IP-адресу интерфейса Ethernet. Кроме того, мы видим, что динамически назначаемый порт, выбранный ядром Solaris, — это 33 188, а затем 33 189 (см. рис. 2.10).
Наконец, заметьте, что приглашение интерпретатора команд изменилось на знак # — это приглашение к вводу команды для привилегированного пользователя. Наш сервер должен обладать правами привилегированного пользователя, чтобы с помощью функции bind связать зарезервированный порт 13. Если у нас нет прав привилегированного пользователя, вызов функции bind оказывается неудачным:
solaris % daytimetcpsrv1
bind error: Permission denied
4.7. Функции fork и exec
Прежде чем рассматривать создание параллельного сервера (что мы сделаем в следующем разделе), необходимо описать функцию Unix fork. Эта функция является единственным способом создания нового процесса в Unix.
#include <unistd.h>
pid_t fork(void);
Возвращает: 0 в дочернем процессе, идентификатор дочернего процесса в родительском процессе, -1 в случае ошибки
Если вы никогда не встречались с этой функцией, трудным для понимания может оказаться то, что она вызывается один раз, а возвращает два значения. Одно значение эта функция возвращает в вызывающем процессе (который называется родительским процессом) — этим значением является идентификатор созданного процесса (который называется дочерним процессом). Второе значение (нуль) она возвращает в дочернем процессе. Следовательно, по возвращаемому значению можно определить, является ли данный процесс родительским или дочерним.
Причина того, что функция fork возвращает в дочернем процессе нуль, а не идентификатор родительского процесса, заключается в том, что у дочернего процесса есть только один родитель, и дочерний процесс всегда может получить идентификатор родительского, вызвав функцию getppid. У родителя же может быть любое количество дочерних процессов, и способа получить их идентификаторы не существует. Если родительскому процессу требуется отслеживать идентификаторы своих дочерних процессов, он должен записывать возвращаемые значения функции fork.
Все дескрипторы, открытые в родительском процессе перед вызовом функции fork, становятся доступными дочерним процессам. Вы увидите, как это свойство используется сетевыми серверами: родительский процесс вызывает функцию accept, а затем функцию fork. Затем присоединенный сокет совместно используется родительским и дочерним процессами. Обычно дочерний процесс использует присоединенный сокет для чтения и записи, а родительский процесс только закрывает присоединенный сокет.
Существует два типичных случая применения функции fork:
1. Процесс создает свои копии таким образом, что каждая из них может обрабатывать одно задание. Это типичная ситуация для сетевых серверов. Далее в тексте вы увидите множество подобных примеров.
2. Процесс хочет запустить другую программу. Поскольку единственный способ создать новый процесс — это вызвать функцию fork, процесс сначала вызывает функцию fork, чтобы создать свою копию, а затем одна из копий (обычно дочерний процесс) вызывает функцию exec (ее описание следует за описанием функции fork), чтобы заменить себя новой программой. Этот сценарий типичен для таких программ, как интерпретаторы командной строки.
Единственный способ запустить в Unix на выполнение какой-либо файл — вызвать функцию exec. (Мы будем часто использовать общее выражение «функция exec», когда неважно, какая из шести функций семейства exec вызывается.) Функция exec заменяет копию текущего процесса новым программным файлом, причем в новой программе обычно запускается функция main. Идентификатор процесса при этом не изменяется. Процесс, вызывающий функцию exec, мы будем называть вызывающим процессом, а выполняемую при этом программу — новой программой.
ПРИМЕЧАНИЕВ старых описаниях и книгах новая программа ошибочно называется «новым процессом». Это неверно, поскольку новый процесс не создается.
Различие между шестью функциями exec заключается в том, что они допускают различные способы задания аргументов:
■ выполняемый программный файл может быть задан или именем файла (filename), или полным именем (pathname);
■ аргументы новой программы либо перечисляются один за другим, либо на них имеется ссылка через массив указателей;
■ новой программе либо передается окружение вызывающего процесса, либо задается новое окружение.
#include <unistd.h>
int execl(const char *pathname, const char *arg0, ... /* (char*)0 */ );
int execv(const char *pathname, char *const argv[]);
int execle(const char *pathname, const char *arg0 ... /* (char*)0,
char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp[]);