Всем привет! Я являюсь создателем распределённого поисковика rats-search на базе DHT (GitHub). Его принцип работы довольно прост: поисковик собирает торренты у всех участников сети и формирует большую распределённую базу для поиска, включая метаданные (например, описания и прочую информацию).
В этой статье я хочу рассказать о своей новой библиотеке для построения распределённых приложений (p2p), где знание IP-адресов участников не обязательно, а поиск ведётся через различные протоколы — DHT, mDNS, peer-exchange и другие. Думаю, с учётом постоянных неприятностей, которые происходят вокруг, это может оказаться полезным ;).
Немного истории
rats-search — проект довольно старый, я написал его много лет назад на Electron. Сейчас появилась необходимость переписать p2p-составляющую на C++ для улучшения качества связи и производительности.
Для начала я ради интереса попробовал перенести всё на libp2p и был неприятно удивлён его производительностью. Буквально при ~100 пирах процессор (Ryzen 5700) был загружен на 100% в момент шагов по DHT сети, а потребление памяти достигало 500–600 МБ. Конечно, я понимаю, что libp2p использует модные корутинные библиотеки типо it- и тянет за собой 300–400 МБ зависимостей в node_modules, но это же не дело.
Да, это JS, и сравнивать его напрямую с C++ некорректно, но даже JS-приложения обычно не дают таких плохих результатов. Более того, libp2p поддерживает не все языки: какие-то реализации урезаны, какие-то отсутствуют вовсе. Например, в версии для C++ протокол mDNS просто не реализован. В итоге эталонными считаются только реализации для Go и JS, и они заметно отличаются друг от друга.
Почему я решил написать свою библиотеку
Я решил отказаться от этой «комбайн-реализации» и написать свою собственную. В качестве основы был выбран эффективный низкоуровневый язык, а к нему уже можно строить биндинги на других языках. Это позволяет получить универсальную и при этом действительно эффективную единую реализацию, которую можно использовать и в других проектах.
Так появилась librats — новая библиотека на C++, которая станет будущим ядром Rats, а также может применяться и в любых других распределённых приложениях.
Что уже реализовано

-
Поиск участников через DHT
-
Коммуникация между участниками
-
Обмен данными / файлами / директориями
-
Протокол mDNS для поиска участников в локальных сетях
-
Historical peers — база данных участников для повторных подключений
-
Протокол gossipsub — для построения обмена сообщениями в больших распределённых сетях
-
Обмен пирами (peer exchange)
-
Поддержка пользовательских (кастомных) протоколов
-
ICE / STUN протоколы
-
Шифрование на основе протокола noise (Curve25519 + ChaCha20-Poly1305)
Поддерживаемые платформы и языки

-
Сборка под Windows, macOS, Linux, Android
-
Компиляторы: GCC, Clang, MSVC
-
Использование: напрямую в C++, а также в C и Java через JNI; в Android — через NDK

Примеры
Теперь давайте рассмотрим технические примеры и решения некоторых задач.
Базовый пример: создание клиента
#include "librats.h" #include <iostream> #include <thread> #include <chrono> int main() { // Создаём простой P2P клиент librats::RatsClient client(8080); // Настройка колбэка при подключении client.set_connection_callback([](socket_t socket, const std::string& peer_id) { std::cout << "✅ Новый пир подключился: " << peer_id << std::endl; }); // Настройка колбэка для сообщений client.set_string_data_callback([](socket_t socket, const std::string& peer_id, const std::string& message) { std::cout << "💬 Сообщение от " << peer_id << ": " << message << std::endl; }); // Запускаем клиент if (!client.start()) { std::cerr << "Не удалось запустить клиент" << std::endl; return 1; } std::cout << "🐀 librats клиент запущен на порту 8080" << std::endl; // Подключение к другому пиру (необязательно) // client.connect_to_peer("127.0.0.1", 8081); // Отправляем сообщение всем подключенным пирам client.broadcast_string_to_peers("Hello from librats!"); // Оставляем программу работать std::this_thread::sleep_for(std::chrono::minutes(1)); return 0; }
В основе работы лежит передача трёх типов данных:
-
бинарных,
-
текстовых,
-
JSON-данных.
Можно выбирать слушателей и настраивать тип передачи данных в зависимости от задачи.
Определение собственного протокола
Можно установить собственный протокол. Это позволяет автоматически находить только тех участников, которые работают именно с вашим протоколом или приложением.
#include "librats.h" #include <iostream> int main() { librats::RatsClient client(8080); // Настройка пользовательского протокола для вашего приложения client.set_protocol_name("my_app"); client.set_protocol_version("1.0"); std::cout << "Protocol: " << client.get_protocol_name() << " v" << client.get_protocol_version() << std::endl; std::cout << "Discovery hash: " << client.get_discovery_hash() << std::endl; client.start(); // Запуск DHT-обнаружения с пользовательским протоколом if (client.start_dht_discovery()) { // Объявляем о своём присутствии client.announce_for_hash(client.get_discovery_hash()); // Поиск других пиров, использующих тот же протокол client.find_peers_by_hash(client.get_discovery_hash(), [](const std::vector<std::string>& peers) { std::cout << "Found " << peers.size() << " peers" << std::endl; }); } return 0; }
Построение простого чата
Довольно типичная задача — организация чата. Для начала можно использовать простые функции, без применения mesh-сети.
#include "librats.h" #include <iostream> #include <string> int main() { librats::RatsClient client(8080); // Настройка обработчиков сообщений с использованием современного API client.on("chat", [](const std::string& peer_id, const nlohmann::json& data) { std::cout << "[CHAT] " << peer_id << ": " << data["message"].get<std::string>() << std::endl; }); client.on("user_join", [](const std::string& peer_id, const nlohmann::json& data) { std::cout << "[JOIN] " << data["username"].get<std::string>() << " присоединился" << std::endl; }); // Колбэк при подключении client.set_connection_callback([&](socket_t socket, const std::string& peer_id) { std::cout << "✅ Пир подключился: " << peer_id << std::endl; // Отправляем приветственное сообщение nlohmann::json welcome; welcome["username"] = "User_" + client.get_our_peer_id().substr(0, 8); client.send("user_join", welcome); }); client.start(); // Отправляем чат-сообщение nlohmann::json chat_msg; chat_msg["message"] = "Hello, P2P chat!"; chat_msg["timestamp"] = std::time(nullptr); client.send("chat", chat_msg); return 0; }
Работа с mesh-сетями
Если требуется построить более крупную сеть (например, для обмена сообщениями между большим количеством узлов), можно использовать протокол gossipsub.
#include "librats.h" #include <iostream> int main() { librats::RatsClient client(8080); // Настройка обработчиков сообщений для топиков client.on_topic_message("news", [](const std::string& peer_id, const std::string& topic, const std::string& message) { std::cout << "📰 [" << topic << "] " << peer_id << ": " << message << std::endl; }); client.on_topic_json_message("events", [](const std::string& peer_id, const std::string& topic, const nlohmann::json& data) { std::cout << "🎉 [" << topic << "] Событие: " << data["type"].get<std::string>() << std::endl; }); // Уведомления о присоединении/выходе пиров client.on_topic_peer_joined("news", [](const std::string& peer_id, const std::string& topic) { std::cout << "➕ " << peer_id << " присоединился к " << topic << std::endl; }); client.start(); client.start_dht_discovery(); // Подписка на топики client.subscribe_to_topic("news"); client.subscribe_to_topic("events"); // Публикация сообщений client.publish_to_topic("news", "Breaking: librats is awesome!"); nlohmann::json event; event["type"] = "celebration"; event["reason"] = "successful_connection"; client.publish_json_to_topic("events", event); std::cout << "📊 Пиров в 'news': " << client.get_topic_peers("news").size() << std::endl; return 0; }
Обмен данными
Библиотека поддерживает обмен данными, включая файлы и целые директории.
#include "librats.h" #include <iostream> int main() { librats::RatsClient client(8080); // Настройка колбэков передачи файлов client.on_file_transfer_progress([](const librats::FileTransferProgress& progress) { std::cout << "📁 Передача " << progress.transfer_id.substr(0, 8) << ": " << progress.get_completion_percentage() << "% завершено" << " (" << (progress.transfer_rate_bps / 1024) << " KB/s)" << std::endl; }); client.on_file_transfer_completed([](const std::string& transfer_id, bool success, const std::string& error) { if (success) { std::cout << "✅ Передача завершена: " << transfer_id.substr(0, 8) << std::endl; } else { std::cout << "❌ Ошибка передачи: " << error << std::endl; } }); // Автоматическое принятие входящих файловых передач client.on_file_transfer_request([](const std::string& peer_id, const librats::FileMetadata& metadata, const std::string& transfer_id) { std::cout << "📥 Входящий файл: " << metadata.filename << " (" << metadata.file_size << " байт) от " << peer_id.substr(0, 8) << std::endl; return true; // Автопринятие }); // Разрешаем запросы файлов из директории "shared" client.on_file_request([](const std::string& peer_id, const std::string& file_path, const std::string& transfer_id) { std::cout << "📤 Запрос: " << file_path << " от " << peer_id.substr(0, 8) << std::endl; return file_path.find("../") == std::string::npos; // Защита от выхода за пределы пути }); client.start(); // Настройка параметров передачи librats::FileTransferConfig config; config.chunk_size = 64 * 1024; // чанки по 64KB config.max_concurrent_chunks = 4; // 4 параллельных чанка config.verify_checksums = true; // проверка целостности client.set_file_transfer_config(config); // Примеры передач (замените "peer_id" на реальный ID пира) // std::string file_transfer = client.send_file("peer_id", "my_file.txt"); // std::string dir_transfer = client.send_directory("peer_id", "./my_folder"); // std::string file_request = client.request_file("peer_id", "remote_file.txt", "./downloaded_file.txt"); std::cout << "Файловая передача готова. Подключите пиров и обменивайтесь файлами!" << std::endl; return 0; }
Настройка конфигурации
Например, можно задать конфигурацию для логирования:
#include "librats.h" #include <iostream> int main() { librats::RatsClient client(8080); // Включение и настройка логирования client.set_logging_enabled(true); client.set_log_file_path("librats_app.log"); client.set_log_level("INFO"); // DEBUG, INFO, WARN, ERROR client.set_log_colors_enabled(true); client.set_log_timestamps_enabled(true); // Настройка ротации лог-файлов client.set_log_rotation_size(5 * 1024 * 1024); // максимальный размер файла 5MB client.set_log_retention_count(3); // хранить 3 старых лог-файла std::cout << "📝 Логирование в: " << client.get_log_file_path() << std::endl; std::cout << "📊 Уровень логирования: " << static_cast<int>(client.get_log_level()) << std::endl; std::cout << "🎨 Цветное логирование: " << (client.is_log_colors_enabled() ? "Да" : "Нет") << std::endl; client.start(); // Все операции librats теперь будут логироваться client.broadcast_string_to_peers("Это действие будет зафиксировано в логах!"); // Очистка лог-файла при необходимости (раскомментируйте для использования) // client.clear_log_file(); return 0; }
И можно поработать с конфигурацией сохранения данных:
#include "librats.h" #include <iostream> int main() { librats::RatsClient client(8080); // Set custom data directory for config files client.set_data_directory("./my_app_data"); // Load saved configuration (if exists) if (client.load_configuration()) { std::cout << "📄 Loaded existing configuration" << std::endl; } else { std::cout << "📄 Using default configuration" << std::endl; } // Get our persistent peer ID std::cout << "🆔 Our peer ID: " << client.get_our_peer_id() << std::endl; client.start(); // Try to reconnect to previously connected peers int reconnect_attempts = client.load_and_reconnect_peers(); std::cout << "🔄 Attempted to reconnect to " << reconnect_attempts << " previous peers" << std::endl; // Configuration is automatically saved when client stops // Files created: config.json, peers.rats, peers_ever.rats // Manual save if needed client.save_configuration(); client.save_historical_peers(); std::cout << "💾 Configuration will be saved to: " << client.get_data_directory() << std::endl; return 0; }
В коде предусмотрено сохранение не только конфигурации, но и истории подключений. Это полезно для будущих запусков: если нужно быстро восстановить соединения, можно воспользоваться сохранёнными данными.
Представьте ситуацию: «птица залетела» в сервера местного законопослушного провайдера и часть маршрутов или соединений внезапно оказалась заблокирована. В таком случае наличие базы известных пиров позволяет быстро восстановить работу сети без необходимости заново искать всех участников.
Ну и напоследок, раз заявлены другие языки, пример на Java на Android. Я решил привести пример Activity целиком, фокусируем внимание на setupRatsClient():
public class MainActivity extends AppCompatActivity { private static final String TAG = "LibRatsExample"; private static final int PERMISSION_REQUEST_CODE = 1; private RatsClient ratsClient; private TextView statusText; private TextView messagesText; private EditText hostInput; private EditText portInput; private EditText messageInput; private Button startButton; private Button connectButton; private Button sendButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initViews(); checkPermissions(); setupRatsClient(); } private void initViews() { statusText = findViewById(R.id.statusText); messagesText = findViewById(R.id.messagesText); hostInput = findViewById(R.id.hostInput); portInput = findViewById(R.id.portInput); messageInput = findViewById(R.id.messageInput); startButton = findViewById(R.id.startButton); connectButton = findViewById(R.id.connectButton); sendButton = findViewById(R.id.sendButton); startButton.setOnClickListener(this::onStartClicked); connectButton.setOnClickListener(this::onConnectClicked); sendButton.setOnClickListener(this::onSendClicked); // Устанавливаем значения по умолчанию hostInput.setText("192.168.1.100"); portInput.setText("8080"); messageInput.setText("Hello from Android!"); updateUI(); } private void checkPermissions() { String[] permissions = { Manifest.permission.INTERNET, Manifest.permission.ACCESS_NETWORK_STATE, Manifest.permission.ACCESS_WIFI_STATE, Manifest.permission.CHANGE_WIFI_MULTICAST_STATE }; boolean allGranted = true; for (String permission : permissions) { if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (!allGranted) { ActivityCompat.requestPermissions(this, permissions, PERMISSION_REQUEST_CODE); } } private void setupRatsClient() { try { // Включаем логирование RatsClient.setLoggingEnabled(true); RatsClient.setLogLevel("INFO"); // Создаём клиент на порту 8080 ratsClient = new RatsClient(8080); // Настраиваем колбэки ratsClient.setConnectionCallback(new ConnectionCallback() { @Override public void onConnection(String peerId) { runOnUiThread(() -> { appendMessage("Подключились к пиру: " + peerId); updateUI(); }); } }); ratsClient.setStringCallback(new StringMessageCallback() { @Override public void onStringMessage(String peerId, String message) { runOnUiThread(() -> { appendMessage("Сообщение от " + peerId + ": " + message); }); } }); ratsClient.setDisconnectCallback(new DisconnectCallback() { @Override public void onDisconnect(String peerId) { runOnUiThread(() -> { appendMessage("Отключились от пира: " + peerId); updateUI(); }); } }); appendMessage("Клиент LibRats успешно создан"); appendMessage("Версия: " + RatsClient.getVersionString()); } catch (Exception e) { Log.e(TAG, "Не удалось создать RatsClient", e); appendMessage("Ошибка: " + e.getMessage()); } } private void onStartClicked(View view) { if (ratsClient == null) return; try { int result = ratsClient.start(); if (result == RatsClient.SUCCESS) { appendMessage("Клиент успешно запущен"); appendMessage("Наш Peer ID: " + ratsClient.getOurPeerId()); updateUI(); } else { appendMessage("Не удалось запустить клиент: " + result); } } catch (Exception e) { Log.e(TAG, "Ошибка при запуске клиента", e); appendMessage("Ошибка при запуске клиента: " + e.getMessage()); } } private void onConnectClicked(View view) { if (ratsClient == null) return; String host = hostInput.getText().toString().trim(); String portStr = portInput.getText().toString().trim(); if (host.isEmpty() || portStr.isEmpty()) { Toast.makeText(this, "Введите хост и порт", Toast.LENGTH_SHORT).show(); return; } try { int port = Integer.parseInt(portStr); int result = ratsClient.connectWithStrategy(host, port, RatsClient.STRATEGY_AUTO_ADAPTIVE); if (result == RatsClient.SUCCESS) { appendMessage("Подключение к " + host + ":" + port); } else { appendMessage("Не удалось подключиться: " + result); } } catch (NumberFormatException e) { Toast.makeText(this, "Неверный номер порта", Toast.LENGTH_SHORT).show(); } catch (Exception e) { Log.e(TAG, "Ошибка подключения", e); appendMessage("Ошибка подключения: " + e.getMessage()); } } private void onSendClicked(View view) { if (ratsClient == null) return; String message = messageInput.getText().toString().trim(); if (message.isEmpty()) { Toast.makeText(this, "Введите сообщение", Toast.LENGTH_SHORT).show(); return; } try { // Получаем подключённых пиров String[] peerIds = ratsClient.getPeerIds(); if (peerIds.length == 0) { Toast.makeText(this, "Нет подключённых пиров", Toast.LENGTH_SHORT).show(); return; } // Отправляем первому подключённому пиру int result = ratsClient.sendString(peerIds[0], message); if (result == RatsClient.SUCCESS) { appendMessage("Отправлено: " + message); messageInput.setText(""); } else { appendMessage("Не удалось отправить сообщение: " + result); } } catch (Exception e) { Log.e(TAG, "Ошибка при отправке сообщения", e); appendMessage("Ошибка при отправке сообщения: " + e.getMessage()); } } private void appendMessage(String message) { Log.d(TAG, message); messagesText.append(message + "\n"); // Прокрутка вниз messagesText.post(() -> { int scrollAmount = messagesText.getLayout().getLineTop(messagesText.getLineCount()) - messagesText.getHeight(); if (scrollAmount > 0) { messagesText.scrollTo(0, scrollAmount); } else { messagesText.scrollTo(0, 0); } }); } private void updateUI() { if (ratsClient == null) { statusText.setText("Статус: Не инициализирован"); startButton.setEnabled(false); connectButton.setEnabled(false); sendButton.setEnabled(false); return; } try { int peerCount = ratsClient.getPeerCount(); statusText.setText("Статус: " + peerCount + " пиров подключено"); // Включение/отключение кнопок в зависимости от состояния startButton.setEnabled(true); connectButton.setEnabled(true); sendButton.setEnabled(peerCount > 0); } catch (Exception e) { statusText.setText("Статус: Ошибка"); Log.e(TAG, "Ошибка обновления UI", e); } } @Override protected void onDestroy() { super.onDestroy(); if (ratsClient != null) { try { ratsClient.stop(); ratsClient.destroy(); } catch (Exception e) { Log.e(TAG, "Ошибка при уничтожении клиента", e); } } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == PERMISSION_REQUEST_CODE) { boolean allGranted = true; for (int result : grantResults) { if (result != PackageManager.PERMISSION_GRANTED) { allGranted = false; break; } } if (!allGranted) { Toast.makeText(this, "Для LibRats необходимы сетевые разрешения", Toast.LENGTH_LONG).show(); } } } }
Использование из C в виде библиотеки
На C доступны основные функции внутри библиотеки, их можно посмотреть:
https://github.com/DEgITx/librats/blob/master/src/librats_c.h — весь список, аналог вызовов из C++
пример использования когда на C:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #include "librats_c.h" // Глобальный дескриптор клиента static rats_client_t client = NULL; static volatile int running = 1; // Функции-колбэки void on_peer_connected(void* user_data, const char* peer_id) { printf("Пир подключился: %s\n", peer_id); } void on_peer_disconnected(void* user_data, const char* peer_id) { printf("Пир отключился: %s\n", peer_id); } void on_string_message(void* user_data, const char* peer_id, const char* message) { printf("Сообщение от %s: %s\n", peer_id, message); } void signal_handler(int sig) { printf("Завершение работы...\n"); running = 0; } int main(int argc, char* argv[]) { printf("Пример\n"); printf("========================\n\n"); // Установка обработчика сигнала signal(SIGINT, signal_handler); // Создаём клиента на порту 8080 client = rats_create(8080); if (!client) { fprintf(stderr, "Не удалось создать клиента!\n"); return 1; } // Настраиваем колбэки rats_set_connection_callback(client, on_peer_connected, NULL); rats_set_disconnect_callback(client, on_peer_disconnected, NULL); rats_set_string_callback(client, on_string_message, NULL); // Запускаем клиента if (rats_start(client) != RATS_SUCCESS) { fprintf(stderr, "Не удалось запустить клиента!\n"); rats_destroy(client); return 1; } // Выводим наш Peer ID char* peer_id = rats_get_our_peer_id(client); if (peer_id) { printf("Наш Peer ID: %s\n", peer_id); rats_string_free(peer_id); } printf("Клиент запущен на порту 8080\n"); printf("Нажмите Ctrl+C для остановки\n\n"); // Если переданы аргументы командной строки — пробуем подключиться if (argc >= 3) { const char* host = argv[1]; int port = atoi(argv[2]); printf("Подключение к %s:%d...\n", host, port); rats_connect(client, host, port); } // Главный цикл int counter = 0; while (running) { sleep(1); counter++; // Каждые 5 секунд отправляем сообщение if (counter % 5 == 0) { int peer_count = rats_get_peer_count(client); if (peer_count > 0) { char message[100]; snprintf(message, sizeof(message), "Здарова: %d", counter / 5); int sent = rats_broadcast_string(client, message); printf("Широковещательная отправка %d пирам: %s\n", sent, message); } else { printf("Нет подключённых пиров. Запустите другой экземпляр командой:\n"); printf(" %s localhost 8080\n", argv[0]); } } } // Очистка printf("Остановка клиента...\n"); rats_stop(client); rats_destroy(client); printf("Баюшки!\n"); return 0; }
Заключение
Ну и напоследок — о производительности. Помните те 500–600 МБ памяти при работе p2p через libp2p? Давайте посмотрим, что получается с librats:

…не правда ли, результат явно лучше?
Надеюсь, эта библиотека окажется полезной. Спасибо за внимание! Возможно, я расширю статью по мере добавления биндингов на другие языки и реализации новых функций.
ссылка на оригинал статьи https://habr.com/ru/articles/943652/
Добавить комментарий