libuv для C++: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

libuv для C++: Полное руководство по асинхронному вводу-выводу

Добро пожаловать в исчерпывающее руководство по библиотеке libuv для языка C++. Эта статья создана для образовательного портала PythonLib и предназначена для разработчиков, которые хотят освоить асинхронное программирование на C++ с использованием одной из самых надежных и производительных библиотек.

1. Что такое libuv и зачем она нужна?

libuv — это кроссплатформенная библиотека на языке C, предназначенная для асинхронного ввода-вывода (I/O). Изначально она была разработана для проекта Node.js, чтобы абстрагировать работу с event loop (циклом событий) и неблокирующими операциями ввода-вывода под разные операционные системы. Сегодня libuv используется не только в Node.js, но и в таких проектах, как Julia, Luvit, Pyuv (Python) и многих других.

Зачем она нужна? В современном программировании часто требуется выполнять несколько задач одновременно, не блокируя основной поток выполнения. Традиционные подходы, такие как многопоточность, сложны в реализации и подвержены ошибкам (гонки данных, deadlocks). libuv предлагает элегантную альтернативу — асинхронную модель, основанную на цикле событий. Вы регистрируете обработчики (колбэки) для различных событий (чтение из сокета, таймер, сигнал), а libuv автоматически вызывает их, когда событие происходит, не блокируя выполнение других задач.

Для C++ разработчиков libuv предоставляет мощный инструмент для создания высокопроизводительных сетевых серверов, клиентов, инструментов для работы с файлами и системных утилит. Она берет на себя всю низкоуровневую работу с epoll (Linux), kqueue (macOS), IOCP (Windows) и другими механизмами, предоставляя единый, простой API. Это позволяет писать код, который будет работать на любой платформе без изменений.

2. Установка libuv

Установка libuv зависит от вашей операционной системы и используемого пакетного менеджера. Поскольку libuv — это C-библиотека, она не устанавливается через pip, npm или cargo. Вот основные способы установки:

Linux (Ubuntu/Debian)

sudo apt update
sudo apt install libuv1-dev

macOS (Homebrew)

brew install libuv

Windows (vcpkg)

vcpkg install libuv

Сборка из исходников (для всех платформ)

git clone https://github.com/libuv/libuv.git
cd libuv
mkdir build && cd build
cmake .. -DBUILD_TESTING=OFF
make -j$(nproc)
sudo make install

После установки вы можете скомпилировать свою программу, указав линковщику на библиотеку:

g++ -std=c++11 -o my_program my_program.cpp -luv

3. Быстрый старт: Минимальный рабочий пример

Давайте напишем простейшую программу, которая создает цикл событий и запускает таймер, который срабатывает один раз через 1 секунду.

#include <uv.h>
#include <cstdio>

// Колбэк для таймера
void on_timer(uv_timer_t* handle) {
    printf("Таймер сработал!\n");
    // Останавливаем цикл событий
    uv_stop(handle->loop);
}

int main() {
    // 1. Создаем цикл событий
    uv_loop_t* loop = uv_default_loop();

    // 2. Создаем и инициализируем таймер
    uv_timer_t timer;
    uv_timer_init(loop, &timer);

    // 3. Запускаем таймер: сработает один раз через 1000 мс
    uv_timer_start(&timer, on_timer, 1000, 0);

    // 4. Запускаем цикл событий
    printf("Запуск цикла событий...\n");
    uv_run(loop, UV_RUN_DEFAULT);

    // 5. Очищаем ресурсы
    uv_loop_close(loop);

    return 0;
}

Объяснение:

  1. uv_default_loop() — получает глобальный цикл событий по умолчанию.
  2. uv_timer_init() — инициализирует структуру таймера.
  3. uv_timer_start() — запускает таймер. Параметры: указатель на таймер, колбэк, задержка (мс), интервал повтора (0 — один раз).
  4. uv_run() — запускает бесконечный цикл обработки событий.
  5. Внутри колбэка on_timer мы вызываем uv_stop(), чтобы выйти из цикла.

Скомпилируйте и запустите:

g++ -std=c++11 quickstart.cpp -luv -o quickstart
./quickstart

4. Основные методы и функции libuv

В этом разделе мы рассмотрим 8 ключевых функций/классов libuv, которые покроют 90% ваших задач. Для каждой приведена сигнатура, описание и рабочий пример.

4.1. uv_default_loop()

Сигнатура: uv_loop_t* uv_default_loop(void)

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

#include <uv.h>
#include <cstdio>

int main() {
    uv_loop_t* loop = uv_default_loop();
    if (loop == nullptr) {
        fprintf(stderr, "Не удалось получить цикл по умолчанию\n");
        return 1;
    }
    printf("Цикл событий получен: %p\n", (void*)loop);
    uv_loop_close(loop);
    return 0;
}

4.2. uv_run()

Сигнатура: int uv_run(uv_loop_t* loop, uv_run_mode mode)

Что делает: Запускает цикл событий. Режимы: UV_RUN_DEFAULT (бесконечный, пока не станет пуст), UV_RUN_ONCE (обработать одно событие или ждать), UV_RUN_NOWAIT (обработать доступные события без ожидания). Возвращает 0, если цикл завершился, ненулевое значение — если еще есть активные обработчики.

#include <uv.h>
#include <cstdio>

int main() {
    uv_loop_t* loop = uv_default_loop();
    // Запускаем в режиме UV_RUN_ONCE — обработает одно событие и вернется
    int result = uv_run(loop, UV_RUN_ONCE);
    printf("uv_run вернул: %d\n", result);
    uv_loop_close(loop);
    return 0;
}

4.3. uv_timer_start()

Сигнатура: int uv_timer_start(uv_timer_t* handle, uv_timer_cb cb, uint64_t timeout, uint64_t repeat)

Что делает: Запускает таймер. timeout — задержка до первого срабатывания (мс). repeat — интервал повторения (0 — однократно). Возвращает 0 при успехе.

#include <uv.h>
#include <cstdio>

void on_repeat(uv_timer_t* handle) {
    static int count = 0;
    printf("Повтор #%d\n", ++count);
    if (count >= 5) {
        uv_timer_stop(handle);
        uv_stop(handle->loop);
    }
}

int main() {
    uv_loop_t* loop = uv_default_loop();
    uv_timer_t timer;
    uv_timer_init(loop, &timer);
    // Запускаем с задержкой 500 мс, повтор каждые 1000 мс
    uv_timer_start(&timer, on_repeat, 500, 1000);
    uv_run(loop, UV_RUN_DEFAULT);
    uv_loop_close(loop);
    return 0;
}

4.4. uv_tcp_init() и uv_tcp_bind()

Сигнатура: int uv_tcp_init(uv_loop_t* loop, uv_tcp_t* handle)
int uv_tcp_bind(uv_tcp_t* handle, const struct sockaddr* addr, unsigned int flags)

Что делает: uv_tcp_init инициализирует TCP-обработчик. uv_tcp_bind привязывает его к адресу и порту. Флаги: UV_TCP_IPV6ONLY.

#include <uv.h>
#include <cstdio>

int main() {
    uv_loop_t* loop = uv_default_loop();
    uv_tcp_t server;
    uv_tcp_init(loop, &server);

    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", 8080, &addr);
    int bind_result = uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
    if (bind_result != 0) {
        fprintf(stderr, "Ошибка привязки: %s\n", uv_strerror(bind_result));
        return 1;
    }
    printf("TCP сервер привязан к порту 8080\n");
    uv_loop_close(loop);
    return 0;
}

4.5. uv_listen()

Сигнатура: int uv_listen(uv_stream_t* stream, int backlog, uv_connection_cb cb)

Что делает: Начинает прослушивание входящих соединений на потоке (TCP, pipe). backlog — размер очереди ожидания. Колбэк cb вызывается при новом соединении.

#include <uv.h>
#include <cstdio>

void on_connection(uv_stream_t* server, int status) {
    if (status < 0) {
        fprintf(stderr, "Ошибка соединения: %s\n", uv_strerror(status));
        return;
    }
    printf("Новое соединение!\n");
}

int main() {
    uv_loop_t* loop = uv_default_loop();
    uv_tcp_t server;
    uv_tcp_init(loop, &server);

    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", 8080, &addr);
    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);

    int listen_result = uv_listen((uv_stream_t*)&server, 128, on_connection);
    if (listen_result != 0) {
        fprintf(stderr, "Ошибка прослушивания: %s\n", uv_strerror(listen_result));
        return 1;
    }
    printf("Сервер слушает порт 8080...\n");
    uv_run(loop, UV_RUN_DEFAULT);
    uv_loop_close(loop);
    return 0;
}

4.6. uv_accept()

Сигнатура: int uv_accept(uv_stream_t* server, uv_stream_t* client)

Что делает: Принимает входящее соединение. server — слушающий сокет, client — новый клиентский сокет (должен быть инициализирован). Возвращает 0 при успехе.

#include <uv.h>
#include <cstdio>

void on_connection(uv_stream_t* server, int status) {
    if (status < 0) return;
    uv_tcp_t* client = (uv_tcp_t*)malloc(sizeof(uv_tcp_t));
    uv_tcp_init(server->loop, client);
    if (uv_accept(server, (uv_stream_t*)client) == 0) {
        printf("Клиент принят\n");
    } else {
        free(client);
    }
}

int main() {
    uv_loop_t* loop = uv_default_loop();
    uv_tcp_t server;
    uv_tcp_init(loop, &server);
    struct sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", 8080, &addr);
    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);
    uv_listen((uv_stream_t*)&server, 128, on_connection);
    uv_run(loop, UV_RUN_DEFAULT);
    uv_loop_close(loop);
    return 0;
}

4.7. uv_read_start()

Сигнатура: int uv_read_start(uv_stream_t* stream, uv_alloc_cb alloc_cb, uv_read_cb read_cb)

Что делает: Начинает асинхронное чтение данных из потока. alloc_cb вызывается для выделения буфера, read_cb — когда данные получены или произошла ошибка.

#include <uv.h>
#include <cstdio>
#include <cstdlib>

void alloc_buffer(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf) {
    buf->base = (char*)malloc(suggested_size);
    buf->len = suggested_size;
}

void on_read(uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) {
    if (nread > 0) {
        printf("Прочитано %ld байт: %.*s\n", nread, (int)nread, buf->base);
    } else if (nread < 0) {
        fprintf(stderr, "Ошибка чтения: %s\n", uv_strerror((int)nread));
    }
    free(buf->base);
}

int main() {
    // Пример для демонстрации — чтение из stdin
    uv_loop_t* loop = uv_default_loop();
    uv_tty_t tty;
    uv_tty                    

Рекомендаций