W. R. Stevens : Глава 4
Элементарные сокеты TCP
В этой главе будут описаны функции , необходимые для работоспособного клиента и сервера TCP.
Сам клиент и сервер будут созданы в следующей , 5-й главе.
Будут описаны параллельные серверы - когда множество клиентов соединяются с одним и тем же сервером.
При этом сервер каждый раз будет выполнять функцию fork , порождающую новый серверный процесс.
Типичный сценарий взаимодействия между клиентом и сервером :
TCP-сервер
------------
| socket() |
------------
|
------------
| bind() |
------------
|
------------
| listen() |
------------
|
------------
| accept() |
TCP-клиент ------------
------------ |
| socket() | Блокировка для соединения
------------ с клиентом
| |
------------ Установка соединения |
| connect()| <---------------------> |
------------ (3-этапное рукопожатие TCP) |
| |
------------ Данные(запрос) -------------
| write() | ---------------------------------------> | read() |
------------ -------------
| |
| |
------------ Данные (ответ) -------------
| read() | <------------------------------------------ | write() |
------------ -------------
| !
| !
------------ Уведомление о конце файла -------------
| close() | -------------------------------> | read() |
------------ -------------
|
-------------
| close() |
-------------
Функция socket
int socket(int family , int type , int protocol);
Константа family может принимать значения :
AF_INET
AF_INET6
AF_LOCAL
AF_ROUTE
AF_KEY
Константа type может принимать значения:
SOCK_STREAM
SOCK_DGRAM
SOCK_RAW
protocol обычно равен нулю , за исключением символьных сокетов
Не все сочетания family и type допустимы . Допустимые сочетания :
===========================================================================
AF_INET AF_INET6 AF_LOCAL AF_ROUTE AF_key
===========================================================================
SOCK_STREAM TCP TCP Да
SOCK_DGRAM UDP UDP Да
SOCK_RAW IPv6 Да Да
===========================================================================
При успешном выполнении функции socket() возвращается целое неотрицательное число - sockfd.
Функция connect
Функция используется клиентом.
int connect(int sockfd , const struct sockaddr *servaddr,socklen_t addrlen);
Возвращает либо 0 , либо -1.
В случае протокола TCP функция инициирует 3-этапное рукопожатие. Возможные ошибки :
ETIMEOUT
ECONNREFUSED
EHOSTUNREACH
ENETUNREACH
Функция bind
Функция связывает сокет с локальным адресом протокола.
Это комбинация 32-разрядного (IPv4) либо 128-разрядного (IPv6) адреса протокола
с 16-разрядным номером порта TCP либо UDP.
int bind(int sockfd , const struct sockaddr *myaddr,socklen_t addrlen);
Функция позволяет нам либо задать ip и порт , либо ничего не задавать.
Если мы зададим нулевой порт , то при вызове bind ядро само динамически выберет порт.
Функция listen
Функция вызывается на сервере и выполняет 2 действия :
1. Переводит сокет из состояния CLOSED в состояние LISTEN
2. 2-й аргумент этой функции задает максимальное число соединений , которое ядро может поместить
в очередь этого сокета.
int listen(int sockfd , int backlog);
Для сокета ядро может поддерживать 2 очереди :
1. Очередь не полностью установленных соединений.Эти сокеты находятся в состоянии SYN_RCVD
2. Очередь полностью установленных соединений. Эти сокеты находятся в состоянии ESTABLISHED.
Аргумент backlog задает максимальное значение для обоих очередей.Не нужно этому аргументу присваивать ноль,
лучше просто закрыть сокет. Этот параметр является одним из основных в серверах http ,
и он может быть явно больше 5 - например , 64.
Если очередь заполнена , то протокол TCP будет просто игнорировать вновь приходящий SYN.
Функция accept
Функция вызывается сервером TCP для возвращения следующего установленного соединения
из начала очереди полностью установленных соединений.
Если очередь пуста , процесс переходит в состояние ожидания
int accept(int sockfd , struct sockaddr *cliaddr,socklen_t addrlen);
Если функция возвращает положительное число , это означает , что ядро создало новый дескриптор.
Этот дескриптор будет использоваться конкретным клиентом.
Первый аргумент функции - прослушивающий сокет , существующий как правило в единственном экземпляре .
Затем ядро создает по одному присоединенному сокету для каждого клиентского соединения ,
принятого с помощью accept.
cliaddr - адрес протокола клиентского процесса.
addrlen - размер адреса.
Можно cliaddr и addrlen сделать пустыми указателями , если они нам не нужны.
В следующем листинге показано , как присоединенный сокет закрывается при каждом прохождении цикла ,
но прослушиваемый сокет остается открытым. Здесь 2-й и 3-й аргументы accept - пустые указатели ,
поскольку нам не нужна идентификация клиента.
// intro/daytimetcpsrv1.c
#include "unp.h"
#include < time.h>
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t len;
struct sockaddr_in servaddr, cliaddr;
char buff[MAXLINE];
time_t ticks;
listenfd = Socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(13); /* daytime server */
Bind(listenfd, (SA *) &servaddr, sizeof(servaddr));
Listen(listenfd, LISTENQ);
for ( ; ; ) {
len = sizeof(cliaddr);
connfd = Accept(listenfd, (SA *) &cliaddr, &len);
printf("connection from %s, port %d\n",
Inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff)),
ntohs(cliaddr.sin_port));
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
Write(connfd, buff, strlen(buff));
Close(connfd);
}
}
Мы вызываем здесь функцию inet_ntop для преобразования 32-битного IP-адреса
в строку ASCII (точечно-десятичную запись) , затем вызываем ntohs для преобразования сетевого порядка байтов
в порядок байтов узла.
Адрес IP-сервера - 127.0.0.1 - это т.н. loopback address , здесь и клиент , и сервер запускаются
на одной машине.Сервер должен обладать админскими правами.
Функции fork и exec
pid_t fork(void);
Функция fork возвращает : 0 в дочернем процессе , идентификатор дочернего процесса в родительском процессе.
В случае ошибки возвращает -1. Функция , вызываемая 1 раз , возвращает 2 значения :-)
Дочернему процессу возвращается ноль , а не id-шник родителя . потому что у дочернего процесса
всегда есть возможность узнать идентификатор родителя - функция getppid.
Все дескрипторы . открытые в родительском процессе , становятся доступны дочернему после fork.
Существует 2 типичных случая использования fork :
1. Процесс создает копию , чтобы та могла обработать одно задание - применяется серверами.
2. Запуск другой программы - после fork идет вызов функции exec .
Exec - пожалуй единственный способ запустить файл на выполнение.
Параллельные серверы
Сервер , представленный в предыдущем листинге , является последовательным.
Он связан с одним клиентом .
В следующем примере показан параллельный сервер , обслуживающий несколько клиентов одновременно.
pid_t pid;
int listenfd , connfd;
listenfd = Socket( ...);
Bind(listenfd , ...);
Listen(listenfd,LISTENQ);
for(;;) {
connfd = Accept(listenfd , ...);
if ((pid == Fork()) ==0) {
Close(listenfd);
doit(connfd);
Close(connfd);
exit(0);
}
Close(connfd);
}
Здесь после установки соединения форкается дочерний процесс , который обслуживает клиента ,
а родительский процесс ждет другое соединение. doit -условная функция для обслуживания клиента.
Вызов функции Close сразу после doit , кстати , необязателен , поскольку последующий exit все равно
всё прибьет . Стивенс тут выражается так - это , мол , уже дело вкуса программера.
А вот вызов Close после цикла обязателен , иначе TCP -соединение не закроется.
Функции getsockname и getpeername
Эти функции возвращают либо локальный , либо удаленный адрес сокета.
int getsockname(int sockfd, struct sockaddr *localadd, socklen_t *addrlen);
int getpeername(int sockfd, struct sockaddr *peeraddr, socklen_t *addrlen);
Функция getsockname нужна клиенту для получения IP-адреса и номера локального порта.
Если функция bind вызвана с номером порта 0 , то функция getpeername вернет нужный номер порта.
Сервер с помощью функции getsockname может получить локальный ip-адрес соединения.
|