Глава 7. Сокеты | FreeBSD Documentation Portal
Description: Сокеты FreeBSD
Keywords: Sockets,Protocols

# Глава 7. Сокеты

Этот перевод может быть устаревшим. Для того, чтобы помочь с переводом, пожалуйста, обратитесь к Сервер переводов FreeBSD.

## 7.1. Обзор

Сокеты BSD выводят межпроцессное взаимодействие на новый уровень. Теперь взаимодействующие процессы не обязательно должны выполняться на одной машине. Они всё ещё могут, но не обязаны.

Не только эти процессы не обязаны выполняться на одной машине, они также могут работать под разными операционными системами. Благодаря BSD-сокетам, ваше ПО на FreeBSD может легко взаимодействовать с программой, работающей на Macintosh®, другой — на рабочей станции Sun™, и ещё одной — под Windows® 2000, при этом все они подключены к локальной сети на основе Ethernet.

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

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

## 7.2. Сетевое взаимодействие и разнообразие

Мы уже упоминали о разнообразии сетевых технологий. Множество различных систем должны взаимодействовать друг с другом. И они должны говорить на одном языке. Также они должны понимать этот язык одинаковым образом.

Часто думают, что язык тела универсален. Но это не так. В ранней юности отец взял меня с собой в Болгарию. Мы сидели за столиком в парке Софии, когда к нам подошел продавец, предлагая купить жареный миндаль.

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

Затем я вспомнил, что мне говорили, будто в Болгарии покачивание головой из стороны в сторону означает да. Быстро я начал кивать головой вверх-вниз. Продавец заметил, забрал свои орехи и ушёл. Для непосвящённого наблюдателя я не изменил язык тела: я продолжал использовать движения головой — покачивание и кивание. Изменился смысл языка тела. Сначала продавец и я интерпретировали одни и те же жесты как имеющие совершенно разный смысл. Мне пришлось скорректировать свою собственную интерпретацию этих жестов, чтобы продавец меня понял.

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

## 7.3. Протоколы

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

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

При передаче данных от одного компьютера к другому всегда используется более одного протокола. Эти протоколы располагаются слоями. Данные можно сравнить с внутренней частью лука: необходимо снять несколько слоёв «кожи», чтобы добраться до данных. Это лучше всего проиллюстрировано на рисунке:

Рисунок 1. Уровни протоколов

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

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

Увы, наше программное обеспечение не может определить, как организованы сырые данные: это последовательность значений RGB, последовательность интенсивностей в градациях серого или, возможно, цвета в кодировке CMYK? Представлены ли данные 8-битными квантами, или они имеют размер 16 бит, а может быть, 4 бита? Из скольких строк и столбцов состоит изображение? Должны ли определённые пиксели быть прозрачными?

Я думаю, вы поняли…​

Чтобы наше программное обеспечение понимало, как обрабатывать сырые данные, они кодируются в формате PNG. Это мог бы быть GIF или JPEG, но выбран PNG.

И PNG — это протокол.

В этот момент я слышу, как некоторые из вас кричат: "Нет, это не так! Это формат файла!"

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

Увы, если бы мы получили только PNG-файл, наше программное обеспечение столкнулось бы с серьёзной проблемой: как ему узнать, что данные представляют изображение, а не текст, звук или что-то ещё? Во-вторых, как ему определить, что изображение сохранено в формате PNG, а не GIF, JPEG или каком-либо другом формате изображений?

Для получения этой информации мы используем другой протокол: HTTP. Этот протокол может точно сообщить нам, что данные представляют изображение и используют протокол PNG. Он также может сообщить некоторые другие сведения, но давайте сосредоточимся на уровнях протоколов здесь.

Итак, теперь у нас есть некоторые данные, упакованные в протокол PNG, который в свою очередь упакован в протокол HTTP. Как мы получили их с сервера?

Используя TCP/IP поверх Ethernet, вот как. Действительно, это ещё три протокола. Вместо того чтобы продолжать изнутри наружу, я теперь расскажу про Ethernet, просто потому что так проще объяснить остальное.

Ethernet — это интересная система соединения компьютеров в локальной сети (LAN). У каждого компьютера есть карта сетевого интерфейса (NIC — Network Interface Card) с уникальным 48-битным идентификатором, называемым адресом. Не существует двух сетевых интерфейсов Ethernet в мире с одинаковым адресом.

Эти сетевые карты соединены между собой. Когда один компьютер хочет связаться с другим в той же локальной сети Ethernet, он отправляет сообщение по сети. Каждая сетевая карта видит это сообщение. Однако, согласно протоколу Ethernet, данные содержат адрес сетевой карты назначения (среди прочего). Таким образом, только одна из всех сетевых карт обратит на него внимание, остальные проигнорируют его.

Но не все компьютеры подключены к одной сети. Тот факт, что мы получили данные через наш Ethernet, не означает, что они возникли в нашей локальной сети. Они могли попасть к нам из другой сети (которая может быть даже не на основе Ethernet), соединённой с нашей сетью через Интернет.

Все данные передаются через Интернет с использованием IP, что означает Internet Protocol. Его основная роль — сообщать нам, откуда в мире пришли данные и куда они должны быть направлены. Он не гарантирует, что мы получим данные, только что мы узнаем, откуда они пришли, если мы их получим.

Даже если мы получим данные, IP не гарантирует, что различные фрагменты данных придут в том же порядке, в котором их отправил другой компьютер. Например, мы можем получить центр нашего изображения до того, как получим его верхний левый угол, а после — нижний правый.

Это TCP (Transmission Control Protocol), который запрашивает у отправителя повторную отправку потерянных данных и располагает их в правильном порядке.

В итоге потребовалось пять различных протоколов, чтобы один компьютер мог сообщить другому, как выглядит изображение. Мы получили данные, упакованные в протокол PNG, который был упакован в протокол HTTP, который был упакован в протокол TCP, который был упакован в протокол IP, который был упакован в протокол Ethernet.

О, и кстати, вероятно, на пути были задействованы и несколько других протоколов. Например, если наша локальная сеть была подключена к Интернету через дозвон, то использовался протокол PPP над модемом, который, в свою очередь, использовал один (или несколько) из различных модемных протоколов, и так далее, и так далее, и так далее…​

Как разработчик, вы уже должны задаваться вопросом: "Как я должен со всем этим справляться?"

К счастью для вас, вам не нужно разбираться во всём этом. Вам придётся разобраться в некоторой части, но не во всей. В частности, вам не нужно беспокоиться о физическом подключении (в нашем случае Ethernet и, возможно, PPP и т.д.). Также вам не нужно разбираться с протоколом IP или протоколом TCP.

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

Получив данные, вам предстоит решить, что с ними делать. В нашем случае потребуется понимание протокола HTTP и структуры файла PNG.

Используя аналогию, все межсетевые протоколы становятся серой зоной: не столько потому, что мы не понимаем, как они работают, а потому, что нас это больше не беспокоит. Интерфейс сокетов берёт на себя заботу об этой серой зоне:

Рисунок 2. Уровни протоколов, покрываемые сокетами

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

## 7.4. Модель сокетов

Сокеты BSD построены по базовой модели UNIX®: Все является файлом. Таким образом, в нашем примере сокеты позволят нам получить, образно говоря, HTTP-файл. Затем нам предстоит извлечь из него PNG-файл.

Из-за сложности межсетевого взаимодействия мы не можем просто использовать системный вызов `open` или функцию `open()` в языке C. Вместо этого необходимо выполнить несколько шагов для "открытия" сокета.

Однако, как только мы это сделаем, мы можем начать обращаться с сокетом так же, как и с любым файловым дескриптором: мы можем `читать` из него, `писать` в него, передавать его через `канал` и, в конечном итоге, `закрывать` его.

## 7.5. Основные функции сокетов

В то время как FreeBSD предлагает различные функции для работы с сокетами, нам требуется только четыре, чтобы "открыть" сокет. А в некоторых случаях достаточно двух.

### 7.5.1. Разница между клиентом и сервером

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

#### 7.5.1.1. Общие элементы

##### 7.5.1.1.1. `socket`

Функция, используемая как клиентами, так и серверами, это socket(2). Она объявляется следующим образом:

    int socket(int domain, int type, int protocol);

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

Аргумент `domain` указывает системе, какое семейство протоколов следует использовать. Существует множество семейств, некоторые из них специфичны для определённых поставщиков, другие широко распространены. Они объявлены в sys/socket.h.

Используйте `PF_INET` для UDP, TCP и других интернет-протоколов (IPv4).

Для аргумента `type` определено пять значений, также указанных в sys/socket.h. Все они начинаются с “SOCK_”. Наиболее распространённое — `SOCK_STREAM`, которое указывает системе, что запрашивается надёжный сервис потоковой доставки (это TCP при использовании с `PF_INET`).

Если бы вы запросили `SOCK_DGRAM`, вы бы запросили сервис доставки датаграмм без установления соединения (в нашем случае, UDP).

Если вы хотите управлять низкоуровневыми протоколами (такими как IP) или даже сетевыми интерфейсами (например, Ethernet), вам потребуется указать `SOCK_RAW`.

Наконец, аргумент `protocol` зависит от двух предыдущих аргументов и не всегда имеет смысл. В таком случае используйте значение `0`.

| Неподключенный сокет Нигде в функции `socket` мы не указали, к какой другой системе должны быть подключены. Наш только что созданный сокет остаётся неподключённым. Это сделано намеренно: если проводить аналогию с телефоном, мы только что подключили модем к телефонной линии. Мы не сказали модему совершить звонок или ответить, если телефон зазвонит.

##### 7.5.1.1.2. `sockaddr`

Различные функции семейства сокетов ожидают адрес (или указатель, если использовать терминологию языка C) небольшой области памяти. Различные объявления на языке C в файле sys/socket.h ссылаются на неё как на `struct sockaddr`. Эта структура объявлена в том же файле:

    /*
     * Structure used by kernel to store most
     * addresses.
     */
    struct sockaddr {
    	unsigned char	sa_len;		/* total length */
    	sa_family_t	sa_family;	/* address family */
    	char		sa_data[14];	/* actually longer; address value */
    };
    #define	SOCK_MAXADDRLEN	255		/* longest possible addresses */

Обратите внимание на неопределённость, с которой объявлено поле `sa_data` — просто как массив из `14` байт, с комментарием, намекающим, что их может быть больше `14`.

Эта неопределённость вполне преднамеренна. Сокеты — это очень мощный интерфейс. Хотя большинство людей, возможно, считают их не более чем интерфейсом для Интернета — и большинство приложений, вероятно, используют их именно для этого в наши дни — сокеты могут быть использованы практически для любого вида межпроцессного взаимодействия, из которых Интернет (или, точнее, IP) — лишь один из них.

sys/socket.h ссылается на различные типы протоколов, с которыми работают сокеты, как на семейства адресов, и перечисляет их непосредственно перед определением `sockaddr`:

    /*
     * Address families.
     */
    #define	AF_UNSPEC	0		/* unspecified */
    #define	AF_LOCAL	1		/* local to host (pipes, portals) */
    #define	AF_UNIX		AF_LOCAL	/* backward compatibility */
    #define	AF_INET		2		/* internetwork: UDP, TCP, etc. */
    #define	AF_IMPLINK	3		/* arpanet imp addresses */
    #define	AF_PUP		4		/* pup protocols: e.g. BSP */
    #define	AF_CHAOS	5		/* mit CHAOS protocols */
    #define	AF_NS		6		/* XEROX NS protocols */
    #define	AF_ISO		7		/* ISO protocols */
    #define	AF_OSI		AF_ISO
    #define	AF_ECMA		8		/* European computer manufacturers */
    #define	AF_DATAKIT	9		/* datakit protocols */
    #define	AF_CCITT	10		/* CCITT protocols, X.25 etc */
    #define	AF_SNA		11		/* IBM SNA */
    #define AF_DECnet	12		/* DECnet */
    #define AF_DLI		13		/* DEC Direct data link interface */
    #define AF_LAT		14		/* LAT */
    #define	AF_HYLINK	15		/* NSC Hyperchannel */
    #define	AF_APPLETALK	16		/* Apple Talk */
    #define	AF_ROUTE	17		/* Internal Routing Protocol */
    #define	AF_LINK		18		/* Link layer interface */
    #define	pseudo_AF_XTP	19		/* eXpress Transfer Protocol (no AF) */
    #define	AF_COIP		20		/* connection-oriented IP, aka ST II */
    #define	AF_CNT		21		/* Computer Network Technology */
    #define pseudo_AF_RTIP	22		/* Help Identify RTIP packets */
    #define	AF_IPX		23		/* Novell Internet Protocol */
    #define	AF_SIP		24		/* Simple Internet Protocol */
    #define	pseudo_AF_PIP	25		/* Help Identify PIP packets */
    #define	AF_ISDN		26		/* Integrated Services Digital Network*/
    #define	AF_E164		AF_ISDN		/* CCITT E.164 recommendation */
    #define	pseudo_AF_KEY	27		/* Internal key-management function */
    #define	AF_INET6	28		/* IPv6 */
    #define	AF_NATM		29		/* native ATM access */
    #define	AF_ATM		30		/* ATM */
    #define pseudo_AF_HDRCMPLT 31		/* Used by BPF to not rewrite headers
    					 * in interface output routine
    					 */
    #define	AF_NETGRAPH	32		/* Netgraph sockets */
    #define	AF_SLOW		33		/* 802.3ad slow protocol */
    #define	AF_SCLUSTER	34		/* Sitara cluster protocol */
    #define	AF_ARP		35
    #define	AF_BLUETOOTH	36		/* Bluetooth sockets */
    #define	AF_MAX		37

Используемый для IP — это AF_INET. Это символ для константы `2`.

Это семейство адресов, указанное в поле `sa_family` структуры `sockaddr`, определяет, как именно будут использоваться нечетко названные байты `sa_data`.

В частности, когда семейство адресов — AF_INET, можно использовать `struct sockaddr_in` из netinet/in.h везде, где ожидается `sockaddr`:

    /*
     * Socket address, internet style.
     */
    struct sockaddr_in {
    	uint8_t		sin_len;
    	sa_family_t	sin_family;
    	in_port_t	sin_port;
    	struct	in_addr sin_addr;
    	char	sin_zero[8];
    };

Мы можем визуализировать его организацию следующим образом:

Рисунок 3. Структура `sockaddr_in`

Три важных поля — это `sin_family`, которое находится в байте 1 структуры, `sin_port`, 16-битное значение, расположенное в байтах 2 и 3, и `sin_addr`, 32-битное целочисленное представление IP-адреса, хранящееся в байтах 4–7.

Теперь попробуем заполнить его. Предположим, мы пытаемся написать клиент для протокола daytime, который просто указывает, что его сервер записывает текстовую строку с текущей датой и временем в порт 13. Мы хотим использовать TCP/IP, поэтому нам нужно указать `AF_INET` в поле семейства адресов. `AF_INET` определен как `2`. Давайте используем IP-адрес `132.163.96.1`, который является сервером времени федерального правительства США (`time.nist.gov`).

Рисунок 4. Конкретный пример sockaddr_in

Кстати, поле `sin_addr` объявлено как имеющее тип `struct in_addr`, который определён в netinet/in.h:

    /*
     * Internet address (a structure for historical reasons)
     */
    struct in_addr {
    	in_addr_t s_addr;
    };

В дополнение, `in_addr_t` является 32-битным целым числом.

`132.163.96.1` — это просто удобная форма записи 32-битного целого числа, в которой перечисляются все его 8-битные байты, начиная с старшего.

До сих пор мы рассматривали `sockaddr` как абстракцию. Наш компьютер не хранит `short` целые числа как единую 16-битную сущность, а как последовательность 2 байт. Аналогично, он хранит 32-битные целые числа как последовательность 4 байт.

Предположим, мы написали что-то вроде этого:

    sa.sin_family      = AF_INET;
    sa.sin_port        = 13;
    sa.sin_addr.s_addr = (((((132 << 8) | 163) << 8) | 96) << 8) | 1;

Как будет выглядеть результат?

Ну, это, конечно, зависит от многого. На компьютере с процессором Pentium® или другим на базе x86 это будет выглядеть так:

Рисунок 5. `sockaddr_in` в системе с архитектурой Intel

На другой системе это может выглядеть так:

Рисунок 6. `sockaddr_in` в системе с порядком байтов от старшего к младшему

И на PDP это может выглядеть иначе. Однако два приведённых выше варианта являются наиболее распространёнными на сегодняшний день.

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

Почему?

Потому что при обмене данными с другим компьютером вы обычно не знаете, хранит ли он данные, начиная со старшего байта (MSB) или с младшего байта (LSB).

Вы можете задаться вопросом: "Значит, сокеты не будут это делать за меня?"

Не будут.

Хотя этот ответ может сначала вас удивить, помните, что общий интерфейс сокетов понимает только поля `sa_len` и `sa_family` структуры `sockaddr`. Вам не нужно беспокоиться о порядке байтов (конечно, в FreeBSD `sa_family` занимает всего 1 байт, но многие другие UNIX®-системы не имеют `sa_len` и используют 2 байта для `sa_family`, ожидая данные в том порядке, который является родным для компьютера).

Но остальные данные — это просто `sa_data[14]` с точки зрения сокетов. В зависимости от семейства адресов сокеты просто передают эти данные по назначению.

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

Аналогично, мы указываем IP-адрес, чтобы сообщить всем на пути, куда отправлять наши данные. Сокеты, опять же, просто пересылают их как данные.

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

Мы будем называть порядок байтов, который использует наш компьютер, порядком байтов хоста или просто хост-порядком.

Существует соглашение о передаче многобайтовых данных по IP старшим байтом вперёд. Это мы будем называть порядком байтов сети или просто сетевым порядком.

Вот, если бы мы скомпилировали приведённый выше код для компьютера на базе Intel, наш порядок байтов хоста выдал бы:

Рисунок 7. Порядок байтов на хосте в системе Intel

Но порядок байтов в сетевом формате требует, чтобы данные хранились начиная со старшего байта (MSB):

Рисунок 8. Порядок байтов в сети

К сожалению, наш порядок хоста полностью противоположен порядку сети.

У нас есть несколько способов решения этой проблемы. Один из них — инвертировать значения в нашем коде:

    sa.sin_family      = AF_INET;
    sa.sin_port        = 13 << 8;
    sa.sin_addr.s_addr = (((((1 << 8) | 96) << 8) | 163) << 8) | 132;

Это обманет наш компилятор, заставив его сохранить данные в порядке байтов сети. В некоторых случаях это именно тот способ, который нужен (например, при программировании на ассемблере). Однако в большинстве случаев это может вызвать проблему.

Предположим, вы написали программу на C, использующую сокеты. Вы знаете, что она будет работать на Pentium®, поэтому вводите все константы в обратном порядке и приводите их к порядку байтов сети. Она работает хорошо.

Затем, однажды, ваш надёжный старый Pentium® превращается в ржавый старый Pentium®. Вы заменяете его системой, у которой порядок байтов хоста совпадает с сетевым порядком байтов. Вам нужно перекомпилировать все ваше программное обеспечение. Все ваши программы продолжают работать хорошо, кроме той одной программы, которую вы написали.

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

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

К счастью, вы не первый, кто столкнулся с этой проблемой. Кто-то уже создал функции htons(3) и htonl(3) на языке C для преобразования `short` и `long` соответственно из порядка байтов хоста в порядок байтов сети, а также функции ntohs(3) и ntohl(3) на языке C для обратного преобразования.

На системах с порядком старший байт первый эти функции не выполняют никаких действий. На системах с порядком младший байт первый они преобразуют значения в правильный порядок.

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

#### 7.5.1.2. Функции клиента

Обычно клиент инициирует подключение к серверу. Клиент знает, к какому серверу он собирается обратиться: он знает его IP-адрес и порт, на котором работает сервер. Это похоже на то, как вы поднимаете трубку и набираете номер (адрес), а затем, когда кто-то отвечает, просите соединить со специалистом по непонятным символам (порт).

##### 7.5.1.2.1. `connect`

Как только клиент создал сокет, ему нужно подключить его к определённому порту на удалённой системе. Для этого используется connect(2):

    int connect(int s, const struct sockaddr *name, socklen_t namelen);

Аргумент `s` — это сокет, то есть значение, возвращаемое функцией `socket`. Аргумент `name` — это указатель на структуру `sockaddr`, которую мы подробно обсуждали. Наконец, `namelen` сообщает системе, сколько байт находится в нашей структуре `sockaddr`.

Если `connect` завершается успешно, он возвращает `0`. В противном случае возвращается `-1`, а код ошибки сохраняется в `errno`.

Существует множество причин, по которым `connect` может завершиться неудачей. Например, при попытке подключения к интернету, IP-адрес может не существовать, быть недоступен, перегружен или на указанном порту может не быть сервера. Или же подключение может быть явно отклонено по определённым причинам.

##### 7.5.1.2.2. Наш первый клиент

Теперь мы знаем достаточно, чтобы написать очень простого клиента, который получит текущее время от `132.163.96.1` и выведет его в stdout.

    /*
     * daytime.c
     *
     * Programmed by G. Adam Stanislav
     */
    #include <stdio.h>
    #include <string.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <unistd.h>

    int main() {
      int s, bytes;
      struct sockaddr_in sa;
      char buffer[BUFSIZ+1];

      if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        return 1;
      }

      memset(&sa, '\0', sizeof(sa));

      sa.sin_family = AF_INET;
      sa.sin_port = htons(13);
      sa.sin_addr.s_addr = htonl((((((132 << 8) | 163) << 8) | 96) << 8) | 1);
      if (connect(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
        perror("connect");
        close(s);
        return 2;
      }

      while ((bytes = read(s, buffer, BUFSIZ)) > 0)
        write(1, buffer, bytes);

      close(s);
      return 0;
    }

Вперед! Введите это в вашем редакторе, сохраните как daytime.c, затем скомпилируйте и запустите:

    % cc -O3 -o daytime daytime.c
    % ./daytime

    52079 01-06-19 02:29:25 50 0 1 543.9 UTC(NIST) *
    %

В данном случае дата была 19 июня 2001 года, время — 02:29:25 UTC. Естественно, ваши результаты могут отличаться.

#### 7.5.1.3. Функции сервера

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

Интерфейс сокетов предоставляет три основные функции для обработки этого.

##### 7.5.1.3.1. `bind`

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

Существует 65535 IP-портов, но сервер обычно обрабатывает запросы, поступающие только на один из них. Это как сказать оператору телефонной комнаты, что мы сейчас на месте и готовы отвечать на звонки по определённому внутреннему номеру. Мы используем bind(2), чтобы указать сокетам, на каком порту мы хотим обслуживать запросы.

    int bind(int s, const struct sockaddr *addr, socklen_t addrlen);

Помимо указания порта в `addr`, сервер может включать свой IP-адрес. Однако он может просто использовать символическую константу INADDR_ANY, чтобы указать, что будет обслуживать все запросы на указанный порт, независимо от его IP-адреса. Этот символ, наряду с несколькими аналогичными, объявлен в netinet/in.h

    #define	INADDR_ANY		(u_int32_t)0x00000000

Предположим, мы пишем сервер для протокола daytime поверх TCP/IP. Напомним, что он использует порт 13. Наша структура `sockaddr_in` будет выглядеть так:

Рисунок 9. Пример sockaddr_in сервера

##### 7.5.1.3.2. `listen`

Продолжая аналогию с офисным телефоном, после того как вы сообщили оператору АТС, на каком внутреннем номере вы будете находиться, вы заходите в свой офис и убеждаетесь, что ваш телефон подключен и звонок включен. Кроме того, вы активируете функцию ожидания вызова, чтобы слышать звонок даже во время разговора с кем-то.

Сервер обеспечивает все это с помощью функции listen(2).

    int listen(int s, int backlog);

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

##### 7.5.1.3.3. `accept`

После того как вы услышите телефонный звонок, вы принимаете вызов, отвечая на звонок. Теперь вы установили соединение с вашим клиентом. Это соединение остаётся активным, пока вы или ваш клиент не повесите трубку.

Сервер принимает соединение, используя функцию accept(2).

    int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

Обратите внимание, что в этот раз `addrlen` является указателем. Это необходимо, потому что в данном случае именно сокет заполняет структуру `addr` — `sockaddr_in`.

Возвращаемое значение является целым числом. Действительно, `accept` возвращает новый сокет. Этот новый сокет будет использоваться для обмена данными с клиентом.

Что происходит со старым сокетом? Он продолжает ожидать новые запросы (помните переменную `backlog`, которую мы передали в `listen`?), пока мы не закроем его (`close`).

Теперь новый сокет предназначен только для обмена данными. Он полностью подключен. Мы не можем снова передать его в `listen`, чтобы принимать дополнительные соединения.

##### 7.5.1.3.4. Наш первый сервер

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

Это лучше всего достигается созданием дочернего процесса после привязки порта. Затем основной процесс завершается и возвращает управление оболочке (или любой другой программе, которая его вызвала).

Дочерний процесс вызывает `listen`, затем запускает бесконечный цикл, который принимает соединение, обслуживает его и в конечном итоге закрывает свой сокет.

    /*
     * daytimed - a port 13 server
     *
     * Programmed by G. Adam Stanislav
     * June 19, 2001
     */
    #include <stdio.h>
    #include <string.h>
    #include <time.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>

    #define BACKLOG 4

    int main() {
        int s, c;
        socklen_t b;
        struct sockaddr_in sa;
        time_t t;
        struct tm *tm;
        FILE *client;

        if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
            perror("socket");
            return 1;
        }

        memset(&sa, '\0', sizeof(sa));

        sa.sin_family = AF_INET;
        sa.sin_port   = htons(13);

        if (INADDR_ANY)
            sa.sin_addr.s_addr = htonl(INADDR_ANY);

        if (bind(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
            perror("bind");
            return 2;
        }

        switch (fork()) {
            case -1:
                perror("fork");
                return 3;
            default:
                close(s);
                return 0;
            case 0:
                break;
        }

        listen(s, BACKLOG);

        for (;;) {
            b = sizeof sa;

            if ((c = accept(s, (struct sockaddr *)&sa, &b)) < 0) {
                perror("daytimed accept");
                return 4;
            }

            if ((client = fdopen(c, "w")) == NULL) {
                perror("daytimed fdopen");
                return 5;
            }

            if ((t = time(NULL)) < 0) {
                perror("daytimed time");
                return 6;
            }

            tm = gmtime(&t);
            fprintf(client, "%.4i-%.2i-%.2iT%.2i:%.2i:%.2iZ\n",
                tm->tm_year + 1900,
                tm->tm_mon + 1,
                tm->tm_mday,
                tm->tm_hour,
                tm->tm_min,
                tm->tm_sec);

            fclose(client);
        }
    }

Начинаем с создания сокета. Затем заполняем структуру `sockaddr_in` в `sa`. Обратите внимание на условное использование INADDR_ANY:

    if (INADDR_ANY)
            sa.sin_addr.s_addr = htonl(INADDR_ANY);

Его значение равно `0`. Поскольку мы только что использовали `memset`, чтобы установить в 0 все байты структуры, будет избыточным снова устанавливать его в `0`. Но если мы перенесём наш код на другую систему, где INADDR_ANY, возможно, не равен нулю, нам нужно будет присвоить его `sa.sin_addr.s_addr`. Большинство современных компиляторов C достаточно умны, чтобы заметить, что INADDR_ANY — это константа. Пока она равна нулю, они оптимизируют все условное выражение из кода.

После успешного вызова `bind` мы готовы стать демоном: используем `fork` для создания дочернего процесса. В обоих процессах, родительском и дочернем, переменная `s` является нашим сокетом. Родительскому процессу он больше не нужен, поэтому он вызывает `close`, затем возвращает `0`, чтобы сообщить своему родителю об успешном завершении.

Между тем, дочерний процесс продолжает работать в фоновом режиме. Он вызывает `listen` и устанавливает размер очереди ожидания (`backlog`) равным `4`. Здесь не требуется большое значение, так как daytime — это не протокол, который часто запрашивают клиенты, и, кроме того, он может мгновенно обрабатывать каждый запрос.

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

1. Вызовите `accept`. Он ожидает здесь, пока клиент не свяжется с ним. В этот момент он получает новый сокет, `c`, который можно использовать для обмена данными с этим конкретным клиентом.

2. Он использует функцию C `fdopen` для преобразования сокета из низкоуровневого дескриптора файла в указатель типа `FILE` в стиле C. Это позволит в дальнейшем использовать `fprintf`.

3. Он проверяет время и выводит его в формате ISO 8601 в «файл» `client`. Затем он использует `fclose` для закрытия файла. Это также автоматически закроет сокет.

Мы можем обобщить это и использовать в качестве модели для многих других серверов:

Рисунок 10. Последовательный Сервер

Эта блок-схема подходит для последовательных серверов, то есть серверов, которые могут обслуживать одного клиента за раз, как это было возможно с нашим daytime сервером. Это возможно только в тех случаях, когда между клиентом и сервером не происходит реального "диалога": как только сервер обнаруживает подключение клиента, он отправляет некоторые данные и закрывает соединение. Вся операция может занять наносекунды, и она завершена.

Преимущество этой блок-схемы в том, что, за исключением короткого момента после того, как родительский процесс выполняет `fork` и до его завершения, всегда активен только один процесс: Наш сервер не занимает много памяти и других системных ресурсов.

Обратите внимание, что мы добавили инициализацию демона в нашу блок-схему. Нам не нужно было инициализировать собственный демон, но это подходящее место в потоке выполнения программы для настройки обработчиков `signal`, открытия необходимых файлов и т. д.

Почти все элементы блок-схемы могут быть использованы буквально на множестве различных серверов. Элемент serve является исключением. Мы рассматриваем его как "чёрный ящик", то есть нечто, что вы проектируете специально для своего сервера и просто "подключаете к остальной системе."

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

Теперь сохраните приведённый исходный код в файл daytimed.c (обычно имена демонов оканчиваются буквой `d`). После компиляции попробуйте запустить его:

    % ./daytimed
    bind: Permission denied
    %

Что произошло? Как вы помните, протокол daytime использует порт 13. Однако все порты ниже 1024 зарезервированы для суперпользователя (в противном случае любой мог бы запустить демон, притворяясь, что обслуживает часто используемый порт, создавая угрозу безопасности).

Попробуйте снова, на этот раз как суперпользователь:

    # ./daytimed
    #

Что…​ Ничего? Давайте попробуем ещё раз:

    # ./daytimed

    bind: Address already in use
    #

Каждый порт может быть связан только одной программой одновременно. Наша первая попытка действительно была успешной: она запустила дочерний демон и завершилась без ошибок. Он продолжает работать и будет работать до тех пор, пока вы его не завершите командой kill, пока какой-либо из его системных вызовов не завершится с ошибкой или пока вы не перезагрузите систему.

Хорошо, мы знаем, что он работает в фоновом режиме. Но работает ли он? Как мы можем убедиться, что это настоящий сервер daytime? Просто:

    % telnet localhost 13

    Trying ::1...
    telnet: connect to address ::1: Connection refused
    Trying 127.0.0.1...
    Connected to localhost.
    Escape character is '^]'.
    2001-06-19T21:04:42Z
    Connection closed by foreign host.
    %

telnet попробовал использовать новый IPv6, но не смог. Затем он повторил попытку с IPv4, и это удалось. Демон работает.

Если у вас есть доступ к другой UNIX®-системе через telnet, вы можете использовать её для проверки удалённого доступа к серверу. Мой компьютер не имеет статического IP-адреса, поэтому я сделал следующее:

    % who

    whizkid          ttyp0   Jun 19 16:59   (216.127.220.143)
    xxx              ttyp1   Jun 19 16:06   (xx.xx.xx.xx)
    % telnet 216.127.220.143 13

    Trying 216.127.220.143...
    Connected to r47.bfm.org.
    Escape character is '^]'.
    2001-06-19T21:31:11Z
    Connection closed by foreign host.
    %

Снова, это сработало. Сработает ли это с использованием доменного имени?

    % telnet r47.bfm.org 13

    Trying 216.127.220.143...
    Connected to r47.bfm.org.
    Escape character is '^]'.
    2001-06-19T21:31:40Z
    Connection closed by foreign host.
    %

Кстати, telnet выводит сообщение Connection closed by foreign host после того, как наш демон закрыл сокет. Это показывает, что использование `fclose(client);` в нашем коде действительно работает, как заявлено.

## 7.6. Вспомогательные функции

Библиотека C в FreeBSD содержит множество вспомогательных функций для программирования сокетов. Например, в нашем примере клиента мы жёстко прописали IP-адрес `time.nist.gov`. Но мы не всегда знаем IP-адрес. Даже если знаем, наше программное обеспечение будет более гибким, если позволит пользователю ввести IP-адрес или даже доменное имя.

### 7.6.1. `gethostbyname`

Хотя нет возможности передать имя домена напрямую в какие-либо функции сокетов, стандартная библиотека C в FreeBSD предоставляет функции gethostbyname(3) и gethostbyname2(3), объявленные в netdb.h.

    struct hostent * gethostbyname(const char *name);
    struct hostent * gethostbyname2(const char *name, int af);

Оба возвращают указатель на структуру `hostent`, содержащую много информации о домене. Для наших целей поле `h_addr_list[0]` структуры указывает на `h_length` байтов правильного адреса, уже сохранённого в порядке байтов сети.

Это позволяет нам создать гораздо более гибкую — и гораздо более полезную — версию нашей программы daytime:

    /*
     * daytime.c
     *
     * Programmed by G. Adam Stanislav
     * 19 June 2001
     */
    #include <stdio.h>
    #include <string.h>
    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <netdb.h>

    int main(int argc, char *argv[]) {
      int s, bytes;
      struct sockaddr_in sa;
      struct hostent *he;
      char buf[BUFSIZ+1];
      char *host;

      if ((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket");
        return 1;
      }

      memset(&sa, '\0', sizeof(sa));

      sa.sin_family = AF_INET;
      sa.sin_port = htons(13);

      host = (argc > 1) ? argv[1] : "time.nist.gov";

      if ((he = gethostbyname(host)) == NULL) {
        herror(host);
        return 2;
      }

      memcpy(&sa.sin_addr, he->h_addr_list[0], he->h_length);

      if (connect(s, (struct sockaddr *)&sa, sizeof sa) < 0) {
        perror("connect");
        return 3;
      }

      while ((bytes = read(s, buf, BUFSIZ)) > 0)
        write(1, buf, bytes);

      close(s);
      return 0;
    }

Теперь мы можем ввести доменное имя (или IP-адрес, это работает в обоих направлениях) в командной строке, и программа попытается подключиться к его серверу daytime. В противном случае по умолчанию будет использоваться `time.nist.gov`. Однако даже в этом случае мы будем использовать `gethostbyname` вместо жёсткого указания `132.163.96.1`. Таким образом, даже если его IP-адрес изменится в будущем, мы всё равно сможем его найти.

Поскольку получение времени от локального сервера занимает практически нулевое время, вы можете запустить daytime дважды подряд: сначала для получения времени от `time.nist.gov`, а затем от вашей собственной системы. После этого вы можете сравнить результаты и увидеть, насколько точны часы вашей системы:

    % daytime ; daytime localhost

    52080 01-06-20 04:02:33 50 0 0 390.2 UTC(NIST) *
    2001-06-20T04:02:35Z
    %

Как видно, моя система опережала время NIST на две секунды.

### 7.6.2. `getservbyname`

Иногда вы можете быть не уверены, какой порт использует определённая служба. В таких случаях очень полезна функция getservbyname(3), также объявленная в netdb.h:

    struct servent * getservbyname(const char *name, const char *proto);

Структура `servent` содержит `s_port`, в котором находится соответствующий порт, уже в порядке байтов сети.

Если бы мы не знали правильный порт для службы daytime, мы могли бы найти его следующим образом:

    struct servent *se;
      ...
      if ((se = getservbyname("daytime", "tcp")) == NULL {
        fprintf(stderr, "Cannot determine which port to use.\n");
        return 7;
      }
      sa.sin_port = se->s_port;

Обычно порт известен. Но если вы разрабатываете новый протокол, вы можете тестировать его на неофициальном порту. Когда-нибудь вы зарегистрируете протокол и его порт (если не где-то ещё, то хотя бы в вашем /etc/services, где `getservbyname` ищет). Вместо возврата ошибки в приведённом выше коде вы просто используете временный номер порта. Как только вы добавите протокол в /etc/services, ваше программное обеспечение найдёт его порт без необходимости переписывать код.

## 7.7. Многозадачные серверы

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

Это требует значительных изменений в нашей блок-схеме:

Рисунок 11. Многозадачный сервер

Мы переместили службу из демона в её собственный серверный процесс. Однако, поскольку каждый дочерний процесс наследует все открытые файлы (а сокет обрабатывается так же, как файл), новый процесс наследует не только "принятый дескриптор", т.е. сокет, возвращённый вызовом `accept`, но и главный сокет, т.е. тот, который был открыт главным процессом в самом начале.

Однако серверному процессу этот сокет не нужен, и он должен немедленно вызвать ему `close`. Аналогично, демону больше не нужен сокет, принятый вызовом accept, и он не только должен, но и обязан вызвать ему `close` — в противном случае рано или поздно закончатся доступные файловые дескрипторы.

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

В UNIX® процесс на самом деле не завершается. Вместо этого он возвращается к своему родителю. Обычно родительский процесс `ждёт` (wait) завершения своего дочернего процесса и получает возвращаемое значение. Однако наш демон-процесс не может просто остановиться и ждать. Это бы свело на нет всю цель создания дополнительных процессов. Но если он никогда не выполняет `wait`, его дочерние процессы станут зомби — более не функционирующими, но всё ещё бродящими вокруг.

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

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

Изменено: 15 марта 2026 г. by Vladlen Popolitov

Назад
Главная
Далее