сетевое программирование в Unix. Часть 2. TCP

Характерная особенность сокетов в том, что их можно объяснять на пальцах часами. А потом слушатель все равно вынужден сам пройти по граблям. Или, может, я рассказчик фиговый?.. :)
...Есть в БГУИР такая кафедра — "Экономическая информатика". Дает весьма эклектичное образование. Многие примеры из этой части взяты в материалах одного курсового этой кафедры. Название его звучало как "Лабораторный практикум: сетевое программирование на С++". Убейте меня, не знаю откуда препод взял задания. Известно точно, что он абсолютный нуль в сетях, программировании и С++. Есть вероятность 99%, что стырил с сайта какого-то московского ВУЗа (граничные условия были — cygwin и gcc). Курсовой пришлось писать мне, и, как видите, этот труд не пропал даром. :)
Как уже упоминалось в предыдущей части, существует две модели передачи данных, основанных на сокетах. С установлением соединения и без оного.
Напоминаю, что мы работаем с сокетом как с файлом. Поэтому все socket-specific системные вызовы принимают в качестве первого параметра дескриптор файла.

простейший TCP-клиент.


Как ни странно — чаще всего в его роли выступает небезызвестный telnet. Утилита telnet есть во всех мне известных реализациях стека tcp/ip. Набираем telnet <host> <port> и наслаждаемся прямым общением с сервером. Благо классические TCP-based протоколы — текстовые.
Но мы изучаем все-таки программирование сокетов, поэтому обещанный пример с подробным разбором:

[tcpclient.cpp]
1 #include <sys/types.h>
2 #include <sys/socket.h>
3 #include <netinet/in.h>
4 #include <arpa/inet.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <stdio.h>
8 int main() {
9 struct sockaddr_in addr;
10 memset(&addr,0,sizeof(addr));
11 addr.sin_family=AF_INET;
12 addr.sin_port=htons(1212);
13 addr.sin_addr.s_addr=inet_addr("127.0.0.1");
14 int sock;
15 sock=socket(PF_INET,SOCK_STREAM,0);
16 connect(sock,(sockaddr *)&addr,sizeof(addr));
17 char buf[80+1];
18 memset(buf,0,81);
19 read(sock,buf,80);
20 puts(buf);
21 shutdown(sock,0);
22 close(sock);
23 return 0;
24 }

Эта программка соединяется с localhost, порт 1212, считывает последовательность байт и выводит ее на экран.
А теперь самое вкусное — обгладывание костей, любезно предоставленных нашим информационным спонсором. Строки 1-4: заголовочные файлы. Содержат объявления функций и типов данных для работы через сетевые сокеты. 5 и 7 строки — библиотечные функции Си. 6 строка — заголовочный файл стандартных системных вызовов.
9, 10, 11-13 строки — объявление, зачистка и заполнение структуры, содержащей адрес соединения. Обращаю особое внимание на то, как инициализируется порт и адрес назначения. Адрес и порт хранятся в особом формате, так называемый network byte order. Прямое присваивание обычно ничего не даст, потому что i386 и network byte order не совпадают. Макрос htons(3) занимается преобразованием числа в сетевой вид. Функция inet_addr(3) преобразует строковое значение IP-адреса в числовое. AF_INET из 11 строки — указание типа сокетов (сетевые в нашем случае).
И наконец, ключевой момент. 15 строка — создание сокета (TCP-сокета естественно) и 16 строка — соединение с сервером. В connect(2) используется указатель на адрес , потому что размер структуры с адресом может варьироваться в зависимости от типа сокета (сетевой или локальный).
Далее идет считывание (19 строка) и вывод на экран (20 строка) полученного.
21 и 22 строки — закрыть соединение и освободить файловый дескриптор, им использованный.

TCP-сервер

В качестве примера использована реализация сервиса daytime.
Суть протокола daytime: выдать время и закрыть соединение со стороны сервера.

[daytime.cpp]
1 #include <sys/types.h>
2 #include <sys/socket.h>
3 #include <netinet/in.h>
4 #include <arpa/inet.h>
5 #include <string.h>
6 #include <time.h>
7 #include <unistd.h>
8 #include <stdio.h>
9 char * daytime() {
10 time_t now;
11 now=time(NULL);
12 return ctime(&now);
13 }
14 int main() {
15 struct sockaddr_in addr;
16 memset(&addr,0,sizeof(addr));
17 addr.sin_family=AF_INET;
18 addr.sin_port=htons(1212);
19 addr.sin_addr.s_addr=INADDR_ANY;//inet_addr("127.0.0.1");
20 int sock, c_sock;
21 sock=socket(PF_INET,SOCK_STREAM,0);
22 bind(sock,(struct sockaddr *)&addr,sizeof(addr));
23 listen(sock,5);
24 for (;;) {
25 c_sock=accept(sock,NULL,NULL);
26 char buf[81];
27 memset(buf,0,81);
28 strncpy(buf,daytime(),80);
29 write(c_sock,buf,strlen(buf));
30 shutdown(c_sock,0);
31 close(c_sock);
32 puts("answer tcp");
33 }
34 return 0;
35 }


Пойдем по порядку. Сервер отличается от клиента режимом работы. Он должен:
a) занять адрес и порт (22 строка), bind(2);
b) включить прослушивание порта (23 строка), listen(2);
с) принимать и обрабатывать соединения (24-33 строки), accept(2).
Подчеркнем отличия от клиента: в 19 строке указывается специальная константа INADDR_ANY. Она обозначает, что соединения будут приниматься на всех интерфейсах. Можно задать и конкретный адрес (в стиле TCP-клиента). Так работают многие сервисы, которым можно задать адрес привязки в конфиге (apache, squid).
С 24 по 33 строку у нас бесконечный цикл. До первого Ctrl+C, естественно. Сервер до полной и окончательной победы занимается приемом соединений.
Очень интересная особенность:каждое входящее соединение порождает свой сокет (см. 25 строку). Контрольный сокет (sock) и клиентский сокет (c_sock) — разделены. Обмен данными идет через c_sock. Соединения принимаются на контрольный (sock).
Есть два важных следствия разделения на контрольный сокет и сокеты соединений:
1) упрощение работы программиста — нет необходимости отслеживать, кто и куда прислал данные, разные по сути понятия — выделены;
2) возможность распараллеливания обработки соединений (подробнее об этом расскажу позже).
В примере используется простая последовательная обработка. Пока предыдущее соединение не обработано — входящие соединения ждут своей очереди. Это имеет смысл только для простых случаев — когда время обработки запроса мало и задержка для вновь поступающих невелика.
9-13 строки — получение текущего времени. Эта функция используется в строке 28 для формирования результата.

заключение

В следующем разделе мы продолжим изучать "сокеты за 21 день" на примерах UDP-клиента и сервера.

mend0za.


обсуждение статьи


Сетевые решения. Статья была опубликована в номере 01 за 2004 год в рубрике программирование

©1999-2024 Сетевые решения