Выберите posix вызовы для работы с сокетами

Обновлено: 03.07.2024

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

Интерфейс сокетов впервые появился в BSD Unix. Программный интерфейс сокетов описан в

стандарте POSIX.1 и в той или иной мере поддерживается всеми современными операционными


Сокеты могут динамически создаваться и уничтожаться. При создании сокета вызывающему

процессу возвращается дескриптор файла, который используется для установки или разрыва

соединения, а также чтения и записи данных.

Каждый сокет поддерживает определенный тип работы в сети, указываемый при его создании.

Наиболее распространенными типами сокетов являются:

· Надежный, ориентированный на соединение байтовый поток.

· Надежный, ориентированный на соединение поток пакетов.

· Ненадежная передача пакетов.

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

эквивалент «трубы» (канала между процессами на одной машине). Байты подаются в канал с

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

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

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

512 байт, а получатель запрашивает 2560 байт по сокету типа 1, он получит все 2560 байт сразу.

При использовании сокета типа 2 ему будут выданы только первые 512 байт. Чтобы получить

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

еще четыре раза.

Третий тип сокетапредоставляет пользователю доступ к «голой» сети. Этот тип сокета особенно полезен для приложений реального времени и ситуаций, в которых пользователь хочет реализовать специальную схему обработки ошибок. Сеть может терять пакеты или доставлять их в неверном порядке. В отличие от сокетов первых двух типов, сокет типа 3 не предоставляет никаких гарантий доставки. Преимущество этого режима заключается в более высокой производительности, которая в некоторых ситуациях оказывается важнее надежности (например, для доставки мультимедиа, при которой скорость ценится существенно выше, нежели сохранность данных по дороге).

При создании сокета один из параметров указывает протокол, используемый для него. Для надежных байтовых потоков, как правило, используется протокол TCP (Transmission Control

Protocol – протокол управления передачей). Для ненадежной передачи пакетов обычно применяется протокол UDP (User Data Protocol – пользовательский протокол данных). Для надежного потока пакетов специального протокола нет.

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

Этот адрес может принадлежать к одному из нескольких пространств адресов. Наиболее распространенным пространством является пространство адресов Интернета, использующее 32-

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

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


Организация стока поверхностных вод: Наибольшее количество влаги на земном шаре испаряется с поверхности морей и океанов (88‰).


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

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


Опора деревянной одностоечной и способы укрепление угловых опор: Опоры ВЛ - конструкции, предназначен­ные для поддерживания проводов на необходимой высоте над землей, водой.

В данной статье будет рассмотрено понятие сокета в операционной системе Linux: основные структуры данных, как они работают и можно ли управлять состоянием сокета с помощью приложения. В качестве практики будут рассмотрены инструменты netcat и socat.

Что такое сокет?

Сокет - это абстракция сетевого взаимодействия в операционной системе Linux. Каждому сокету соответствует пара IP-адрес + номер порта. Это стандартное определение, к которому привыкли все, спасибо вики. Хотя нет, вот здесь лучше описано. Поскольку сокет является только лишь абстракцией, то связка IP-адрес + номер порта - это уже имплементация в ОС. Верное название этой имплементации - "Интернет сокет". Абстракция используется для того, чтобы операционная система могла работать с любым типом канала передачи данных. Именно поэтому в ОС Linux Интернет сокет - это дескриптор, с которым система работает как с файлом. Типов сокетов, конечно же, намного больше. В ядре ОС Linux сокеты представлены тремя основными структурами:

struct socket - представление сокета BSD, того вида сокета, который стал основой для современных "Интернет сокетов";

struct sock - собственная оболочка, которая в Linux называется "INET socket";

struct sk_buff - "хранилище" данных, которые передает или получает сокет;

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

socket - создание сокета;

bind - действие используется на стороне сервера. В стандартных терминах - это открытие порта на прослушивание, используя указанный интерфейс;

listen - используется для перевода сокета в прослушивающее состояние. Применяется к серверному сокету;

connect - используется для инициализации соединения;

accept - используется сервером, создает новое соединение для клиента;

send/recv - используется для работы с отправкой/приемом данных;

close - разрыв соединения, уничтожение сокета.

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

netcat

Оригинальная утилита появилась 25 лет назад, больше не поддерживается. На cегодняшний день существуют порты, которые поддерживаются различными дистрибутивами: Debian, Ubuntu, FreeBSD, MacOS. В операционной системе утилиту можно вызвать с помощью команды nc, nc.traditional или ncat в зависимости от ОС. Утилита позволяет "из коробки" работать с сокетами, которые используют в качестве транспорта TCP и UDP протоколы. Примеры сценариев использования, которые, по мнению автора, наиболее интересны:

перенаправление входящих/исходящих запросов;

трансляция данных на экран в шестнадцатеричном формате.


Введем команду на открытие порта на машине Destination: nc -ulvvp 7878

Запускаем соединение из машины Source: nc 10.0.2.4 4545

В итоге получаем возможность читать данные от машины Source:


В машине Destination:


Пример с трансляцией данных в шестнадцатеричном формате можно провести так же, но заменить команду на Destination или добавить еще один пайп на Repeater:

nc -l -p 4545 -o file

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


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

socat

STDIO -> TCP Socket;

FILE -> TCP Socket;

TCP Socket -> Custom Application;

UDP Socket -> Custom Application;

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

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

Чтобы комфортно пользоваться этим инструментом, нужно запомнить шаблон командной строки, который ожидает socat:

socat additionalOptions addr1 addr2

additionalOptions - опции, которые могут добавлять возможности логирования информации, управления направлением передачи данных;

addr1 - источник данных или приемник (влияет использование флага U или u), это может быть сокет, файл, пайп или виртуальный терминал;

addr2 - источник данных или приемник (влияет использование флага U или u), это может быть сокет, файл, пайп или виртуальный терминал;

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

Например, чтобы использовать socat как netcat в качестве TCP сервера, можно запустить вот такую команду:

socat TCP-LISTEN:4545, STDOUT

Для коннекта можно использовать netcat:

nc localhost 4545


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

socat TCP-LISTEN:4545,reuseaddr,keepalive,fork STDOUT

Дополнительные параметры распространяются на те действия, которые socat может выполнять по отношению к адресу. Полный список опций можно найти здесь в разделе "SOCKET option group".

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

Статья написана в преддверии старта курса Network engineer. Basic. Всех, кто желает подробнее узнать о курсе и карьерных перспективах, приглашаем записаться на день открытых дверей, который пройдет уже 4 февраля.

Файловый дескриптор, возвращаемый вызовом socket, может использоваться в следующих целях:

сервером - для приема входящих соединений системным вызовом accept;

клиентом – для выполнения операций ввода-вывода после того, как соединение будет установлено вызовом connect.

После создания сокета сервер должен присвоить ему адрес системным вызовом bind:

bind - присваивает адрес сокету

int bind (

int socket_fd, /* файловый дескриптор сокета */

const struct sockaddr *sa, /* адрес сокета */

socklen_t sa_len /* длина адреса */

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

В большинстве систем для адресов из домена AF_UNIX системный вызов bind создает новый файл сокета - он не может повторно использовать существующий файл. Чтобы подготовить сокет к приему входящих соединений, сервер должен обратиться к вызову listen:

listen - подготавливает сокет к приему запросов на соединение и устанавливает ограничение на размер очереди запросов

int listen (

int socket_fd, /* файловый дескриптор сокета */

int backlog /* максимальное число запросов на соединение в очереди */

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

Запросы на соединение помещаются в очередь, где дожидаются обработки. Второй аргумент backlog ограничивает размер этой очереди. Когда очередь заполнена до отказа, системный вызов connect на стороне клиента возвращает признак ошибки с кодом ECONNREFUSED.

Прием соединений на стороне сервера выполняется системным вызовом accept:

accept - принимает новое соединение и создает новый сокет

int accept (

int socket_fd, /* файловый дескриптор сокета */

struct sockaddr *sa, /* адрес сокета или NULL*/

socklen_t *sa_len /* длина адреса */

/* Возвращает файловый дескриптор или -1 в случае ошибки (код ошибки - в errno) */

Обычно системный вызов accept блокируется до поступления запроса на соединение от другого процесса, после чего создает новый сокет для принятого соединения и возвращает новый файловый дескриптор. Если аргумент sa не NULL, по указанному адресу записываются сведения о сокете, соединение с которым было принято. При этом в аргумент sa_len передается указатель на переменную, которая хранит размер области памяти, отведенной под сведения о сокете. По возвращении из системного вызова эта переменная будет содержать фактический объем данных, записанных в sa. Клиент после создания сокета вызывает connect, которому передает адрес сервера:

connect - устанавливает соединение

int connect (

int socket_fd, /* файловый дескриптор сокета */

const struct sockaddr *sa, /* адрес сокета */

socklen_t sa_len /* длина адреса */

/* Возвращает 0 в случае успеха или -1 в случае ошибки (код ошибки - в errno) */

Системный вызов connect блокируется до того момента, как запрос на соединение будет принят сервером, но он не возвращает новый файловый дескриптор – клиент выполняет операции ввода-вывода над прежним дескриптором. Если установлен флаг O_NONBLOCK, вызов connect не будет ожидать установления соединения, а сразу же вернет признак ошибки с кодом EINPROGRESS. Запрос на соединение при этом не теряется, а помещается в очередь. Все последующие обращения к connect в этот период будут заканчиваться ошибкой с кодом EALREADY. После того как соединение будет установлено, то можно использовать файловый дескриптор по своему усмотрению.

API для ввода-вывода в современных ОС позволяют обрабатывать сотни и тысячи одновременно открытых сетевых запросов либо файлов. Для этой цели не нужно создавать множество потоков - достаточно запустить специальный цикл событий на одном потоке, начав мультиплексирование ввода-вывода

Содержание

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

  • Что означают понятия “неблокирующий”, “асинхронный”, “событийный” для ввода-вывода
  • Смысл добавления флага O_NONBLOCK для файловых дескрипторов через fcntl
  • Почему неблокирующий ввод-вывод часто сочетается с мультиплексированием через select , epoll и kqueue
  • Как неблокирующий режим ввода-вывода взаимодействует со средствам опроса дескрипторов, такими как epoll

Термины: неблокирующий, асинхронный, событийный

Блокирующий режим

По умолчанию все файловые дескрипторы в Unix-системах создаются в “блокирующем” режиме. Это означает, что системные вызовы для ввода-вывода, такие как read , write или connect , могут заблокировать выполнение программы вплоть до готовности результата операции. Легче всего понять это на примере чтения данных из потока stdin в консольной программе. Как только вы вызываете read для stdin, выполнение программы блокируется, пока данные не будут введены пользователем с клавиатуры и затем прочитаны системой. То же самое происходит при вызове функций стандартной библиотеки, таких как fread , getchar , std::getline , поскольку все они в конечном счёте используют системный вызов read . Если говорить конкретнее, ядро погружает процесс в спящее состояние, пока данные не станут доступны в псевдо-файле stdin. То же самое происходит и для любых других файловых дескрипторов. Например, если вы пытаетесь читать из TCP-сокета, вызов read заблокирует выполнение, пока другая сторона TCP-соединения не пришлёт ответные данные.

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

  • неблокирующий режим ввода-вывода
  • мультиплексирование с помощью системного API, такого как select либо epoll

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

Неблокирующий режим (O_NONBLOCK)

Файловый дескриптор помещают в “неблокирующий” режим, добавляя флаг O_NONBLOCK к существующему набору флагов дескриптора с помощью fcntl :

С момента установки флага дескриптор становится неблокирующим. Любые системные вызовы для ввода-вывода, такие как read и write , в случае неготовности данных в момент вызова ранее могли бы вызвать блокировку, а теперь будут возвращать -1 и глобальную переменную errno устанавливать в EWOULDBLOCK , Это интересное изменение поведения, но само по себе мало полезное: оно лишь является базовым примитивом для построения эффективной системы ввода-вывода для множества файловых дескрипторов.

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

Такой подход работает, но имеет свои минусы:

  • Если данные приходят очень медленно, программа будет постоянно просыпаться и тратить впустую процессорное время
  • Когда данные приходят, программа, возможно, не прочитает их сразу, т.к. выполнение приостановлено из-за nanosleep
  • Увеличение интервала сна уменьшит бесполезные траты процессорного времени, но увеличит задержку обработки данных
  • Увеличение числа файловых дескрипторов с таким же подходом к их обработке увеличит долю расходов на проверки наличия данных

Для решения этих проблем операционная система предоставляет мультиплексирование ввода-вывода.

Мультиплексирование ввода-вывода (select, epoll, kqueue и т.д.)

Существует несколько мультиплексирующих системных вызовов:

  • Вызов select существует во всех POSIX-совместимых системах, включая Linux и MacOSX существует только на Linux существует на FreeBSD и других *BSD

Все три варианта реализуют единый принцип: делегировать ядру задачу по отслеживанию прихода данных для операций чтения/записи над множеством файловых дескрипторов. Все варианты могут заблокировать выполнение, пока не произойдёт какого-либо события с одним из дескрипторов из указанного множества. Например, вы можете сообщить ядру ОС, что вас интересуют только события чтения для файлового дескриптора X, события чтения-записи для дескриптора Y, и только события записи - для Z.

Все мультиплексирующие системные вызовы, как правило, работают независимо от режима файлового дескриптора (блокирующего или неблокирующего). Программист может даже все файловые дескрипторы оставить блокирующими, и после select либо epoll возвращённые ими дескрипторы не будут блокировать выполнение при вызове read или write , потому что данные в них уже готовы. Есть важное исключение для epoll , о котором скажем далее.

Как O_NONBLOCK сочетается с мультиплексером select

Допустим, мы пишем простую программу-daemon, обслуживающее клиентские приложения через сокеты. Мы воспользуемся мультиплексером select и блокирующими файловыми дескрипторами. Для простоты предположим, что мы уже открыли файлы и добавили их в переменную read_fds , имеющую тип fd_set (то есть “набор файлов”). Ключевым элементом цикла событий, обрабатывающего файл, будет вызов select и дальнейшие вызовы read для каждого из дескрипторов в наборе.

Тип данных fd_set представляет просто массив файловых дескрипторов, где с каждым дескриптором связан ещё и флаг (0 или 1). Примерно так могло бы выглядеть объявление:

Функция select принимает несколько объектов fd_set . В простейшем случае мы передаём один fd_set с набором файлов для чтения, а select модифицирует их, проставляя флаг для тех дескрипторов, из которых можно читать данные. Также функция возвращает число готовых для обработки файлов. Далее с помощью макроса FD_ISSET(index, &set) можно проверить, установлен ли флаг, т.е. можно ли читать данные без блокировки.

Такой подход работает, но давайте предположим, что размер буфера buf очень маленький, а объём пакетов данных, читаемых из дескрипторов файлов, очень велик. Например, в примере выше размер buf всего 1024 байта, допустим что через сокеты приходят пакеты по 64КБ. Для обработки одного пакета потребуется 64 раза вызвать select , а затем 64 раза вызвать read . В итоге мы получим 128 системных вызовов, но каждый вызов приводит к одному переключению контекста между kernel и userspace, в итоге обработка пакета обходится дорого.

Можем ли мы уменьшить число вызовов select ? В идеале, для обработки одного пакета мы хотели бы вызвать select только один раз. Чтобы сделать это, потребуется перевести все файловые дескрипторы в неблокирующий режим. Ключевая идея - вызывать read в цикле до тех пор, пока вы не получите код ошибки EWOULDBLOCK , обозначающий отсутствие новых данных в момент вызова. Идея реализована в примере:

В этом примере при наличии буфера в 1024 байта и входящего пакета в 64КБ мы получим 66 системных вызовов: select будет вызван один раз, а read будет вызываться 64 раза без каких-либо ошибок, а 65-й раз вернёт ошибку EWOULDBLOCK .

Мультиплексер epoll в режиме edge-triggered

Группа вызовов epoll является наиболее развитым мультиплексером в ядре Linux и способна работать в двух режимах:

  • level-triggered - похожий на select упрощённый режим, в котором файловый дескриптор возвращается, если остались непрочитанные данные
    • если приложение прочитало только часть доступных данных, вызов epoll вернёт ему недопрочитанные дескрипторы
    • если приложение прочитало только часть доступных данных, в данном режиме оно всё равно будет заблокировано до прихода каких-либо новых данных

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

    • В случае level-triggered режима вызов epoll_wait пройдёт по списку файловых дескрипторов и проверит, не соблюдается ли в данный момент условие, которое интересует приложение, что может привести к возврату из epoll_wait без какой-либо блокировки.
    • В случае edge-triggered режима ядро пропускает такую проверку и усыпляет поток, пока не обнаружит событие прихода данных на одном из файловых дескрипторов, за которыми следит поток. Такой режим превращает epoll в мультиплексер с алгоритмической сложностью O(1): после прихода новых данных из файла ядро сразу же знает, какой процесс надо пробудить.

    Читайте также: