POST запросы к нашему HTTP серверу

Зачем нам может понадобится вообще говоря 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 сегмента) :

фотка 1

На картинке выше сервер после получения каждолго сегмента отвечает клиенту ACK пакетом нулевой длины (это нормально).

Ниже можно скачать лог выполнения запроса , представленного на картинке выше. Для убыстрения поиска обращайте вниминие на значение контрольной суммы пакета в WinShark. Далее в текстовом логе по этому значению можно быстро найти этот пакет.

Далее меняем размер окна MSS 1460 , TCP_WND 2920 , пересобираем проект и все опять работает нормально.

Выводы

На данном этапе можно константировать , что наш сервер стабильно принимает 5 сегментов за пару секунд (при полном логгировании) . Память не утекает - все блоки логично очищаются. И это радует !

Можно отметить , что делать TCP_WND менее двух MSS нельзя, но делать более двух MSS при наличии большого количества повторных передач тоже никакого смысла нет.

Файлы для скачивания

* USB_RNIDS_POST 5SEGMENTS_OK [zip]
лог вызовов функций post запроса на стороне сервера (LWIP 1.4.1)