TCP это основа HTTP

Основные промежуточные результаты :

На данном этапе у нас работает http сервер на LWIP 1.4.1. Вариант именно RAW на FreeRTOS. Динамическое выделение памяти мы убрали отовсюду (malloc) , используем только диспетчер памяти от LWIP (HTTPD_USE_MEM_POOL ).
FreeRTOS мы делаем static и таким образом FreeRTOS у нас не занимается динамическим выделением памяти.

К сожалению вынужден признать , что LWIP 2.1.2 похоже не просто глюк на глюке , а что-то еще похуже....

Теперь о том как работает TCP в LWIP 1.4.1 .

У нас есть указатель на цепочку структур tcp_active_pcbs , tcp_tw_pcbs , tcp_listen_pcbs. Это сути цепочки элементов (структур), где каждый головной элемент указывает на следующий и так далее. Общее число элементов меняется во время работы.

В tcp_active_pcbs хранится указатель на цепочку активных соединений.

Но еще есть и соединения , у которых истекло время ожидания активной работы. Эти соединения хранятся в tcp_tw_pcbs цепочке.

Можно сразу обратится к функции tcp_input, именно она изучает поступающие tcp пакеты. Сначала она ищет по параметрам поступившего пакета (remote_port, local_port , remote_ip , local_ip ) пакет в цепочке активных соединений tcp_active_pcbs.

Далее если не нашли в tcp_active_pcbs ищем в tcp_tw_pcbs и если находим вызываем tcp_timewait_input и выходим.

Далее если не нашли и в tcp_tw_pcbs мы ищем просто в списке прослушиваемых портов tcp_listen_pcbs.listen_pcbs ( по local_port и local_ip) . Если находим то вызываем tcp_listen_input и на выход.

И наконец остается только вариант одного из активных соединений. Тут мы вызываем tcp_process и получаем данные (флаг PSH).

Цепочка прослушиваемых портов

Эта цепочка tcp_listen_pcbs. Здесь просто собраны прослушиваемые порты. Состояние LISTEN , то есть когда соединение еще не установлено.

Вот в какие состояния переходят в процессе обмена TCP пакетами сервер и клиент:

фотка 1

Чтобы разобраться с tcp надо обязательно разобраться с вариантами все возможных состояний tcp соединения . К счастью их не так много.

const char * const tcp_state_str [ ] =
{ "CLOSED" , "LISTEN" , "SYN_SENT" , "SYN_RCVD" , "ESTABLISHED" , "FIN_WAIT_1" ,
		"FIN_WAIT_2" , "CLOSE_WAIT" , "CLOSING" , "LAST_ACK" , "TIME_WAIT" };

Первый tcp пакет , который мы получим , это SYN . В этот момент у нас (на сервере) нет еще активного соединения и мы попадаем в tcp_listen_input. Если у принятого пакета флаг TCP_SYN установлен мы создаем новое соединение tcp_alloc (выделяем память) и инициализируем свойства нового соединения и заносим созданную структуру npcb в цепочку активных соединений tcp_active_pcbs с свойством состояния SYN_RCVD. И отсылаем обратно ответ через tcp_output с флагами TCP_SYN | TCP_ACK.

Клиент передает первый пакет SYN. В пакете клиента Seq установлен в конкретное значение. Ack не становлен (то просто 0).

Сервер отвечает клиенту пакетом SYN/ACK, где придумывает Seq сервера !=0. Cервер устанавливает обязательно ack сервера = последний seq клиента+1.

Клиент отвечает пакетом ACK , где seq (клиента) = последний ack сервера +1.

Таким образом каждый (клиент и сервер) придумывают себе seq (каждый сам себе). Ack - ом они подтверждают прием от оппонента.

В отсылаемом обратно ответе сервера клиенту есть несколько важных опций : например Window size value. В коде LWIP этот параметр устанавливается дефайном #define TCP_SND_BUF (2 * TCP_MSS) и у нас при TCP_MSS=1460 получается 2920.
TCP_MSS - это максимальный размер передаваемого сегмента (одного пакета).

Таким образом клиент узнает , что подряд можно передать не больше 2920 байт или (2 сегмента) и надо обязательно ждать ответа с подтверждением ACK от сервера.

Все далее со вторым пакетом мы в tcp_listen_input не попадем, а попадем уже в tcp_process для обработки пакета в уже созданном активном соединении.

Цепочка активных соединений

Это цепочка tcp_active_pcbs . Здесь наше соединение в состоянии SYN_RECV. Идет обмен данными. Отрабатывает функция tcp_process.

Второй пакет , который придет, это ACK является ответом клиента на TCP_SYN |TCP_ACK и просто подтверждает установление соединения клиентом. Здесь сервер ничего не отвечает (если все в порядке с переданными параметрами) и устанавливает состояние соединению ESTABLISHED.

Третий пакет - это уже сами данные , передаваемый клиентом запрос. Принимаем данные (запрос) также в tcp_process case ESTABLISHED : функция tcp_receive(). Здесь клиент передает в пакете флаги PSH & ACK.

PSH как раз означает - передача данных. В tcp_receive есть макрос TCP_EVENT_RECV , тут как раз и вызывается http_recv и начинается подготовка ответа на HTTP запрос.

Далее подготавливаем пакет данных (контент страницы) через http_send , http_write , tcp_write и отсылаем данные через tcp_output (причем получилось два пакета). В первом пакете ответа флаг ACK, во втором (завешающем) FIN , ACK , PSH .

ACK - это подтверждение клиенту , что запрос принят и выполнен.
PSH - понятно , это передаются данные ,которые надо принять клиенту.
FIN - это требование завершения соединения от сервера клиенту.

Далее клиент отвечает тремя пакетами [ACK] [ACK] [FIN,ACK] .

В этих передачах соблюдется особый алгоритм контроля через поля Seq и Ack. Эта технология защищает от путаницы пакетов, первый пришел позже второго и т.д.

фотка 2

С каждым новым соединением инициатор устанавливает новые произвольные значения для Seq и Ack. Надо еще учесть , что в программе Winshark Seq и Ack пересчитываются как будто они идут с нуля.

Закрытие соединения

После того как клиент запросил данные у HTTP сервера и сервер отдал контент запрашиваемой страницы, клиент посылает команду закрытия соединения с флагом FIN, ACK и сервер отвечает ACK и закрывает соединение со свой стороны. Тут тоже задействован механихм Seq/Ack:

фотка 3


Теперь осталось разобрать только состояние TIME-WAIT соединения.

Цепочка TIME-WAIT соединений

Здесь собираются все соединения, переведенные LWIP в статус TIME-WAIT . Зачем это надо?

Инициатором закрытия соединения может быть любой участник : и клиент и сервер. Мы являемся сервером и если мы инициатор закрытия соединения , то мы должны перейти в конце в состояние TIME-WAIT на 1-2 минуты. Это делается для того , чтобы если клиент не закрыл соединение (на самом деле) или какие-то пакеты еще в пути (не дошли), чтобы все это гарантировано принять и отработать. На это и дается 1-2 минуты.

Таймер для соединения TIME_WAIT устанавливается в 1 — 2 минуты. После чего соединение должно быть уничтожено (у сервера). На картинке ниже Хост 1 это наш сервер.

фотка 4

Кстати в нашем случае у нас соединение закрывает клиент и поэтому механизм TIME_WAIT (у сервера) не используется.

Небольшие выводы

По изучению кода реализации tcp протокола можно сказать , что объем кода немалый и мелочей там нет. Значит надо запастись терпением и заставить себя хотя бы проанализировать основные части кода.

Не надо путать Seq клиента и Seq сервера. И не надо путать Ack клиента и Ack сервера.

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

* USB_RNIDS1.4.1_http_server_log [zip]
Это лог , где хорошо видна иерархия вызываемых функциий