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

Сокет (Socket) - это программная конечная точка сетевого соединения, через которую приложение может отправлять и получать данные.

Если провести аналогию с файловой системой, то:

  • файл - интерфейс для работы с диском;
  • сокет - интерфейс для работы с сетью.

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


Немного истории

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

Ситуация изменилась в начале 1980-х годов, когда в Калифорнийском университете в Беркли была разработана реализация TCP/IP для BSD UNIX. Вместе с ней появился программный интерфейс, получивший название Berkeley Sockets.

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

  • Linux;
  • BSD;
  • macOS;
  • Windows (Winsock является адаптацией Berkeley Sockets).

Сегодня практически любой сетевой язык программирования - будь то C, C++, Go, Rust или C# - внутри использует именно концепцию сокетов.


Почему нужен интерфейс сокетов?

Передача данных по сети - задача достаточно сложная. Необходимо:

  • разбивать данные на сегменты;
  • формировать IP-пакеты;
  • вычислять контрольные суммы;
  • отслеживать потерянные пакеты;
  • выполнять повторную передачу;
  • управлять перегрузкой сети;
  • устанавливать соединение;
  • закрывать соединение.

Все эти механизмы реализуются транспортным уровнем и нижележащими уровнями сетевого стека.

Программисту вовсе не нужно самостоятельно реализовывать TCP или UDP. Вместо этого он работает с простым набором функций:

  • создать сокет;
  • подключиться;
  • отправить данные;
  • получить данные;
  • закрыть соединение.

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

Именно поэтому говорят, что Socket API абстрагирует транспортный уровень, позволяя разработчику работать с сетью на более высоком уровне.

Важно понимать различие между протоколом и интерфейсом.

Протокол определяет:

  • формат сообщений;
  • правила передачи;
  • порядок установления соединения;
  • обработку ошибок.

Интерфейс определяет:

  • какие функции вызывает программист;
  • какие параметры они принимают;
  • какие данные возвращают.

Berkeley Sockets

Практически все реализации Socket API основаны на классическом интерфейсе Berkeley Sockets.

Рассмотрим основные функции.

socket()

Создает новый сокет.

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

Результатом является дескриптор сокета.

bind()

Связывает сокет с определенным IP-адресом и номером порта. Обычно используется сервером.

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

- listen()

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

- accept()

Принимает новое входящее соединение. 

Очень важно понимать, что после вызова accept() появляется новый сокет, предназначенный для общения именно с данным клиентом.

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

- connect()

Используется клиентом.

Функция инициирует установление соединения с удаленным сервером. Для TCP именно здесь начинается процедура трехстороннего рукопожатия (Three-Way Handshake).

- send()

Передает данные удаленной стороне.

Для TCP данные помещаются в поток передачи.

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

- receive()

Получает данные из сокета.

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

- close()

Закрывает соединение.

После вызова этой функции освобождаются системные ресурсы, связанные с сокетом. Для TCP также выполняется корректное завершение соединения.


Source: Orkhan Alishov's notes