В современных операционных системах (например, POSIX-системы: Linux, BSD, macOS) сетевое взаимодействие организовано по принципу разделения:
-
Ядро ОС (kernel space)
- Реализует сетевой стек (TCP/IP, UDP, ICMP и т.д.).
- Обрабатывает пакеты, маршрутизацию, контроль над протоколами и очередями.
-
Пространство пользователя (user space)
- Обычные программы (процессы) взаимодействуют с сетью через системные вызовы.
- Основной интерфейс для сетевого программирования — API сокетов (Sockets API).
Сокет — программная абстракция, представляющая конечную точку сетевого соединения.
Через сокеты пользовательские программы могут:
- устанавливать соединения (TCP);
- отправлять и получать датаграммы (UDP);
- обрабатывать входящие соединения (серверы);
- работать с различными протоколами (Internet, Unix-domain sockets и т.п.).
Сокет — это объект, который:
- ассоциирован с:
- типом протокола (потоковый/датаграммный),
- адресом (IP-адрес + порт для Internet-сокетов);
- имеет дескриптор — целое число, которое процесс использует так же, как файловый дескриптор.
В POSIX:
- сокеты являются частным случаем файловых дескрипторов:
- можно использовать
read(),write(),close()наряду со специальными вызовамиsend(),recv()и др.
- можно использовать
Для интернет-сокетов (семейство AF_INET / AF_INET6):
- адрес сокета определяется:
- IP-адресом (IPv4 или IPv6),
- номером порта (целое число, например 80, 443, 8080).
- порт → логический идентификатор приложения на узле.
Адрес записывается как:
IP:ПОРТ, например192.168.1.10:8080.
Чаще всего:
AF_INET— IPv4-сокеты (Internet);AF_INET6— IPv6-сокеты;AF_UNIX(илиAF_LOCAL) — локальные Unix-сокеты (общение процессов на одной машине через файловую систему);- другие (менее частые):
AF_PACKET,AF_NETLINKи т.д.
Основные типы:
-
SOCK_STREAM— потоковый сокет:- ориентирован на соединение;
- обычно использует протокол TCP;
- обеспечивает надёжную доставку, контроль очередности, отсутствие дубликатов;
- данных как непрерывный поток байтов.
-
SOCK_DGRAM— датаграммный сокет:- без установления соединения;
- обычно использует протокол UDP;
- доставка не гарантируется (потери, дублирование, изменение порядка);
- данные передаются отдельными сообщениями (датаграммами).
Другие типы (для полноты):
SOCK_RAW— сырые сокеты для работы на уровне IP-пакетов;SOCK_SEQPACKETи др. — реже применяются в базовом курсе.
Далее рассматриваем классический POSIX-сокетный интерфейс (на примерах на C-подобном псевдокоде).
Создаёт сокет и возвращает его дескриптор.
int sockfd = socket(int domain, int type, int protocol);Параметры:
domain— семейство адресов:AF_INET,AF_INET6,AF_UNIX, ...
type— тип сокета:SOCK_STREAM,SOCK_DGRAM, ...
protocol— конкретный протокол (обычно 0, чтобы выбрать по умолчанию для данного domain/type).
Результат:
- При успехе: целое число — дескриптор сокета.
- При ошибке:
-1.
Ассоциирует сокет с конкретным локальным адресом (IP/порт или файловый путь для Unix-сокетов).
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);- Для сервера важно закрепить сокет за определённым портом, чтобы клиенты знали, куда подключаться.
Пример (IPv4, порт 8080):
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY); // слушать на всех интерфейсах
addr.sin_port = htons(8080);
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));Переводит потоковый сокет в режим прослушивания (для сервера).
int listen(int sockfd, int backlog);backlog— максимальная длина очереди входящих подключений.
После listen():
- сокет готов принимать соединения через
accept().
Принимает входящее соединение на сокете, находящемся в состоянии прослушивания (listen).
int client_fd = accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);Результат:
- при успешном установлении соединения:
- возвращает новый сокет
client_fd, ассоциированный с конкретным клиентом; - исходный
sockfdостаётся слушать новые подключения;
- возвращает новый сокет
- при ошибке —
-1.
Можно получить данные о подключившемся клиенте через addr (IP, порт).
Используется клиентом для установки соединения с сервером (для потоковых сокетов).
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);sockfd— сокет, созданный черезsocket();addr— адрес сервера.
После успешного connect():
- сокет готов к обмену данными (
send()/recv()илиwrite()/read()).
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);send()отправляет данные;recv()получает данные.
Особенности:
- могут передать/принять меньше байт, чем запрошено — важно проверять возвращаемое значение;
flagsобычно 0, но могут указывать особенности (например,MSG_DONTWAIT,MSG_OOB).
Аналогично можно использовать:
write(sockfd, buf, len)read(sockfd, buf, len)
для простого случая.
Для UDP (без предварительного connect()):
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);sendto()— отправляет датаграмму на указанный адрес;recvfrom()— получает датаграмму и при необходимости сообщает, откуда она.
int close(int sockfd);
int shutdown(int sockfd, int how);close()— закрывает сокет (как и любой файловый дескриптор).shutdown()— позволяет закрыть только направление:SHUT_RD— закрыть чтение;SHUT_WR— закрыть запись (отправка FIN);SHUT_RDWR— закрыть и чтение, и запись.
Стандартная последовательность действий:
socket(AF_INET, SOCK_STREAM, 0)— создать сокет.bind()— привязать к адресу (IP/порт).listen()— начать слушать.- В цикле:
accept()— принять новое соединение (получаемclient_fd).- для
client_fd:recv()/send()— обмен данными;close(client_fd)— завершить соединение.
Псевдокод:
int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, SOMAXCONN);
for (;;) {
int c = accept(s, ...);
// обработка клиента
recv(c, buf, ...);
send(c, ...);
close(c);
}socket(AF_INET, SOCK_STREAM, 0)— создать сокет.connect()— подключиться к серверу.send()/recv()илиwrite()/read()— обмен данными.close()— закрыть сокет.
Для аккуратного завершения обмена можно предварительно вызвать shutdown() и закрыть только половину соединения (только чтение или только запись).
Псевдокод:
int s = socket(AF_INET, SOCK_STREAM, 0);
connect(s, ...);
send(s, ...);
recv(s, ...);
close(s);- Сервер:
socket(AF_INET, SOCK_DGRAM, 0);bind()на нужный порт;recvfrom()иsendto()в цикле.
- Клиент:
socket(AF_INET, SOCK_DGRAM, 0);sendto()на адрес сервера;- при необходимости —
recvfrom()для ответа.
netstat/ss— посмотреть открытые порты и текущие соединения;python -m http.server <port>— быстро поднять HTTP‑эндпоинт для теста;telnet <host> <port>илиnc— вручную установить TCP-соединение и убедиться, что обмен байтами работает.
По умолчанию вызовы:
accept(),connect(),recv(),send()и др.
работают в блокирующем режиме:
- если данных нет,
recv()ждёт; - если нет входящих соединений,
accept()ждёт; - если соединение устанавливается,
connect()может ожидать.
Удобно для простых программ, но:
- неэффективно, если нужно обслуживать множество клиентов в одном потоке.
Можно перевести сокет в неблокирующий режим:
- через
fcntl()илиioctl():int flags = fcntl(sockfd, F_GETFL, 0); fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
В этом режиме:
- операции ввода/вывода завершаются немедленно;
- если операция не может быть выполнена — возвращается ошибка (обычно
EWOULDBLOCKилиEAGAIN).
Это позволяет:
- не блокировать поток ожиданием данных;
- использовать опрос и мультиплексирование.
Для одновременной работы с несколькими сокетами:
- используются системные вызовы мультиплексирования:
select()— классический, может работать с множеством дескрипторов, но имеет ограничения по числу дескрипторов.poll()— более гибкий, без жёсткого лимита.epoll(Linux) — эффективен для большого числа соединений:epoll_create(),epoll_ctl(),epoll_wait().
Идея:
- регистрировать интерес к событиям (чтение/запись/ошибка) на сокетах;
- ждать, пока какой-то сокет «готов»;
- затем выполнять операции на этом сокете.
Это основа для серверов с большим количеством клиентов (напр. веб-серверы).
Для IPv4:
struct sockaddr_in {
sa_family_t sin_family; // AF_INET
in_port_t sin_port; // порт (сетевой порядок байт)
struct in_addr sin_addr; // адрес
unsigned char sin_zero[8];
};Для общего интерфейса используется абстрактная:
struct sockaddr {
sa_family_t sa_family;
char sa_data[14];
};Поэтому часто применяют приведение типов:
(struct sockaddr*)&addr.
Сетевой порядок байтов — big-endian.
Хост может быть little-endian (чаще всего x86).
Функции:
htons()— host to network short (16 бит);htonl()— host to network long (32 бита);ntohs(),ntohl()— обратные.
Используются для портов и IPv4-адресов в структуре sockaddr_in.
Многие современные программы используют:
int getaddrinfo(const char *node, const char *service,
const struct addrinfo *hints,
struct addrinfo **res);Позволяет:
- по имени хоста и строке порта (
"80") получить список возможных структурsockaddr, универсально поддерживающий IPv4 и IPv6.
getnameinfo() — обратная операция (адрес → имя, порт → строка).
Сетевой стек (TCP/IP, UDP и т.д.) реализован:
- в ядре ОС:
- обработка пакетов;
- установление соединений;
- управление окнами, буферами, таймерами.
Сокет в пространстве пользователя:
- лишь интерфейс к функциям ядра.
- У каждого сокета есть буфер отправки и буфер приёма.
- Ядро:
- кладёт приходящие данные в буфер;
- передаёт их приложению по запросу
recv().
Это позволяет:
- сглаживать разницу в скоростях обработки;
- абстрагироваться от конкретных сетевых задержек и особенностей.
- соединение-ориентированный протокол;
- гарантирует:
- доставку данных без потерь (если соединение не нарушено);
- порядок следования байтов;
- отсутствие дубликатов.
Приложения:
- веб (HTTP/HTTPS),
- SSH,
- FTP (контрольный канал),
- и многие другие.
- без установления соединения;
- не даёт гарантии доставки (могут быть потери, дубликаты, изменения порядка);
- на уровне приложения можно реализовывать свои механизмы надёжности.
Приложения:
- потоковое аудио/видео (где важнее скорость, чем надёжность);
- DNS-запросы;
- онлайн-игры (иногда).
-
API сокетов — основной интерфейс сетевого программирования в пространстве пользователя.
- Программы вызывают функции сокетного API, а ядро реализует сетевой стек.
-
Основные понятия:
- сокет — конечная точка соединения (адрес+порт);
- семейства адресов (
AF_INET,AF_INET6,AF_UNIX); - типы сокетов (
SOCK_STREAM— TCP,SOCK_DGRAM— UDP).
-
Базовые системные вызовы:
socket()— создать сокет;bind()— привязать к адресу/порту (обычно на стороне сервера);listen()— поставить сокет в режим ожидания соединений (TCP-сервер);accept()— принять входящее соединение (создаёт новый сокет для клиента);connect()— подключиться к удалённому серверу (клиент);send()/recv()илиwrite()/read()— передача данных (TCP);sendto()/recvfrom()— высылать/принимать датаграммы (UDP);close()/shutdown()— закрыть соединение.
-
Типовые схемы:
- TCP-сервер:
socket→bind→listen→ циклaccept→recv/send→close; - TCP-клиент:
socket→connect→send/recv→close; - UDP:
socket→bind(сервер) →sendto/recvfrom.
- TCP-сервер:
-
Блокирующий/неблокирующий режим:
- по умолчанию операции блокируют поток;
O_NONBLOCK+select()/poll()/epoll()позволяют обрабатывать множество сокетов в одном потоке.
-
Вспомогательные функции:
htons()/htonl()и обратные — для перевода в сетевой порядок байт;getaddrinfo()— универсальное разрешение имён (IPv4/IPv6).
-
Роль ядра:
- ядро ОС реализует TCP/IP и управляет реальными пакетами;
- сокет — интерфейс, позволяющий приложению «разговаривать» с сетевым стеком.
Этот конспект можно использовать как развёрнутый ответ на экзаменационный билет
«Сети в пространстве пользователя: API сокетов».