Основные промежуточные результаты :
На данном этапе у нас работает 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 пакетами сервер и клиент:
Чтобы разобраться с 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. Эта технология защищает от путаницы пакетов, первый пришел позже второго и т.д.
С каждым новым соединением инициатор устанавливает новые произвольные значения для Seq и Ack. Надо еще учесть , что в программе Winshark Seq и Ack пересчитываются как будто они идут с нуля.
Закрытие соединения
После того как клиент запросил данные у HTTP сервера и сервер отдал контент запрашиваемой страницы, клиент посылает команду закрытия соединения с флагом FIN, ACK и сервер отвечает ACK и закрывает соединение со свой стороны. Тут тоже задействован механихм Seq/Ack:
Теперь осталось разобрать только состояние TIME-WAIT соединения.
Цепочка TIME-WAIT соединений
Здесь собираются все соединения, переведенные LWIP в статус TIME-WAIT . Зачем это надо?
Инициатором закрытия соединения может быть любой участник : и клиент и сервер. Мы являемся сервером и если мы инициатор закрытия соединения , то мы должны перейти в конце в состояние TIME-WAIT на 1-2 минуты. Это делается для того , чтобы если клиент не закрыл соединение (на самом деле) или какие-то пакеты еще в пути (не дошли), чтобы все это гарантировано принять и отработать. На это и дается 1-2 минуты.
Таймер для соединения TIME_WAIT устанавливается в 1 — 2 минуты. После чего соединение должно быть уничтожено (у сервера). На картинке ниже Хост 1 это наш сервер.
Кстати в нашем случае у нас соединение закрывает клиент и поэтому механизм TIME_WAIT (у сервера) не используется.
Небольшие выводы
По изучению кода реализации tcp протокола можно сказать , что объем кода немалый и мелочей там нет. Значит надо запастись терпением и заставить себя хотя бы проанализировать основные части кода.
Не надо путать Seq клиента и Seq сервера. И не надо путать Ack клиента и Ack сервера.
Файлы для скачивания
*
Это лог , где хорошо видна иерархия вызываемых функциий