Готовый перевод Michael J. Donahoo "TCP/IP Sockets in C, 2nd Edition" / Michael J. Donahoo "Программирование TCP/IP сокетов в Си, 2-ое издание": 2.2 IPv4 TCP сервер

Внимание! Этот перевод, возможно, ещё не готов, так как модераторы установили для него статус «идёт перевод»

IPv4 TCP Сервер.

Теперь мы обратим внимание на создание TCP сервера. Задача сервера заключается в создании точки для подключения и последующего ожидания подключения к ней клиента.

Выделим четыре основных шага для простой коммуникации с TCP сервером:

1. Создание TCP сокета с помощью socket().

2. Назначить номер порта сокету с помощью bind().

3. Сообщим системе разрешить подключения к порту используя listen().

4. Повторно (в цикле) выполнять следующее:

• Вызвать accept() для получения нового сокета, для каждого клиентского соединения.

• Связаться с клиентом посредством нового сокета используя send() и recv().

• Закрыть клиентское соединение с помощью close().

Создание сокета, отправление, получение данных и закрытие соединения происходят так же, как и у клиента.

Отличие в использовании сервером сокетов заключается в том что он связывает адрес с сокетом и потом использует сокет как способ получения других сокетов которые обеспечивают подключения клиентов. (Мы будем подробно останавливатся на этом в процессе комментирования последующего программного кода.)

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

TCPEchoServer4.c

1 #include
2 #include
3 #include
4 #include types.h>
5 #include socket.h>
6 #include in.h>
7 #include inet.h>
8 #include "Practical.h"
9
10 static const int MAXPENDING = 5; // Максимум исходящих запросов на соединение
11
12 int main(int argc, char *argv[]) {
18 Chapter 2: Basic TCP Sockets
13
14 if (argc != 2) // Проверка правильности количества аргументов
15 DieWithUserMessage("Parameter(s)", "");
16
17 in_port_t servPort = atoi(argv[1]); // Первый аргумент: локальный порт
18
19 // Создаем сокет для входящих подключений
20 int servSock; // Дескриптор сокета для сервера
21 if ((servSock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
22 DieWithSystemMessage("socket() failed");
23
24 // Создаем структуру локального адреса
25 struct sockaddr_in servAddr; // Локальный адрес
26 memset(&servAddr, 0, sizeof(servAddr)); // Обнуляем структуру
27 servAddr.sin_family = AF_INET; // семейство адресов IPv4
28 servAddr.sin_addr.s_addr = htonl(INADDR_ANY); // Любой входящий интерфейс
29 servAddr.sin_port = htons(servPort); // Локальный порт
30
31 // Связываем с локальным адресом
32 if (bind(servSock, (struct sockaddr*) &servAddr, sizeof(servAddr)) < 0)
33 DieWithSystemMessage("bind() failed");
34
35 // Маркируем сокет таким образом, чтобы он ожидал входящие соединения
36 if (listen(servSock, MAXPENDING) < 0)
37 DieWithSystemMessage("listen() failed");
38
39 for (;;) { // Бесконечный цикл
40 struct sockaddr_in clntAddr; // Адрес клиента
41 // Устанавливаем величину структуры адреса клиента (in-out параметр)
42 socklen_t clntAddrLen = sizeof(clntAddr);
43
44 // Ждем соединения с клиентом
45 int clntSock = accept(servSock, (struct sockaddr *) &clntAddr, &clntAddrLen);
46 if (clntSock < 0)
47 DieWithSystemMessage("accept() failed");
48
49 // clntSock: соединение с клиентом достигнуто!
50
51 char clntName[INET_ADDRSTRLEN]; // Строка, содержащая адрес клиента
52 if (inet_ntop(AF_INET, &clntAddr.sin_addr.s_addr, clntName,
53 sizeof(clntName)) != NULL)
54 printf("Handling client %s/%d\n", clntName, ntohs(clntAddr.sin_port));
55 else
56 puts("Unable to get client address");
57
58 HandleTCPClient(clntSock);
59 }
60 // NOT REACHED
61 }

TCPEchoServer4.c

1. Настройка программы и расшифровка параметров: строки 1-17

Мы конвертировали номер порта из строки в числовое значение с помощью функции atoi(); если первый аргумент не число, atoi() вернет 0, который приведет к ошибке позже, когда мы вызовем bind().

2. Создание сокета и настройка: строки 19-37

Создание TCP сокета: строки 20-22

Мы создаем потоковый сокет так же, как мы делали это для клиента.

Ввод желаемого адреса конечной точки: строки 25-29

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

Так как мы пишем для IPv4, мы используем для этого структуру sockaddr_in. Так как нам, вобщем, все равно, какой адрес является действующим (любой адрес работающий адрес сервера нам подойдет), то доверим его выбор системе, указав шаблон INADDR_ANY как желаемый интернет адрес.

(Как правило, это работает для любого сервера и избавляет сервер от необходимости определять реальный интернет адрес.)

Перед установкой адреса и номера порта в структуре sockaddr_in мы конвертируем их к сетевому байтовому представлению при помощи функций htonl() и htons(). (Смотрите пункт 5.1.2 для получения полного представления.)

Привязка сокета к установленному адресу и порту: строки 32-33

Как отмечалось выше, сокет сервера должен быть связан с локальным адресом и портом; функцией, которая выполняется это связывание, является bind().

Обратите внимание, что в то время как клиент должен указать адрес сервера в функции connect(), сервер должен указать свой собственный адрес в функции bind(). Именно эта часть информации (т.е. адрес и порт сервера), нужна для установки соединения; ни одному из них (ни серверу, ни клиенту) нет необходимости знать адрес клиента.

Имейте в виду, что функция bind() может не выполниться по различным причинам; одной из самых важных таких причин является то, что некоторый другой сокет уже связан с указанным портом (см. пункт 7.5).

Так что в некоторых системах нужны специальные права, чтобы привязать некоторый порт (обычно, с номером меньшим 1024).

Установка сокета на прослушивание: строки 36-37

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

3. Итеративная обработка входящих соединений: строки 39-59

Прием входящих подключений: строки 40-47

Как уже говорилось выше, TCP сокет, для которого вызвана функция listen(), используется иначе, чем это было в клиентском приложении.

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

В этот момент функция accept() возвращает указатель на новый сокет, который уже подключен к уже активному удаленному сокету.

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

Обратите внимание, что сокет, на который ссылается возвращенный дескриптор, уже подключен; среди всего прочего это означает, что он готов к передаче и приему данных. (Для получения дополнительной информации о том, что происходит в конкретной реализации смотри пункт 7.4.1 в главе 7.)

Сообщение о подключенном клиенте: строки 51-56

В этом месте текста clntAddr содержит адрес и номер порта присоединившегося клиента; мы предоставляем "номер звонившего" функции проверки и потом выводим информацию о клиенте.

Как и следовало ожидать, функция inet_ntop() является обратной функции inet_pton(), которую мы использовали на клиенте.

Она берет бинарное представление адреса клиента и преобразует его в четырехкомпонентную строку.

Поскольку операции с портами и адресами производятся в так называемом сетевом порядке байтов (пункт 5.1.2) мы должны преобразовать номер порта перед передачей его функции printf() (функция inet_pton() заботится об этом для адресов).

Обработчик эхо ответа для клиента: строка 58

HandleTCPClient() берет на себя "протокол приложения". Мы обсудим эту функцию ниже. Таким образом мы убираем из текущей функции часть функциональности сервера, связанную с выдачей "эха".

У нас есть отдельная функция, которая реализует "эхо" часть нашего сервера.

Хотя реализация этого протокола приложения содержит всего несколько строк, это хороший стиль программирования, отделить данную часть от остального серверного кода. Это способствует повторному использованию кода.

HandleTCPClient() получает данные с указанного сокета и посылает их обратно на тот же сокет, эта операция повторяется до тех пор, пока функция recv() возвращает положительное значение (положительное значение сигнализирует о том, что что-то было получено).

recv() блокируется до тех пор, пока что-нибудь будет получено от клиента или пока клиент закроет соединение. Когда клиент нормально закроет соединение, функция recv() вернет 0.

Функцию HandleTCPClient() можно найти в файле TCPServerUtility.c.

HandleTCPClient()

1 void HandleTCPClient(int clntSocket) {
2 char buffer[BUFSIZE]; // Буфер для эхо строки
3
4 // Получаем сообщение от клиента
5 ssize_t numBytesRcvd = recv(clntSocket, buffer, BUFSIZE, 0);
6 if (numBytesRcvd < 0)
7 DieWithSystemMessage("recv() failed");
8
9 // Посылаем полученную строку и получаем снова до тех пор, пока не будет достигнут конец потока символов
10 while (numBytesRcvd > 0) { // 0 означает, что достигнут конец потока
11 // Эхо сообщение обратно клиенту
12 ssize_t numBytesSent = send(clntSocket, buffer, numBytesRcvd, 0);
13 if (numBytesSent < 0)
14 DieWithSystemMessage("send() failed");
15 else if (numBytesSent != numBytesRcvd)
16 DieWithUserMessage("send()", "sent unexpected number of bytes");
17
18 // Смотрим, есть ли еще полученные данные
19 numBytesRcvd = recv(clntSocket, buffer, BUFSIZE, 0);
20 if (numBytesRcvd < 0)
21 DieWithSystemMessage("recv() failed");
22 }
23
24 close(clntSocket); // Закрываем сокет клиента
25 }

HandleTCPClient()

Предположим, мы скомпилируем TCPEchoServer4.c, DieWithMessage.c, TCPServerUtility.c и AddressUtility.c в исполняемую программу TCPEchoServer4 и запустим эту программу на компьютере с интернет (IPv4) адресом 169.1.1.1, портом 5000.

Предположим также, что мы запустим нашего клиента на компьютере с интернет адресом 169.1.1.2 и подключимся к серверу.

На севере будет выведена примерно следующая информация:

% TCPEchoServer4 5000
Handling client 169.1.1.2
В это же время на клиенте будет выведено следующее:
% TCPEchoClient4 169.1.1.1 "Echo this!" 5000
Received: Echo this!

Сервер связывает свой сокет с портом 5000 и ждет соединения от клиента.

Клиент соединяется с сервером, посылает сообщение “Echo this!” на сервер и получает эхо в ответ.

В этой команде мы должны писать TCPEchoClient с номером порта в командной строке, потому что клиент общается с нашим эхо сервером, а он работает с портом 5000 а не с обычным портом для этих целей номер 7.

Мы уже упоминали, что ключевым принципом написания сетевых приложений с помощью сокетов является принцип защитного программирования: ваш код не должен делать предположений о том, что будет получено по сети.

Что делать, если вы хотите "поиграть" с вашим TCP сервером, чтобы посмотреть его реакцию на различные варианты неправильного поведения клиента?

Вы можете написать клиент TCP, который посылает различные неправильные сообщения и печатает результаты, однако, это может быть достаточно утомительным и трудоемким.

Быстрой альтернативой является использование программы telnet, которая доступна на большинстве систем.

Это утилита командной строки, которая подключается к серверу, посылаем ему текст, который вы вводите, и выводит ответ.

Telnet требует указания двух параметров: сервера и порта. Например, для обращения по telnet к нашему эхо серверу нужно ввести следующую команду:

% telnet 169.1.1.1 5000

Теперь введите вашу строку для получения эха и telnet распечатает ответ сервера.

Поведение telnet может отличаться в зависимости от его реализации, поэтому вам может понадобиться изучить его поведение в вашей системе.

Теперь, когда мы изучили полноценный клиент и сервер, давайте посмотрим на отдельные функции, которые составляют Sockets API, немного более подробно.

Внимание! Этот перевод, возможно, ещё не готов.
Его статус: идёт перевод

Переведено на Нотабеноиде
http://notabenoid.org/book/29398/100186

Переводчики: victor8632, BioForceGun, prefer211

Настройки

Готово:

100.00% КП = 1.0