Linux. Полное руководство.
Глава 27. Создание сетевого приложения в Linux.
В главе 6 я ввел основные сетевые понятия, перечислил самые популярные протоколы и вкратце рассказал об основе сети Интернет — протоколе TCP/IP. В этой главе я собираюсь подробнее остановиться на протоколах TCP/IP и ICMP и показать, какие средства вы можете использовать для написания собственных приложений для работы в сети.
27.1. Протокол TCP/IP.
27.1.1. Многоуровневая архитектура стека TCP/IP.
Протокол TCP/IP был создан в конце 60-х — начале 70-х годов агентством DARPA Министерства Обороны США (U.S. Department of Defense Advanced Research Projects Agency). Основные этапы развития этого протокола отмечены в таблице 27.1.
Этапы развития протокола TCP/IP Таблица 27.1.
| Год | Событие |
|---|---|
| 1970 г. | Введен в использования протокол NCP (Network Control Protocol) для узлов сети Arpanet |
| 1972 г. | Вышла первая спецификация Telnet (см. RFC 318) |
| 1973 г. | Введен протокол FTP (RFC 454) |
| 1974 г. | Разработана программа TCP (Transmission Control Program) |
| 1981 г. | Опубликован стандарт протокола IP (RFC 791) |
| 1982 г. | Объединение протоколов TCP и IP в одно целое — TCP/IP |
| 1983 г. | Сеть Arpanet переведена на протокол TCP (ранее использовался протокол NCP) |
| 1904 г. | Введена доменная система имен DNS |
Как видите, все стандарты интернет-протоколов опубликованы в документах RFC. Документы RFC (Request for Comments) — это документы, в которых описывается устройство сети Интернет. Они создаются сообществом Интернет (Internet Society, ISOC). Любой член ISOC может опубликовать свой стандарт в документе RFC. Документы RFC делятся на пять типов:
1. Требуется (Required) — данный стандарт должен быть реализован на всех основных узлах TCP/IP.
2. Рекомендуется (Recommended) — обычно такие спецификации RFC также реализуются.
3. Выборочно (Elective) — реализация не обязательна.
4. Ограниченное использование (Limited use) — не рекомендуется для всеобщего применения.
5. Не рекомендуется (Not recommended) — не рекомендуются.
Протоколы семейства TCP/IP можно представить в виде модели, состоящей из четырех уровней: прикладного, основного, межсетевого и сетевого (таблица 27.2).
Уровни стека протоколов TCP/IP Таблица 27.2.
| Уровень 1 | Прикладной уровень (уровень приложения, Application Layer) |
| Уровень 2 | Основной (транспортный) уровень (Transport Layer) |
| Уровень 3 | Межсетевой уровень (уровень Internet, Internet Layer) |
| Уровень 4 | Уровень сетевых интерфейсов (Network Interface Layer) |
Каждый из этих уровней выполняет определенную задачу для организации надежной и производительной работы сети.
27.1.1.1. Уровень сетевого интерфейса.
Этот уровень лежит в основании всей модели протоколов семейства TCP/IP. Уровень сетевого интерфейса отвечает за отправку в сеть и прием из сети кадров, которые содержат информацию. Кадр (frame) — это единица данных, которыми обмениваются компьютеры в сети Ethernet. Для обозначения блоков данных определенных уровней используются термины кадр (frame), пакет (packet), дейтаграмма (datagram), сегмент (segment). Все эти термины обозначают транспортируемые отдельно блоки данных, и их можно считать синонимами. Название блока пересылаемых данных изменяется в зависимости от уровня (рис. 27.1).
Рис. 27.1. Пересылка блока данных в стеке протоколов TCP/IP.
27.1.1.2. Межсетевой уровень.
Протоколы Интернет инкапсулируют блоки данных в пакеты (дейтаграммы) и обеспечивают необходимую маршрутизацию. К основным интернет-протоколам относятся:
♦ IP (Internet Protocol) — предназначен для отправки и маршрутизации пакетов.
♦ ARP (Address Resolution Protocol) — используется для получения MAC-адресов (аппаратных адресов) сетевых адаптеров.
♦ ICMP (Internet Control Message Protocol) — предназначен для отправки извещений и сообщений об ошибках при передаче пакетов.
♦ IGMP (Internet Group Management Protocol) — используется узлами для сообщения маршрутизаторам, которые поддерживают групповую передачу, о своем участии в группах.
♦ RIP (Route Internet Protocol) и OSPF (Open Shortest Path First) — протоколы маршрутизации.
На этом уровне реализуется передача пакетов без установки соединения — дейтаграммным способом. Межсетевой уровень обеспечивает перемещение пакетов по сети с использованием наиболее рационального маршрута (протокол OSPF). Основная функция межсетевого уровня — передача пакетов через составную сеть, поэтому этот уровень также называется уровнем Интернет.
27.1.1.3. Транспортный (основной) уровень.
Этот уровень обеспечивает сеансы связи между компьютерами. Существует два транспортных протокола: TCP (Transmission Control Protocol) и UDP (User Datagram Protocol). Протокол TCP ориентирован на установление соединения, то есть перед передачей данных компьютеры «договариваются» между собой. Обычно по этому протоколу передаются большие объемы данных или данные, для которых требуется подтверждение их приема. Этот протокол используется большинством сетевых приложений, так как обеспечивает достаточную надежность при передаче данных.
Протокол UDP не ориентирован на соединение и не гарантирует доставку пакетов (дейтаграмм), зато он работает быстрее TCP. Обычно по этому протоколу передаются небольшие объемы данных. Ответственность за доставку данных несет сетевая программа.
27.1.1.4. Уровень приложений.
Этот уровень является вершиной модели TCP/IP. На этом уровне работают практически все распространенные утилиты и службы: DNS, Telnet, WWW. Gopher, WAIS. SNMP, FIT, TFTP, SMTP, POP, IMAP.
Таблица 27.3 показывает соответствие уровней стека протокола TCP/IP семиуровневой модели OSI.
Соответствие уровней стека TCP/IP модели OSI Таблица 27.3.
| Уровень модели OSI | Протокол | Уровень стека TCP/IP |
|---|---|---|
| 7, 6 | WWW (HTTP), FTP, TFTP, SMTP, POP, telnet, WAIS, SNMP | 1 |
| 5, 4 | TCP, UDP | 2 |
| 3 | IP, ICMP, RIP, OSPF, ARP | 3 |
| 2, 1 | Ethernet, PPP, SLIP | 4 |
27.1.2. Структура пакетов IP и TCP.
Протокол IP не ориентирован на соединение, поэтому не обеспечивает надежную доставку данных. Поля, описание которых приведено в таблице 27.4, представляют собой IP-заголовок и добавляются к пакету при его получении с транспортного уровня.
Структура заголовка IP-пакета Таблица 27.4.
| Поле | Описание |
|---|---|
| Source IP-address | IP-адрес отправителя пакета |
| Destination IP-address | IP-адрес получателя пакета |
| Protocol | Протокол: TCP или UDP |
| Checksum | Контрольная сумма для проверки целостности пакета |
| TTL (Time to Live) | Время жизни пакета: определяет, сколько секунд дейтаграмма может находиться в сети. Предотвращает бесконечное блуждание пакетов в сети. Значение TTL автоматически уменьшается на одну или более секунд при проходе через каждый маршрутизатор сети |
| Version | Версии протокола IP — 4 или 6 (4 бита) |
| Header Length | Длина заголовка пакета (4 бита). Минимальный размер заголовка — 20 байтов |
| Type of Service () | Тип обслуживания; обозначение требуемого для этого пакета качества обслуживания при доставке через маршрутизаторы IP-сети. Здесь определяются приоритет, задержки, пропускная способность (8 битов) |
| Total Length | Длина дейтаграммы IP-протокола (16 битов) |
| Identification | Идентификатор пакета. Если пакет фрагментирован (разбит на части), то все фрагменты имеют одинаковый идентификатор (15 битов) |
| Fragmentation Rags | 3 бита для флагов фрагментации и 2 бита для текущего использования |
| Fragmentation Offset | Смещение фрагмента: указывает на положение фрагментов относительно начала поля данных IP-пакета. Если фрагментации нет, смещение равно 0x0 (13 битов) |
| Options and Padding | Опции |
Протокол TCP в отличие от протокола IP ориентирован на установление соединения и обеспечивает надежную доставку данных. Структура TCP-пакета описана в таблице 27.5.
Структура заголовка TCP-пакета Таблица 27.5.
| Поле | Описание |
|---|---|
| Source port | Порт TCP узла-отправителя |
| Destination Port | Порт TCP узла-получателя |
| Sequence Number | Номер последовательности пакетов |
| Acknowledgement Number | Номер подтверждения: порядковый номер байта, который локальный узел рассчитывает получить следующим |
| Data Length | Длина TCP-пакета |
| Reserved | Зарезервировано для будущего использования |
| Hags | Флаги: описание содержимого сегмента |
| Window | Показывает доступное место в окне протокола TCP |
| Checksum | Контрольная сумма для проверки целостности пакета |
| Urgent Pointer () | Указатель срочности: при отправке срочных данных (поле Flags) в этом поле задается граница области срочных данных |
27.2 Протокол ICMP.
27.2.1. Для чего используется протокол ICMP.
Протокол межсетевых управляющих сообщений используется для диагностических целей. Например, система А передала системе Б неверный пакет. Система Б с помощью протокола ICMP может «сказать» системе А, что посланный ею пакет некорректен. Также по этому протоколу производится диагностика сети. Многие из вас используют утилиту ping, чтобы определить, доходят ли отправленные вами пакеты до какой-то определенной машины, даже не подозревая о том, что ping использует как раз протокол ICMP.
С помощью протокола ICMP можно определить не только тип неисправности, но и максимально уточнить ее причину. Например, тип сообщения 3 («Адресат недостижим») свидетельствует о том, что посланный нам пакет не может «добраться» до адресата. Кроме типа сообщения, возвращается его код, позволяющий детализировать неисправность. Например, код 0 означает, что сеть недоступна, 1 — узел недоступен. 2 — протокол недоступен и т.д. В таблице 27.8 ниже перечислены типы и коды сообщений ICMP.
Обязательно ли посылать сообщение об ошибке, получив некорректный пакет? Нет, из главы 19 вы узнали, что такие пакеты (например, отправленные на запрещенный для доступа извне порт) можно просто игнорировать. Первый подход «вежливее», второй безопаснее. Представьте, что злоумышленник отправляет с 1000 машин 1000 пакетов с указанием неверного порта на вашу систему. Тогда ваша система только тем и будет заниматься, что отправлять сообщения об ошибках. Это называется атакой на отказ. Конечно, современные брандмауэры позволяют избавиться от этого неприятного явления, но не у всех они установлены и настроены должным образом.
27.2.2. Структура ICMP-пакета.
Для лучшей диагностики ошибки вместе с ICMP-пакетом передается заголовок исходного пакета, вызвавшего ошибку. Также передается специальный указатель на позицию заголовка, позволяющий определить, что именно вызвала ошибку. В случае с неправильным номером порта указатель будет установлен на поле, содержащее номер порта. Благодаря этому система-источник может вычислить процесс и сокет, вызвавшие ошибку.
Структуры различных ICMP-пакетов отличаются друг от друга в зависимости от типа пакета. Например, пакет, сообщающий о недоступности адресата (Destination Unreachable Message), выглядит так:
Сообщение Destination Unreachable Message Таблица 27.6.
| Тип | Код | Контрольная сумма |
|---|---|---|
| Не используется | ||
| Интернет-заголовок плюс первые 64 бита оригинального сообщения (пакета) |
Что такое тип и код, вы уже знаете, с контрольной суммой тоже все ясно. А последнее поле необходимо, поскольку оно поможет идентифицировать процесс, вызвавший ошибку.
Точно такую же структуру имеют сообщение об истечении лимита времени (Time Exceeded Message) и сообщение об обрыве источника Source Quench Message (тип 4).
А вот сообщение о неверном параметре (например, указан неверный номер порта) выглядит уже по-другому (таблица 27.7).
Сообщение Parameter Problem Message Таблица 27.7.
| Тип | Код | Контрольная сумма |
|---|---|---|
| Указатель | Не используется | |
| Интернет-заголовок плюс первые 64 бита оригинального сообщения (пакета) |
Поле Указатель в сообщении о неверном параметре (Parameter Problem Message) указывает на то место в заголовке, которое вызвало ошибку.
Сообщение о переадресации (Redirect Message) имеет следующую структуру:
Сообщение Redirect Message Таблица 27.8.
| Тип | Код | Контрольная сумма |
|---|---|---|
| Адрес шлюза | ||
| Интернет-заголовок плюс первые 64 бита оригинального сообщений (пакета) |
Чтобы понять, что такое сообщение о переадресации, рассмотрим следующий пример. Система Б определяет, что посланный системой А пакет некорректен. Системе Б нужно отправить системе А сообщение об ошибке. Система Б определяет, что единственным маршрутом назад для данного пакета является маршрут через систему А. Тогда система Б посылает системе А два пакета: первый с сообщением о некорректном пакете, а второе — сообщение переадресации, докладывающее, что у системы А проблемы с таблицей маршрутизации, которая, возможно, содержит ошибку.
Сообщения типа эхо-запрос (ping, тип 8) и эхо-ответ (pong, тип 0) имеют следующую структуру:
Сообщения Echo или Echo Reply Message Таблица 27.9.
| Тип | Код | Контрольная сумма |
|---|---|---|
| Идентификатор | Последовательность | |
| Данные |
Поля Идентификатор и Последовательность могут использоваться источником эха для передачи вспомогательной информации. Например, идентификатор может использоваться как порт при использовании протоколов TCP/UDP для идентификации службы, а номер последовательности может увеличивается на единицу при отправке каждого запроса (то есть выступать в роли счетчика).
27.2.3. Тип и код ICMP-сообщения.
В следующей таблице перечислены все типы ICMP-сообшений. Об их структуре вы можете прочитать в документе RFC 792. Типы 17 и 18 описаны в документе RFC 950.
Типы IСМР-сообщений Таблица 27.8.
| Тип | Код | Название сообщения | Описание |
|---|---|---|---|
| 0 | Echo Reply Message | Эхо-ответ | |
| 0 | Код всегда равен 0 | ||
| 3 | Destination Unreachable Message | Адресат недоступен | |
| 0 | Сеть недоступна | ||
| 1 | Узел недоступен — что-то случилось с компьютером возможно, он просто выключен | ||
| 2 | Протокол недоступен — запрашиваемый протокол ни поддерживается | ||
| 3 | Порт недоступен — на машине ни одна служба не связана с указанным номером порта | ||
| 4 | Длина пакета слишком велика, а в его заголовке установлен флаг DF (Don't Fragment), то есть не фрагментировать. Для передачи большого пакета его нужно фрагментировать (разбить на части), а так как установлен флаг DF, фрагментация, а следовательно, и передача пакета невозможна | ||
| 5 | Ошибочный маршрут источника | ||
| 4 | Source Quench Message | Обрыв источника | |
| 0 | Код всегда равен 0 | ||
| 5 | Redirect Message | Переадресация | |
| 0 | Переадресация пакетов для сети | ||
| 1 | Переадресация пакетов для узла | ||
| 2 | Переадресация пакетов для сети и типа обслуживания (TOS, Type Of Service) | ||
| 3 | Переадресация пакетов для узла и типа обслуживания (TOS, Type Of Service) | ||
| 8 | Echo Message | Эхо-запрос | |
| 0 | Код всегда равен 0 | ||
| 11 | Time Exceeded Message | Превышен лимит времени | |
| 0 | При передаче превышено «время жизни» (TTL, Time То Live) | ||
| 1 | Превышено время реассемблирования (сборки) фрагментов | ||
| 12 | Parameter Problem Message | Ошибочный параметр | |
| 0 | Указатель на ошибочный параметр (табл. 27.7) | ||
| 13 | Time stamp Message | Запрос временной метки | |
| 0 | Код всегда равен 0 | ||
| 14 | Timestamp Reply Message | Ответ о временной метке | |
| 0 | Код всегда равен 0 | ||
| 15 | Information Request | Информационный запрос (запрашивается номер нашей сети) | |
| 0 | Код всегда равен 0 | ||
| 16 | Information Reply Message | Информационный ответ (возвращается номер кашей сети) | |
| 0 | Код всегда равен 0 | ||
| 17 (*) | Information Request | Информационный запрос (запрашивается маска адреса) | |
| 0 | Код всегда равен 0 | ||
| 18 (*) | Information Reply Message | Информационный ответ (возвращается маска адреса) | |
| 0 | Код всегда равен 0 |
27.2.4. Функции для работы с протоколом ICMP.
Для работы с протоколом ICMP существует 12 основных функций. Все эти функции описаны в файле /usr/src/linux/net/ipv4/icmp.c. У вас нет этого файла? Тогда установите исходники ядра (странно, почему вы до сих пор этого не сделали).
♦ icmp_address() — отправка ответа на запрос о маске адреса;
♦ icmp_discard() — удаляет ICMP-пакет;
♦ icmp_echo() — эхо-запрос;
♦ icmp_init() — инициализирует служебные подпрограммы протокола ICMP в операционной системе;
♦ icmp_out_count() — увеличивает счетчик отправленных пакетов;
♦ icmp_rcv() — прием ICMP-пакета;
♦ icmp_redirect() — отправка сообщения переадресации;
♦ icmp_send() — отправка ICMP-сообщения;
♦ icmp_timestamp() — ответ на запрос о времени создания;
♦ icmp_unreach() — отправляет сообщение об ошибке;
♦ xrlim_allow() — решает, отправлять ли ICMP-пакет или нет;
♦ xrlim_init() — ограничение скорости передачи ICMP-пакетов (в версии ядра 2.0).
27.2.4.1. Технические подробности.
Прежде чем перейти к рассмотрению функций ICMP, разберемся, как же ICMP-пакеты принимаются операционной системой. Собственно, ICMP-пакет принимается операционной системой Linux так же, как и любой другой пакет. Драйвер сетевой платы (или другого сетевого устройства) собирает полный пакет данных, затем он строит структуру sk_buff.
Листинг 27.1. Структура sk_buff.
Struct sk_buff {
/* Эти два члена должны быть первыми */
struct sk_buff* next; /* Следующий буфер в списке*/
struct sk_buff* prev;/* Предыдущий буфер в списке*/
struct sk_buff_head * list; /* "Голова" списка */
struct sock *sk; /* Сокет */
struct timeval stamp; /* Время прибытия пакета */
struct net_device *dev; /* Сетевое устройство */
/* Заголовок транспортного уровня */
union {
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr* igmph;
struct iphdr *ipiph;
struct spxhdr *spxh;
unsigned char *raw;
} h;
/* Заголовок сетевого уровня */
union {
struct iphdr *iph;
struct ipv6hdr* ipv6h;
struct arphdr *arph;
struct ipxhdr *ipxh;
unsigned char *raw;
} nh;
union {
struct ethhdr *ethernet;
unsigned char *raw;
} mac;
struct dst_entry *dst;
char cb[48];
unsigned int len; /* Длина данных */
unsigned int data_len;
unsigned int csum; /* Контрольная сумма */
unsigned char __unused, /* He используется */
cloned, /* Заголовок должен клонироваться */
pkt_type, /* Класс пакета */
ip_summed; /* контрольная сумма IP */
__u32 priority; /* Приоритет пакета */
atomic_t users; /* Счетчик пользователей — см.
datagram.c, tcp.c */
unsigned short protocol; /* Протокол пакета */
unsigned short security; /* Уровень безопасности */
unsigned int truesize; /* Размер буфера */
unsigned char *head; /* Заголовок буфера */
unsigned char *data; /* Указатель заголовка данных */
unsigned char *tail; /* Указатель "хвоста" */
unsigned char *end; /* Конечный указатель */
void (*destructor)(struct sk_buff*);
#ifdef CONFIG_NETFILTER.
unsigned long nfmark;
/* Cache info */
__u32 nfcache;
/* Ассоциированное соединение */
struct nf_ct_info *nfct;
#ifdef CONFIG_NETFILTER_DEBUG.
unsigned int nf_debug;
#endif.
#endif /* CONFIG_NETFILTER*/
#if defined(CONFIG_HIPPI).
union {
__u32 ifield;
} private;
#endif.
#ifdef CONFIG_WET_SCHED.
__u32 tc_index; /* Индекс контроля трафика */
#endif.
};
Данная структура описана в файле /usr/src/linux/include/linux/skbuff.h. После формирования структуры sk_buff она передается драйвером функции netif_rx.
Int netif_rx(struct sk_buff *skb);
Функция netif_rx() описана в файле /usr/src/linux/net/core/dev.c. Она получает пакет от драйвера сетевого устройства и ставит его в очередь (очередь называется backlog) протокола высшего уровня. Функция возвращает следующие значения:
♦ NET_RX_SUCCESS — пакет удачно поставлен в очередь;
♦ NET_RX_CN_LOW — имеется небольшая «пробка» при постановке пакета в очередь, но скоро она «рассосется»;
♦ NET_RX_CN_MOD — «пробка» чуть больше — средней «длины»;
♦ NET_RX_CN_HIGH — очень большая «пробка»;
♦ NET_RX_DROP — пакет был удален.
Если в очереди backlog находятся более 300 пакетов, новый пакет будет удален без какого-либо предупреждения.
Драйвер сетевого устройства при формировании структуры sk_buff устанавливает ее поле protocol. Функция netif_rx(), как уже было сказано, передает пакет «наверх», затем функция net_bh() использует значение поля protocol, установленное драйвером, для вызова соответствующей протоколу программы и передает этой программе пакет. Для протокола ICMP такой программой является функция icmp_rcv(), описанная в файле /usr/src/linux/net/ipv4/icmp.с.
Int icmp_rcv(struct sk_buff *skb);
Функция icmp_rcv выполняет следующие действия:
1. Увеличивает значение счетчика входящих пакетов ICMP_INC_STATS_BH(IcmpInMsgs).
2. Удаляет пакет, если его размер слишком мал.
3. Проверяет контрольную сумму: если она ошибочна, увеличивает счетчик некорректных пакетов.
4. Проверяет тип ICMP-пакета. Если тип пакета превышает максимальное значение 18, то пакет удаляется. При этом увеличивается счетчик некорректных пакетов.
5. Вызывает функцию-обработчик ICMP-сообщения в зависимости от значения типа сообщения.
Соответствие номеров типа служебным функциям (описаны в файле icmp.c) Таблица 27.9.
| Тип | Функция | Описание |
|---|---|---|
| 0, 1, 2, 6, 7, 9, 10, 12, 14-16, 18 | void icmp_discard(struct sk_buff *skb) | Удаляет пакет |
| 3, 4, 11 | void icmp_unreach(struct sk_buff *skb) | Данная функция определяет максимальный размер модуля передачи (MTU, Maximum Transmission Unit), а также передает информацию об ошибке любому модулю, которому нужна эта информация |
| 5 | void icmp_redirect(struct sk_buff *skb) | Пакет переадресации, который означает примерно следующее: «Не посылайте мне пакет по адресу xxx.xxx.ххх.xxx, потому что он будет отправлен обратно» |
| 8 | void icmp_echo(struct sk_buff *skb) | Функция для отправки эхо-запроса. Эхо-ответ (тип 0) отправляется ядром автоматически (если вы не отключили эту возможность) |
| 13 | void icmp_timestamp(struct sk_buff *skb) | Запрос о времени создания пакета — один из самых эффективных методов измерения производительности глобальных сетей в реальном времени. Метод заключается в следующем: на тестируемый узел посылаются небольшие пакеты, которые сразу же удаляются. Информация из последнего пакета копируется в пакет эхо-ответа, в этот пакет также вставляется информация о текущем времени. После этого пакет ставится в очередь как обычно |
| 17 | void icmp_address (struct sk_buff *skb) | Отправка ответа на запрос о маске адреса |
Рассмотренные выше функции обрабатывают входящие ICMP-пакеты, Но мы ведь можем не только принимать ICMP-пакеты, но и передавать, поэтому есть также и «исходящие» функции.
Для отправки ICMP-сообщений используется функция.
Void icmp_send(struct sk_buff *skb_in, int type, int code,
u32 info);
Данная функция отправляет сообщения типа type, с кодом code и телом info.
После отправки пакета неплохо бы проверить, будет ли он реально отправлен. Это можно сделать с помощью функции.
Int xrlim_allow(struct dst_entry *dst, int timeout);
Эта функция определяет, отправлять или нет текущий пакет из очереди. Если пакет должен быть удален, функция возвращает 0, иначе — 1.
После отправки ICMP-пакета нужно увеличить счетчик отправленных пакетов. Это можно сделать с помощью функции icmp_out_count():
Void icmp_out_count(int type);
Данная функция увеличивает счетчик оправленных пакетов типа type.
На этом обзор протокола ICMP в Linux заканчивается и мы переходим к рассмотрению механизма гнезда (сокета), с помощью которого можно реализовать двунаправленный обмен данными между двумя компьютерами.
27.3. Программирование сокетов.
27.3.1. Что такое сокет?
Сокет — это двунаправленный канал между двумя компьютерами в сети, который обеспечивает конечную точку соединения. «Двунаправленный» означает, что данный могут передаваться в двух направлениях — от клиента к серверу и наоборот. Понятие сокета — абстрактное, это как бы программный соединитель, через который обмениваются данными программа-сервер и программа-клиент.
Сокет-интерфейс используется для получения доступа к транспортному уровню протокола TCP/IP и представляет собой набор системных вызовов операционной системы и библиотечных функций на языке С. Все эти функции можно условно разделить на три группы:
♦ управляющие функции;
♦ функции установления связи;
♦ функции сетевого ввода/вывода.
Общий алгоритм работы сетевой программы, использующей сокеты:
1. Подготовить (создать) сокет — функция socket().
2. Связать сокет — функция bind().
3. Установить связь с удаленным компьютером (клиенту — установить связь, а серверу — ожидать установления связи).
4. Произвести обмен данными — функции recv() и send().
5. Завершить сеанс связи — close() и shutdown().
Библиотечные функции для работы с сокетами находятся в заголовочном файле sys/socket.h, поэтому для любой сетевой программы обязательна следующая директива:
#include <sys/socket.h>
27.3.2. Создание и связывание сокета.
Основная задача управляющих функций — организовать взаимодействие двух компьютеров, точнее процессов, а также завершить сеанс связи этих процессов. К управляющим функциям относятся функции:
♦ socket() — создание сокета;
♦ bind() — связывание сокета;
♦ close() и shutdown() — завершение сеанса связи.
Начнем по порядку, а именно, с функции socket(). Ее прототип следующий:
#include <sys/types.h>
#include <sys/socket.h>
Extern int socket(int __domain, int __type,
int __protocol) __THROW;
Первый аргумент определяет набор протоколов. Особо вдаваться в подробности не будем — просто всегда в качестве параметра domain передавайте значение AF_INET, что означает использование стека протоколов TCP/IP.
Аргумент type позволяет установить режим работы: с установлением соединения и без такового — значения SOCK_STREAM и SOCK_DGRAM соответственно. Для непосредственного доступа к протоколам IPv4 используется параметр SOCK_RAW. Для его использования нужно подключить заголовочный файл:
#include <netinet/in.h>
Третий параметр лучше всего установить равным 0. В этом случае будет выбран протокол по умолчанию в зависимости от режима работы:
TCP, если мы выбрали режим SOCK_STREAM;
UDP, если мы выбрали SOCK_DGRAM.
Если вы установили значение SOCK_RAW, вы можете указывать в качестве последнего параметра непосредственно значения из файла /etc/protocols. Фрагмент этого файла приведен ниже.
Листинг 27.2. Фрагмент файла /etc/protocols.
Ip 0 IP # Протокол Интернета.
Icmp 1 ICMP # Протокол ICMP.
Igmp 2 IGMP # Протокол IGMP.
# (Internet Group Management Protocol).
Ggp 3 GGP # Протокол GGP (gateway-gateway ).
Tcp 6 TCP # Протокол TCP.
Udp 17 UDP # Протокол UDP.
Если сокет создан успешно, функция возвращает дескриптор сокета — целое положительное число. В случае ошибки функция возвращает значение -1 (отрицательное число). Вот небольшой пример:
Int sock;
Sock = socket(AF_INET, SOCK_STREAM, 0);
If (sock==-1) {
printf("Ошибка при создании сокета\n");
exit(1);
}
Чтобы связать созданный нами сокет с локальным портом, например, 1234, нужно использовать системный вызов bind():
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
Extern int bind(int fd, struct sockaddr *addr,
socklen_t len) __THROW;
Первый аргумент функции задает дескриптор нашего сокета. Второй — это указатель на структуру типа sockaddr. Все структуры данного типа определены в файле socket.h:
# define __SОСКАDDR_ALLTYPES \
__SOCKADDR_ONETYPE (sockaddr) \
__SOCKADDR_ONETYPE (sockaddr_at) \
__SОСКАDDR_ONETYPE (sockaddr_ax25) \
__SOCKADDR_ONETYPE (sockaddr_dl) \
__SOCKADDR_ONETYPE (sockaddr_eon) \
__SОСКАDDR_ONETYPE (sockaddr_in) \
__SOCKADDR_ONETYPE (sockaddr_in6) \
__SОСКАDDR_ONETYPE (sockaddr_inarp) \
__SOCKADDR_ОNЕТYРЕ (sockaddr_ipx) \
__SОСКАDDR_ONETYPE (sockaddr_iso) \
__SОСКАDDR_ONETYPE (sockaddr_ns) \
__SOCKADDR_ONETYPE (sockaddr_un) \
__SOCKADDR_ONETYPE (sockaddr_x25).
Мы программируем для сети TCP/IP, поэтому будем использовать структуру sockaddr_in (для IPv4) или sockaddr_in6 (для IPv6).
Последний аргумент — это длина выбранной нами структуры (sockaddr_in) в байтах.
Структура sockaddr_in определена в файле in.h так:
Struct sockaddr_in {
__SОСКАDDR_COMMON(sin_);
in_port_t sin_port; /* Номер порта */
struct in_addr sin_addr; /* IP-адрес */
unsigned char sin_zero[sizeof (struct sockaddr) -
__SОСКАDDR_COMMON_SIZE -
sizeof(in_port_t) - sizeof (struct in_addr)];
};
/* для IPv6. */
Struct sockaddr_in6 {
__SОСКАDDR_COMMON(sin6_);
in_port_t sin6_port; /* Порт транспортного уровня */
uint32_t sin6_flowinfo; /* Информация потока IPv6 */
struct in6_addr sin6_addr; /* адрес IPv6 */
uint32_t sin6_scope_id; /* IPv6-идентификатор */
};
Поля структуры sockaddr_in означают следующее:
♦ sin_ — набор используемых протоколов. Так как мы используем TCP/IP, данное поле должно содержать значение AF_INET;
♦ sin_port — номер порта;
♦ sin_addr — структура, определяющая адрес узла;
♦ sin_zero — обычно не используется.
Структура struct in_addr, определяющая адрес узла, также описана в файле in.h:
Struct in_addr {
in_addr_t s_addr;
};
Обычно поле s_addr должно принимать значение INADDR_ANY — сейчас поясню почему. Структура sockaddr_in должна быть заполнена ДО вызова функции bind(). Если поле sin_addr.s_addr принимает значение INADDR_ANY, то функция bind() автоматически привяжет к сокету адрес локального компьютера и нам не нужно будет указывать его явно — так наша программа будет универсальной.
Функция bind() возвращает 0 в случае успеха, и -1, если произошла ошибка. Вот небольшой пример использования этой функции:
Struct sockaddr_in client;
...
Client.sin_family = AF_INET;
Client.sin_addr.s_addr = INADDR_ANY;
Client.sin_port = 1235;
Bind(sock, (struct sockaddr *)&client, sizeof(client));
27.3.3. Установление связи с удаленным компьютером.
Устанавливать связь можно как на стороне сервера, так и на стороне клиента. На стороне клиента используется только один вызов — connect(), который «спрашивает» у сервера: «Могу ли я подключиться?», то есть передает запрос на установление соединения. На сервере используются функции:
♦ listen() — ожидание клиента;
♦ accept() — подтверждение запроса клиента на установление соединения.
Сервер должен постоянно прослушивать сокет — ожидать новых клиентов. Как только новый клиент посылает запрос на установление соединения, сервер может либо разрешить ему подключиться (connect), либо запретить (например, если сервер уже обслуживает другого клиента).
Функция listen().
Вызов listen() «заставляет» программу-сервер работать в режиме ожидания запроса на соединение от клиента. Прототип этой функции следующий:
#include <sys/socket.h>
Extern int listen (int __fd, int __n) __THROW;
Первый параметр — это дескриптор сокета, а второй — максимальное количество запросов на установление связи (другими словами, максимальное количество клиентов).
Как и функция bind(), функция listen() в случае успеха возвращает 0. Пример вызова функции:
If (listen (sock1, 3) != 0) {
printf("Ошибка при вызове listen(sock1, 3)\n");
exit(1);
}
Функция connect().
Используется программой-клиентом для отправки запроса на подключение к серверу. Прототип функции следующий:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
Extern int connect (int __fd, struct sockaddr_in *addr,
socklen_t __len) __THROW;
Первый параметр — это дескриптор сокета, созданного функцией socket() и привязанного функцией bind(). Привязку сокета функцией bind() выполнять не обязательно: если сокет не был привязан до вызова connect(), привязка будет выполнена автоматически.
Второй параметр — это указатель на структуру типа sockaddr_in, содержащую информацию о сервере: его IP-адрес, номер порта, а также семейство протоколов.
Последний параметр — это размер структуры sockaddr_in в байтах. В случае успеха функция возвращает 0, а в случае ошибки —1.
Вот пример использования вызова connect:
Struct sockaddr_in server;
Struct hostent *h;
...
// определяем IP-адрес сервера.
H = gethostbyname("server.domain.ru");
Memcpy((char*)&server.sin_addr, h->h_addr, h->h_length);
// Определяем порт сервера.
Server.sin_port = 1234;
// Определяем семейство протоколов.
Server.sin_family = AF_INET;
// Вызов функции connect().
Connect(sock, &server, sizeof(server));
Если вы используете режим без установления соединения (SOCK_DGRAM), вызов connect() необязателен.
Функция accept().
Если максимальное число клиентов не превышено, сервер может принять запрос клиента. Для этого используется функция accept(). Данная функция используется только при работе в режиме с установлением соединения. Прототип функции следующий:
#include <sys/socket.h>
#include <netinet/in.h>
Extern int accept(int __fd, struct sockaddr_in *addr,
socklen_t *__restrict __len) __THROW;
Первый параметр — это дескриптор сокета, второй — указатель на структуру, где можно разместить адрес клиента, причем данную структуру инициализировать не нужно. Последний параметр — размер структуры, указанной во втором параметре.
Системный вызов accept() работает так. Сначала он извлекает из очереди listen() запрос на соединение и создает новый сокет, через который будет производиться обмен данными с клиентом, например:
// получаем сокет клиента.
Sock2 = accept(sock1, &client, &ans_len);
// передаем клиенту информацию.
Write(sock2, MSG_TO_SEND, sizeof(MSG_TO_SEND));
Если вызов accept() завершился успехом, структура addr, задаваемая во втором параметре, будет содержать IP-адрес клиента.
Если очередь listen() пуста, то наш сервер будет ожидать появления нового клиента. В случае ошибки функция accept() возвращает отрицательное значение.
27.3.4. Функция gethostbyname().
Пользователям обычно удобнее указать символьное имя сервера, чем его IP-адрес. Для разрешения имени служит функция gethostbyname(). Вот ее прототип:
#include <netinet/in.h>
#include <netdb.h>
Struct hostent *gethostbyname(char *name);
Данная функция возвращает указатель на структуру типа hostent, содержащую следующие поля:
♦ char *h_name — доменное имя узла;
♦ char **h_aliases — псевдонимы узла, если таковые определены;
♦ char *h_addr — IP-адрес узла;
♦ int h_addrtype — набор используемых протоколов (в нашем случае — AF_INET);
♦ int h_length — длина адреса узла.
Примеры использования функции:
Struct hostent *h;
H = gethostbyname(*argv);
If (h==NULL) {
printf("Невозможно разрешить имя: `%s`\n", *argv);
exit(1);
}
// Выводим IP-адрес. Вывод в виде: имя -> адрес.
Printf("%s -> %s \n", *argv,
Inet_ntoa(*((struct in_addr *)h->h_addr_list[0])));
Узнать свой собственный адрес можно с помощью функции getsockname():
Extern int getsockname(int __fd, __SOCKADDR_ARG __addr,
socklen_t *__restrict __len) __THROW;
Ей нужно передать три параметра — дескриптор сокета, адрес структуры, которая будет содержать информацию о нашем узле (его адрес). Третий параметр будет содержать длину адресной структуры.
27.3.5. Функции сетевого ввода/вывода.
После успешного установления соединения можно начать обмен данными. Для отправки и получения данных можно использовать обыкновенные функции для работы с файлами — read() и write(), только вместо дескриптора файла нужно указывать дескриптор сокета. Однако рекомендуется использовать системные вызовы send() и recv(), которые предназначены именно для работы с сокетами. Эти системные вызовы будут рассмотрены ниже.
Если вы работаете в режиме без установления соединения, вам нужно использовать функции sendto() и recvfrom(). Первая функция отправляет данные, а вторая — принимает. Функция sendto() вместе с данными позволяет указать адрес получателя, a recvfrom() возвращает не только полученные данные, но и адрес отправителя.
Обмен данными в режиме SOCK_STREAM.
Для отправления данных используется функция send():
#include <sys/types.h>
#include <sys/socket.h>
Extern ssize_t send (int __fd, __const void *__buf,
size_t __n, int __flags) __THROW;
Первый параметр — дескриптор сокета, второй — указатель на область памяти, которая содержит передаваемые данные. Третий параметр — это размер передаваемых данных в байтах. Последний параметр позволяет определить поведение функции send(): если он равен 0, то вызов send() полностью аналогичен вызову write().
Нужно отметить особенность работы этой функции; если буфер сокета __fd переполнен, функция переводит программу в состояние ожидания освобождения буфера. Такое может случиться, если узел-приемник по каким-то причинам не успевает принять данные.
Функция возвращает число байтов отправленных данных или -1 в случае ошибки.
Для приема данных используется функция recv():
#include <sys/types.h>
#include <sys/socket.h>
Extern ssize_t recv(int __fd, void *__buf, size_t __n,
int __flags) __THROW;
Первый параметр, как обычно, задает дескриптор сокета. В случае успешного приема данных они будут размешены в буфере __buf — второй параметр функции recv(). Третий параметр задает размер области, на которую указывает второй параметр. Если четвертый параметр (флаги) принимает значение 0, то вызов recv() аналогичен вызову read(). Четвертый параметр может принимать следующие значения:
♦ MSG_PEEK — прочитанные данные не удаляются. Следующий вызов функции recvfrom() опять возвратит эти данные.
♦ MSG_WAITALL — процесс будет блокирован до получения всего запрошенного объема данных, а не до получения первого сообщения. Только для сокетов SOCK_STREAM!
Если через указанный сокет ничего нельзя принять, функция переводит программу в состояние ожидания — до появления данных в канале связи.
Функция возвращает количество принятых байтов или -1 в случае ошибки.
Обмен данными в режиме SOCK_DGRAM.
Функция sendto() позволяет отправить данные по протоколу UDP (без установления соединения), указав при этом узел-приемник:
Extern ssize_t sendto(int __fd, __const void *__buf,
size_t __n, int __flags, __CONST_SOCKADDR_ARG __addr,
socklen_t __addr_len) __THROW;
Назначение первых четырех аргументов такое же, как и функции send(), а последние два аргумента задают структуру типа struct sockaddr_in, содержащую информацию об адресе узла-приемника, и размер этой структуры соответственно. Аргумент __addr — это адрес структуры sockaddr_in, а не она сама!
Как и функция send(), функция sendto() возвращает количество байтов отправленных данных или -1, если произошла ошибка.
Функция recvfrom() позволяет получить данные по протоколу UDP:
Extern ssize_t recvfrom(int __fd, void *__restrict __buf,
Size_t __n, int __flags, __SOCKADDR_ARG __addr,
Socklen_t *__restrict __addr_len) __THROW;
Назначение первых четыре аргументов такое же, как и у функции recv(). Предпоследний аргумент позволяет указать структуру, в которую будет записана информация об адресе узла-отправителя. Помните: нужно передать адрес структуры, а не саму структуру. Последний параметр задает длину этой структуры.
Функция возвращает количество принятых данных или -1 в случае ошибки. Проверить ошибку можно и по-другому: если структура адреса узла отправителя пуста (равна NULL), значит, произошла ошибка.
27.3.6. Завершение сеанса связи.
Для закрытия сеанса связи можно использовать один из двух системных вызовов: close() или shutdown().
Системный вызов close() также используется для закрытия файлов. Вот прототип этой функции:
Int close(int __fd);
Данной функции нужно передать всего один параметр — дескриптор сокета.
Однако вызов close() использовать не рекомендуется из-за специфики его работы: он закрывает сокет грубо, не дожидаясь завершения передачи данных. В результате использования close() вероятность повреждения принимаемых или передаваемых данных очень высока. В принципе, использовать close() можно на клиенте, но на сервере это недопустимо: сначала нужно использовать shutdown(), а потом уже close().
Вызов shutdown() используется для завершения сеанса связи, при этом еще не переданные данные будут переданы другой стороне. Прототип функции:
Extern int shutdown(int __fd, int __how) __THROW;
Первый параметр — это дескриптор сокета, а второй может принимать одно из трех значений:
♦ SHUT_RD (или 0) — передать данные, которые еще не переданы, но их отправка уже началась, и больше не принимать данные для чтения.
♦ SHUT_WR (или 1) — передать данные и запретить прием данных через сокет.
♦ SHUT_RDWR (или 2) — передать данные и запретить вообще обмен через сокет — ни приема, ни передачи.
27.3.7. Программа-сервер.
В этом пункте мы напишем две программы — сервер и клиент. Программа-сервер после запуска сразу же перейдет в режим ожидания («прослушивания») новых клиентов. Максимальное количество клиентов —3. Как только подключится клиент, сервер отправит ему сообщение «What is your name?», в ответ на которое клиент передаст свое имя — «Denis». Сервер прочитает переданную клиентом информацию и выведет ее на консоль. Клиент, в свою очередь, выведет на консоль запрос сервера.
С целью упрощения исходного кода как сервера, так и клиента, обработку ошибок производить не будем, поэтому будьте готовы к тому, что ваш клиент выдаст сообщение Segmentation fault в ответ на неверно заданные параметры. Я рекомендую в качестве имени сервера использовать localhost и обе программы запускать на одном компьютере — это же только демонстрация.
Вот исходный код программы-сервера:
Листинг 27.3. Программа-сервер.
#include <sys/types.h>
#include <netdb.h>
#include <memory.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#define SERVER_PORT 1234.
#define BUF_SIZE 64.
#define MSG_TO_SEND "What is your name?\n".
Int main() {
int sock1, sock2;
int ans_len, total=0;
char buffer[BUF_SIZE];
struct sockaddr_in sin, client;
sock1 = socket(AF_INET, SOCK_STREAM, 0);
memset((char *)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = SERVER_PORT;
bind(sock1, (struct sockaddr *)&sin, sizeof(sin));
printf("Server running...\n");
listen(sock1, 3);
while (1) {
ans_len = sizeof(client);
sock2 = accept(sock1, &client, &ans_len);
write(sock2, MSG_TO_SEND, sizeof(MSG_TO_SEND));
total+=1;
ans_len = read(sock2, buffer, BUF_SIZE);
write(1, buffer, ans_len);
printf("Client no %d\n", total);
shutdown(sock2, 0);
close(sock2);
}
return 0;
}
Теперь разберемся, что есть что. Сначала мы определяем некоторые макросы: номер порта, который будет прослушивать сервер, размер буфера передаваемых данных и текст запроса клиенту.
Стандартные номера портов определены в файле netinet/in.h:
Enum {
IPPORT_ECHO = 7, /* Echo service. */
IPPORT_DISCARD = 9, /* Discard transmissions service. */
IPPORT_SYSTAT = 11, /* System status service. */
IPPORT_DAYTIME = 13, /* Time of day service. */
IPPORT_NETSTAT = 15, /* Network status service. */
IPPORT_FTP = 21, /* File Transfer Protocol. */
IPPORT_TELNET =23, /* Telnet protocol. */
IPPORT_SMTP = 25, /* Simple Mail Transfer Protocol. */
IPPORT_TIMESERVER = 37, /* Timeserver service. */
IPPORT_NAMESERVER = 42, /* Domain Name Service. */
IPPORT_WHOIS = 43, /* Internet Whois service. */
IPPORT_MTP = 57,
IPPORT_TFTP = 69, /* Trivial File Transfer Protocol. */
IPPORT_RJE = 77,
IPPORT_FINGER = 79, /* Finger service. */
IPPORT_TTYLINK = 87,
IPPORT_SUPDUP = 95, /* SUPDUP protocol. */
IPPORT_EXECSERVER = 512, /* execd service. */
IPPORT_LOGINSERVER = 513, /* rlogind service. */
IPPORT_CMDSERVER = 514,
IPPORT_EFSSERVER = 520,
/* UDP ports. */
IPPORT_BIFFUDP = 512,
IPPORT_WHOSERVER = 513,
IPPORT_ROUTESERVER = 520,
/* Ports less than this value are reserved.
for privileged processes. */
IPPORT_RESERVED = 1024,
/* Ports greater this value are reserved.
for (non-privileged) servers. */
IPPORT_USERRESERVED = 5000.
};
Нам понадобятся сразу два сокета: первый — это сокет сервера, а через второй сокет мы будем производить обмен данными с клиентом.
Int sock1, sock2;
Следующие переменные: ans_len используется для хранения размера передаваемой клиентом информации — фактического размера структуры struct sockaddr_in, а total — это счетчик числа клиентов, используемый для вывода порядкового номера клиента.
Переменная buffer размера BUF_SIZE — это наш буфер для обмена информацией. Нам нужны две структуры типа sockaddr_in — одна для сервера (sin) и одна для клиента (client).
В строке.
Sock1 = socket(AF_INET, SОСК_STREAM, 0);
Мы создаем наш, «серверный», сокет: набор протоколов — TCP/IP, режим — с установлением соединения.
Затем мы инициализируем структуру sin:
Memset((char *)&sin, '\0', sizeof(sin));
Sin.sin_family = AF_INET; // TCP/IP.
Sin.sin_addr.s_addr = INADDR_ANY; // можем работать на.
// любом адресе.
Sin.sin_port = SERVER_PORT; // указываем порт (1234).
После создания сокета и инициализации структуры sin, нужно связать наш сокет с адресом и портом сервера:
Bind(sock1, (struct sockaddr *)&sin, sizeof(sin));
Оператор listen(sock1, 3) означает, что мы будем прослушивать сокет sock1 (порт 1234) и максимальное число клиентов не должно превышать 3.
Как и любой нормальный сервер, мы должны работать в бесконечном цикле, постоянно обрабатывая запросы клиентов. В бесконечном цикле мы:
1. получаем размер структуры client.
Ans_len = sizeof(client);
2. создаем сокет sock2, через который будем обмениваться данными с клиентом. Если в очереди listen нет клиентов, мы переходим в состояние ожидания.
Sock2 = accept(sock1, &client, &ans_len);
3. как только подключится клиент, мы отправим ему сообщение MSG_TO_SEND.
Write(sock2, MSG_TO_SEND, sizeof(MSG_TO_SEND));
4. увеличиваем счетчик клиентов.
Total+=1;
5. получаем размер прочитанных данных, сами данные записываются в буфер buffer.
Ans_len = read(sock2, buffer, BUF_SIZE);
6. выводим прочитанные данные на стандартный вывод.
Write(1, buffer, ans_len);
7. завершаем сеанс связи.
Shutdown(sock2, 0);
8. закрываем сокет.
Close(sock2);
Конечно, любой нормальный сервер при поступлении определенных сигналов, например, SIG_HUP, должен корректно перезапуститься или вообще завершить работу. Наш сервер этого не делает — обработку сигналов, я надеюсь, вы можете добавить сами.
Теперь мы можем откомпилировать нашу программу:
$ gcc -о server server.c.
Запускаем:
./server.
Программа перешла в состояние ожидания новых клиентов.
27.3.8. Программа-клиент.
Программа-клиент несколько проще, чем сервер. Вот ее листинг:
Листинг 27.4. Программа-клиент.
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <memory.h>
#include <stdio.h>
#define SERVER_HOST "localhost".
#define SERVER_PORT 1234.
#define CLIENT_PORT 1235.
#define MSG "Denis\n".
Main() {
int sock;
int ans_len;
int BUF_SIZE = 64;
char buffer[BUF_SIZE];
struct hostent *h;
struct sockaddr_in client, server;
sock = socket(AF_INET, SOCK_STREAM, 0);
memset((char *)&client, '\0', sizeof(client));
client.sin_family = AF_INET;
client.sin_addr.s_addr = INADDR_ANY;
client.sin_port = CLIENT_PORT;
bind(sock, (struct sockaddr *)&client, sizeof(client));
memset((char *)&client, '\0', sizeof(server));
h = gethostbyname(SERVER_HOST);
server.sin_family = AF_INET;
memcpy((char *)&server.sin_addr.h->h_addr, h->h_length);
server.sin_port = SERVER_PORT;
connect(sock, &server, sizeof(server));
ans_len = recv(sock, buffer, BUF_SIZE, 0);
write(1, buffer, ans_len);
send(sock, MSG, sizeof(MSG), 0);
close(sock);
exit(0);
}
Константа MSG — это сообщение, которое будет передано серверу. Как и в случае с сервером, нам понадобятся две структуры типа sockaddr_in:
Struct hostent *h;
Struct sockaddr_in client, server;
Структура типа hostent нам нужна для получения адреса сервера.
Создаем сокет, заполняем информацию о клиенте и связываем сокет:
Sock = socket(AF_INET, SOCK_STREAM, 0);
Memset((char *)&client, '\0' , sizeof(client));
Client.sin_family = AF_INET;
Client.sin_addr.s_addr = INADDR_ANY;
Client.sin_port = CLIENT_PORT;
Bind(sock, (struct sockaddr *)&client, sizeof(client));
Перед подключением к серверу нужно определить его IP-адрес:
H = gethostbyname(SERVER_HOST);
Подключаемся к серверу:
Server.sin_family = AF_INET; // набор протоколов.
Memcpy((char *)&server.sin_addr, h->h_addr, h->h_length);
// задаем адрес сервера.
Server.sin_port = SERVER_PORT; // указываем порт сервера.
Connect(sock, &server, sizeof(server));
После подключения к серверу принимаем его запрос, выводим на стандартный вывод, отправляем серверу свое сообщение и закрываем сокет:
Ans_len = recv(sock, buffer, BUF_SIZE, 0);
Write(1, buffer, ans_len);
Send(sock, MSG, sizeof(MSG), 0);
Close(sock);
27.3.9. Установка опций сокета.
Поскольку мы используем набор протоколов AF_INET, то в этом пункте будем рассматривать только те опции сокетов, которые относятся к этому набору. Для работы с опциями сокета используются две функции:
♦ getsockopt() — получение опций сокета;
♦ setsockopt() — установка опций сокета.
Прототипы этих функций выглядят так:
#include <sys/socket.h>
Int getsockopt(int sd, int level, int option_name,
void *restrict option_value, socklen_t *restrict option_len);
Int setsockopt(int sd, int level, int option_name,
const void *option_value, socklen_t option_len);
Первый параметр, sd, — это дескриптор сокета. Второй параметр — уровень доступа (существует только один уровень — SOL_SOCKET). Следующий параметр, option_name, — это название опции, значение которой вы хотите изменить (см. таблицу 27.10). Последние два параметра — это значение опции и его размер.
Наиболее часто используемые опции сокетов Таблица 27.10.
| Название опции | Описание |
|---|---|
| SO_DEBUG | Включить/выключить (1/0) запись отладочной информации для сокета |
| SO_BROADCAST | Включить/выключить (1/0) отправку широковещательных сообщений |
| SO_REUSEADDR | Опция разрешает/запрещает использование локальных адресов |
| SO_KEEPALIVE | Сохраняет неактивные соединения "в живых" путем посылки сообщений. Если данный сокет не отвечает на сообщения, соединение будет разорвано, а процессу, который осуществлял запись в сокет, будет послан сигнал SIGPIPE. Для включения KEEPALIVE нужно установить значение 1, для выключения — 0 |
| SO_SNDBUF | Устанавливает размер буфера отправки, значение целого типа |
| SO_RCVBUF | Устанавливает размер буфера приема, значение целого типа |
| SO_SNDTIMEO | Установка таймаута для отправки сообщений. По умолчанию таймаут равен 0, то есть его вообще нет. Нужно передать значение типа struct timeval |
| SO_RCVTIMEO | Установка таймаута для приема сообщений. По умолчанию таймаут равен 0, то есть его вообще нет. Нужно передать значение типа struct timeval |
| TCP_NODELAY | Отключить (1) механизм буферизации сообщений, то есть они будут отправляться сразу, без задержки. Для включения механизма буферизации нужно указать значение 0 |
| TCP_MAXSEG | Установить максимальный сегмент данных. Значение целого типа |
| TCP_NOPUSH | Не использовать проталкивание (1) |
| TCP_NOOPT | Не использовать опции TCP (1). Для использования опций передайте значение 0 |
В случае успешной установки параметра функция setsockopt() возвращает 0; в случае ошибки возвращается -1, а переменная errno устанавливается следующим образом:
♦ EBADF — неверный дескриптор сокета:
♦ ENOTSOCK — указанный дескриптор является файлом, а не сокетом;
♦ EFAULT — нет доступа к адресу, на который указывает указатель optval, то есть данный адрес находится за пределами видимости приложения.
Функция getsockopt() возвращает значение параметра. Кроме вышеперечисленных параметров, функция getsockopt() может использовать следующие параметры:
♦ SO_ERROR — возвращает номер ошибки (будет в возвращаемом значении);
♦ SO_TYPE — возвращает тип сокета.
Рассмотрим небольшой пример работы с опциями сокетов. Мы установим размер буфера TCP.
#include "sock.h".
#include "stdio.h".
Main() {
int sd; /* дескриптор сокета */
int optval; /* значение опции */
int optlen; /* длина optval */
int new_buffsize = 8192; /* новый размер буфера */
/* создаем сокет */
sd = socket(AF_INET, SOCK_STREAM, 0);
/* считывание длины буфера TCP */
optlen = sizeof(optval);
getsockopt(sd, SOL_SOCKET, SO_SNDBUF, &optval, &optlen);
printf("Size of send buffer %d\n", optval);
getsockopt(sd, SOL_SOCKET, SO_RCVBUF, &optval, &optlen);
printf("Size of recv buffer %d\n", optval);
/* изменяем длину буфера */
setsockopt(sd, SOL_SOCKET, SO_RCVBUF,
&new_buffsize, sizeof(new_buffsize));
setsockopt(sd, SOL_SOCKET, SO_SNDBUF,
&new_buffsize, sizeof(new_buffsize));
/* выводим измененную информацию */
getsockopt(sd, SOL_SOCKET, SO_SNDBUF, &optval, &optlen);
printf("New size of send buffer %d\n", optval);
getsockopt(sd, SOL_SOCKET, SO_RCVBUF, &optval, &optlen);
printf("New size of recv buffer %d\n", optval);
}
27.3.10. Сигналы и сокеты.
С сокетами связаны три сигнала:
♦ SIGIO — сокет готов к вводу/выводу. Сигнал посылается процессу, который связан с сокетом;
♦ SIGURG — сокет получил экспресс-данные (мы их использовать не будем, поэтому особо останавливаться на них нет смысла);
♦ SIGPIPE — запись в сокет больше невозможна. Сигнал посылается процессу, связанному с сокетом. Например, функция write() вызывает сигнал SIGPIPE, если удаленный процесс завершен или связь по сети невозможна.
Пример обработки сигнала SIGPIPE приведен ниже.
Листинг 27.6. Обработка сигнала SIGPIPE.
#include "sock.h".
#include <signal.h>
/* обработчик сигнала SIGPIPE */
Sigpipe_handler() {
err_quit("Получен SIGPIPE \n");
}
Main() {
int sock; /* дескриптор сокета */
/* установка обработчика сигнала SIGPIPE */
signal(SIGPIPE, sigpipe_handler);
/* работа с сокетом */
}
27.3.11. Мультиплексирование.
В этой главе мы рассматривали пример программы-сервера, обрабатывающей запросы только от одного клиента. На практике все выглядит намного сложнее: серверу приходится одновременно обрабатывать запросы многих клиентов. Для мультиплексирования запросов клиентов используется системный вызов select(). Этот вызов использует, например, суперсервер xinetd.
Листинг 27.7. Мультиплексирование запросов.
#include "sock.h".
#include <sys/time.h>
Main() {
int sock; /* дескриптор исходного сокета */
int new_sock; /* дескриптор, полученный с помощью accept */
int retval; /* возвращаемое значение */
struct sockaddr_in server; /* адрес сокета */
fd_set readv; /* переменная для select */
fd_set writev; /* переменная для select */
struct timeval tout; /* тайм-аут для select */
/* бесконечный цикл ожидания */
for (;;) {
/* процесс ждет операцию ввода-вывода на сокете;
одновременно можно ждать и другие операции */
FD_ZERO(&readv);
FD_ZERO(&writev);
FD_SET(sock, &readv);
FD_SET(sock, &writev);
tout.tv_sec = 10; /* 10 секунд */
retval = select(sock+1, &readv, &writev, 0, &to);
/* если select возвращает нулевое значение, значит тайм-аут */
if (retval == 0) {
err_ret("timeout");
continue;
}
/* в противном случае, ищем соответствующий дескриптор */
if ( (FD_ISSET(sock, &readv)) || (FD_ISSET(sock, &writev))) {
/* прием связи с сокета */
new_sock = accept(sock, (struct sockaddr *)0, (int *)0);
/* работа с сокетом new_sock */
...
/* закрытие текущей связи */
close(new_sock);
} else {
err_ret("Это не сокет! Проверьте все дескрипторы\n");
}
}
}
Системный вызов select() принимает 5 аргументов:
Int select(int fd, fd_set *input, fd_set *output,
fd_set *error, struct timeval *timeout);
Первый аргумент, fd, — это файловый дескриптор, который может быть сокетом. Следующие три аргумента задают множества файловых дескрипторов для ожидания условий ввода (input), вывода (output) и ошибок (error). Последний аргумент — это тайм-аут.
Множества файловых дескрипторов инициализируются с помощью трех макросов:
FD_ZERO(fd_set);
FD_SET(fd, fd_set);
FD_CLR(fd, fd_set);
Первый макрос полностью очищает множество, следующие два макроса, соответственно, добавляют и удаляют файловый дескриптор. Мы использовали два макроса для ввода и два для вывода. Сначала мы полностью очистили множество, а потом добавили в него соответствующие дескрипторы:
FD_ZERO(&readv);
FD_ZERO(&writev);
FD_SET(sock, &readv);
FD_SET(sock, &writev);
Особого разговора требует последний параметр — тайм-аут. Тайм-аут можно задавать в секундах и миллисекундах. Например, следующие операторы объявляют тайм-аут длительностью 2 секунды и 5 миллисекунд:
Struct timeval tout; /* тайм-аут для select */
Tout.tv_sec = 2; /* 2 секунды */
Tout.tv_usec = 5; /* 5 миллисекунд */
Если вы хотите не использовать тайм-аут (то есть ждать бесконечно), укажите NULL в качестве последнего аргумента.
Функция select() возвращает число файловых дескрипторов, на которых выполнились ожидаемые условия (ввод/вывод/ошибка) или -1 при ошибке.
Вот еще один пример использования функции select(). Мы будем ожидать ввода из файла и из сокета. Если будет достигнут тайм-аут в 20 секунд, пользователь увидит соответствующее сообщение; в противном случае он увидит сообщение: «Получен ввод из файла/сокета».
Листинг 27.8. Еще один пример использования select().
#include <unistd.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/select.h>
Int k;
Int sock;
Int fd;
Int max_fd;
Fd_set input;
Struct timeval timeout;
/* инициализация файла и сокета */
...
/* Инициализируем множество ввода */
FD_ZERO(input);
FD_SET(fd, input);
FD_SET(sock, input);
Max_fd = (sock > fd ? sock : fd) + 1;
/* Задаем тайм-аут */
Timeout.tv_sec = 20;
K = select(max_fd, &input, NULL, NULL, &timeout);
If (k < 0).
perror("Ошибка при вызове select");
Else if (k == 0) puts("TIMEOUT");
Else {
/* Получен ввод */
if (FD_ISSET(fd, input)).
printf("Получен ввод из файла");
if (FD_ISSET(sock, input)).
printf("Получен ввод из сокета");
}
Вроде бы код программы очень прост, но комментария заслуживает макрос FD_ISSET. С его помощью мы проверяем, есть ли во множестве ввода ввод из какого-либо источника.
27.3.12. Неблокирующие операции.
Некоторые функции для работы с сокетами блокируют программу в случае, если удаленный процесс не осуществил требуемую операцию. Примеры таких функций:
♦ accept();
♦ connect();
♦ read();
♦ write().
Блокирование процесса очень нежелательно, поскольку во время ожидания можно было бы заняться чем-нибудь другим: например, обработать информацию, поступившую с другого сокета. Вы можете объявить сокеты неблокирующими с помощью системного вызовы ioctl().
Особенности работы некоторых функций в неблокирующем режиме:
♦ функция accept() сразу же завершает работу с ошибкой EWOULDBLOCK;
♦ функция connect() тоже завершает работу, но с другой ошибкой: EINPROGRESS;
♦ функции чтения (read(), recv(), recvfrom()) возвращают -1 или 0, если нет считываемых данных.
Ясное дело, что в таком режиме нужно периодически проверять наличие данных — ведь теперь процесс не будет их ожидать: если их нет, то функции просто возвратят -1 или 0.
Пример создания неблокирующих сокетов приведен ниже:
Листинг 27.9. Использование системного вызова ioctl().
#include "sock.h".
#include <sys/ioctl.h>
Void main() {
int sock;
int on = 1, off = 0; /* значение дня ioctl() */
/* Создаем неблокирующий сокет */
ioctl(sock, FIONBIO, &on);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.
- 268.
- 269.
- 270.
- 271.
- 272.
- 273.
- 274.
- 275.
- 276.
- 277.
- 278.
- 279.
- 280.
- 281.
- 282.
- 283.
- 284.
- 285.
- 286.
- 287.
- 288.
- 289.
- 290.
- 291.
- 292.
- 293.
- 294.
- 295.
- 296.
- 297.
- 298.
- 299.
- 300.
- 301.
- 302.
- 303.
- 304.
- 305.
- 306.
- 307.
- 308.
- 309.
- 310.
- 311.
- 312.
- 313.
- 314.
- 315.
- 316.
- 317.
- 318.
- 319.
- 320.
- 321.
- 322.
- 323.
- 324.
- 325.
- 326.
- 327.
- 328.
- 329.
- 330.
- 331.
- 332.
- 333.
- 334.
- 335.
- 336.
- 337.
- 338.
- 339.
- 340.
- 341.
- 342.
- 343.
- 344.
- 345.
- 346.
- 347.
- 348.
- 349.
- 350.
- 351.
- 352.
- 353.
- 354.
- 355.
- 356.
- 357.
- 358.
- 359.
- 360.
- 361.
- 362.
- 363.
- 364.
- 365.
- 366.
- 367.
- 368.
- 369.
- 370.
- 371.
- 372.
- 373.
- 374.
- 375.
- 376.
- 377.
- 378.
- 379.
- 380.
- 381.
- 382.
- 383.
- 384.
- 385.
- 386.
- 387.
- 388.
- 389.
- 390.
- 391.
- 392.
- 393.
- 394.
- 395.
- 396.
- 397.
- 398.
- 399.
- 400.
- 401.
- 402.
- 403.
- 404.
- 405.
- 406.
- 407.
- 408.
- 409.
- 410.
- 411.
- 412.
- 413.
- 414.
- 415.
- 416.
- 417.
- 418.
- 419.
- 420.
- 421.
- 422.
- 423.
- 424.
- 425.
- 426.
- 427.
- 428.
- 429.
- 430.
- 431.
- 432.
- 433.
- 434.
- 435.
- 436.
- 437.
- 438.
- 439.
- 440.
- 441.
- 442.
- 443.
- 444.
- 445.
- 446.
- 447.
- 448.
- 449.
- 450.
- 451.
- 452.
- 453.
- 454.
- 455.
- 456.
- 457.
- 458.
- 459.
- 460.
- 461.
- 462.
- 463.
- 464.
- 465.
- 466.
- 467.
- 468.
- 469.
- 470.
- 471.