Компьютерные сети: от IPC к C/Winsock
Сети как IPC между разными компьютерами
Берём одну задачу — передать данные из программы A программе B на другом компьютере — и вводим каждое сетевое понятие только после преграды, которую оно решает.
Сети как IPC между разными компьютерами
Задача: программа A должна передать данные программе B
В прошлом семестре на операционных системах мы изучали IPC — inter-process communication. Два процесса находились на одном компьютере и обменивались данными через объекты, которыми управляет операционная система:
process A -> pipe / shared memory / message queue -> process B
Теперь изменим только одно условие: process B работает на другом компьютере.
Компьютер A Компьютер B
program A program B
Важно сразу назвать задачу точно. Нам не нужно «передать что-то компьютеру». Самому компьютеру эти данные не нужны. Нужно, чтобы одна программа передала данные другой программе.
Данными может быть что угодно:
- слово
Привет; - фотография;
- число температуры;
- фрагмент видео;
- команда игровому серверу.
Для сети это разные по смыслу данные, но одна и та же общая задача. Возьмём
Привет только потому, что этот пример легко увидеть целиком.
На доске хочется нарисовать прямую стрелку:
program A -- «Привет» --> program B
Но положить человеческое слово в провод, оптоволокно или радиоканал нельзя. Сначала нужно понять, что именно физически пойдёт между устройствами.
Шаг 1. Смысл превращается в байты, биты и сигнал
Program A хранит не абстрактный смысл слова, а его представление в памяти.
Если стороны выбрали UTF-8, строка Привет выглядит так:
П р и в е т
D0 9F D1 80 D0 B8 D0 B2 D0 B5 D1 82
6 Unicode-символов = 12 байт = 96 бит
Получается первая цепочка:
смысл -> символы -> байты -> биты -> физический сигнал
На принимающей стороне преобразования пойдут в обратную сторону:
физический сигнал -> биты -> байты -> символы -> смысл
Здесь мы пока упрощаем физику. Один бит не обязан соответствовать одному отдельному импульсу: конкретная технология кодирует последовательности битов изменениями электрического, оптического или радиосигнала. Для первого шага нам важна граница: приложение создаёт байты, а среда переносит сигнал.
Но теперь посмотрим на ситуацию глазами получателя. Он видит не нашу красивую строку, а длинную последовательность изменений сигнала.
Шаг 2. Получатель не видит границ сообщения
Представим, что по одной среде подряд пришли тысячи битов. Внутри них могут
находиться несколько изображений, ответы разных сервисов и наше слово
Привет.
Сам сигнал не подписан:
...0110100011010000100111111101000110000000...
где здесь начало?
где конец?
Получатель не может догадаться:
- какие биты относятся к одному фрагменту данных;
- сколько байтов нужно собрать;
- текст это, число или часть картинки;
- что делать после получения;
- как распознать недопустимую последовательность.
Даже если обе стороны умеют восстанавливать биты из сигнала, этого мало. Им нужно заранее договориться, как устроен обмен.
Шаг 3. Протокол даёт сторонам общие правила
Протокол — это соглашение между отправителем и получателем о формате данных и поведении сторон.
Короткая формула:
protocol = format + behavior
Например, наше учебное соглашение могло бы требовать такую запись:
[тип данных][длина данных][сами данные]
Тогда поле длины сообщает, сколько следующих байтов относится к одной порции, а поле типа — как эти байты интерпретировать. Правила поведения дополнительно говорят, кто начинает обмен, какой ответ ожидается и что считать ошибкой.
Протокол работает только потому, что обе стороны реализуют одни и те же правила. Если отправитель записал длину одним способом, а получатель читает её другим, правильный смысл не восстановится.
Зафиксируем результат шага:
Протокол не переносит данные сам. Он устраняет неоднозначность: стороны знают, как сформировать и как разобрать обмен.
Казалось бы, можно придумать одно огромное соглашение на все случаи. Но тогда каждое устройство должно было бы знать вообще всё — от смысла фотографии до физики конкретного кабеля.
Шаг 4. Одного протокола недостаточно
Во время одной передачи возникают независимые вопросы:
- Что означают байты для программы?
- Какой обмен внутри компьютера должен получить эти данные?
- Как найти путь через домашний роутер, сеть провайдера и другие сети?
- Как на текущем участке дойти до следующего устройства?
- Как закодировать данные в сигнал конкретной среды?
Один участник пути не обязан знать ответы на все вопросы. Например, роутеру
важно выбрать следующий участок пути, но не нужно понимать, что внутри лежит
слово Привет. А программе важно разобрать смысл данных, но не нужно управлять
электрическим сигналом сетевой карты.
Так мы приходим не к одному протоколу, а к стеку протоколов. Чтобы этот стек не превратился в хаос, большую задачу делят на уровни.
Шаг 5. Слои делят большую задачу на отдельные вопросы
У каждого слоя одна область ответственности:
- верхние слои ближе к смыслу, который нужен программе;
- нижние слои ближе к доставке через устройства и физическую среду;
- каждый слой пользуется услугами слоя ниже;
- содержимое слоя выше для него является просто payload — полезной нагрузкой.
На отправителе данные идут сверху вниз. Каждый слой добавляет служебную информацию, необходимую для его задачи. Это называется encapsulation — инкапсуляция.
На получателе данные идут снизу вверх. Каждый слой читает и снимает свою служебную информацию, а оставшийся payload передаёт выше. Это decapsulation — декапсуляция.
Отправитель Получатель
данные приложения данные приложения
| ^
v |
[служебные поля верхних слоёв] снимаются в обратном порядке
| ^
v |
[служебные поля нижних слоёв] --> [принятые данные]
Это не несколько независимых копий сообщения. Вниз передаётся одна вложенная конструкция: каждый следующий слой оборачивает результат предыдущего.
Где здесь модель OSI
OSI — учебная модель, которая делит сетевую коммуникацию на семь уровней. Она нужна не для механического заучивания номеров, а чтобы для любой проблемы спросить: какой именно вопрос сейчас решается?
| Уровень OSI | На какой вопрос отвечает | Примеры |
|---|---|---|
| 7. Application | Что программа хочет сообщить другой программе? | HTTP, DNS, наш учебный формат |
| 6. Presentation | Как представить, сжать или зашифровать данные? | UTF-8, JSON, форматы изображений |
| 5. Session | Как организовать и поддерживать диалог? | Состояние сеанса, точки продолжения |
| 4. Transport | Как вести обмен между конечными приложениями? | TCP, UDP |
| 3. Network | Как доставить данные через несколько сетей? | IP и маршрутизация |
| 2. Data Link | Как передать данные соседу на текущем участке? | Ethernet, Wi-Fi |
| 1. Physical | Как представить данные сигналом в среде? | Электрический, оптический, радиосигнал |
В реальном Internet stack границы проведены не точно так же. Уровни 5–7 OSI часто рассматривают вместе как Application, а ниже говорят о Transport, Internet/Network, Link и Physical. Поэтому фразы «семь уровней OSI» и «четыре-пять уровней Internet stack» не противоречат друг другу: это разные модели одной коммуникации.
Ещё одна важная поправка: на уровне обычно существует не один-единственный протокол, а семейство протоколов. Конкретный обмен использует подходящий набор.
Теперь применим модель к реальному пути. Компьютеры редко соединены одним проводом:
Компьютер A -> домашний роутер -> провайдер -> Internet -> Компьютер B
На каждом участке нижние детали могут меняться, а Network layer продолжает вести данные к адресу назначения. Допустим, адрес компьютера B нам уже известен. Решена ли исходная задача?
Шаг 6. Адрес привёл нас к компьютеру, но не к программе
IP-адрес назначения позволяет сетевому уровню вести пакет к нужному узлу или сетевому интерфейсу. Промежуточные роутеры читают необходимую служебную информацию и выбирают следующий участок пути.
Но на компьютере B одновременно могут работать:
- браузер;
- мессенджер;
- база данных;
- наша учебная серверная программа.
Все они используют один и тот же компьютер и могут принимать сетевые данные. Значит, адрес назначения ответил только на вопрос «на какой узел?», но не ответил на вопрос «какому обмену внутри этого узла?».
Нужен ещё один номер — точка входа для нужной коммуникации.
Порт: номер точки входа
В первой модели порт — это номер точки входа на компьютере. Вместе с адресом назначения он уточняет, куда доставить данные:
Компьютер B, IP 203.0.113.20
:80 -> web service
:5432 -> database service
:27015 -> наша учебная программа
Если данные пришли на destination port 27015, операционная система понимает,
что они относятся к коммуникации нашей учебной программы, а не браузера или
базы данных.
Эта модель намеренно простая. Точнее, номер существует в пространстве конкретного протокола уровня Transport, а ОС сопоставляет его с локальной точкой коммуникации. Порт не является PID процесса и не приклеен к программе навсегда: программа может завершиться, запуститься снова или использовать несколько точек входа.
Пока нас интересует destination port на стороне сервера. Исходный номер, который ОС обычно выбирает клиенту автоматически, разберём позже — он не нужен, чтобы понять первый входящий путь.
Но кто хранит все эти сопоставления, разбирает служебные поля и управляет общей сетевой картой? Обычная программа не должна делать это самостоятельно.
Почему программа не работает с сетью напрямую
Program A и program B работают в user space. Сетевая карта, драйверы, таблица маршрутов, очереди и состояние сетевых протоколов находятся под контролем ОС, в kernel space.
Такое разделение необходимо, потому что:
- одна сетевая карта одновременно обслуживает много процессов;
- процессам нужна изоляция и проверка прав;
- разные адаптеры требуют разных драйверов;
- ОС выбирает маршрут и обрабатывает служебные заголовки;
- ОС управляет ограниченными очередями и планирует передачу через устройство.
Поэтому обычная программа не пишет удалённой программе прямо из своего user space. Она просит свою локальную ОС выполнить сетевую операцию. На другой стороне локальная ОС компьютера B принимает данные и только затем отдаёт их нужному процессу.
Нужен объект, через который процесс будет обращаться к этой сетевой службе ОС.
Socket: точка связи процесса с сетевым стеком
Сначала полезная простая модель:
Сокет — это управляемая ОС коробочка: программа кладёт туда байты для отправки и забирает оттуда принятые байты.
Метафора показывает правильную границу, но сокет — не просто buffer. Точнее, socket — это kernel-managed communication endpoint, доступный процессу через handle. С ним связаны выбранный протокол, локальные и удалённые адреса, состояние обмена и очереди данных.
Теперь можно уточнить историю с destination port:
program B в user space
|
| handle
v
socket endpoint в kernel space
local address: 203.0.113.20
local port: 27015
receive queue: принятые байты
Именно ОС связывает локальный address и port с endpoint. Когда данные приходят, ядро по служебной информации выбирает нужный endpoint, помещает байты в его очередь, а program B читает их через handle.
Для первого объяснения достаточно destination port. Позже мы уточним, что существующее TCP-соединение различается полной парой конечных адресов и номеров, поэтому один сервер может одновременно общаться со многими клиентами.
Socket не является отдельным уровнем OSI. Это программный интерфейс к сетевым возможностям ОС и объект, в котором ОС хранит состояние конкретной коммуникации.
Абстрактный flow: отправить и принять
До конкретного языка вся история выглядит так:
- Program A формирует данные и кодирует их в bytes.
- Program A передаёт bytes своему локальному endpoint — операция SEND.
- Ядро A помещает bytes в очередь и проводит их вниз через стек.
- Слои добавляют служебные данные для доставки, маршрута и текущего участка.
- Сетевая карта превращает подготовленные bits в signal.
- Промежуточные устройства пересылают данные к компьютеру B.
- Ядро B проводит принятые данные вверх через стек.
- По destination port и остальной служебной информации ядро выбирает endpoint.
- Bytes оказываются в его receive queue.
- Program B забирает доступные bytes — операция RECEIVE — и разбирает прикладной протокол.
program A
-> local OS endpoint
-> layers down
-> signal and network path
-> layers up
-> destination endpoint in OS B
-> program B
Эта модель не зависит ни от C, ни от Windows. Теперь мы готовы просто назвать операции, которыми её выражает конкретный API.
Только теперь: C и Winsock
В Windows программа на C обычно работает с сетью через Winsock. Его функции не создают новую сетевую реальность — они дают имена уже разобранным шагам.
| Функция | Роль в нашей модели |
|---|---|
WSAStartup() | Инициализирует использование Winsock процессом |
socket() | Создаёт локальный endpoint в ОС и возвращает SOCKET handle |
connect() | Просит TCP установить соединение с remote IP и port |
send() | Передаёт bytes локальной transport system ОС |
recv() | Копирует доступные принятые bytes из ОС в buffer программы |
closesocket() | Закрывает handle и сообщает ОС, что endpoint больше не нужен |
Минимальный жизненный цикл клиента:
WSAStartup(...);
SOCKET s = socket(...);
connect(s, ...);
send(s, bytes, byte_count, 0);
recv(s, buffer, buffer_size, 0);
closesocket(s);
WSACleanup();
У серверной программы появляются ещё три операции:
| Функция | Роль |
|---|---|
bind() | Связывает server endpoint с local address и port |
listen() | Переводит его в режим ожидания TCP-подключений |
accept() | Возвращает новый connected socket для конкретного клиента |
server: socket() -> bind() -> listen() -> accept() -> recv() / send()
client: socket() -> connect() ---------------------> send() / recv()
Listening socket продолжает ждать новые подключения. accept() возвращает
отдельный connected socket, через который сервер обменивается bytes с одним
клиентом. Эту разницу подробно и с кодом разберём в следующей главе.
Пока не раскладываем connect() на SYN, SYN-ACK и ACK. На этом уровне достаточно
точной формулировки: вызов просит TCP в локальном ядре установить соединение с
указанным endpoint.
И ещё одна граница доказательства: успешный send() сообщает о результате
локальной операции. Он сам по себе не доказывает, что program B уже вызвала
recv() и обработала данные.
Собираем весь путь
Теперь можно пройти историю один раз от начала до конца, не вводя новых понятий:
- Program A хочет передать program B слово
Привет. - UTF-8 превращает 6 Unicode-символов в 12 байт.
- Прикладной протокол задаёт формат и правила интерпретации этих bytes.
send()передаёт bytes через socket handle локальной ОС.- Стек последовательно добавляет служебные данные своих слоёв.
- Нижний слой превращает bits в signal физической среды.
- Роутеры ведут данные к destination IP компьютера B.
- Ядро B поднимает данные по стеку и читает destination port.
- ОС выбирает соответствующий socket endpoint и помещает bytes в receive queue.
recv()копирует доступные bytes в user-space buffer program B.- Program B разбирает прикладной протокол и декодирует UTF-8.
- Только в этой последней точке для program B снова появляется слово
Привет.
Главная мысль главы:
Между двумя программами нет прямой стрелки. Есть две границы user/kernel, два socket endpoint, стек протоколов и физический путь; каждый элемент решает только свою часть задачи.
Проверьте причинную цепочку
Мост к следующей главе
Общая карта готова. В следующей главе приблизим участок
program -> socket -> kernel: увидим отдельно user buffer, SOCKET handle,
kernel endpoint и очереди ОС, а затем соберём настоящий Winsock client/server
через loopback. Поля TCP handshake и поведение byte stream раскроем позже,
когда для них появится отдельный вопрос.
Официальные источники
- Microsoft Learn: Windows network architecture and the OSI model
- Microsoft Learn: Winsock functions
- Microsoft Learn: socket function
- Microsoft Learn: bind function
- Microsoft Learn: listen function
- Microsoft Learn: accept function
- Microsoft Learn: connect function
- Microsoft Learn: send function
- Microsoft Learn: recv function
- RFC 1122: Requirements for Internet Hosts — Communication Layers
- RFC 9293: Transmission Control Protocol
- RFC 1392: Internet Users' Glossary
- Unicode Standard: Encoding Forms