4.7 KiB
Важные нюансы работы встроенного lwIP TCP-стека
Краткая памятка по тонким местам src/lwip_tcp/, критичным для прокси.
Полное описание модуля — в src/lwip_tcp/lwip_tcp_doc.md.
Ключевые константы (lwip_tcp_opts.h)
| Константа | Значение | Смысл |
|---|---|---|
TCP_MSS |
1460 | максимальный размер сегмента |
TCP_WND |
8×MSS = 11680 | окно приёма lwIP (объявляется пиру) |
TCP_SND_BUF |
16×MSS = 23360 | буфер отправки |
TCP_TMR_INTERVAL |
250 мс | базовый интервал таймера |
tmr_interval_ms |
TCP_TMR_INTERVAL/4 = 62.5 мс |
фактический шаг tcp_fasttmr |
TCP_WND_UPDATE_THRESHOLD |
TCP_WND/4 = 2920 |
порог немедленного ACK окна |
Важно: таймер uasync использует timebase 0.1 мс — uasync_set_timeout(ua, N, …)
задаёт N×0.1 мс. Например, 5000 = 500 мс.
Контракт recv_cb — главный источник багов
lwIP вызывает recv_cb(arg, pcb, pbuf, err) для данных, уже извлечённых из pcb.
Дальше приложение само решает судьбу данных и окна:
| Действие | Результат |
|---|---|
скопировать + tcp_recved(pcb, len) |
данные приняты, окно восстановлено (норма) |
return != LERR_OK |
lwIP кладёт pbuf в refused_data и отдаст позже — без потери, окно закрывается |
pbuf_free(p) без tcp_recved |
данные потеряны, окно не восстановлено → схлопывание окна |
refused_data — штатный backpressure lwIP: одноканальный (один pbuf), ре-доставка
через tcp_fasttmr (62.5 мс) или при следующем входящем сегменте.
Ошибка, из-за которой падал tcp_proxy_full: при backpressure прокси делал
pbuf_free(p) без tcp_recved. Каждый такой сброс уменьшал rcv_wnd на MSS; после
~8 пакетов окно падало в 0, OS-TCP переставал слать, и передача умирала.
Управление окном и «деградация 62.5 мс»
tcp_recved(pcb, len) увеличивает rcv_wnd и через tcp_update_rcv_ann_wnd решает,
слать ли ACK окна немедленно. Немедленный ACK идёт только если прирост окна
wnd_inflation ≥ TCP_WND_UPDATE_THRESHOLD (=2920 = 2×MSS); иначе ACK откладывается
до tcp_fasttmr (62.5 мс).
Отсюда классическая ловушка: если после backpressure окно восстанавливать по одному
пакету (tcp_recved(1460) за раз), прирост 1460 < 2920 → ACK уходит раз в 62.5 мс →
пир шлёт 1 пакет за 62.5 мс ≈ 23 КБ/с. Это выглядит как «пропускная способность
застряла», хотя сеть свободна.
Правильно: восстанавливать окно пачкой (несколько tcp_recved подряд либо один
tcp_recved(TCP_WND_MAX - rcv_wnd)), тогда прирост ≥ 2920 → ACK немедленный → пир
сразу возобновляет бурст.
Рекомендуемый паттерн для relay-потребителя (как в tcp_proxy_client.c)
- При получении данных — копировать в ограниченную очередь, не вызывать
tcp_recved(окно само плавно закрывается; очередь ограниченаTCP_WND). - Дрейн очереди по сигналу освобождения нижележащего канала
(
etcp_router_on_send_ready) + retry-таймер (force=1) как safety-net. tcp_recved(len)вызывать только после успешной отправки — окно восстанавливается пачкой по мере дрена, ACK уходит немедленно.- Никогда не сбрасывать pbuf без
tcp_recved; не держать данные вrefused_dataдольше одного пакета (single-slot).
См. реализацию: tcp_proxy_client_tx_queue_drain_cb / tcp_proxy_client_fin_flush
в src/proxy/tcp_proxy_client.c.