Программирование ARM LwIP Raw/TCP Fri, November 27 2020  

Поделиться

Нашли опечатку?

Пожалуйста, сообщите об этом - просто выделите ошибочное слово или фразу и нажмите Shift Enter.

LwIP Raw/TCP Печать
Добавил(а) microsin   

Перед вызовом любой TCP-функции обязательно должна быть вызвана функция lwip_init().

void tcp_tmr(void);

После этого вызова Вы должны вызывать tcp_tmr() через каждый интервал TCP_TMR_INTERVAL в миллисекундах (по умолчанию 250 мс).

Однако с некоторого времени (версии 1.4.0), нужно вызывать одну функцию для обработки всех таймеров для всех протоколов в стеке. Добавьте вызов этой функции в главный цикл main или что-то аналогичное:

sys_check_timeouts();

[Идентификация соединения]

Функция tcp_arg() задает аргумент, который будет передан во все callback-функции для соединения.

void tcp_arg (struct tcp_pcb * pcb, void * arg);

Аргумент pcb задает блок управления соединением TCP (Protocol Control Block, сокращенно PCB), и аргумент arg это указатель на некие Ваши собственные данные. Этот аргумент можно использовать в приложении для любой цели; чаще всего Вы будете использовать этот аргумент, чтобы идентифицировать этот определенный экземпляр своего приложения.

[Настройка соединения TCP]

Соединение TCP идентифицируется через PCB. Есть 2 способа настройки соединения.

Пассивное соединение (прослушивание порта, Listen). Пассивное соединение это сервер TCP, который ждет подключения клиентов. Процесс по шагам:

1. Вызовите tcp_new, чтобы создать PCB. Опционально вызовите tcp_arg, чтобы связать специфическое для приложения значение с этим pcb.
2. Вызовите tcp_bind, чтобы указать локальный IP-адрес и порт TCP.
3. Вызовите tcp_listen или tcp_listen_with_backlog. Замечание: эти функции освободят PCB, предоставленный как аргумент, и возвратят прослушивающий pcb меньшего размера (listener pcb, например tpcb = tcp_listen(tpcb)).
4. Вызовите tcp_accept, чтобы указать функцию, которая должна быть вызвана при поступлении нового соединения. Обратите внимание, что нет возможности принять/подтвердить сокет до указания callback, потому что это все работает на tcpip_thread.

Активное соединение. Запускает клиента TCP, который соединяется с сервером. Процесс по шагам:

1. Вызовите tcp_new, чтобы создать PCB. Опционально вызовите tcp_arg, чтобы связать специфическое для приложения значение с этим pcb.
2. Опционально вызовите tcp_bind, чтобы указать локальный IP-адрес и TCP-порт.
3. Вызовите tcp_connect.

[Функции соединения TCP]

struct tcp_pcb * tcp_new(void);

Создает новый PCB соединения. Изначально соединение находится в состоянии "закрыто" (closed). Если не хватает памяти для создания нового PCB, то будет возвращен NULL.

err_t tcp_bind(struct tcp_pcb * pcb, struct ip_addr * ipaddr, u16_t port);

Привязывает PCB к локальному IP-адресу и номеру порта. IP-адрес может быть указан как IP_ADDR_ANY, чтобы привязать соединение ко всем локальным адресам IP. Если для порта указано значение 0, то функция выберет доступный (свободный) порт. Соединение должно быть в состоянии "closed".

Если другое соединение уже привязано к тому же самому порту, то функция вернет ERR_USE, иначе вернет ERR_OK.

struct tcp_pcb * tcp_listen(struct tcp_pcb * pcb);

Эта функция устанавливает локальный порт для прослушивания локальных соединений. Параметр pcb указывает соединение, которое должно быть в состоянии "closed", и должно быть привязано к локальному порту функцией tcp_bind().

Функция tcp_listen() вернет новый PCB соединения, и тот PCB, который передавался ранее как аргумент в функцию, будет освобожден. Причина такого поведения - нужно меньше памяти для соединения, которое прослушивается, Таким образом, tcp_listen () освобождает память, необходимую для исходного соединения, и выделяет новый меньший блок памяти для прослушивающего соединения.

После вызова tcp_listen() нужно вызвать tcp_accept(). Пока этого не сделаете, приходящие соединения для этого порта буду сбрасываться (abort).

Функция tcp_listen() может вернуть NULL, если нет доступной памяти для прослушивающего соединения. Если это произошло, то не будет освобождена память, связанная с переданным в качестве аргумента pcb в вызов tcp_listen().

struct tcp_pcb * tcp_listen_with_backlog(struct tcp_pcb * pcb, u8_t backlog);

То же самое, что и tcp_listen(), но ограничивает количество ожидающих соединений в очереди прослушивания значением, указанным аргументом ожидания backlog. Чтобы использовать это, нужно установить TCP_LISTEN_BACKLOG=1 в файле настроек lwipopts.h.

void tcp_accept(struct tcp_pcb * pcb,
                err_t (* accept)(void * arg, struct tcp_pcb * newpcb,
                                 err_t err));

Дает команду для PCB запустить прослушивание входящих соединений. Перед этим должна быть вызвана tcp_listen(). Когда новое соединение поступило на локальный порт, будет вызвана указанная в аргументе функция с PCB для нового соединения.

Backlog: до 2.0 был необходим callback для подтверждения соединения.

void tcp_accepted(struct tcp_pcb * pcb);

Информирует lwIP, что входящее соединение было принято. Обычно это вызывается из called-функции принятия соединения (accept callback). Это позволило lwIP выполнять задачи обслуживания, например, разрешить поставить дальнейшие входящие соединения в очередь ожидания прослушивания (backlog). Параметр pcb это прослушивающий PCB, не новое соединение.

Это устарело в пользу реализации новой схемы соединения.

err_t tcp_connect(struct tcp_pcb * pcb, struct ip_addr * ipaddr,
                   u16_t port, err_t (* connected)(void * arg,
                                                   struct tcp_pcb * tpcb,
                                                   err_t err));

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

Функция tcp_connect() вернет управление немедленно, она не ждет, пока соединение будет должным образом установлено. Вместо этого она вызовет функцию, указанную как четвертый аргумент (аргумент connected), когда соединение установится. Если соединение не установилось должным образом (либо из-за отказа другого хоста, либо из-за того, что другой хост не ответил), то будет вызвана функция обработки ошибки с соответствующим образом установленным аргументом err.

Функция tcp_connect() может возвратить ERR_MEM, если не было доступной памяти для постановки в очередь сегмента SYN. Если SYN был действительно успешно поставлен в очередь, то функция tcp_connect() вернет ERR_OK.

[Отправка данных TCP]

Чтобы отправить данные на соединении TCP:

1. Вызовите tcp_sent(), чтобы указать callback-функцию для подтверждений.
2. Вызовите tcp_sndbuf(), чтобы найти максимальное количество данных, которое можно отправить.
3. Вызовите tcp_write(), чтобы поставить данные в очередь.
4. Вызовите tcp_output(). чтобы принудительно отправить данные.

u16_t tcp_sndbuf(struct tcp_pcb * pcb);

Вернет количество места для байт, доступного в очереди вывода (output queue).

err_t tcp_write(struct tcp_pcb * pcb, void * dataptr, u16_t len, u8_t apiflags);

Ставит в очередь данные, на которые указывает аргумент dataptr. Длина данных передается через параметр len.

Аргумент apiflags может иметь один из следующих бит:

TCP_WRITE_FLAG_COPY показывает, что lwIP должна выделить новый блок памяти и скопировать туда данные. Если этот бит не указан, то новая память не будет выделена, и к данным будет осуществляться обращение только по указателю.

TCP_WRITE_FLAG_MORE показывает, что в сегменте TCP не должен быть установлен флаг push.

Функция tcp_write() потерпит сбой и вернет ERR_MEM, если длина данных превышает размер текущего буфера отправки, или если дина очереди исходящего сегмента больше, чем верхний предел, заданный в файле опций lwipopts.h (TCP_SND_QUEUELEN). Если функция вернула ERR_MEM, то приложение должно подождать, пока некоторые поставленные ранее в очередь данные не будут успешно приняты другим хостом (при этом место в очереди передачи освободится), после чего сделать новую попытку.

err_t tcp_output(struct tcp_pcb * pcb);

Принуждает немедленно отправить все поставленные в очередь данные.

void tcp_sent(struct tcp_pcb * pcb,
               err_t (* sent)(void * arg, struct tcp_pcb * tpcb,
                              u16_t len));

Указывает callback-функцию, которая должна быть вызвана, когда данные были подтверждены другим хостом. Аргумент len, переданный в callback-функцию, дает количество байт, которое было подтверждено в последнем подтверждении.

[Прием данных TCP]

Получение данных TCP основано на вызове callback-функции. Эта функция специфична для приложения, она будет вызвана, когда поступят новые данные.

Протокол TCP задает окно, которое говорит хосту отправки, сколько данных он может послать через соединение. Размер окна для всех соединений определено константой TCP_WND, которая может быть переназначена в файле опций lwipopts.h. Когда приложение обработало поступившие данные, оно должно вызвать функцию tcp_recved() чтобы показать, что TCP может увеличить окно приема.

void tcp_recv(struct tcp_pcb * pcb,
               err_t (* recv)(void * arg, struct tcp_pcb * tpcb,
                              struct pbuf * p, err_t err));

Установит callback-функцию, которая будет вызвана при поступлении новых данных. Если не было ошибок, и callback-функция вернула ERR_OK, то она отвечает за освобождение pbuf. Иначе она не должна освобождать pbuf, чтобы код ядра lwIP мог хранить его.

Если другой хост закрыл соединение, то callback-функция будет вызвана с NULL pbuf для индикации этого факта.

void tcp_recved(struct tcp_pcb * pcb, u16_t len);

Должна быть вызвана, когда приложение обработало данные, и приготовилось к получению большего количества данных. Цель этого - объявление окна большего размера, когда данные были обработаны. Аргумент len показывает длину обработанных данных.

[Опрос приложения (polling)]

Когда соединение в состоянии ожидания (то есть данные либо не передаются, либо не принимаются), lwIP будет постоянно опрашивать приложение путем вызова указанной callback-функции. Это может использоваться либо как сторожевой таймер, чтобы прибить соединения, которые слишком долго простаивают, или как метод ожидания освобождения памяти. Например, если вызов tcp_write() был неудачен из-за недоступности свободной памяти, приложение может использовать функционал опроса для вызова tcp_write() снова, когда соединение некоторое время бездействовало.

void tcp_poll(struct tcp_pcb * pcb,
               err_t (* poll)(void * arg, struct tcp_pcb * tpcb),
               u8_t interval);

Задает интервал опроса и callback-функцию, которая должна вызваться для опроса приложения. Интервал задается в количестве грубых интервалов срабатывания таймера TCP, которые обычно происходят дважды в секунду. Интервал 10 означает, что приложение будет опрашиваться каждые 5 секунд.

[Закрытие и обрыв соединения]

err_t tcp_close(struct tcp_pcb * pcb);

Закрывает соединение. Функция может возвратить ERR_MEM, если не было было доступной памяти для закрытия соединения. Если это произошло, то приложение должно подождать и попробовать снова, либо использовать acknowledgment callback или функционал опроса. Если закрытие было успешно, то функция вернет ERR_OK.

После вызова tcp_close() кодом TCP освобождается PCB.

Обратите внимание, что данные все еще могут приниматься на закрытом соединении, пока другой хост не подтвердит закрытие.

void tcp_abort(struct tcp_pcb * pcb);

Обрывает соединение отправкой сегмента RST (reset) другому хосту. Блок PCB освобождается. Эта функция никогда не заканчивается неудачей.

ВНИМАНИЕ: когда эта функция вызывается из одной из callback-функций TCP, убедитесь, что всегда возвращаете ERR_ABRT, иначе есть риск доступа к освобожденной памяти или утечки памяти!

[Обработчик ошибки]

Если соединение было оборвано из-за ошибки, или попытка соединения потерпела неудачу (либо таймаут, либо reset), то приложение оповещается об этом событии через err callback. Ошибки, которые могут оборвать соединение, включают нехватку памяти. Вызываемая callback-функция устанавливается функцией tcp_err().

void tcp_err(struct tcp_pcb * pcb,
             void (* err)(void * arg, err_t err));

Функция error callback не получает pcb, переданный как параметр, потому что PCB может быть уже освобожден. Если Вам нужно узнать что-нибудь о своем приложении внутри этой функции, то используйте параметр arg, как в любой другой функции.

Алгоритм Найгла. Краткий обзор см. в Википедии [3].

// Разрешает алгоритм nagle:
tcp_nagle_enable ( struct tcp_pcb * aPcb );
 
// Запрещает алгоритм nagle:
tcp_nagle_disable ( struct tcp_pcb * aPcb );
 
// Возвратит true, если алгоритм не разрешен:
tcp_nagle_disabled ( struct tcp_pcb * aPcb );

Пример (для xNetCann, created with NETCONN_TCP):

tcp_nagle_enable ( xNetConn->pcb.tcp );

Если эти макросы не определены в Вашей версии lwIP, то нужно обновится до самой новой версии (2.1.2 на момент перевода документации [1]). Если Вы не можете сделать обновление, то используйте (как аварийную замену):

xNewConn->pcb.tcp->flags |= TF_NODELAY для запрета
xNewConn->pcb.tcp->flags &= ~TF_NODELAY для разрешения
( xNewConn->pcb.tcp->flags & TF_NODELAY ) != 0 для опроса, разрешено или нет

TCP Keepalive. В файл lwipopts.h нужно добавить:

#define LWIP_TCP_KEEPALIVE 1

Тогда на каждом сокете TCP, для которого Вам нужна поддержка keepalive, это нужно разрешить:

/* Включение TCP Keepalive для этого pcb: */
pcb->so_options |= SOF_KEEPALIVE;
 
/* Если Вам нужно поменять время между сообщениями keep alive.
   Время устанавливается в миллисекундах: */
pcb->keep_intvl = 75000; /* 75 секунд */

[Диаграмма последовательности действий примера Raw TCP]

Из-за того, что raw-реализация TCP предназначена для выполнения главным образом через callback-функции, его работа как правило тесно связана с приемом и обработкой отдельных сообщений. Следовательно, полезно иметь по крайней мере некоторое знакомство с протоколом TCP низкого уровня. Для тех, у кого нет предыдущего опыта работы с lwIP, иногда не ясно, какие делать вызовы функций, и когда. Следующая таблица показывает диаграмму взаимодействия между другим компьютером в сети (remote client) и локальным сервером lwIP. Взаимодействие показано для типичного (успешного) соединения по протоколу запрос-ответ (такому как например HTTP).

Remote client Сообщение TCP Действие
стека lwIP
Действие
сервера lwIP
Описание
Установка сессии lwIP, сетевой удаленный клиент (remote client) / локальный сервер lwIP
      ← tcp_new() Создается TCP PCB.
      ← tcp_bind() Привязка к номеру порта.
      ← tcp_listen_with_backlog() Создает прослушивание порта (выделяется новый PCB).
      ← tcp_accept() Устанавливает callback-функцию принятия соединения.
      ← tcp_arg() Устанавливает аргумент callback-функции (указатель на структуру данных сервера).
connect →       Клиент подключился к серверу.
  SYN →     Сетевой удаленный клиент посылает сигнал SYN.
  ← SYN/ACK     lwIP отвечает сообщением SYN/ACK.
← (соединение возвращено)       Сетевой клиент оповещен об успешном соединении.
  ACK →     Сетевой клиент посылает сообщение ACK для завершения трехстороннего рукопожатия.
    (запускается callback-функция accept) →   lwIP оповещает приложение о новой сессии (с новым PCB).
      ← tcp_accepted() Сервер подтверждает соединение, декрементирует счетчик "ожидающих" сессий.
      ← tcp_arg() Сервер выделяет новую структуру сессии, устанавливает новый аргумент callback-функции.
      ← tcp_recv() Сервер устанавливает новую callback-функцию приема (recv).
      ← tcp_err() Сервер устанавливает callback-функцию error/abort.
      ← tcp_sent() Сервер устанавливает новую callback-функцию передачи (sent).
      ← Сервер возвращается из callback-функции accept со статусом OK.  
    (пометка блока PCB активным)    
Соединение установлено (теперь данные могут быть отправлены в любую сторону)
send → Данные TCP →     Клиент отправил запрос данных.
    lwIP запускает callback-функцию приема сервера recv    
      ← tcp_write(response_data, len) Сервер записал данные ответа для клиента.
    lwIP ставит в очередь сегмент TCP    
      ← tcp_write(response_data2, len2) Сервер записал еще данные для клиента.
    lwIP ставит в очередь сегмент TCP   Сегмент может комбинироваться с предыдущим сегментом.
      ← tcp_recved() Сервер уведомляет lwIP об объявлении окна большего размера.
      ← Сервер возвращает управление из callback-функции приема recv со статусом OK.  
  ← Данные TCP lwIP ищет поставленные в очередь сегменты для отправки   lwIP отправляет сегмент (сегменты) данных клиенту, включая ACK для ранее принятых данных клиента.

Примечания: tcp_write() просто ставит данные TCP в очередь для последующей передачи; этот вызова не начинает реальную передачу. Тем не менее, когда tcp_write() вызывается из callback-функции приема (recv), как в этом примере, нет необходимости вызывать tcp_output() для начала передачи отправляемых данных (действительно, tcp_output() специально отказывается делать что-либо, если она вызывается из recv callback). После возврата из recv callback стек автоматически начнет передавать любые данные - и ACK для предыдущего пакета сетевого клиента объединяется с первым сегментом исходящих данных. Если tcp_write() вызывается из какого-то другого места (возможно как результат некоего события вне обработки lwIP), то может быть необходимо вызвать tcp_output, чтобы инициировать передачу данных.

Remote server Сообщение TCP Действие
стека lwIP
Действие
клиента lwIP
Описание
Установка сессии lwIP (локальный клиент / сетевой удаленный сервер lwIP)
      ← tcp_new() Создается TCP PCB.
      ← [tcp_bind()] Опционально: привязка к определенному номеру порта и/или IP-адресу.
      ← tcp_arg() Выделяет специфичную для клиента структуру сессии, устанавливая её как аргумент callback-функции.
      ← tcp_err() Устанавливает callback-функцию error/abort (используется для сигнализации о неудачном соединении).
      ← tcp_arg() Устанавливает аргумент callback-функции (указатель на структуру данных сервера).
      ← tcp_recv() Устанавливает callback-функцию приема recv(†).
      ← tcp_sent() Сервер устанавливает callback-функцию передачи sent(†).
      ← tcp_connect() Соединение, с предоставлением callback-фунции connected оповещения о завершении соединения.
  ← SYN ← lwIP генерирует SYN   lwIP генерирует SYN-пакет для удаленного сетевого сервера.
  SYN/ACK →     Удаленный сетевой сервер отправляет SYN/ACK.
    lwIP запускает callback-функцию connected →   С точки зрения lwIP сессия теперь установлена, будет сгенерирован завершающий ACK трехстороннего рукопожатия TCP при возврате из callback-функции.
Соединение установлено (теперь данные могут быть отправлены в любую сторону)
      ← tcp_write(request_data, len) Клиент записал данные запроса для сервера.
    lwIP ставит в очередь сегмент TCP    
      ← tcp_write(request_data2, len2) Клиент записал еще данные для сервера.
    lwIP ставит в очередь сегмент TCP   Сегмент может комбинироваться с предыдущим сегментом.
      ← tcp_output() Клиент уведомляет lwIP о реальной генерации отправляемых пакетов(*).
      ← Клиент возвращает управление из callback-фукнции connected.  
  ← Данные TCP     lwIP генерирует один или большее количество пакетов данных.
send → Данные TCP → lwIP запускает callback-функцию приема клиента recv   См. предыдущую таблицу.

Примечания:

(†) Callback-функции приема (recv) и передачи (sent) могут быть установлены после установки соединения (т. е. в callback-функции connected), если это необходимо.

(*) Обратите внимание, что вызов tcp_output в действительности не нужен, если данные записаны клиентом в callback-функции connected, поскольку lwIP будет автоматически генерировать ACK после возврата из callback-функции. В других случаях это может потребоваться, однако см. также примечания к предыдущей таблице.

В случае неудачного соединения соединяющийся клиент будет оповещен об этом через error callback-функцию, которая была установлена вызовом tcp_err().

Завершение сессии (сценарий 1 - shutdown со стороны удаленного сетевого клиента):

Remote client Сообщение TCP Действие
стека lwIP
Действие
сервера lwIP
Описание
close или shutdown (SHUT_WR) →       Клиент завершает запись конца сокета.
  FIN →     Удаленный клиент посылает пакет с установленным битом FIN.
  ← ACK lwIP распознает FIN, немедленно генерируя ACK   lwIP PCB входит в состояние CLOSE_WAIT.
    lwIP запускает callback-функцию приема сервера recv с аргументом pbuf, равным NULL   lwIP сигнализирует серверу об окончании файла (end-of-file)
      [← tcp_write()] Сервер отправил последние данные данные ответа для клиента (если они есть).
...
      ← tcp_close() Сервер освобождает приватные структуры данных, завершая соединение.
  ← FIN lwIP генерирует FIN   lwIP оповещает удаленного сетевого клиента, что сервер закрыл соединение (PCB входит в состояние LAST_ACK).
  ACK →     Клиент подтверждает последний FIN.
    lwIP обрабатывает последний ACK   PCB входит в состояние CLOSED (и затем освобождается).

Завершение сессии (сценарий 2 - shutdown со стороны локального сервера):

Remote client Сообщение TCP Действие
стека lwIP
Действие
сервера lwIP
Описание
      ← tcp_close() Сервер завершает соединение.
  ← FIN lwIP генерирует пакет FIN   PCB входит в состояние FIN_WAIT_1
  ACK → lwIP обновляет PCB   PCB входит в состояние FIN_WAIT_2.
[send] → [Данные TCP] →     Сервер (опционально) отправил последние данные(*).
...
close →       Клиент закрывает сокет.
  FIN →     Удаленный сетевой клиент отправляет пакет FIN.
  → ACK ← lwIP распознает FIN, генерируя ACK   lwIP PCB входит в состояние TIME_WAIT.
    lwIP запускает callback-функцию приема сервера recv с аргументом pbuf, равным NULL →   Клиент сигнализирует серверу lwIP об окончании файла (end-of-file)
        Сервер освобождает приватные структуры данных.
...
    Истекло время таймера TIME_WAIT   lwIP освобождает PCB.

Примечание (*): обратите внимание, что lwIP может запустить callback-функцию приема сервера recv после вызова tcp_close(). Если Вы не хотите принять данные, то обязательно обнуляйте callback-функцию приема recv (т. е. вызовите tcp_recv с NULL-указателем).

[Ссылки]

1. LwIP Application Developers Manual Raw/TCP site:lwip.fandom.com.
2. LwIP: распространенные ошибки.
3. Nagle's algorithm site:wikipedia.org.

 

Добавить комментарий


Защитный код
Обновить

Top of Page