1. Основы сокетов и TCP в C: жизненный цикл и структуры данных
Жизненный цикл TCP-сервера и абстракция сокетов
Сетевое взаимодействие в современных операционных системах строится вокруг концепции сокетов (от англ. socket — разъём). В операционных системах семейства Linux и UNIX сокет представляет собой абстракцию, позволяющую приложению отправлять и получать данные так же, как если бы оно читало или писало в обычный файл.
С технической точки зрения сокет — это дескриптор файла (целое число), который операционная система связывает с сетевым соединением. Когда программа хочет отправить данные по сети, она просто записывает их в этот дескриптор, а всю сложную работу по маршрутизации, упаковке в пакеты и контролю доставки берёт на себя ядро ОС.
> Сокет — это конечная точка двустороннего канала связи между двумя программами, работающими в сети. Он привязывается к номеру порта, чтобы транспортный уровень мог идентифицировать приложение, которому предназначены данные. > > Основы сетевого программирования
Для создания надёжных серверов используется протокол TCP (Transmission Control Protocol). В отличие от UDP, TCP гарантирует доставку данных, сохраняет порядок следования байтов и автоматически запрашивает повторную передачу при потерях. Это делает его идеальным выбором для веб-серверов, баз данных и мессенджеров.
Этапы работы TCP-сервера
Жизненный цикл любого TCP-сервера на языке C состоит из строгой последовательности системных вызовов. Пропуск или неправильный порядок этих шагов приведёт к ошибке.
!Схема жизненного цикла TCP-соединения
Важнейшая концепция, которую часто упускают новички: сервер работает с двумя типами сокетов. Первый — это слушающий сокет (listening socket), который только принимает новые подключения. Второй — подключённый сокет (connected socket), который создаётся функцией accept для каждого нового клиента. Слушающий сокет никогда не передаёт данные, он работает как швейцар на входе, направляя гостей к их столикам (подключённым сокетам).
Структуры данных и сетевой байтовый порядок
Прежде чем вызывать функции API, необходимо разобраться с тем, как в C описываются сетевые адреса. Исторически сложилось так, что API сокетов разрабатывалось универсальным, чтобы поддерживать разные протоколы (IPv4, IPv6, локальные UNIX-сокеты).
Из-за этого в C используется своеобразный полиморфизм. Базовой структурой является sockaddr, но на практике для IPv4 используется sockaddr_in.
| Структура | Назначение | Особенности |
| :--- | :--- | :--- |
| struct sockaddr | Универсальная структура для любых адресов | Используется только для приведения типов (кастинга) в функциях bind и accept |
| struct sockaddr_in | Специфичная структура для протокола IPv4 | Содержит удобные поля для IP-адреса и порта. Легко заполняется программистом |
Определение структуры sockaddr_in выглядит так:
Проблема порядка байтов (Endianness)
Разные процессоры хранят многобайтовые числа в памяти по-разному. Архитектура x86 (Intel/AMD) использует порядок Little-Endian (младший байт по меньшему адресу). Однако в компьютерных сетях стандартом является Big-Endian (старший байт по меньшему адресу), который также называют сетевым байтовым порядком.
Представьте, что вы пишете дату. В России принято писать день-месяц-год (01.12.2023), а в США — месяц-день-год (12.01.2023). Если не договориться о едином формате, возникнет путаница. То же самое происходит с IP-адресами и портами.
Чтобы код работал корректно на любой архитектуре, перед записью порта и адреса в структуру их нужно конвертировать с помощью специальных функций:
htons() (Host TO Network Short) — конвертирует 16-битное число (порт).htonl() (Host TO Network Long) — конвертирует 32-битное число (IP-адрес).Номер порта может принимать значения в диапазоне . Порты до 1024 зарезервированы системой (например, 80 для HTTP), поэтому для своих серверов лучше использовать порты выше 1024.
Практическая реализация: шаг за шагом
Рассмотрим создание базового TCP-сервера, который принимает подключение и отправляет клиенту приветственное сообщение. Это фундамент, на котором в дальнейшем будет построена многопоточная архитектура.
Шаг 1: Создание сокета
Функция socket возвращает файловый дескриптор.
Здесь AF_INET указывает на использование IPv4, а SOCK_STREAM — на использование протокола TCP.
Шаг 2: Настройка SO_REUSEADDR
Это критически важная практика. Когда вы останавливаете сервер и сразу запускаете его снова, ОС может удерживать порт в состоянии TIME_WAIT ещё несколько минут. Попытка привязать сокет к этому порту выдаст ошибку "Address already in use". Чтобы избежать этого, устанавливаем опцию SO_REUSEADDR.
Шаг 3: Привязка (bind)
Заполняем структуру sockaddr_in и привязываем сокет к порту 8080 и всем доступным сетевым интерфейсам (INADDR_ANY).
Обратите внимание на приведение типа (struct sockaddr *)&address. Это классический паттерн сетевого программирования на C.
Шаг 4: Прослушивание (listen)
Переводим сокет в режим ожидания. Второй аргумент — это backlog, размер очереди ожидающих подключений. Если одновременно придут 15 клиентов, а backlog равен 10, последние 5 получат отказ в соединении, пока сервер не вызовет accept для первых.
Шаг 5: Принятие соединения (accept)
Функция accept блокирует выполнение программы до тех пор, пока не появится новый клиент. Как только клиент подключается, функция возвращает новый дескриптор.
Шаг 6: Обмен данными и закрытие
Теперь мы можем отправить данные клиенту через client_fd с помощью функции send и закрыть соединение.
Этот код описывает синхронный, однопоточный сервер. Он может обслужить только одного клиента за раз. Если первый клиент подключится и не закроет соединение, второй клиент будет висеть в очереди backlog, ожидая своей очереди. Именно поэтому для обслуживания множества клиентов одновременно применяются потоки (threads) или механизмы мультиплексирования (epoll), которые будут подробно разобраны в следующих частях курса.