Skip to content

Latest commit

 

History

History
517 lines (348 loc) · 21.1 KB

File metadata and controls

517 lines (348 loc) · 21.1 KB

Сети в пространстве пользователя — API сокетов

1. Общая идея: сети и сокеты в пространстве пользователя

В современных операционных системах (например, POSIX-системы: Linux, BSD, macOS) сетевое взаимодействие организовано по принципу разделения:

  1. Ядро ОС (kernel space)

    • Реализует сетевой стек (TCP/IP, UDP, ICMP и т.д.).
    • Обрабатывает пакеты, маршрутизацию, контроль над протоколами и очередями.
  2. Пространство пользователя (user space)

    • Обычные программы (процессы) взаимодействуют с сетью через системные вызовы.
    • Основной интерфейс для сетевого программирования — API сокетов (Sockets API).

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

  • устанавливать соединения (TCP);
  • отправлять и получать датаграммы (UDP);
  • обрабатывать входящие соединения (серверы);
  • работать с различными протоколами (Internet, Unix-domain sockets и т.п.).

2. Понятие сокета

2.1. Абстракция сокета

Сокет — это объект, который:

  • ассоциирован с:
    • типом протокола (потоковый/датаграммный),
    • адресом (IP-адрес + порт для Internet-сокетов);
  • имеет дескриптор — целое число, которое процесс использует так же, как файловый дескриптор.

В POSIX:

  • сокеты являются частным случаем файловых дескрипторов:
    • можно использовать read(), write(), close() наряду со специальными вызовами send(), recv() и др.

2.2. Адреса и порты

Для интернет-сокетов (семейство AF_INET / AF_INET6):

  • адрес сокета определяется:
    • IP-адресом (IPv4 или IPv6),
    • номером порта (целое число, например 80, 443, 8080).
  • порт → логический идентификатор приложения на узле.

Адрес записывается как:

  • IP:ПОРТ, например 192.168.1.10:8080.

3. Классификация сокетов

3.1. По семейству протоколов (domain, family)

Чаще всего:

  • AF_INET — IPv4-сокеты (Internet);
  • AF_INET6 — IPv6-сокеты;
  • AF_UNIX (или AF_LOCAL) — локальные Unix-сокеты (общение процессов на одной машине через файловую систему);
  • другие (менее частые): AF_PACKET, AF_NETLINK и т.д.

3.2. По типу (type)

Основные типы:

  1. SOCK_STREAMпотоковый сокет:

    • ориентирован на соединение;
    • обычно использует протокол TCP;
    • обеспечивает надёжную доставку, контроль очередности, отсутствие дубликатов;
    • данных как непрерывный поток байтов.
  2. SOCK_DGRAMдатаграммный сокет:

    • без установления соединения;
    • обычно использует протокол UDP;
    • доставка не гарантируется (потери, дублирование, изменение порядка);
    • данные передаются отдельными сообщениями (датаграммами).

Другие типы (для полноты):

  • SOCK_RAW — сырые сокеты для работы на уровне IP-пакетов;
  • SOCK_SEQPACKET и др. — реже применяются в базовом курсе.

4. Основные системные вызовы сокетного API

Далее рассматриваем классический POSIX-сокетный интерфейс (на примерах на C-подобном псевдокоде).

4.1. socket()

Создаёт сокет и возвращает его дескриптор.

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.

4.2. bind()

Ассоциирует сокет с конкретным локальным адресом (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));

4.3. listen()

Переводит потоковый сокет в режим прослушивания (для сервера).

int listen(int sockfd, int backlog);
  • backlog — максимальная длина очереди входящих подключений.

После listen():

  • сокет готов принимать соединения через accept().

4.4. accept()

Принимает входящее соединение на сокете, находящемся в состоянии прослушивания (listen).

int client_fd = accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

Результат:

  • при успешном установлении соединения:
    • возвращает новый сокет client_fd, ассоциированный с конкретным клиентом;
    • исходный sockfd остаётся слушать новые подключения;
  • при ошибке — -1.

Можно получить данные о подключившемся клиенте через addr (IP, порт).

4.5. connect()

Используется клиентом для установки соединения с сервером (для потоковых сокетов).

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd — сокет, созданный через socket();
  • addr — адрес сервера.

После успешного connect():

  • сокет готов к обмену данными (send() / recv() или write() / read()).

4.6. send() и recv() (для потоковых сокетов)

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)

для простого случая.

4.7. sendto() и recvfrom() (для датаграммных сокетов)

Для 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() — получает датаграмму и при необходимости сообщает, откуда она.

4.8. close() и shutdown()

int close(int sockfd);
int shutdown(int sockfd, int how);
  • close() — закрывает сокет (как и любой файловый дескриптор).
  • shutdown() — позволяет закрыть только направление:
    • SHUT_RD — закрыть чтение;
    • SHUT_WR — закрыть запись (отправка FIN);
    • SHUT_RDWR — закрыть и чтение, и запись.

5. Базовые схемы: клиент и сервер

5.1. TCP-сервер (потоковый сокет)

Стандартная последовательность действий:

  1. socket(AF_INET, SOCK_STREAM, 0) — создать сокет.
  2. bind() — привязать к адресу (IP/порт).
  3. listen() — начать слушать.
  4. В цикле:
    • 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);
}

5.2. TCP-клиент

  1. socket(AF_INET, SOCK_STREAM, 0) — создать сокет.
  2. connect() — подключиться к серверу.
  3. send()/recv() или write()/read() — обмен данными.
  4. close() — закрыть сокет.

Для аккуратного завершения обмена можно предварительно вызвать shutdown() и закрыть только половину соединения (только чтение или только запись).

Псевдокод:

int s = socket(AF_INET, SOCK_STREAM, 0);
connect(s, ...);
send(s, ...);
recv(s, ...);
close(s);

5.3. UDP (датаграммный) сервер и клиент

  • Сервер:
    • socket(AF_INET, SOCK_DGRAM, 0);
    • bind() на нужный порт;
    • recvfrom() и sendto() в цикле.
  • Клиент:
    • socket(AF_INET, SOCK_DGRAM, 0);
    • sendto() на адрес сервера;
    • при необходимости — recvfrom() для ответа.

5.4. Утилиты для быстрой проверки

  • netstat/ss — посмотреть открытые порты и текущие соединения;
  • python -m http.server <port> — быстро поднять HTTP‑эндпоинт для теста;
  • telnet <host> <port> или nc — вручную установить TCP-соединение и убедиться, что обмен байтами работает.

6. Блокирующий и неблокирующий режим, мультиплексирование

6.1. Блокирующий режим (по умолчанию)

По умолчанию вызовы:

  • accept(), connect(), recv(), send() и др.

работают в блокирующем режиме:

  • если данных нет, recv() ждёт;
  • если нет входящих соединений, accept() ждёт;
  • если соединение устанавливается, connect() может ожидать.

Удобно для простых программ, но:

  • неэффективно, если нужно обслуживать множество клиентов в одном потоке.

6.2. Неблокирующий режим

Можно перевести сокет в неблокирующий режим:

  • через fcntl() или ioctl():
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

В этом режиме:

  • операции ввода/вывода завершаются немедленно;
  • если операция не может быть выполнена — возвращается ошибка (обычно EWOULDBLOCK или EAGAIN).

Это позволяет:

  • не блокировать поток ожиданием данных;
  • использовать опрос и мультиплексирование.

6.3. select(), poll(), epoll (кратко)

Для одновременной работы с несколькими сокетами:

  • используются системные вызовы мультиплексирования:
  1. select() — классический, может работать с множеством дескрипторов, но имеет ограничения по числу дескрипторов.
  2. poll() — более гибкий, без жёсткого лимита.
  3. epoll (Linux) — эффективен для большого числа соединений:
    • epoll_create(), epoll_ctl(), epoll_wait().

Идея:

  • регистрировать интерес к событиям (чтение/запись/ошибка) на сокетах;
  • ждать, пока какой-то сокет «готов»;
  • затем выполнять операции на этом сокете.

Это основа для серверов с большим количеством клиентов (напр. веб-серверы).


7. Вспомогательные функции и структуры

7.1. Структуры адресов

Для 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.

7.2. Преобразование форматов и байтового порядка

Сетевой порядок байтов — big-endian.
Хост может быть little-endian (чаще всего x86).

Функции:

  • htons() — host to network short (16 бит);
  • htonl() — host to network long (32 бита);
  • ntohs(), ntohl() — обратные.

Используются для портов и IPv4-адресов в структуре sockaddr_in.

7.3. getaddrinfo() и getnameinfo()

Многие современные программы используют:

int getaddrinfo(const char *node, const char *service,
                const struct addrinfo *hints,
                struct addrinfo **res);

Позволяет:

  • по имени хоста и строке порта ("80") получить список возможных структур sockaddr, универсально поддерживающий IPv4 и IPv6.

getnameinfo() — обратная операция (адрес → имя, порт → строка).


8. Связь сокетов и ядра ОС

8.1. Где реализован сетевой стек

Сетевой стек (TCP/IP, UDP и т.д.) реализован:

  • в ядре ОС:
    • обработка пакетов;
    • установление соединений;
    • управление окнами, буферами, таймерами.

Сокет в пространстве пользователя:

  • лишь интерфейс к функциям ядра.

8.2. Буферизация и очереди

  • У каждого сокета есть буфер отправки и буфер приёма.
  • Ядро:
    • кладёт приходящие данные в буфер;
    • передаёт их приложению по запросу recv().

Это позволяет:

  • сглаживать разницу в скоростях обработки;
  • абстрагироваться от конкретных сетевых задержек и особенностей.

9. Надёжность и особенности протоколов

9.1. TCP (через SOCK_STREAM)

  • соединение-ориентированный протокол;
  • гарантирует:
    • доставку данных без потерь (если соединение не нарушено);
    • порядок следования байтов;
    • отсутствие дубликатов.

Приложения:

  • веб (HTTP/HTTPS),
  • SSH,
  • FTP (контрольный канал),
  • и многие другие.

9.2. UDP (через SOCK_DGRAM)

  • без установления соединения;
  • не даёт гарантии доставки (могут быть потери, дубликаты, изменения порядка);
  • на уровне приложения можно реализовывать свои механизмы надёжности.

Приложения:

  • потоковое аудио/видео (где важнее скорость, чем надёжность);
  • DNS-запросы;
  • онлайн-игры (иногда).

10. Краткий конспект (для запоминания)

  1. API сокетов — основной интерфейс сетевого программирования в пространстве пользователя.

    • Программы вызывают функции сокетного API, а ядро реализует сетевой стек.
  2. Основные понятия:

    • сокет — конечная точка соединения (адрес+порт);
    • семейства адресов (AF_INET, AF_INET6, AF_UNIX);
    • типы сокетов (SOCK_STREAM — TCP, SOCK_DGRAM — UDP).
  3. Базовые системные вызовы:

    • socket() — создать сокет;
    • bind() — привязать к адресу/порту (обычно на стороне сервера);
    • listen() — поставить сокет в режим ожидания соединений (TCP-сервер);
    • accept() — принять входящее соединение (создаёт новый сокет для клиента);
    • connect() — подключиться к удалённому серверу (клиент);
    • send()/recv() или write()/read() — передача данных (TCP);
    • sendto()/recvfrom() — высылать/принимать датаграммы (UDP);
    • close()/shutdown() — закрыть соединение.
  4. Типовые схемы:

    • TCP-сервер: socketbindlisten → цикл acceptrecv/sendclose;
    • TCP-клиент: socketconnectsend/recvclose;
    • UDP: socketbind (сервер) → sendto/recvfrom.
  5. Блокирующий/неблокирующий режим:

    • по умолчанию операции блокируют поток;
    • O_NONBLOCK + select()/poll()/epoll() позволяют обрабатывать множество сокетов в одном потоке.
  6. Вспомогательные функции:

    • htons()/htonl() и обратные — для перевода в сетевой порядок байт;
    • getaddrinfo() — универсальное разрешение имён (IPv4/IPv6).
  7. Роль ядра:

    • ядро ОС реализует TCP/IP и управляет реальными пакетами;
    • сокет — интерфейс, позволяющий приложению «разговаривать» с сетевым стеком.

Этот конспект можно использовать как развёрнутый ответ на экзаменационный билет
«Сети в пространстве пользователя: API сокетов».