Компьютерные сети: от IPC к C/Winsock
Socket без магии: как процесс передаёт байты ядру
Связываем C/Winsock-код с объектами ОС: user buffer, SOCKET handle, kernel endpoint, очереди, listening socket и connected socket.
Socket без магии: как процесс передаёт байты ядру
Задача: понять объект до первого вызова socket()
В первой главе мы перенесли process B на другой компьютер. Теперь откроем C-код и почти сразу увидим строку:
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
Если до этого просто сказать «мы создали сокет», строка выглядит магией. Поэтому начнём не с параметров функции, а с уже знакомой границы операционной системы.
user space kernel space
process A ---- системный API ----> ресурсы и сетевой стек ОС
На первом проходе используем простую модель:
Сокет — это «коробочка» ОС для сетевого обмена. Процесс получает ручку к этой коробочке, кладёт байты через
send()и забирает черезrecv().
Этой модели достаточно, чтобы осмысленно читать первый Winsock-код. Затем мы добавим точность и увидим, что «коробочка» содержит не один буфер.
Три вещи, которые нельзя смешивать
Рядом существуют три разных объекта.
1. User buffer
Это обычная память процесса:
const unsigned char hello[12] = {
0xD0, 0x9F, 0xD1, 0x80, 0xD0, 0xB8,
0xD0, 0xB2, 0xD0, 0xB5, 0xD1, 0x82
};
Массив принадлежит приложению. В нём лежат 12 байт UTF-8-представления слова
Привет.
2. SOCKET handle
Переменная s находится в памяти процесса, но её значение — не пакет, не
указатель на kernel buffer и не адрес удалённого компьютера. Это непрозрачная
ручка, через которую Winsock находит ресурс ОС.
SOCKET s = 524 условный номер ручки, а не адрес данных
3. Kernel-managed endpoint
Сам endpoint контролирует операционная система. С ним связаны:
- выбранный transport protocol;
- local и remote endpoints, когда они известны;
- send queue и receive queue;
- состояние соединения и ошибки;
- режим I/O и другие параметры.
Поэтому две фразы одновременно полезны:
простая модель: socket — коробочка для байтов
точная модель: SOCKET — handle на endpoint, у которого есть очереди и state
Что делает socket()
До первого socket call процесс инициализирует Winsock:
WSADATA wsa;
int startup = WSAStartup(MAKEWORD(2, 2), &wsa);
if (startup != 0) {
/* startup уже содержит код ошибки */
return 1;
}
Затем socket() просит ОС создать endpoint заданного типа:
SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET) {
int error = WSAGetLastError();
WSACleanup();
return 1;
}
После успешного вызова ресурс существует, но роль ещё не определена. Сам по себе этот endpoint не является ни подключённым клиентом, ни слушающим сервером. Роль появляется из следующих операций.
Две линии: клиент и сервер
Для первого чтения достаточно видеть жизненный цикл целиком.
Клиент:
WSAStartup -> socket -> connect -> send/recv -> closesocket -> WSACleanup
Сервер:
WSAStartup -> socket -> bind -> listen -> accept
|
+-> recv/send on accepted socket
-> closesocket
closesocket(listener) -> WSACleanup
Пройдите клиента по шагам. На каждой строке видно, какой объект меняется — и, что важнее, чего до этой строки ещё не существует:
Каждый вызов отвечает на один вопрос:
| Вызов | Просьба процесса к ОС |
|---|---|
socket() | создать endpoint и вернуть handle |
bind() | связать endpoint с local address и port |
listen() | принимать новые попытки TCP connection |
connect() | установить connection с remote endpoint |
accept() | выдать отдельный connected socket для одного peer |
send() | принять bytes приложения в локальный transport path |
recv() | скопировать доступные bytes в user buffer |
closesocket() | освободить владение socket handle |
Пока не надо запоминать все параметры. Сначала важно понимать, какой объект меняется после каждого вызова.
Почему серверу нужны два socket
Минимальная серверная последовательность:
SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
bind(listener, ...);
listen(listener, SOMAXCONN);
SOCKET peer = accept(listener, NULL, NULL);
После accept() одновременно существуют:
listener
local endpoint: 127.0.0.1:27015
role: принимать новые connections
peer
local endpoint: 127.0.0.1:27015
remote endpoint: client IP + client port
role: передавать byte stream конкретного connection
Listening socket похож не на почтовый ящик с письмами, а на входную дверь.
accept() открывает отдельный разговор с одним посетителем. Данные клиента
приходят в receive queue connected socket peer; recv() вызывается на
peer, а listener продолжает ждать следующих клиентов.
Куда попадают байты после send()
Пусть клиент вызывает:
int n = send(client, (const char *)hello, 12, 0);
Для обычного blocking socket возможны три важных результата:
n > 0— локальный transport принялnбайт этого вызова;SOCKET_ERROR— вызов не передал положительное число байт, код ошибки надо сразу сохранить черезWSAGetLastError();nне обязан равняться запрошенным12: работу с partial I/O мы откроем в главе о TCP stream.
Положительный результат не означает, что process B уже вызвал recv(). Между
этими событиями остаётся путь:
user buffer A
-> send()
-> kernel send queue A
-> TCP/IP
-> driver/NIC or loopback path
-> TCP/IP on B
-> kernel receive queue B
-> recv()
-> user buffer B
Вместо фразы «scheduler флашит socket» используем более точную:
После локального принятия байтов сетевой стек, его очереди, driver и NIC продолжают обработку. Это не синхронная доставка из одного
send()прямо в удалённыйrecv().
Первый опыт: loopback без физической сети
Начнём с 127.0.0.1. Client и server будут отдельными процессами, но оба
останутся на одном компьютере.
Это полезно, потому что мы временно убираем:
- кабель и радиосреду;
- switch и ARP;
- маршрутизаторы;
- firewall между двумя физическими hosts.
Остаётся предмет этого занятия: process, user/kernel boundary, socket objects и вызовы Winsock.
Перед запуском scaffolded client/server предскажите:
- сколько socket handles будет существовать после
accept(); - из какого handle сервер вызовет
recv(); - что докажет
send() == 12; - пройдут ли 12 байт через физический Ethernet-кабель.
После запуска найдите listener и connected endpoint:
Get-NetTCPConnection -LocalPort 27015 |
Format-Table LocalAddress, LocalPort, RemoteAddress, RemotePort, State, OwningProcess
netstat -ano | findstr :27015
В коде отметьте разными цветами:
- user-space arrays и variables;
- Winsock calls, пересекающие API boundary;
- предполагаемые kernel objects;
- место приобретения и освобождения каждого ресурса.
Ошибки и cleanup — гигиена с первого занятия
Пока мы не изучаем всю систему ошибок Winsock, но соблюдаем четыре правила:
WSAStartup()возвращает код ошибки напрямую.socket()возвращаетINVALID_SOCKETпри неуспехе.- Большинство последующих socket calls возвращают
SOCKET_ERROR; после него сразу сохраняютWSAGetLastError(). - Каждый успешно полученный socket handle закрывается ровно один раз через
closesocket(); после завершения работы процесса с Winsock вызываетсяWSACleanup().
Эти правила нужны не ради шаблонного boilerplate. Handle представляет ресурс ОС, а значит у него есть владелец и жизненный цикл.
Первый взгляд на TCP handshake
connect() и accept() не встречаются как два вызова функций. Между ними
работают TCP state machines двух ОС. В обычном happy path они обмениваются
SYN, SYN+ACK и ACK, после чего connection становится established.
На этом занятии не требуется запоминать sequence numbers, окна, backlog
internals, TIME_WAIT и retransmission timers. Мы вернёмся к ним после
Ethernet и IP, когда сможем объяснить наблюдаемый packet path, а не только три
названия флагов.
Главный инвариант сейчас такой: handshake выполняет TCP в ОС. accept() даёт
server process handle уже созданного connected endpoint; listener при этом не
превращается в data socket.
Частые заблуждения
«Переменная SOCKET и есть kernel buffer». Нет. Это handle на ресурс ОС.
«После accept() listener можно использовать для recv()». Нет. Data I/O
конкретного peer выполняется на connected socket, который вернул accept().
«send() == 12 означает, что сервер напечатал Привет». Мы доказали
только локальное принятие 12 байт. Доставка и чтение требуют других
наблюдений.
«Loopback показывает работу Ethernet». Loopback проверяет software path внутри host. Реальный NIC и физический link появятся в следующей главе.
Проверьте модель
Короткий итог
- Сокет можно сначала понимать как коробочку ОС, но
SOCKETв коде — handle. - Endpoint включает очереди, адреса, protocol state и ошибки.
- Client и server используют разные последовательности Winsock calls.
- После
accept()у сервера есть listener и отдельный connected socket. send(), передача по сети иrecv()— разные события.- Loopback позволяет изучить software path до реальной физической сети.
Мост к следующей главе
На loopback байты не покидали компьютер. Теперь запустим тот же обмен между двумя машинами в одной локальной сети и откроем скрытый нижний участок: NIC, физический сигнал, Ethernet frame, MAC, ARP и решение switch.