Раздел 19 — Сеть и сокеты

Как программы на разных компьютерах обмениваются данными

Прогресс курса Раздел 19 из 20

Что вы освоите в этом разделе

2 академических часа теории и 4 часа практики: практическая работа №19 «Сетевое программирование сокетов» — на портале; в ней вы переведёте свой сервер из практической работы №18 с именованного канала на сеть. После раздела — тест по всему блоку В (текущий контроль). Сборка программ с сокетами: g++ -std=c++17 -Wall -Wextra файл.cpp -o файл.exe -lws2_32 — -lws2_32 в конце команды.
01

Адрес, порт, клиент и сервер

Каналы из раздела 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

Сервер — программа, которая заранее занимает порт и ждёт. Клиент знает адрес и порт сервера и подключается сам. Это та же схема, что с именованным каналом: имя канала там — адрес и порт здесь.

02

TCP и UDP

TCPUDP
Соединениеесть: сначала подключение, потом обменнет: каждая посылка — сама по себе
Доставкавсе байты, по порядку, без повторов; потерянное передаётся зановодатаграмма может потеряться, прийти дважды или не по порядку
Границы данныхнет — поток байтов, как байтовый каналесть — одна датаграмма читается целиком
Где используетсявеб, почта, передача файлов, чатвидео и голос, игры, DNS — где важнее скорость, чем каждый байт
В WinsockSOCK_STREAM, IPPROTO_TCPSOCK_DGRAM, IPPROTO_UDP; sendto, recvfrom

В курсе — TCP: он даёт надёжную доставку, а программе остаётся договориться о границах сообщений (пункт 06).

03

Winsock 2

Сокеты в Windows — библиотека Winsock 2, функции живут в ws2_32.dll (раздел 18). Три обязательных вещи:

ЧтоВ Winsock
сокеттип SOCKET; ошибка создания — INVALID_SOCKET; закрыть — closesocket, а не CloseHandle
ошибка функциирезультат SOCKET_ERROR (−1); причина — WSAGetLastError(), текст — FormatMessageA, как в разделе 14
частые коды10048 — порт занят; 10054 — соединение сброшено; 10060 — тайм-аут; 10061 — в соединении отказано (порт никто не слушает); 11001 — узел не найден
04

Сервер: 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 — адрес и порт клиента
05

Клиент: 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 работают с тем же сокетом. Порт клиента система выбирает сама — его видно в журнале сервера.

06

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 байта размера, потом ровно столько байт.

07

По сети: брандмауэр, несколько клиентов, тайм-ауты

08

Ловушки раздела

ОшибкаПризнакСообщение / кодКак избежать
нет -lws2_32не собираетсяundefined reference to `__imp_socket'-lws2_32 в конце команды сборки
нет WSAStartupвсе функции сокетов отказывают10093WSAStartup первым, WSACleanup последним
нет htons у портаклиент не подключается к «своему» порту10061htons(port)
порт занят (сервер уже запущен)bind отказывает10048другой порт или завершить старый сервер
сервер не запущен или брандмауэрклиент не подключается10061 / 10060сначала сервер; разрешение брандмауэра; проверить через localhost
ждут, что один send = один recvсообщения склеиваются или рвутся — иногда, по сети чаще—протокол с границами: строки, заголовок длины
принятое выводится как строка без '\0'мусор после данных—разбор по длине recv
recv вернул 0, а цикл продолжаетсябесконечный цикл после отключения клиента—0 — конец соединения
CloseHandle вместо closesocketсокет не закрыт—closesocket для каждого сокета: слушающего и каждого клиентского
длина запроса не ограниченапереполнение буфера на сервере—буфер фиксированного размера и проверка в readLine

Ключевые выводы раздела

Запомните главное

  • Программу в сети находят по IP-адресу и порту; 127.0.0.1 — этот компьютер
  • TCP — соединение и надёжный поток байтов без границ; UDP — датаграммы без гарантий
  • Winsock: WSAStartup/WSACleanup, сборка с -lws2_32, ошибки — WSAGetLastError
  • Сервер: socket, bind (htons!), listen, accept — новый сокет на клиента
  • Клиент: getaddrinfo (имя → адрес), connect, freeaddrinfo
  • send может отправить не всё, recv — вернуть любую часть; границы сообщений задаёт протокол
  • Несколько клиентов — поток на клиента; запросы из сети проверяются как чужой ввод
Связи раздела. Опирается на раздел 10 — строки char и argv; раздел 11 — структуры и reinterpret_cast; раздел 16 — поток на клиента, критическая секция; раздел 17 — протокол «запрос — ответ», байтовый поток и writeAll; раздел 18 — DLL и -l, куча и freeaddrinfo. Нужен для: раздела 20 — зачётные задачи; итогового проекта «Консольный чат».
Практическая работа №19 и тест по блоку В — на учебном портале. Задания по вариантам, критерии оценивания и сдача — в курсе на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 18: Виртуальная память, кучи, DLL Практика на портале