You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

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)

  1. При получении данных — копировать в ограниченную очередь, не вызывать tcp_recved (окно само плавно закрывается; очередь ограничена TCP_WND).
  2. Дрейн очереди по сигналу освобождения нижележащего канала (etcp_router_on_send_ready) + retry-таймер (force=1) как safety-net.
  3. tcp_recved(len) вызывать только после успешной отправки — окно восстанавливается пачкой по мере дрена, ACK уходит немедленно.
  4. Никогда не сбрасывать 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.