Как работают сокеты

Обновлено: 21.11.2024

Почему WebSocket?

Обзор протокола

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

— RFC 6455 — Протокол WebSocket

Установление соединения WebSocket — рукопожатие открытия WebSocket

Соединения WebSocket могут быть установлены только с URI, которые следуют этой схеме. То есть, если вы видите URI со схемой ws:// (или wss:// ), то и клиент, и сервер ДОЛЖНЫ следовать протоколу подключения WebSocket, чтобы следовать спецификации WebSocket.

После того как клиент получает ответ сервера, открывается соединение WebSocket для начала передачи данных.

Протокол WebSocket

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

Я не буду описывать здесь каждую часть протокола кадра. Подробную информацию см. в RFC 6455. Вместо этого я расскажу о наиболее важных моментах, чтобы мы могли лучше понять протокол WebSocket.

Финбит

Первый бит заголовка WebSocket — это бит Fin. Этот бит устанавливается, если этот кадр является последними данными для завершения этого сообщения.

Биты RSV1, RSV2, RSV3

Эти биты зарезервированы для использования в будущем.

код операции

Каждый фрейм имеет код операции, который определяет, как интерпретировать полезные данные этого фрейма.

< td>0x08

Установка этого бита в 1 включает маскирование. Веб-сокеты требуют, чтобы вся полезная нагрузка была замаскирована с использованием случайного ключа (маски), выбранного клиентом. Ключ маскирования объединяется с данными полезной нагрузки с помощью операции XOR перед отправкой данных в полезную нагрузку. Эта маскировка не позволяет кешам неправильно интерпретировать кадры WebSocket как кэшируемые данные. Почему мы должны предотвратить кеширование данных WebSocket? Безопасность.

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

Длина полезной нагрузки

Поля «Длина полезной нагрузки» и «Расширенная длина полезной нагрузки» используются для кодирования общей длины данных полезной нагрузки для этого кадра. Если данные полезной нагрузки малы (менее 126 байт), длина кодируется в поле Payload len. По мере роста полезной нагрузки мы используем дополнительные поля для кодирования длины полезной нагрузки.

Маскирующий ключ

Как обсуждалось с битом MASK, все кадры, отправляемые клиентом на сервер, маскируются 32-битным значением, которое содержится в кадре. Это поле присутствует, если бит маски установлен в 1, и отсутствует, если бит маски установлен в 0.

Полезные данные

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

Закрытие соединения WebSocket — рукопожатие закрытия WebSocket

Чтобы закрыть соединение WebSocket, отправляется закрывающий кадр (код операции 0x08 ). В дополнение к коду операции кадр закрытия может содержать тело, указывающее причину закрытия. Если какая-либо сторона соединения получает кадр закрытия, она должна отправить кадр закрытия в ответ, и больше данные не должны передаваться по соединению. Как только обе стороны получают кадр закрытия, TCP-соединение разрывается.Сервер всегда инициирует закрытие соединения TCP.

Дополнительные ссылки

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

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

Для программиста сокет выглядит и ведет себя так же, как низкоуровневый файловый дескриптор. Это связано с тем, что такие команды, как read() и write(), работают с сокетами так же, как с файлами и каналами.

Сокеты были впервые представлены в 2.1BSD, а затем доработаны до их текущей формы в 4.2BSD. Функция сокетов теперь доступна в большинстве последних выпусков систем UNIX.

Где используется сокет?

Сокет Unix используется в среде клиент-серверных приложений. Сервер — это процесс, выполняющий некоторые функции по запросу клиента. Большинство протоколов прикладного уровня, таких как FTP, SMTP и POP3, используют сокеты для установления соединения между клиентом и сервером, а затем для обмена данными.

Типы сокетов

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

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

Потоковые сокеты — доставка в сетевом окружении гарантирована. Если вы отправите через потоковый сокет три элемента «A, B, C», они прибудут в том же порядке — «A, B, C». Эти сокеты используют TCP (протокол управления передачей) для передачи данных. Если доставка невозможна, отправитель получает индикатор ошибки. Записи данных не имеют границ.

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

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

Последовательные пакетные сокеты. Они похожи на потоковый сокет, за исключением того, что границы записи сохраняются. Этот интерфейс предоставляется только как часть абстракции сокетов сетевых систем (NS) и очень важен в большинстве серьезных приложений NS. Сокеты с последовательными пакетами позволяют пользователю манипулировать заголовками протокола последовательного пакета (SPP) или протокола дейтаграмм Интернета (IDP) в пакете или группе пакетов либо путем записи прототипа заголовка вместе с любыми данными, которые должны быть отправлены, либо путем указание заголовка по умолчанию, который будет использоваться со всеми исходящими данными, и позволяет пользователю получать заголовки входящих пакетов.

Что дальше?

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

В этом посте я собираюсь подробно объяснить, как стек TCP/IP работает в Linux. В частности, я рассмотрю, как системные вызовы сокетов взаимодействуют со структурами данных ядра и как ядро ​​взаимодействует с реальной сетью. Частично мотивация для этого поста состоит в том, чтобы объяснить, как работает переполнение очереди прослушивания, поскольку это связано с проблемой, над которой я работал на работе.

Как работают установленные соединения

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

Для каждого файлового дескриптора TCP, отслеживаемого ядром, существует структура, отслеживающая некоторую специфичную для TCP информацию (например, порядковые номера, текущий размер окна и т. д.), а также приемный буфер (или "очередь") и буфер записи (или «очередь»). Я буду использовать термины «буфер» и «очередь» как синонимы.Если вам интересно узнать больше, вы можете посмотреть реализацию структур сокетов в файле net/sock.h ядра Linux.

Когда на сетевой интерфейс (NIC) поступает новый пакет данных, ядро ​​уведомляется либо прерыванием от NIC, либо опросом NIC для получения данных. Как правило, работает ли ядро ​​в режиме прерывания или в режиме опроса, зависит от объема сетевого трафика; когда сетевая карта очень занята, для ядра более эффективно опрашивать, но если сетевая карта не занята, циклы ЦП и мощность можно сэкономить с помощью прерываний. Linux называет эту технику NAPI, буквально «Новый API».

Когда ядро ​​получает пакет от сетевого адаптера, оно декодирует пакет и выясняет, с каким TCP-соединением связан пакет, на основе исходного IP-адреса, исходного порта, целевого IP-адреса и порта назначения. Эта информация используется для поиска структуры sock в памяти, связанной с этим соединением. Предполагая, что пакет находится в последовательности, полезные данные затем копируются в приемный буфер сокета. В этот момент ядро ​​разбудит все процессы, выполняющие блокирующий read(2) или использующие системный вызов мультиплексирования ввода-вывода, такой как select(2) или epoll_wait(2), для ожидания сокета.

Когда процесс пользовательского пространства фактически вызывает read(2) для дескриптора файла, он заставляет ядро ​​удалить данные из буфера приема и скопировать эти данные в буфер, предоставленный системному вызову read(2).

Отправка данных работает аналогично. Когда приложение вызывает write(2), оно копирует данные из предоставленного пользователем буфера в очередь записи ядра. Впоследствии ядро ​​скопирует данные из очереди записи в сетевую карту и фактически отправит данные. Фактическая передача данных на сетевую карту может быть несколько задержана, когда пользователь действительно вызывает write(2), если сеть занята, если окно отправки TCP заполнено, если действуют политики формирования трафика и т. д.

Одним из следствий такого дизайна является то, что очереди приема и записи ядра могут заполниться, если приложение слишком медленно читает или записывает слишком быстро. Поэтому ядро ​​устанавливает максимальный размер очередей чтения и записи. Это гарантирует, что плохо работающие приложения будут использовать ограниченный объем памяти. Например, ядро ​​может ограничить размер каждой очереди приема и записи 100 КБ. Тогда максимальный объем памяти ядра, который может использовать каждый TCP-сокет, составит примерно 200 КБ (поскольку размер других структур данных TCP ничтожен по сравнению с размером очередей).

Читать семантику

Если буфер приема пустой и пользователь вызывает read(2) , системный вызов будет заблокирован до тех пор, пока данные не будут доступны.

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

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

Написать семантику

Если очередь записи не заполнена и пользователь вызывает write(2), системный вызов завершится успешно. Все данные будут скопированы, если в очереди записи достаточно места. Если в очереди записи есть место только для некоторых данных, произойдет частичная запись, и в буфер будет скопирована только часть данных. Вызывающий проверяет это, проверяя возвращаемое значение write(2) .

Если очередь записи полна и пользователь вызывает write(2), системный вызов будет заблокирован.

Как работает новое соединение

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

С точки зрения пользовательского пространства, вновь установленные TCP-соединения создаются вызовом accept(2) в слушающем сокете. Сокет прослушивания — это сокет, который был определен как таковой с помощью системного вызова listen(2).

Прототип accept(2) принимает сокет и два поля, хранящих информацию о другом конце сокета. Значение, возвращаемое accept(2), представляет собой целое число, представляющее дескриптор файла для нового установленного соединения:

Прототип listen(2) принимает дескриптор файла сокета и параметр backlog:

Выделение — это параметр, определяющий, сколько памяти ядро ​​будет резервировать для новых подключений, когда пользователь недостаточно быстро вызывает accept(2).

Первое, что может сделать ядро, — вообще не принимать соединение. Например, ядро ​​может просто отказаться подтверждать входящий пакет SYN.Чаще всего происходит то, что ядро ​​завершает трехстороннее рукопожатие TCP, а затем разрывает соединение с помощью RST. В любом случае результат один и тот же: не нужно выделять буферы приема или записи, если соединение отклонено. Аргумент в пользу этого заключается в том, что если процесс пользовательского пространства не принимает соединения достаточно быстро, правильно будет отклонить новые запросы. Аргумент против этого заключается в том, что это очень агрессивно, особенно если новые соединения со временем «нестабильны».

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

Аргумент в пользу второго варианта заключается в том, что он более щадящий, когда скорость обработки или скорость соединения имеют тенденцию к скачкам. Например, на только что описанном сервере представьте, что 10 новых подключений приходят одновременно, а затем до конца секунды больше нет подключений. Если ядро ​​ставит новые соединения в очередь, то все запросы будут обрабатываться в течение секунды. Если бы ядро ​​отклоняло новые подключения, то только одно из них было бы успешным, даже если процесс мог поддерживать совокупную скорость запросов.

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

Очереди прослушивания и переполнения

Как вы могли подозревать, ядро ​​на самом деле сочетает в себе эти два подхода. Ядро поставит в очередь новые соединения, но только определенное их количество. Количество подключений, которые ядро ​​будет ставить в очередь, контролируется параметром невыполненной работы для listen(2) . Обычно устанавливается относительно небольшое значение. В Linux заголовок socket.h устанавливает значение SOMAXCONN равным 128, и до ядра 2.4.25 это было максимально допустимое значение. В настоящее время максимальное значение указывается в /proc/sys/net/core/somaxconn , но обычно вы все равно найдете программы, использующие SOMAXCONN (или меньшее жестко закодированное значение).

Когда очередь прослушивания заполняется, новые подключения будут отклонены. Это называется переполнением очереди прослушивания. Вы можете наблюдать, когда это происходит, читая /proc/net/netstat и проверяя значение ListenOverflows. Это глобальный счетчик для всего ядра. Насколько мне известно, вы не можете получить статистику переполнения прослушивания для каждого сокета прослушивания.

Энтони Хеддингс

Энтони Хеддингс
Писатель

Энтони Хеддингс (Anthony Heddings) – штатный облачный инженер LifeSavvy Media, технический писатель, программист и эксперт по платформе Amazon AWS. Он написал сотни статей для How-To Geek и CloudSavvy IT, которые были прочитаны миллионы раз. Подробнее.

Shutterstock/ашаркью

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

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

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

Правильное название сокетов unix — Сокеты домена Unix, поскольку все они находятся на одном компьютере. В некотором смысле сокеты — это сеть, которая полностью содержится в ядре; вместо того, чтобы использовать сетевые интерфейсы для отправки данных, эти же данные можно отправлять напрямую между программами.

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

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

Как работают сокеты?

Сокеты просто предоставляют аппаратное обеспечение для перемещения данных. Сокеты на основе TCP называются потоковыми сокетами, куда все данные будут поступать по порядку. Сокеты на основе UDP — это сокеты дейтаграмм, где порядок (или даже доставка) не гарантируется. Существуют также необработанные сокеты, которые не имеют каких-либо ограничений и используются для реализации различных протоколов и утилит, которым необходимо проверять низкоуровневый сетевой трафик, например Wireshark.

Сокеты обычно по-прежнему используют TCP или UDP, поскольку они не представляют собой ничего особенного, кроме причудливого канала внутри ядра. TCP и UDP — это транспортные протоколы, которые определяют, как данные передаются с места на место, но на самом деле не заботятся о том, что это за данные. TCP и UDP обеспечивают платформу для большинства других протоколов, таких как FTP, SMTP и RDP, которые работают на более высоких уровнях.

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

Использование сокетов на практике

Сокеты Unix обычно используются в качестве альтернативы сетевым TCP-подключениям, когда процессы выполняются на одном компьютере. Данные обычно по-прежнему отправляются по тем же протоколам; он просто остается на той же машине и знает, что работает в том же домене (отсюда и название доменных сокетов UNIX), поэтому ему никогда не приходится беспокоить петлевой сетевой интерфейс для подключения к самому себе.

Ярчайший пример — Redis, чрезвычайно быстрое хранилище ключей и значений, полностью работающее в памяти. Redis часто используется на том же сервере, который обращается к нему, поэтому обычно вы можете использовать сокеты. При таких низких уровнях и скорости Redis сокеты обеспечивают повышение производительности на 25 % в некоторых синтетических тестах.

Если вы подключаетесь к базе данных MySQL, вы также можете использовать сокет. Обычно вы подключаетесь к host:port из удаленной системы, но если вы подключаетесь к базе данных на том же сервере (например, REST API обращается к базе данных), вы можете использовать сокеты для ускорения. Это не повлияет на обычное использование, но очень заметно под нагрузкой, более 20% на высокопроизводительном 24-ядерном процессоре со 128 одновременными пользователями и миллионом запросов в секунду. Увидите ли вы пользу от сокетов — это отдельная история, но в этот момент вы, вероятно, все равно захотите заняться репликацией и балансировкой нагрузки.

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

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

  • › CloudFoundry или Kubernetes: какую облачную платформу выбрать?
  • › Как развернуть веб-сервер Caddy с помощью Docker
  • › Как использовать Docker для упаковки приложений CLI
  • › Как развернуть сервер GitLab с помощью Docker
  • › Что будет в React 18?
  • › Что нового в TypeScript 4.6?

Вышеупомянутая статья может содержать партнерские ссылки, которые помогают поддерживать CloudSavvy IT.

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

Значение кода операции Описание
0x00 Этот фрейм продолжает полезную нагрузку из предыдущего фрейма.
0x01 Обозначает текстовый фрейм. Текстовые фреймы декодируются сервером в кодировке UTF-8.
0x02 Обозначает двоичный фрейм. Двоичные кадры доставляются сервером без изменений.
0x03-0x07 Зарезервировано для будущего использования.
Обозначает, что клиент желает закрыть соединение.
0x09 Кадр проверки связи. Служит механизмом сердцебиения, гарантируя, что соединение все еще живо. Получатель должен ответить pong.
0x0a Кадр pong. Служит механизмом сердцебиения, гарантируя, что соединение все еще живо. Получатель должен ответить кадром проверки связи.
0x0b-0x0f Зарезервировано для будущего использования.