diff --git a/lib/platform_compat.h b/lib/platform_compat.h index c14bd3aa..306edc8a 100644 --- a/lib/platform_compat.h +++ b/lib/platform_compat.h @@ -187,19 +187,21 @@ int random_bytes(uint8_t *buffer, size_t len); uint32_t get_interface_ip_by_index(uint32_t netif_index); // Get IPv6 address of interface by its index (ifindex) -// temporary=1: get temporary (privacy) IPv6, temporary=0: get permanent IPv6 +// POSIX: temporary=1 выбирает temporary (privacy), temporary=0 — постоянный адрес. +// Windows игнорирует temporary: предпочитает глобальный адрес, затем локальный, исключая link-local/loopback/::. // Returns 0 on success (fills addr_out with 16 bytes), -1 on error or not found int get_interface_ipv6_by_index(uint32_t netif_index, int temporary, uint8_t* addr_out); -// Get IPv6 address using netlink (Linux-only, accurate IFA_F_* flags) -// prefer_stable=1: prefer permanent/stable (IFA_F_MANAGETEMPADDR) over temporary -// prefer_stable=0: prefer temporary over stable +// Выбрать IPv6: Linux использует netlink с IFA_F_*, прочие POSIX — getifaddrs(), Windows — IP Helper. +// POSIX: prefer_stable=1 предпочитает постоянный адрес, 0 — temporary; допускается fallback на другой тип. +// Windows игнорирует prefer_stable, выбор как у get_interface_ipv6_by_index(). // Returns 0 on success (fills addr_out with 16 bytes), -1 on error int get_interface_ipv6_addr_nl(uint32_t netif_index, int prefer_stable, uint8_t* addr_out); // Determine the interface index for the default route (outgoing traffic) -// Creates a temp UDP socket, connects to a well-known address to force -// kernel routing, then matches the chosen source IP to getifaddrs() +// POSIX: UDP connect к фиксированному внешнему адресу, затем сопоставление локального IP с getifaddrs(). +// Windows: GetBestInterfaceEx к тому же адресу. Результат — выбранный маршрут к этому адресу, +// который может отличаться от default route при более специфичных маршрутах. // family: AF_INET or AF_INET6, returns ifindex (0 on failure) uint32_t get_default_route_netif_index(int family); diff --git a/lib/tcp_io_doc.md b/lib/tcp_io_doc.md index 7c79427e..1526ddd9 100644 --- a/lib/tcp_io_doc.md +++ b/lib/tcp_io_doc.md @@ -2,7 +2,7 @@ ## 1. Назначение -Асинхронный TCP-коннектор, построенный на event loop `uasync` и lock-free очередях `ll_queue`. +Асинхронный TCP-коннектор, построенный на event loop `uasync` и очередях `ll_queue` одного потока. Одна структура `struct tcp_conn` = одно TCP-соединение. Используется в SOCKS/HTTP-прокси, STCP, TCP-прокси и тестах. @@ -12,7 +12,7 @@ - Два memory pool: `entry_pool` (struct ll_entry) и `data_pool` (буферы), всё выделение на горячем пути через пулы. - Backpressure на чтение: high_water/low_water + `queue_waiter_wait`, EPOLLIN вкл/выкл по уровню заполнения очереди. - Backpressure на запись: порог 32 записи в write_queue + `queue_waiter_wait`, EPOLLOUT вкл/выкл при частичной отправке. -- Graceful shutdown: FIN (shutdown SHUT_WR) и CLOSE (close сокета) — оба как сентинелы в write_queue, все предшествующие данные гарантированно отправлены. +- Graceful shutdown: FIN (shutdown SHUT_WR) и CLOSE (close сокета) — сентинелы в write_queue; сначала предшествующие данные передаются ОС. ## 2. Как пользоваться @@ -29,10 +29,11 @@ struct tcp_conn* tc = tcp_conn_create( on_fin_cb, on_error_cb, arg); ``` -После создания нужно установить callback на read_queue и включить отложенную обработку: +После создания нужно установить callback на read_queue. Отложенный первый вызов и уведомления backpressure настраиваются отдельно: ```c queue_set_callback(tc->read_queue, on_read_cb, my_conn); +queue_set_callback_defer(tc->read_queue, 1); // необязательно: первый consumer callback через call_soon queue_set_waiter_defer(tc->read_queue, 1); ``` @@ -44,6 +45,7 @@ static void on_read_cb(struct ll_queue* q, void* arg) { struct ll_entry* e = queue_data_get(q); if (!e) { queue_resume_callback(q); return; } // обработать e->dgram (e->len байт) + memory_pool_free(c->tc->data_pool, e->dgram); queue_entry_free(e); queue_resume_callback(q); // обязательно! } @@ -51,7 +53,8 @@ static void on_read_cb(struct ll_queue* q, void* arg) { ### Запись данных -Пользователь напрямую кладёт entry в `write_queue`, отправка авто (deferred callback сам шлёт когда сокет готов): +Пользователь напрямую кладёт entry в `write_queue`. Первый callback может выполниться синхронно из `queue_data_put`; +продолжение после `queue_resume_callback` откладывается через `uasync_call_soon`: ```c struct ll_entry* e = queue_entry_new_from_pool(tc->entry_pool); @@ -71,7 +74,10 @@ tcp_conn_push_close(tc); // close сокета после отправки вс ``` Можно вызывать из callback-ов (on_fin, on_read_cb и т.д.). -Повторные вызовы игнорируются (fin_local=1/closed=1 — возвращают -1). +`push_fin` возвращает -1 как при уже поставленном FIN (`fin_pending=1`), так и после успешного shutdown (`fin_local=1`). +`push_close` возвращает -1 после фактического закрытия (`closed=1`); до обработки CLOSE повторная постановка не блокируется. +При ошибке shutdown вызывается `on_error`, а `on_fin_sent` не вызывается. Успешный push означает принятие сентинела в очередь; +связанный callback может выполниться ещё до возврата из push. ### Уничтожение @@ -79,7 +85,10 @@ tcp_conn_push_close(tc); // close сокета после отправки вс void tcp_conn_destroy(struct tcp_conn* tc); ``` -Идемпотентен (`destroyed` флаг). Дренирует обе очереди, закрывает сокет, освобождает пулы через отложенный `uasync_call_soon`. +Сразу отменяет connect-таймер и callbacks очередей/сокета, дренирует очереди и закрывает сокет. +Пулы и сам `tc` освобождаются через `uasync_call_soon` в том же потоке. После destroy владелец не использует указатель. +Повторный вызов безопасен только до выполнения отложенного free; NULL безопасен всегда. +Перед остановкой/уничтожением uasync нужно выполнить отложенную очистку (`uasync_drain_immediate` вне текущих callbacks). ### Обработка ошибок @@ -99,7 +108,7 @@ static void on_error_cb(struct tcp_conn* tc, int err, void* arg) { ```c tcp_conn_set_flushed(tc, my_flushed_cb); -// коллбэк вызовется один раз когда write_queue + write_buf полностью опустеют, затем сбросится +// коллбэк вызовется один раз когда write_queue + write_buf полностью опустеют; сбрасывается перед вызовом ``` ### Приостановка чтения @@ -108,12 +117,24 @@ tcp_conn_set_flushed(tc, my_flushed_cb); tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_waiter ``` +### Connect-таймаут + +`tcp_conn_set_connect_timeout(tc, timeout_ms)` заменяет интервал ожидания, начиная с текущего вызова. +Значение <=0 отменяет прежний таймер; положительное значение ограничено `INT_MAX/10` мс из-за единиц uasync (0.1 мс). +Для уже установленного соединения настройка сохраняется без создания таймера. +Возврат: 0 — применено, `-EINVAL` — аргументы/состояние, `-ENOMEM` — создание таймера, `-EIO` — нарушение состояния отмены. +При ошибке прежние настройка и handle сохраняются. Новый таймер резервируется перед отменой старого; +операция выполняется целиком в uasync-потоке без вызова пользовательских callbacks. Вызывающий код проверяет результат. +Истечение вызывает `on_error(ETIMEDOUT)`; завершение connect, фатальная ошибка и destroy отменяют таймер. + ### Ограничения и многопоточность - **Однопоточный.** Всё работает в одном `uasync` event loop. Нельзя вызывать из другого потока. -- **FIN-сентинел в очереди.** После `push_fin` данные продолжат отправляться, сам FIN выполнится только когда все предшествующие данные отправлены. +- **FIN-сентинел в очереди.** После успешного `push_fin` новые данные в write_queue не помещают; + предшествующие данные передаются ОС до shutdown. - **Нельзя вызывать close сокета напрямую.** Только через `tcp_conn_push_close` — иначе нарушится порядок обработки очереди. -- **Можно вызывать push_fin/push_close из callback-ов** — защита от повторного вызова встроена (fin_local/closed флаги). +- **Можно вызывать push_fin/push_close из callback-ов.** FIN защищён от повторной постановки; + CLOSE проверяет только уже выполненное закрытие. Владелец учитывает возможный синхронный callback. ## 3. API @@ -128,7 +149,7 @@ tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_wai | Функция | Описание | |---------|----------| | `tcp_conn_create(ua, sock, entry_data_size, write_chunk_size, read_high_water, read_low_water, rcvbuf_size, on_fin, on_error, arg)` | Создать соединение: аллоцирует tc, два memory pool, два ll_queue, регистрирует сокет в uasync с EPOLLIN+EPOLLOUT. Вовращает NULL при ошибке | -| `tcp_conn_destroy(tc)` | Дренирует read_queue и write_queue, закрывает сокет, освобождает пулы через `uasync_call_soon`. Идемпотентен | +| `tcp_conn_destroy(tc)` | Сразу отменяет таймер/callbacks, дренирует очереди и закрывает сокет; пулы и tc освобождает через `uasync_call_soon` | ### Коллбэки (поля struct tcp_conn) @@ -137,7 +158,7 @@ tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_wai | `on_fin(tc, arg)` | FIN получен от удалённой стороны (recv==0): сразу если read_queue пуста, иначе через `queue_set_empty_callback` после дренажа | | `on_fin_sent(tc, arg)` | FIN отправлен (`shutdown(SHUT_WR)`) — после отправки всех данных перед FIN-сентинелом | | `on_error(tc, err, arg)` | Фатальная ошибка сокета. Вызывается после close сокета. **Обязан вызвать tcp_conn_destroy.** После возврата tc недействителен | -| `on_flushed(tc, arg)` | write_queue + write_buf полностью опустели. Одноразовый — после вызова сбрасывается в NULL. Установка: `tcp_conn_set_flushed()` | +| `on_flushed(tc, arg)` | write_queue + write_buf полностью опустели. Одноразовый — перед вызовом сбрасывается в NULL. Установка: `tcp_conn_set_flushed()` | | `on_closed(tc, arg)` | Сокет закрыт через CLOSE-сентинел (`tcp_conn_push_close`) после отправки всех предшествующих данных | ### Graceful shutdown @@ -145,7 +166,7 @@ tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_wai | Функция | Описание | |---------|----------| | `tcp_conn_push_fin(tc)` | Помещает FIN-сентинел (`dgram=&tcp_fin_sentinel, len=0`) в write_queue. После отправки предшествующих данных: shutdown(SHUT_WR), fin_local=1, вызывает on_fin_sent. Повторный вызов → -1 | -| `tcp_conn_push_close(tc)` | Помещает CLOSE-сентинел (`dgram=NULL, len=0`) в write_queue. После отправки предшествующих данных: close сокета, удаление из uasync, closed=1, вызывает on_closed. Повторный вызов → -1 | +| `tcp_conn_push_close(tc)` | Помещает CLOSE-сентинел (`dgram=NULL, len=0`) в write_queue. После отправки предшествующих данных: close сокета, удаление из uasync, closed=1, вызывает on_closed. После закрытия → -1 | ### Прочее @@ -153,3 +174,4 @@ tcp_conn_pause_read(tc); // убирает EPOLLIN и отменяет read_wai |---------|----------| | `tcp_conn_set_flushed(tc, cb)` | Установить одноразовый коллбэк на полное опустошение write_queue | | `tcp_conn_pause_read(tc)` | Принудительно выключить EPOLLIN и отменить ожидание низкого порога | +| `tcp_conn_set_connect_timeout(tc, ms)` | Заменить/отключить connect-таймер; возвращает 0 или отрицательный код ошибки | diff --git a/lib/timeout_heap.h b/lib/timeout_heap.h index 295ed04b..be1c5cd6 100644 --- a/lib/timeout_heap.h +++ b/lib/timeout_heap.h @@ -16,7 +16,7 @@ extern "C" { #include // For uint64_t #include // For size_t -typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync использует timebase: 0.1 мс). +typedef uint64_t TimeoutTime; // Абсолютное время в шкале владельца (uasync хранит здесь миллисекунды). typedef struct { TimeoutTime expiration; // Sort key (smaller = earlier)