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

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

2.1 IPv4 TCP клиент

Между клиентом и сервером существует важное отличие - на определенных этапах процесса передачи информации они по-разному используют интерфейс сокетов. Сначала мы рассмотрим клиент.

Его работа состоит в том, чтобы установить соединение с пассивно ожидающим контакта сервером.

Связь типичного TCP клиента, включает в себя четыре основных этапа:

1. Создание сокета TCP, используя socket().

2. Установление соединения с сервером, используя connect().

3. Прием и передача, используя send и recv().

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

TCPEchoClient4.c является реализацией TCP эхо клиента для IPv4.
TCPEchoClient4.c
1 #include
2 #include
3 #include
4 #include
5 #include types.h>
6 #include socket.h>
7 #include in.h>
8 #include inet.h>
9 #include "Practical.h"
10
11 int main(int argc, char *argv[]) {
12
13 if (argc<3||argc > 4) // Проверка корректности количества аргументов
14 DieWithUserMessage("Parameter(s)",
15 " []");
16
17 char *servIP = argv[1]; // Первый аргумент: IP адрес сервера (строка с четырьмя числами через точку)
18 char *echoString = argv[2]; // Второй аргумент: строка для эхо ответа
19
20 // Третий агумент (необязательный): порт сервера (числовое значение). 7 - хорошо известный эхо порт
21 in_port_t servPort = (argc == 4) ? atoi(argv[3]) : 7;
22
23 // Создаем достоверный, потоковый сокет, используя ТСР
24 int sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
25 if (sock < 0)
26 DieWithSystemMessage("socket() failed");
27
28 // Конструируем структуру адреса сервера
29 struct sockaddr_in servAddr; // Адрес сервера
30 memset(&servAddr, 0, sizeof(servAddr)); // Обнуляем структуру
31 servAddr.sin_family = AF_INET; // адрес IPv4 семейства
32 // Конвертируем адрес
2.1 IPv4 TCP Client 13
33 int rtnVal = inet_pton(AF_INET, servIP, &servAddr.sin_addr.s_addr);
34 if (rtnVal == 0)
35 DieWithUserMessage("inet_pton() failed", "invalid address string");
36 else if (rtnVal < 0)
37 DieWithSystemMessage("inet_pton() failed");
38 servAddr.sin_port = htons(servPort); // Порт сервера
39
40 // Устанавливаем соединение с эхо сервером
41 if (connect(sock, (struct sockaddr *) &servAddr, sizeof(servAddr)) < 0)
42 DieWithSystemMessage("connect() failed");
43
44 size_t echoStringLen = strlen(echoString); // Определяем длину ввода
45
46 // Посылаем строку на сервер
47 ssize_t numBytes = send(sock, echoString, echoStringLen, 0);
48 if (numBytes < 0)
49 DieWithSystemMessage("send() failed");
50 else if (numBytes != echoStringLen)
51 DieWithUserMessage("send()", "sent unexpected number of bytes");
52
53 // Получаем строку обратно от сервера
54 unsigned int totalBytesRcvd = 0; // Количество полученных байт всего
55 fputs("Received: ", stdout); // Устанавливаем вывод (печать) для эхо строки
56 while (totalBytesRcvd < echoStringLen) {
57 char buffer[BUFSIZE]; // I/O buffer
58 /* Получаем размер буфера (минус 1, чтобы оставить место для символа окончания строки) в байтах от отправителя */
60 numBytes = recv(sock, buffer, BUFSIZE - 1, 0);
61 if (numBytes < 0)
62 DieWithSystemMessage("recv() failed");
63 else if (numBytes == 0)
64 DieWithUserMessage("recv()", "connection closed prematurely");
65 totalBytesRcvd += numBytes; // Сохраняем результат расчета общего количества байт
66 buffer[numBytes] = '\0'; // Символ окончания строки!
67 fputs(buffer, stdout); // Выводим эхо буфер
68 }
69
70 fputc('\n', stdout); // Выводим завершающий перевод строки
71
72 close(sock);
73 exit(0);
74 }

TCPEchoClient4.c

TCPEchoClient4.c делает следующее:

1. Настройка приложения и разбор параметров: строки 1-21

· Включение файлов: строки 1-9

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

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

· Разбор символьных параметров и проверка на здравый смысл: строки 13-21

IPv4 адрес и текстовая строка для отправки эхо-сервису передаются в качестве первых двух параметров.

Третьим параметром клиенту можно сообщить порт сервера, но это необязательно.

Если порт не указывается, клиент использует общепринятый номер порта для эхо-сервиса - 7.

2. Создание TCP сокета: строки 23-26

Мы создаем сокет с помощью функции socket().

В качестве параметров при создании мы указываем, что создаем сокет для IPv4 (AF_INET), использующий потоковый протокол (SOCK_STREAM), называемый TCP (IPPROTO_TCP). В случае успешного выполнения функция socket() возвращает целочисленный дескриптор или "хэндл" для созданного сокета.

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

3. Подготовка адреса и установка соединения: строки 28-42

Подготовка структуры sockaddr_in для сохранения адреса сервера: строки 29-30

Для подключения к сокету, мы должны определить адрес и порт, к которому хотим подключиться.

Определенная нами структура sockaddr_in будет "контейнером" для этой информации.

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

Заполнение структуры sockaddr_in: строки 31-38

Мы должны установить адрес сети (AF_INET), интернет адрес, и номер порта.

Функция inet_pton() преобразует строковое представление интернет адреса сервера (передается как аргумент командной строки в формате четырехкомпонентной системы записи) в 32-битное бинарное представление.

Номер порта сервера был конвертирован из командной строки в бинарное представление ранее; вызов функции htons() (“host to network short”) гарантирует, что двоичное представление будет отформатировано в соответствии с требованиями API. (Необходимость этого описана в Главе 5.)

Подключение: строки 40-42

Функция connect() устанавливает соединение между нашим сокетом и сокетом, определённым с помощью адреса и порта внутри структуры sockaddr_in.

Поскольку Sockets API является общим, указатель на структуру адреса sockaddr_in (которая является специфичной для IPv4) должен быть преобразован к общему типу (sockaddr *). Кроме того, функции должен быть предоставлен фактический размер структуры с данными адреса.

4. Посылаем эхо строку на сервер: строки 44-51

Определяем длину посылаемой строки и сохраняем ее для последующего использования.

Указатель на эхо строку подставляется в вызов функции send(); сама строка была сохранена где-то (как аргумент командной строки) при запуске приложения.

Нам совершенно все равно, где находится строка, нужен только знать адрес первого байта строки и количество байт, которые нужно отправить. (Нужно отметить, что мы не будем посылать символ окончания строки ('\0'), который стоит в конце строки-аргумента, как и в конце любой другой строки в Си).

send() возвращает количество переданных байт, если функция отработала успешно, в противном случае будет возвращено -1.

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

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

Тем не менее, это хорошая идея включать в код проверку на ошибки, так как они могут возникнуть в некоторых контекстах.

5. Получение эхо ответа от сервера: строки 53-70

ТСР - это протокол потока байтов. Одним из подразумеваемых положений данного типа протокола является то, что границы функции send() являются размытыми.

Другими словами: байты посланные через вызов функции send() на одном конце соединения могут быть не все возвращены посредством одного вызова функции recv() на его другом конце. (Мы обсудим этот вопрос более подробно в главе 7.).

Так что мы должны получать байты в цикле до тех пор, пока не получим столько байт, сколько послали.

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

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

Получаем блок байт: строки 57-65

recv() принимает блоки байт пока доступны данные, возвращая количество байт, скопированных в буфер или -1 в случае неудачи.

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

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

Вывод (печать) буфера: строки 66-67

Мы выводим данные, посланные сервером, по мере их поступления.

Мы добавляем символ окончания строки ('\0') в конец каждой части полученных данных, так что их можно рассматривать как строку и для вывода воспользоваться функцией fputs().

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

Сервер может послать нечто совершенно другое (вплоть до размера строки) и все это будет записано в поток стандартного вывода.

Вывод перевода строки: строка 70

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

Закрытие подключения и выход: строки 72-73

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

Наше клиентское приложение (как и все программы в этой книге) используют две функции обработки ошибок:

DieWithUserMessage(const char *msg, const char *detail)
DieWithSystemMessage(const char *msg)

Обе функции выводят заданную пользователем строку сообщения (msg) в поток вывода ошибок, а затем еще строку детального сообщения; потом они вызывают функцию exit() с возвратом кода ошибки, что приводит к прерыванию работы приложения.

Единственное отличие между этими функциями состоит в выводе детального сообщения. Для функции DieWithUserMessage() детальное сообщение может быть задано пользователем.

Для функции DieWithSystemMessage() детальное сообщение формируется системой и базируется на значении специальной переменной errno (которая описывает причину последнего сбоя, если он связан с системным вызовом). Мы вызываем функцию DieWithSystemMessage() только если ошибочная ситуация является результатом системного вызова, который устанавливает errno.

Для сохранения простоты программного кода, наши примеры не содержат много кода, посвященного восстановлению после ошибок - он просто сводится к прерыванию работы программы. Рабочий код, естественно, не должен сдаваться так быстро.

Если же нам нужно предоставить пользователю информацию без выхода, мы используем функцию printf(), если нужны возможности форматирования, и fputs() в противном случае.

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

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

Это создает серьезные уязвимости в системе безопасности. Вместо этого используйте функцию fputs().

Примечание: функции DieWith…() объвлены в заголовочном файле “Practical.h”. Однако, фактическая реализация этих функций содержится в файле DieWithMessage.c, который должен быть откомпилирован и слинкован со всеми примерами приложений, содержащимися в этом тексте.

DieWithMessage.c
1 #include
2 #include
3
4 void DieWithUserMessage(const char *msg, const char *detail) {
5 fputs(msg, stderr);
6 fputs(": ", stderr);
7 fputs(detail, stderr);
8 fputc('\n', stderr);
9 exit(1);
10 }
11
12 void DieWithSystemMessage(const char *msg) {
13 perror(msg);
14 exit(1);
15 }
DieWithMessage.c

Если мы откомпилируем файлы TCPEchoClient4.c и DieWithMessage.c создадим программу TCPEchoClient4, мы сможем соединиться с эхо сервером, находящемуся по интернет адрусу 169.1.1.1, при помощи следующей командной строки:

% TCPEchoClient4 169.1.1.1 "Echo this!"
Received: Echo this!

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

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

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

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

Настройки

Готово:

100.00% КП = 1.0