Зачем нам может понадобится вообще говоря POST запросы. Причина банальна : хочется передавать данные объемом побольше чем позволяет один GET запрос.
Все ,что ниже описано связано с несколькими сегментами (составляющими post запрос) , которые отправляет клиент серверу.
И тут выясняется ,что реализации поддержки POST запросов в LWIP 1.4.1 нет , объявления post функций вроде есть , а реализации нет . А вот в LWIP 2.1.2 уже реализация post есть, но поскольку у нас с 2.1.2 отношения никак не складываются, будем переносить код из 2.1.2 в 1.4.1.
Поддержка post запросов находится в 2 файлах post.c и post.h. Author: Joakim Myrland.
Подключаем к себе в проект , где уже (GET запросы отрабатываются нормально) и почти сразу все начинает как-то работать , но не радуйтесь сразу...
Тестировать работу сервера удобно программой Postman.
Вот такое содержание приходит при post запросе (тип передаваемых данных - json) . Это случай когда вся посылка влезет в размер одного сегмента:
HTTP : POST / HTTP/1.1
Content-Type: application/json
cache-control: no-cache
Postman-Token: 4422432a-90ed-43f9-a686-438ff20e3d0f
User-Agent: PostmanRuntime/7.6.0
Accept: */*
Host: 192.168.7.1
accept-encoding: gzip, deflate
content-length: 29
Connection: keep-alive
{
"command":"asdasdasdsad"
}
И тут есть проблема с content-length :, так как LWIP проверяет Сontent-length : с большой буквы. Сравнение срок в LWIP происходит в самописной функции strnstr .
Исправляем строку
if ( ( * p == * token ) && ( strncmp ( p , token , tokenlen ) == 0 ) )
на
if ( ( * p == tolower(* token ) ) && ( strncmpi ( p , token , tokenlen ) == 0 ) )
и все начинает работать нормально. То есть content-length нормально отрабатывается. Похоже Postman посылает некорректно. Браузер вроде нормально.
Чтобы реально въехать в логику работы надо изучить вот такую (из LWIP) структуру tcp_pcb. Каждый элемент имеет важное значение. Для каждого TCP соединения создается отдельная своя структура tcp_pcb.
/* the TCP protocol control block */
struct tcp_pcb
{
/** common PCB members */
IP_PCB;
/** protocol specific PCB members */
TCP_PCB_COMMON(struct tcp_pcb);
/* ports are in host byte order */
u16_t remote_port;
u8_t flags;
#define TF_ACK_DELAY ((u8_t)0x01U) /* Delayed ACK. */
#define TF_ACK_NOW ((u8_t)0x02U) /* Immediate ACK. */
#define TF_INFR ((u8_t)0x04U) /* In fast recovery. */
#define TF_TIMESTAMP ((u8_t)0x08U) /* Timestamp option enabled */
#define TF_RXCLOSED ((u8_t)0x10U) /* rx closed by tcp_shutdown */
#define TF_FIN ((u8_t)0x20U) /* Connection was closed locally (FIN segment enqueued). */
#define TF_NODELAY ((u8_t)0x40U) /* Disable Nagle algorithm */
#define TF_NAGLEMEMERR ((u8_t)0x80U) /* nagle enabled, memerr, try to output to prevent delayed ACK to happen */
/* the rest of the fields are in host byte order
as we have to do some math with them */
/* Timers */
u8_t polltmr, pollinterval;
u8_t last_timer;
u32_t tmr;
/* receiver variables */
u32_t rcv_nxt; /* next seqno expected */
// это в следующем пакете от клиента
// ожидаемый номер seq
u16_t rcv_wnd; /* receiver window available */
// сколько от целого окна осталось свободного места (байтов) для приема от клиента
u16_t rcv_ann_wnd; /* receiver window to announce */
// размер окна указываемый сервером для клиента
// именно это значение попадает в параметр WIN
// при отправке сервером сегмента клиента
u32_t rcv_ann_right_edge; /* announced right edge of window */
/* Retransmission timer. */
s16_t rtime;
u16_t mss; /* maximum segment size */
/* RTT (round trip time) estimation variables */
u32_t rttest; /* RTT estimate in 500ms ticks */
u32_t rtseq; /* sequence number being timed */
s16_t sa, sv; /* @todo document this */
s16_t rto; /* retransmission time-out */
это количество TCP_SLOW_INTERVAL (500ms) или время , через
которое сервер должен повторно передать пакет
если ответ не пришел вовремя , то есть за rto* TCP_SLOW_INTERVAL ms
u8_t nrtx; /* number of retransmissions */
/* fast retransmit/recovery */
u8_t dupacks;
u32_t lastack; /* Highest acknowledged seqno. */
// [IN] тут сохраняется последний ack от клиента
/* congestion avoidance/control variables */
u16_t cwnd; // current wnd , текущее значение окна
u16_t ssthresh;
/* sender variables */
u32_t snd_nxt; /* next new seqno to be sent */
u32_t snd_wl1; /* Sequence and acknowledgement numbers of last window update. */
// сюда пишем значение seq из последнего принятого пакета с ACK
u32_t snd_wl2;
// сюда пишем значение ack из последнего принятого пакета с ACK
u32_t snd_lbb; /* Sequence number of next byte to be buffered. */
u16_t snd_wnd; /* sender window */
// принятый от клиента размер окна (используется при отсылке клиенту)
// то есть в snd_wnd мы всегда заносим , что на передал с ACK клиент,
// сколько максимум байт клиент может принять без подтверждения
u16_t snd_wnd_max; /* the maximum sender window announced by the remote host */
u16_t acked;
u16_t snd_buf; /* Available buffer space for sending (in bytes). */
// [OUT] тут сервер передает
#define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3)
u16_t snd_queuelen; /* Available buffer space for sending (in tcp_segs). */
// это счетчик tcp сегментов у сервера на очереди для отправки клиенту
#if TCP_OVERSIZE
/* Extra bytes available at the end of the last pbuf in unsent. */
u16_t unsent_oversize;
#endif /* TCP_OVERSIZE */
/* These are ordered by sequence number: */
struct tcp_seg *unsent; /* Unsent (queued) segments. */
struct tcp_seg *unacked; /* Sent but unacknowledged segments. */
#if TCP_QUEUE_OOSEQ
struct tcp_seg *ooseq; /* Received out of sequence segments. */
// это указатель на tcp_seg блок в памяти
// это сегмент , который неожиданно пришел раньше предыдущего
// сюда помещаются все сегменты , которые приходит не логично (не по порядку)
#endif /* TCP_QUEUE_OOSEQ */
struct pbuf *refused_data; /* Data previously received but not yet taken by upper layer */
#if LWIP_CALLBACK_API
/* Function to be called when more send buffer space is available. */
tcp_sent_fn sent;
/* Function to be called when (in-sequence) data has arrived. */
tcp_recv_fn recv;
/* Function to be called when a connection has been set up. */
tcp_connected_fn connected;
/* Function which is called periodically. */
tcp_poll_fn poll;
/* Function to be called whenever a fatal error occurs. */
tcp_err_fn errf;
#endif /* LWIP_CALLBACK_API */
#if LWIP_TCP_TIMESTAMPS
u32_t ts_lastacksent;
u32_t ts_recent;
#endif /* LWIP_TCP_TIMESTAMPS */
/* idle time before KEEPALIVE is sent */
u32_t keep_idle;
#if LWIP_TCP_KEEPALIVE
u32_t keep_intvl;
u32_t keep_cnt;
#endif /* LWIP_TCP_KEEPALIVE */
/* Persist timer counter */
u8_t persist_cnt;
/* Persist timer back-off */
u8_t persist_backoff;
/* KEEPALIVE counter */
u8_t keep_cnt_sent;
};
Пересылка больших данных
Если данные не помещаются в одном пакете (сегменте) [1460 байт], то логично , что отправитель (клиент-браузер) разбивает их на несколько пакетов (сегментов).
Передача клиентом ведется пачками (порциями сегментов) . Одна такая пачка равна размеру окна (TCP_WND), который сервер устанавливает клиенту в ответе SYN/ACK. После такой пачки сервер должен ответить ACK пакетом с нулевой длиной, чтобы дать клиенту понять ,что он (сервер принял) пачку нормально. В этом же пакете сервер может указать новый размер окна (новый размер порции) для следующей пачки. Сервер может также указать размер окна равным 0 , чтобы проинформировать клиента , что он (сервер) занят чем-то и не готов принимать данные.
В tcp обмене пакетами , когда клиент хочет послать их много, происходит полная непоследовательность получения на стороне сервера. Это происходит в основном благодаря тому , что клиент не дождавшись ответа от сервера начинает повторно их посылать (retransmission) и даже уже в другом порядке. И даже уже может разбивать их контент по другому. В результате сервер остается один на один c частью пакетов , которые реально дошли и пытается их собрать в единую посылку (в пределах текущего окна TCP_WND).
Рекомендуемое время задержки для ретрансмиссии пакетов вроде 25 ms (но это не точно). Для нас это очень маленькое время и мы (сервер) не успеваем ответить .
Теперь о главном - чтобы понять логику работы tcp соединения на стороне сервера (то есть в LWIP 1.4.1) надо понять когда и в каких регионах памяти выделяется память для отработки обмена по tcp. И вот тут выясняется , что в этом процессе (по крайней мере на прием) участвует несколько несколько регионов памяти : [TCP_PCB] , [TCP_PCB_LISTEN] , [TCP_SEG] , [PBUF_POOL] . Эти регионы обязательно надо ослеживать и тогда складывается такая примерно картина:
TCP_PCB_LISTEN
Сначала при инициализации tcp мы создаем в [TCP_PCB_LISTEN] блок памяти (со структурой tcp_pcb_listen) , в которой мы просто указываем какой порт мы слушаем.
PBUF_POOL
Каждый приходящий пакет из сети ethernet сначала попадает в регион памяти PBUF_POOL . Блок памяти предваряется структурой struct pbuf . Размер блока памяти равен PBUF_POOL_BUFSIZE
#define PBUF_POOL_BUFSIZE LWIP_MEM_ALIGN_SIZE(TCP_MSS+40+PBUF_LINK_HLEN)
1460+40+14=1514 байт.
То есть в PBUF_POOL сохраняется содержание всего пакета (заголовки , данные).
TCP_PCB
Далее сервер создает в регионе [TCP_PCB] блок памяти (со структурой tcp_pcb), что является фисксацией нового соединения по tcp с клиентом.
Еще при создании нового соединения мы создаем отдельный таймер для нашего tcp соединения. То есть добавляем в регион [SYS_TIMEOUT] новый блок памяти. Интервал устанавливается TCP_TMR_INTERVAL, то есть 250мс. Этот таймер обслуживает только открытые tcp соединения.
Сервер отвечает клиенту SYN/ACK пакетом.
Далее Сервер получает от клиента ACK пакет нулевой длины . Но не факт на самом деле. Сервер получает тот пакет , который успевает получить , а перед этим идущие пакеты могут для сервера пропасть, просто сервер не успевает их принять , так как чем-то занят. То есть сервер может и не получить ACK нулевой длины (как бы он пропадет), а получить следующий пакет уже с данными и даже не первый по порядку (в пределах текущего окна) например с флагами PSH/ACK. PSH - это в последнем сегменте посылается (текущего окна).
В любом случае после получения ACK от клиента сервер создает блок памяти в регионе [HTTPD_STATE], чем обозначает открытие соединения http. Тут размещена структура http_state, в которой есть указатель tcp_pcb * pcb . Ему присваиваем значение адреса на блок памяти в регионе [TCP_PCB] (нашего текущего соединения).
TCP_SEG
Когда приходит очередной сегмент и он не вписывается в последовательность передаваемых сегментов клиента , то это out-of-sequence сегмент и его надо сохранить до будущих времен (см.ooseq).
Для этого есть регион [TCP_SEG]. Здесь создается блок памяти со структурой tcp_seg . В tcp_seg структуре устанавливаем ссылку на [PBUF_POOL] (где находится содержание принятого пакета). В структуре tcp_pcb есть указатель на цепочку сегментов , которые пришли не по порядку ooseq. Добавляем в цепочку ooseq ссылку на наш tcp_seg.
unacked
Также [TCP_SEG] используется для учета не подтвержденных пакетов от сервера клиенту. То есть клиент должен подвердить посланный пакет от сервера , а пока сервер при посылке разместил содержание такого сегмента в [MALLOC_256] и мы его пока не удаляем. Далее создаем в [TCP_SEG] блок памяти под tcp_seg структуру . Делаем ссылку в tcp_seg на содержание [MALLOC_256]. Добавляем в цепочку unacked tcp_pcb структуры регион [TCP_PCB] ссылку на наш tcp_seg. И ждем когда клиент подтверит ACK пакетом наш пакет от сервера.
Это происходит , например, когда приходит первый пакет SYN от клиента к серверу . Таким образом мы увидим в [TCP_SEG] ссылку на наш первый ответ ACK/SYN (в ответ на первый принятый сегмент SYN).
Таким образом в блоке памяти региона [TCP_PCB] , есть связанные цепочки tcp_seg . Цепочка указателей на неподтвержденные клиентом пакеты unacked (tcp_seg *) . Ту мы учитываем не подтвержденые пакеты от сервера клиенту. В ooseq учитываем пакеты от клиента вне последовательности.
Делаем стандартные проверки при получении пакета с ACK :
unacked
Смотрим цепочку unacked в tcp_pcb и удаляем все сегменты вместе с их контентом из [TCP_SEG] и [MALLOC_256], то есть удаляем из [TCP_SEG] сегменты , ожидавшие подтверждения и потом из unacked саму ссылку ссылку на сегмент. Причем удаляются почему-то все элементы в цепочке unacked.
unsent
Далее проверяется цепочка unsent в tcp_pcb, но у нас там еще ничего нет. Это используется для данных , отправляемых сервером клиенту.
Далее идет разбор содержания принятого сегмента (tcplen>0) и что с ним делать. Проверяется попадает ли пришедший сегмент в текущее окно по его параметру seq и переменной rcv_nxt; (next seqno expected - ожидаемое seq в очередном пакете от клиента).
Надо сказать именно эта часть кода (функция tcp_receive) подверглась сильному изменению от версии 1.4.1 к 2.1.2.
Итак сначала проверяем наш пришедший сегмент на предмет , что его seq где-то в пределах нормального (seqno)... Тут не совсем понятно.
Следующая проверка уже очевидная - попадает ли принятый сегмент в текущее окно.
Если принятый сегмент попадает прямо как надо (по порядку) (и перед этим не было out-of-sequence сегментов), то есть ожидаемый номер rcv_nxt точно равен принятому seqno, то остается проверить только , что размер сегмента не выходит за пределы текущего окна. Если правый край выходит за пределы окна , то обрезаем его по значению правого края окна. Далее проверяем ooseq (если он не пустой) на предмет полученных out-of-sequence пакетов ранее и если такие имеются....
Если принятый сегмент попадает в текущее окно , но не по порядку следования сегментов, то есть seqno больше ожидаемого значения (rcv_nxt) или уже была нарушена последовательность сегментов (в пределах окна), то это сегмент out-of-sequence (ooseq), то есть сегмент вне последовательности , но в пределах окна . Это просто один из следующих (или предыдущих) пакетов. Мы сохраняем сегмент в [TCP_SEG] , а его контент в оставляем в [PBUF_POOL]. И помещаем ссылку на него в цепочку ooseg нашего соединения tcp_pcb. Причем в цепочке ooseg мы размещаем сегменты по порядку следования (по Seq).
Работа с цепочкой ooseq заключается в следующем : мы начинаем раскручивать цепочку с головы и проверяем , что seqno пришедшего сегмента равен одному из сегментов цепочки. Если длина пришедшего сегмента почему-то больше сегмента в цепочке , то мы заменяем сегмент в цепочке нашим пришедшим.
rcv_nxt
Основополагающим параметром является rcv_nxt (следующий ожидаемый seq от клиента). По-поводу него можно сказать , что изначально он устанавливается в функции tcp_listen_input по приходу SYN от клиента. Устанавливается rcv_nxt на единицу больше seq от пришедшего в SYN. Далее этот параметр строго увеличивается , но только с теми сегментами , которые четко попадают в последовательность . Сегменты вне последовательности отбрасываются в цепочку ooseq для временного хранения (до востребования).
В ooseq кстати сегменты размещаются по порядку следования параметра Seq.
Там же при инициализации устанавливается еще один параметр npcb->snd_wl1 = seqno - 1; . В последующем snd_wl1 используется вместе с rcv_nxt.
Функция tcp_update_rcv_ann_wnd устанавливает параметр размер окна для каждого посылаемого сегмента от сервера клиенту.
tcp_update_rcv_ann_wnd
Очень не просто для понимания, но это функция в результате устанавливает значение rcv_ann_wnd , которое является выходным параметром WIN каждого сегмента , отправляемого сервером клиенту.
Тут есть важная логика - надо всегда отправлять в WIN значение еще не принятой части текущего окна (это в нулевых ACK пакетах). Например окно размером у нас 1460*3. Если мы приняли первый сегмент (реально первый по номеру seq [1460]) , то мы можем ответить пустым ACkом с параметром WIN=1460*2=2920 , чтобы указать клиенту таким образом , что мы не приняли 2 и 3 последние пакеты. Вообще говоря это дополнительный контроль , так как мы в пакете уже однозначно определяем какой пакет мы подтверждаем (по параметрам Ack и Seq).
Как только мы (сервер) получаем все пакеты (1,2,3=1460*3) , то подтверждаем прием клиенту от сервера пакетом с уже новым WIN , который можно установить произвольно по итогам предыдущего обмена. Таким образом мы открываем новое окно. И все далее повторяется сначала.
Для начала надо включить #define LWIP_HTTPD_POST_MANUAL_WND 1 .
httpd_post_begin должен срабатывать один раз при приеме первого пакета данных.
После приема очередного пакета сервер должен отдавать tcp_send_empty_ack. И вот тут в параметрах tcp ответа можно указать , что мы заняты или наоборот готовы к приему стольки-то данных.
Если все данные приняты (все сегменты получены), то произойдет вызов httpd_post_finished.
Еще есть такая интересная строчка u8_t post_auto_wnd = 0; // !! 1; . К ней есть инфа в доке : post_auto_wnd.
Set this to 0 to let the callback code handle window updates by calling 'httpd_post_data_recved' (to throttle rx speed) default is 1 (httpd handles window updates automatically) .
Но httpd_post_data_recved нигде не вызывается (в LWIP 1.4.1).
tcp_receive
Первое , что отрабатывает функция tcp_receive это проверяет ACK от клиента , то есть установлен ли ACK флаг в сегменте. Если установлен , то значит клиент что-то подтверждает нам (серверу), то есть отвечает на нашу посылку перед этим.
Чтобы разобраться со всем этим , делаем трассировку с выводом каждого значения , которое меняется в структуре tcp_pcb.
Плюс подключаем WireShark и видим , что не все так понятно и идеально, хотя клиент получает запрашиваемую страницу.
Пора сказать , что все приходящие пакеты так или иначе сохраняются в памяти (у нас в PBUF_POOL). Для каждого TCP соединения предусмотрены цепочки сегментов разных типов , сохраняемые как структуры tcp_seg (параметр next указатель на следующий):
unacked - неподтвержденные сегменты (здесь учитываем неподтвержденные клиентом посылки от сервера), прямо с пакета SYN/ACK сервера и начинается. По мере подтверждения клиентом пакет сервера очищается из памяти (сервера).
unsent - не посланные сегменты (кто/куда?), по-видимому это уже ответы сервера клиенту в пределах этого tcp соединения и похоже это в варианте retransmission
ooseq - принятые сегменты , но не порядку посылки клиентом. Механизм сборки включается по дефайну TCP_QUEUE_OOSEQ.
Есть несколько глобальных переменных , которые участвуют в процессе:
static u32_t seqno; // [IN] это Seq от клиента по последнему принятому пакету
static u32_t ackno; // [IN] это Ack от клиента по последнему принятому пакету
static u8_t flags; // [IN] это флаги от клиента по последнему принятому пакету
static u16_t tcplen; // [IN] это дли контента от клиента по последнему принятому пакету
seqno (глобальная переменная) - она учитывает текущий номер Seq , каждый вновь принятый пакет устанавливает этот параметр (см. функцию tcp_input).
ackno - аналогично запоминает параметр Ack последнего принятого пакета (см. tcp_input).
flags - тут сохраняется состав флагов входящих пакетов
lastack - это последний (сырой) ACK, который мы (сервер) получили от клиента.
rcv_ann_wnd - именно это значение попадает в поле WIN (размер окна) у сегмента , посылаемого сервером клиенту.
rcv_nxt - ожидаемое хначение Seq в пакете от клиента серверу.
Логика проверки получения всех данных post запроса основана на контроле количества оставшихся не принятых байт post_content_len_left.
post_content_len_left
Первоначальное значение post_content_len_left естественно устанавливается равным content-length: заголовка из первого пакета.
Когда post_content_len_left станет =0 сработает функция http_handle_post_finished и далее вызовется http_send уже для отслылки данных клиенту.
LWIP_HTTPD_POST_MANUAL_WND
LWIP_HTTPD_POST_MANUAL_WND мы не используем.
http_recv
Эта функция срабатывает , когда приходит сегмент от клиента , который является следующим по порядку следования (см. seqno и rcv_nxt). На сегменты отбрасываемые в ooseq эта функция не срабатывает. Её вызов спрятан в макросе :
TCP_EVENT_RECV( pcb , recv_data , ERR_OK , err );
http_post_rxpbuf
http_post_rxpbuf - функция , куда попадают уже только данные (тело) post запроса. Здесь мы приходящие порции данных закидываем себе в приемный буфер.
Логгирование
Вообще говоря есть очень неплохая идея трассировки (логгирования) отработки функций TCP стека по принципу иерархического списка . Визуально логика очень легко читается.
Какие ошибки портят жизнь программистам ?
Например :
pcb->rcv_ann_right_edge = pcb->rcv_nxt + pcb->rcv_ann_wnd;
приводит иногда к неправильному (с нашей точки зрения) результату. Надо :
pcb->rcv_ann_right_edge = pcb->rcv_nxt + (uint32_t )pcb->rcv_ann_wnd;
Думаю комментарии излишни... В чем суть понятно.
Представляем рабочий вариант отправки клиентом 5 сегментов. MSS 1460 , TCP_WND 4380 (3 сегмента) :
На картинке выше сервер после получения каждолго сегмента отвечает клиенту ACK пакетом нулевой длины (это нормально).
Ниже можно скачать лог выполнения запроса , представленного на картинке выше. Для убыстрения поиска обращайте вниминие на значение контрольной суммы пакета в WinShark. Далее в текстовом логе по этому значению можно быстро найти этот пакет.
Далее меняем размер окна MSS 1460 , TCP_WND 2920 , пересобираем проект и все опять работает нормально.
Выводы
На данном этапе можно константировать , что наш сервер стабильно принимает 5 сегментов за пару секунд (при полном логгировании) . Память не утекает - все блоки логично очищаются. И это радует !
Можно отметить , что делать TCP_WND менее двух MSS нельзя, но делать более двух MSS при наличии большого количества повторных передач тоже никакого смысла нет.
Файлы для скачивания
*
лог вызовов функций post запроса на стороне сервера (LWIP 1.4.1)