скрытое меню

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

Зачем нам может понадобится вообще говоря POST запросы. Причина банальна : хочется передавать данные объемом побольше чем позволяет GET. К тому же GET безопасно кодирует строку перед посылкой.

И тут выясняется ,что реализации поддержки 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 type form-data :

 HTTP : POST / HTTP/1.1
cache-control: no-cache
Postman-Token: 74bb3ac7-b817-4edf-9501-8b405b2f351d
User-Agent: PostmanRuntime/7.6.0
Accept: */*
Host: 192.168.7.1
accept-encoding: gzip, deflate
content-type: multipart/form-data; boundary=--------------------------378210095867263134998454
content-length: 285
Connection: keep-alive

----------------------------378210095867263134998454
Content-Disposition: form-data; name="wifi_login"

111111

И тут есть проблема с 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 посылает некорректно. Браузер вроде нормально.

Пересылка больших данных

Если данные не помещаются в одном пакете (сегменте) [1460 байт], то логично , что отправитель (клиент-браузер) разбивает их на несколько пакетов.

Для начала надо включить #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 нигде не вызывается .

25 ms -рекомендуемое время задержки для ретрансмиссии пакетов. Для нас это очень маленькое время и мы (сервер) не успеваем ответить .

Чтобы реально въехать в логику работы надо изучить вот такую (из LWIP1.4.1) структуру tcp_pcb. Каждый элемент имеет важное значение. Во-первых для каждого соединения строится цепочка таких структур (см. указатель next на следующий принятый сегмент).

Занимается разбором полетов в основном функция tcp_receive . Именно она работает со всеми элементами структуры 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 */
  u16_t rcv_wnd;   /* receiver window available */
  u16_t rcv_ann_wnd; /* receiver window to announce */
  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. */

  /* congestion avoidance/control variables */
  u16_t cwnd;
  u16_t ssthresh;

  /* sender variables */
  u32_t snd_nxt;   /* next new seqno to be sent */
  u32_t snd_wl1, snd_wl2; /* Sequence and acknowledgement numbers of last
                             window update. */
  u32_t snd_lbb;       /* Sequence number of next byte to be buffered. */
  u16_t snd_wnd;   /* sender window */
  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). */
#define TCP_SNDQUEUELEN_OVERFLOW (0xffffU-3)
  u16_t snd_queuelen; /* Available buffer space for sending (in tcp_segs). */

#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. */
#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;
};

Первое , что отрабатывает функция tcp_receive это проверяет ACK от клиента , то есть установлен ли ACK флаг в сегменте. Если установлен , то значит клиент что-то подтверждает нам (серверу), то есть отвечает на нашу посылку перед этим.

Чтобы разобраться со всем этим , делаем трассировку с выводом каждого значения , которое меняется в структуре tcp_pcb.
Плюс подключаем WireShark и видим , что не все так понятно и идеально, хотя клиент получает запрашиваемую страницу.

Стрелками показаны реально принятые пакеты сервером (остальные потеряны), поэтому и возникает retransmission (повторные передачи от клиента).

фотка 1

Все это говорит о том , что наш сервер очень тормозной (оно и неудивительно это на контроллере с ОЗУ 128К).

Пакет retransmission (повторный) ничем не должен отличается от основного , никакими опциями. Но у нас есть огромный непонятный нюанс (должно ли так быть?) : всего 2 пакета надо было передать (содержание 1460 + 885) , клиент делает повторную передачу этих двух пакетов , но содержание их теперь другое (1460 + 1460)... Причем сначала 1460 (это от конца 885:2345) и потом 1460 (это с начала 0:1460). Если еще учесть ,что в первом retransmission Seq=886 Ack=1 , а во втором Seq=886 Ack=1, то можно предположить , что это не ошибка клиента и на стороне сервера можно таки собрать содержание передаваемых пакетов корректно. Но LWIP 1.4.1 или 2.1.2 этого не поддерживают (как я понял).

Еще выясняется , что наконец-то видны отличия версий LWIP 1.4.1 и 2.1.2 . Они как раз появляются здесь в функции tcp_receive . Логика уже начинает меняться.

Дальше по коду наш сервер почему-то решает передать на первый пакет в ответ tcp_send_empty_ack , хотя с SYN/ACK мы клиенту указали MSS 2920 , то способны принять 1460*2 (два полноценных пакета) БЕЗ ПОДТВЕРЖДЕНИЯ. Где логика?

Причем мы передаем его (tcp_send_empty_ack) с параметрами Seq=1 Ack=1 и это точно что-то неправильное. Поэтому WinShark помечает этот пакет TCP DUP ACK.

Ну , что пора "размять мозги" . Будем разбираться с логикой отработки пакетов (сегментов).

Пора сказать , что все приходящие пакеты так или иначе сохраняются в памяти (у нас в PBUF_POOL). Для каждого TCP соединения предусмотрены цепочки сегментов разных типов , сохраняемые как структуры tcp_seg (параметр next указатель на следующий):

unacked - неподтвержденные сегменты (здес учитываем неподтвержденные клиентом посылки сервера), прямо с паекта SYN/ACK сервера и начинается. По мере подтверждения клиентом пакет сервера очищается из памяти (сервера).

unsent - не посланные сегменты (кто/куда?), по-видимому это уже ответы сервера клиенту в пределах этого tcp соединения и похоже это в варианте retransmission

ooseq - принятые сегменты , но не порядку посылки клиентом. Механизм сборки включается по дефайну TCP_QUEUE_OOSEQ.

Есть несколько глобальных переменных , которые участвуют в процессе:

static u32_t seqno , ackno;
static u8_t flags;
static u16_t tcplen;

seqno (глобальная переменная) - она учитывает текущий номер Seq , каждый вновь принятый пакет меняет этот параметр (см. функцию tcp_input).

2123148578 [1]
2123150038 [1461] 6543
2123149463 [883]
2123151497 [2920]

ackno - аналогично запоминает параметр Ack последнего принятого пакета (см. tcp_input).

Что пока понятно со структурой tcp_pcb , это применительно к tcp_receive ссылка на каждый входящий сермент :
flags - тут сохраняется состав флагов входящих пакетов
lastack - это последний (сырой) ACK, который мы (сервер) получили от клиента.

http_recv

В итоге после принятия всех tcp сегментов (пакетов) и их пересборки и если все в сегменты пришли - должна вызваться функция http_recv. Её вызов спрятан в макросе :
TCP_EVENT_RECV( pcb , recv_data , ERR_OK , err );