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;
}
Объяснение:
uv_default_loop()— получает глобальный цикл событий по умолчанию.uv_timer_init()— инициализирует структуру таймера.uv_timer_start()— запускает таймер. Параметры: указатель на таймер, колбэк, задержка (мс), интервал повтора (0 — один раз).uv_run()— запускает бесконечный цикл обработки событий.- Внутри колбэка
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 Также в библиотеке
Rate Limiting в PHP: полное руководство по ограничению запросов
Конфигурация приложений на Go: лучшие практики и инструменты
Работа с изображениями в PHP: resize, crop и оптимизация с GD и Imagick
OpenAPI / Swagger для Java: полное руководство с примерами