Сидни Фейт - TCP/IP Архитектура, протоколы, реализация (включая IP версии 6 и IP Security)
На рис. 21.2 демонстрируется последовательность вызовов в типичном сеансе TCP. Вызовы socket(), bind() и listen() обрабатываются очень быстро, и на них немедленно возвращается ответ.
Рис. 21.2. Последовательность программных вызовов в socket TCP
Вызовы accept(), send() и recv() предполагаются в режиме блокирования (что является их обычным значением по умолчанию). Вызов send блокируется и при переполнении выходного буфера TCP. Вызовы write() и read() можно использовать вместо send() и recv().
21.6 Серверная программа TCP
Рассмотрим подробно пример серверной программы. Сервер предназначен для непрерывной работы. Он будет выполнять следующие действия:
1. Запрашивать у socket создание главного TCB и возвращать значение дескриптора socket, который будет идентифицировать этот TCB в последующих вызовах.
2. Вводить локальный адрес сервера socket в структуру данных программы.
3. Запрашивать связывание, при котором в TCB копируется локальный адрес socket.
4. Создавать очередь, которая сможет хранить сведения о пяти клиентах. Оставшиеся шаги повторяются многократно:
5. Ожидать запросов от клиентов. Когда появляется клиент, создавать для него новый TCB на основе копии главного TCB и записи в него адреса socket клиента и других параметров.
6. Создавать дочерний процесс для обслуживания клиента. Дочерний процесс будет наследовать новый TCB и обрабатывать все дальнейшие операции по связи с клиентом
(ожидать сообщений от клиента, записывать их и завершать работу).
Каждый шаг в программе объясняется в следующем разделе.
/* tcpserv.c
* Для запуска программ ввести "tcpserv". */
/* Сначала включить набор стандартных заголовочных файлов. */
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
main() {
int sockMain, sockClient, length, child;
struct sockaddr_in servAddr;
/* 1. Создать главный блок управления пересылкой. */
if ((sockMain = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("Сервер не может открыть главный socket.");
exit(1);
}
/* 2. Создать структуру данных для хранения локальных IP-адресов
* и портов, которые будут использованы. Предполагается прием
* клиентских соединений от любых локальных IP-адресов
* (INADDR_ANY). Поскольку данный сервер не применяет
* общеизвестный порт, установить port = 0. Это позволит
* связать вызов с присвоением порта серверу и записать
* порт в TCB. */
bzero((char *)&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = 0;
/* 3. Связать запрос, выбор номера порта и
* запись его в TCB. */
if (bind(sockMain, &servAddr, sizeof(servAddr))) {
perror("Связывание сервера неудачно.");
exit(1);
}
/* Чтобы увидеть номер порта, следует использовать
* функцию getsockname(), чтобы скопировать порт в servAddr. */
length = sizeof(servAddr);
if (getsockname(sockMain, &servAddr, &length)) {
perror("Вызов getsockname неудачен.");
exit(1);
}
printf("СЕРВЕР: номер порта - %dn", ntohs(servAddr.sin_port));
/* 4. Создать очередь для хранения пяти клиентов. */
listen(sockMain, 5);
/* 5. Ожидать клиента. При разрешении возвратить новый
* дескриптор socket, который должен использоваться клиентом. */
for(;;) {
if ((sockClient = accept(sockMain, 0, 0)) < 0) {
perror ("Неверный socket для клиента.");
exit(1);
}
/* 6. Создать дочерний процесс для обслуживания клиента. */
if ((child = fork()) < 0) {
perror("Ошибка создания дочернего процесса.");
exit(1);
} else if (child == 0) /* Это код для исполнения дочернего процесса. */
{
close(sockMain); /* Дочерний процесс неинтересен для sockMain.*/
childWork(sockClient);
close(sockClient);
exit(0);
}
/* 7. Это родительский процесс. Его более не интересует
* socket клиента, поскольку его обслуживание передано
* дочернему процессу. Родительский процесс закрывает свой элемент для
* socket клиента и переходит на цикл приема новых accept(). */
close(sockClient);
}
}
/* Дочерний процесс читает один поступивший буфер, распечатывает
* сообщение и завершается. */
#define BUFLEN 81
int childWork(sockClient)
int sockClient;
{
char buf[BUFLEN];
int msgLength;
/* 8. Опустошить буфер. Затем вывести recv для получения сообщения от клиента. */
bzero(buf, BUFLEN);
if ((msgLength = recv(sockClient, buf, BUFLEN, 0)) < 0) {
perror("Плохое получение дочерним процессом.");
exit(1);
}
printf ("SERVER: Socket для клиента - %dn", sockClient);
printf ("SERVER: Длина сообщения - %dn", msgLength);
printf ("SERVER: Сообщение: %snn", buf);
}
21.6.1 Вызовы в серверной программе TCP
1. sockMain = socket (AF_INET, SOCK_STREAM, 0); Вызов socket имеет форму:
дескриптор_socket = socket(адрес_домена, тип_коммуникации, протокол)
Напомним, что интерфейс socket может использоваться для других видов коммуникаций, например XNS. AF_INET указывает на семейство адресов Интернета. SOCK_STREAM запрашивает socket TCP. Эта переменная должна иметь значение SOCK_DGRAM, чтобы создать socket UDP, a SOCK_RAW служит для непосредственного обращения к IP.
Не нужно явно определять никакую другую информацию протокола для TCP (или для UDP). Однако параметр protocol необходим для интерфейса с необработанными данными, а также для некоторых протоколов из других семейств, использующих socket.
2. struct sockaddr_in servAddr;
...
bzero((char *)&servAddr, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = 0;
Программная структура servAddr используется для хранения адресной информации сервера. Вызов bzero() инициализирует servAddr, помещая нули во все параметры. Первая переменная в структуре servAddr указывает, что остальная часть значений содержит данные семейства адресов Интернета.
Следующая переменная хранит локальный IP-адрес сервера. Например, если сервер подключен к локальной сети Ethernet и к сети X.25, может потребоваться ограничить доступ клиентов через интерфейс Ethernet. В данной программе об этом можно не беспокоится. INADDR_ANY означает, что клиенты могут соединяться через любой интерфейс.
Функция htonl() имеет полное название host-to-network-long. Она применяется для преобразования 32-разрядных целых чисел локального компьютера в формат Интернета для 32-разрядного адреса IP. Стандарты Интернета предполагают представление целых чисел с наиболее значимым байтом слева. Такой стиль именуется Big Endian (стиль "тупоконечников"). Некоторые компьютеры хранят данные, располагая слева менее значимые байты, т.е. в стиле Little Endian ("остроконечников"). Если локальный компьютер использует стиль Big Endian, htonl() не будет выполнять никакой работы.
Если сервер взаимодействует через общеизвестный порт, номер этого порта нужно записать в следующую переменную. Поскольку мы хотим, чтобы операционная система сама присвоила порт для нашей тестовой программы, мы вводим нулевое значение.
3. bind(sockMain, &servAddr, sizeof(servAddr)); getsockname(sockMain, &servAddr, &length);
Вызов bind имеет форму:
возвращаемый_код = bind(дескриптор_socket, адресная_структура, длина_адресной_структуры)
Если адресная структура идентифицирует нужный порт, bind попытается получить его на сервере. Если переменная порта имеет значение 0, bind получит один из неиспользованных портов. Функция bind позволяет ввести номер порта и IP-адрес в TCB. Вызов getsockname имеет форму:
возвращаемый_код = getsockname(дескриптор_socket, адресная_структура, длина_адресной_структуры)
Мы запросили у bind выделение порта, но эта функция не сообщает нам, какой именно порт был предоставлен. Для выяснения этого нужно прочитать соответствующие данные из TCB. Функция getsockname() извлекает информацию из TCB и копирует ее в адресную структуру, где можно будет прочитать эти сведения. Номер порта извлекается и выводится следующим оператором: