Как программы на разных компьютерах обмениваются данными
socket, bind, listen, acceptconnectsend и recv по протоколу строк, учитывая, что TCP — поток байтовg++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe -lws2_32 — -lws2_32 в конце команды.Каналы из раздела 17 связывают процессы одного компьютера. Сокет — такой же конец соединения, но между компьютерами сети. Чтобы данные дошли до нужной программы на нужном компьютере, нужны два числа:
| Что | Пример | Смысл |
|---|---|---|
| IP-адрес (IPv4) | 192.168.1.15 | номер компьютера в сети — четыре числа 0…255; свой адрес показывает команда ipconfig |
127.0.0.1, имя localhost | «этот же компьютер»: данные не выходят в сеть — удобно для опытов в двух окнах | |
| порт | 5000 | номер программы на компьютере, 0…65535; до 1024 — стандартные службы (80 — веб, 25 — почта), для своих — от 1024 |
| имя узла | nevabit.ru | переводится в IP-адрес службой DNS |
Сервер — программа, которая заранее занимает порт и ждёт. Клиент знает адрес и порт сервера и подключается сам. Это та же схема, что с именованным каналом: имя канала там — адрес и порт здесь.
| TCP | UDP | |
|---|---|---|
| Соединение | есть: сначала подключение, потом обмен | нет: каждая посылка — сама по себе |
| Доставка | все байты, по порядку, без повторов; потерянное передаётся заново | датаграмма может потеряться, прийти дважды или не по порядку |
| Границы данных | нет — поток байтов, как байтовый канал | есть — одна датаграмма читается целиком |
| Где используется | веб, почта, передача файлов, чат | видео и голос, игры, DNS — где важнее скорость, чем каждый байт |
| В Winsock | SOCK_STREAM, IPPROTO_TCP | SOCK_DGRAM, IPPROTO_UDP; sendto, recvfrom |
В курсе — TCP: он даёт надёжную доставку, а программе остаётся договориться о границах сообщений (пункт 06).
Сокеты в Windows — библиотека Winsock 2, функции живут в ws2_32.dll (раздел 18). Три обязательных вещи:
<winsock2.h> и <ws2tcpip.h> — до <windows.h>, если он нужен (иначе конфликт со старым winsock.h); winsock2.h сам подключает windows.h;-lws2_32 при сборке — иначе компоновщик выдаст undefined reference to `__imp_socket';WSAStartup(MAKEWORD(2, 2), &wsa) в начале и WSACleanup() в конце. Без WSAStartup все функции сокетов вернут ошибку 10093.| Что | В Winsock |
|---|---|
| сокет | тип SOCKET; ошибка создания — INVALID_SOCKET; закрыть — closesocket, а не CloseHandle |
| ошибка функции | результат SOCKET_ERROR (−1); причина — WSAGetLastError(), текст — FormatMessageA, как в разделе 14 |
| частые коды | 10048 — порт занят; 10054 — соединение сброшено; 10060 — тайм-аут; 10061 — в соединении отказано (порт никто не слушает); 11001 — узел не найден |
socket, bind, listen, accept// Фрагмент tcpserver.cpp. Полностью — в примерах раздела
WSADATA wsa;
const int started = WSAStartup(MAKEWORD(2, 2), &wsa); // подключить Winsock версии 2.2
if (started != 0) {
std::cout << "WSAStartup failed, error " << started << '\n';
return 2;
}
SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listener == INVALID_SOCKET) {
printSocketError("socket");
WSACleanup();
return 2;
}
sockaddr_in address = {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY); // принимать на всех сетевых интерфейсах
address.sin_port = htons(static_cast<u_short>(port)); // номер порта — в сетевом порядке байтов
if (bind(listener, reinterpret_cast<sockaddr*>(&address), sizeof(address)) == SOCKET_ERROR
|| listen(listener, SOMAXCONN) == SOCKET_ERROR) {
printSocketError("bind/listen"); // 10048 — порт уже занят
closesocket(listener);
WSACleanup();
return 2;
}
std::cout << "listening on port " << port << '\n';
bool running = true;
while (running) {
sockaddr_in clientAddress = {};
int addressLength = sizeof(clientAddress);
SOCKET client = accept(listener, reinterpret_cast<sockaddr*>(&clientAddress), &addressLength);
if (client == INVALID_SOCKET) {
printSocketError("accept");
} else {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &clientAddress.sin_addr, ip, sizeof(ip));
std::cout << "client " << ip << ':' << ntohs(clientAddress.sin_port) << " connected\n";
running = serveClient(client);
closesocket(client);
std::cout << "client disconnected\n";
}
}
closesocket(listener);
WSACleanup();
| Шаг | Что делает |
|---|---|
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) | создать сокет TCP для IPv4 |
bind | привязать к адресу и порту; INADDR_ANY — ко всем адресам компьютера (и 127.0.0.1, и сетевому) |
listen(s, SOMAXCONN) | перевести в режим ожидания; входящие подключения встают в очередь |
accept | ждать и взять из очереди подключение; возвращает новый сокет для обмена с этим клиентом, а слушающий продолжает ждать других; в clientAddress — адрес и порт клиента |
htons (host to network short) и htonl переставляют байты порта и адреса; ntohs — обратно. Забытый htons — сервер слушает не тот порт: 5000 превращается в 34835.sockaddr_in — адрес IPv4; функции принимают указатель на общий тип sockaddr, отсюда reinterpret_cast — тот же приём «смотри на эти байты как на…», что в разделе 11. inet_ntop переводит адрес в текст "127.0.0.1".getaddrinfo и connect// Фрагмент tcpclient.cpp. Полностью — в примерах раздела
addrinfo hints = {};
hints.ai_family = AF_INET;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
addrinfo* found = nullptr;
const int resolved = getaddrinfo(argv[1], argv[2], &hints, &found); // имя или IP и порт → адрес
if (resolved != 0) {
std::cout << "Cannot resolve " << argv[1] << ", error " << resolved << '\n'; // 11001 — нет такого узла
WSACleanup();
return 2;
}
SOCKET s = socket(found->ai_family, found->ai_socktype, found->ai_protocol);
if (s == INVALID_SOCKET || connect(s, found->ai_addr, static_cast<int>(found->ai_addrlen)) == SOCKET_ERROR) {
printSocketError("connect"); // 10061 — порт никто не слушает
...
}
freeaddrinfo(found);
getaddrinfo принимает имя узла (localhost, nevabit.ru) или IP-адрес текстом и номер порта текстом — прямо из argv — и возвращает список готовых адресов. Список выделен в куче (раздел 18): его освобождают freeaddrinfo. connect устанавливает соединение: после него send и recv работают с тем же сокетом. Порт клиента система выбирает сама — его видно в журнале сервера.
send и recv: поток байтов и протокол строкsend(s, данные, сколько, 0) и recv(s, буфер, размер, 0) работают как WriteFile и ReadFile на байтовом канале: send может отправить не всё, recv возвращает сколько пришло — от 1 байта до размера буфера — или 0, если собеседник закрыл соединение. Границ между send не существует: два send по 3 байта могут прийти одним recv на 6 байт или тремя по 2.
Поэтому о границах договаривается протокол. В курсе — самый простой: каждое сообщение — строка, заканчивается '\n'. Получатель копит байты и отдаёт строку, когда встретил перевод строки; лишние байты ждут следующего вызова:
struct LineReader {
char data[LINE_BUFFER]; // принятые, но ещё не разобранные байты
int length;
};
// Следующая строка из потока TCP (без '\r' и '\n') в line; байты после неё остаются в reader.
// false — соединение закрыто, ошибка или строка длиннее буфера
bool readLine(SOCKET s, LineReader& reader, char line[], int size)
{
while (true) {
for (int i = 0; i < reader.length; ++i) {
if (reader.data[i] == '\n') {
int k = 0;
for (int j = 0; j < i && k < size - 1; ++j) {
if (reader.data[j] != '\r') {
line[k] = reader.data[j];
++k;
}
}
line[k] = '\0';
for (int j = i + 1; j < reader.length; ++j) { // остаток — в начало буфера
reader.data[j - i - 1] = reader.data[j];
}
reader.length -= i + 1;
return true;
}
}
if (reader.length == LINE_BUFFER) {
return false; // строка не поместилась
}
const int received = recv(s, reader.data + reader.length, LINE_BUFFER - reader.length, 0);
if (received <= 0) {
return false; // 0 — собеседник закрыл соединение
}
reader.length += received;
}
}
Отправка — sendAll, такая же, как writeAll в разделе 17. Сервер и клиент в двух окнах:
> tcpserver 5000 > tcpclient localhost 5000
listening on port 5000 connected to localhost:5000
client 127.0.0.1:53712 connected hello, socket
request: hello, socket HELLO, SOCKET
request: shutdown shutdown
client disconnected bye
server stopped quit
Проверить сервер можно и стандартным клиентом Windows — telnet localhost 5000 (включается в «Компонентах Windows»): он отправляет то, что вы печатаете. Протокол строк текстовый, поэтому его видно и отлаживать просто. Двоичные данные (числа, структуры) передают с заголовком длины: сначала 4 байта размера, потом ровно столько байт.
ipconfig (строка «IPv4-адрес»), клиент на другом подключается к этому адресу: tcpclient 192.168.1.15 5000. Компьютеры должны быть в одной сети или сеть должна пропускать порт.listen спросит, разрешить ли программе принимать подключения. Без разрешения подключения с других компьютеров будут отвергаться — клиент получит тайм-аут 10060. В компьютерных классах разрешение может требовать прав администратора — тогда опыты ведут через localhost.listen. Как в разделе 17, решение — поток на клиента (раздел 16): главный поток только принимает accept и передаёт сокет новому потоку. Общие данные потоков защищают критической секцией.recv ждёт данных сколько угодно. Ограничить ожидание: setsockopt(s, SOL_SOCKET, SO_RCVTIMEO, …) с числом миллисекунд — по истечении recv вернёт ошибку 10060.| Ошибка | Признак | Сообщение / код | Как избежать |
|---|---|---|---|
нет -lws2_32 | не собирается | undefined reference to `__imp_socket' | -lws2_32 в конце команды сборки |
нет WSAStartup | все функции сокетов отказывают | 10093 | WSAStartup первым, WSACleanup последним |
нет htons у порта | клиент не подключается к «своему» порту | 10061 | htons(port) |
| порт занят (сервер уже запущен) | bind отказывает | 10048 | другой порт или завершить старый сервер |
| сервер не запущен или брандмауэр | клиент не подключается | 10061 / 10060 | сначала сервер; разрешение брандмауэра; проверить через localhost |
ждут, что один send = один recv | сообщения склеиваются или рвутся — иногда, по сети чаще | — | протокол с границами: строки, заголовок длины |
принятое выводится как строка без '\0' | мусор после данных | — | разбор по длине recv |
recv вернул 0, а цикл продолжается | бесконечный цикл после отключения клиента | — | 0 — конец соединения |
CloseHandle вместо closesocket | сокет не закрыт | — | closesocket для каждого сокета: слушающего и каждого клиентского |
| длина запроса не ограничена | переполнение буфера на сервере | — | буфер фиксированного размера и проверка в readLine |
127.0.0.1 — этот компьютерWSAStartup/WSACleanup, сборка с -lws2_32, ошибки — WSAGetLastErrorsocket, bind (htons!), listen, accept — новый сокет на клиентаgetaddrinfo (имя → адрес), connect, freeaddrinfosend может отправить не всё, recv — вернуть любую часть; границы сообщений задаёт протоколchar и argv; раздел 11 — структуры и reinterpret_cast; раздел 16 — поток на клиента, критическая секция; раздел 17 — протокол «запрос — ответ», байтовый поток и writeAll; раздел 18 — DLL и -l, куча и freeaddrinfo. Нужен для: раздела 20 — зачётные задачи; итогового проекта «Консольный чат».