uIP 1.0
Стек TCP/IP uIP
Автор:
Adam Dunkels, http://www.sics.se/~adam/

Стек uIP предназначен для добавления сети с поддержкой протокола TCP/IP к программам даже на маленьких 8-битных микроконтроллерах. Несмотря на то, что uIP мал и прост, он не требует от других систем, чтобы там были сложные полноразмерные стеки, и может связаться с такими же упрощенными системами. Размер кода составляет около нескольких килобайт, и затраты на RAM могут быть скофигурированы до нескольких сотен байт.

uIP можно найти на странице: http://www.sics.se/~adam/uip/

См. также:
Программы приложений
Опции кофигурации времени компиляции
Функции конфигурации времени выволнения (run-time)
Функции инициализации
Драйвер интерфейса устройтва и переменные, используемые драйверами устройства
Функции uIP, вызываемые из программ приложения (см. ниже) и API protosocket-ов и их нижележащие protothread-ы

Введение

В связи с успехом Интернета набор протоколов TCP/IP стал глобальным стандартом для обмена данными. TCP/IP и нижележащие проктоколы используются для передачи WEB-страниц, передачи e-mail и файлов, и для обмена каждый-с-каждым через сеть Интернет. Для встраиваемых систем (embedded) запуск стандартного TCP/IP делает возможным напрямую подключиться к внутренней локальной сети или даже к глобальной сети Интернет. Встраиваемые устройства с полной поддержкой TCP/IP станут полноценными участниками сети, и смогут связаться с другими хостами в сети.

Традиционные реализации TCP/IP требуют слишком много ресурсов с точки зрения размера кода и использования оперативной памяти, чтобы их можно было использовать в маленьких 8-битных или 16-битных системах. Размер кода в несколько сотен килобайт и затраты RAM несколько сотен килобайт полного стека TCP/IP невозможны для систем, у которых несколько десятков килобайт RAM и меньше 100 килобайт памяти под код.

Реализация uIP разработана так, чтобы работал абсолютный минимум из того, что есть в полном стеке TCP/IP. Стек uIP позволяет использовать только один сетевой интерфейс, и содержит протоколы IP, ICMP, UDP и TCP. Стек uIP написан на языке C.

Множество других реализаций TCP/IP для маленьких систем подразумевают, что embedded-устройство всегда ведет обмен с полноразмерной реализацией TCP/IP, работающей на машине класса рабочей станции. Если сделать такое предположение, то становится возможным удалить определенные механизмы TCP/IP, которые в таких ситуациях используются очень редко. Многие из этих механизмов однако важны, если embedded-устройство обменивается данными с таким же урезанным устройством, например когда работают сервисы и протоколы peer-to-peer (каждый-с-каждым). Стек uIP разработан совместимым с RFC, так чтобы embedded-устройства работали как первоклассные участники сети. Предлагается реализация uIP TCP/IP, которая не адаптировара для какого-то специального приложения.

Обмен данными через TCP/IP

Полный набор стека TCP/IP состоит из большого количества протоколов, начиная от низкоуровневых типа ARP (который транслирует адреса IP в адреса MAC), заканчивая протоколами уровня приложения типа SMTP (который используется для отправки e-mail). Стек uIP главным образом сосредотачивается на реализации протоколов TCP и IP, и высокоуровнеыве протоколы, которые работают поверх них, рассматриваются как "приложение". Протоколы, которые уровенем ниже, часто реализованы в аппаратуре или в firmware (коде микроконтроллера), и они рассматриваются как "сетевое устройство" (network device), управляемое сетевым драйвером устройства (network device driver).

Протокол TCP обеспечивает надежный поток байтов для протоколов вышележащего уровня. Он разбивает поток байт на сегменты подходящего размера и каждый сегмент посылает в своем собственном пакете IP. Пакеты IP отправляются по сети с помощью сетевого драйвера устройства. Если пункт назначения находится не в этой физической сети, то пакет IP (через маршрутизатор) перенаправляется в другие сети. Если максимальный размер пакета в другой сети меньше, чем размер пакета IP, то пакет фрагментируется маршрутизатором на более мелкие пакеты. Если это возможно, размер сегментов TCP выбирается так, чтобы минимизировать такую фрагментацию. Конечный получатель пакета пересоберет все фрагментированные пакеты IP перед тем как передать их на более высокие уровни.

Формальные требования для протоколов в стеке TCP/IP указаны в некотором количестве документов RFC, опубликованных организацией Internet Engineering Task Force, IETF. Каждый протокол в стеке определен в одном или нескольких документах RFC, и в RFC1122 собраны все требования и обновления предыдущих RFC.

Требования RFC1122 можно поделить на 2 категории; одна работает с обменом хост-хост, и другая с обменом между приложением и сетевым стеком. Пример первой категории "TCP ДОЛЖЕН принять опцию TCP в любом сегменте", и пример второй "Здесь ДОЛЖЕН быть механизм, который сообщает приложению об ошибках TCP". Реализации TCP/IP, которые нарушают требования первой категории, не смогут общаться с другими реализациями TCP/IP, и даже могут приводить к отказам сети. Нарушение правил второй категории может повлиять на обмен внутри системы, и не повлияют на обмен данными хост-хост.

В стеке uIP реализованы все требования RFC, которые относятся к обмену данными host-to-host. Однако с целью уменьшить размер кода были удалены определенные механизмы в интерфейсе между приложением и стеком, таки как механизм soft error reporting и динамически конфигурируемые биты типа сервиса (type-of-service, биты для передачи качества трафика) для соединений TCP. Поскольку эти возможности используются только очень немногими приложениями, то их можно удалить без потери главной функциональности.

Главный цикл программы (Main Control Loop)

Стек uIP может работать либо в среде многозадачности (multitasking system), или как основная программа в однозадачной системе (singletasking system). В обоих случаях главный цикл приожения должен постоянно выполнять две вещи:

Если пришел пакет, то в главном цикле приложения должна быть вызвана функция uip_input(). Это функция входной обработки, которая не будет блокировать выполнение кода, и сразу сделает возврат. Когда из нее произведен возврат, то стек или приложение, для которого полученный пакет был предназначен, может произвести один или большее количество пакеов, которые должны быть отправлены в ответ. Если это так, то должен быть вызван сетевой драйвер устройства (network device driver), чтобы отправить эти пакеты.

Периодические таймауты используются для поддержки механизмов TCP, которые зависят от таймеров. Это такие механизмы, как отложенные подтверждения (delayed acknowledgments), повторные передачи (retransmissions) и оценки циклической задержки (round-trip time estimations). В основном цикле приложения должна быть вызвана функция обработчика времени uip_periodic(). Поскольку стек TCP/IP может выполнять повторные передачи, вызванные событием таймера, то сетевой драйвер устройства должен передать пакеты, которые были подготовлены для повторной передачи.

Функции, зависящие от архитектуры системы

Стек uIP требует реализации нескольких функций, которые зависят от специфики системы, на которой работает uIP. Эти функции должны быть жестко настроены под определенную архитектуру, однако вместе с распространяемой библиотекой uIP поставляются стандартные реализации этих функций на языке C.

Вычисление контрольной суммы

Протоколы TCP и IP используют контрольную сумму (CRC), которая вычисляется от частей заголовка и данных пакетов TCP и IP. Поскольку вычисление этой CRC делается от всех байт в каждом пакете, то весьма важно, чтобы вычисление контрольной суммы делалось максимально эффективно. Это часто означает, что вычисление контрольной суммы должно быть построено с учетом особенностей архтиектуры, на которой работает стек uIP (говоря по-русски, лучше всего вычисление контрольной суммы написать на ассемблере, максимально эффективно, с использванием аппаратных возможностей для ускорения вычисления).

Хотя uIP поставляестя со стандартной функцией вычисления контрольной суммы, он также оставляет возможность для специфической реализации (учитывающей конкретную архитектуру) двух функций uip_ipchksum() и uip_tcpchksum(). Вычисление контрольных сумм в этих функциях можно написать на высокооптимизированном ассемблере, а не на обычном языке C.

Протокол TCP использует 32-битные номера последовательности, и реализация TCP будет выполнять много 32-битных сложений как часть нормальной обработки протокола. Поскольку 32-битная арифметика далеко не всегда доступна на многих платформах, для которых предназначена uIP, то uIP оставляет реализацию 32-разрядных сложений на модуля, специфичный для архитектуры, и в основном коде 32-разрядная арифметика не используется.

Хотя в uIP применяет обычное 32-разрядное сложение, но в ней есть поддержка для специфичной реализации функции uip_add32(), учитывающей особенности архитектуры.

Управление памятью (Memory Management)

Для большинства архитектур, для которых предназначена uIP, RAM является дефицитом. С несколькими килобайтами RAM нельзя напрямую применять механизмы, используемые в традиционном стеке TCP/IP.

Стек uIP не используетс явное динамическое выделение памяти. Вместо этого используется одиночный глобальный буфер, в котором удерживаются пакеты, и фиксированную таблицу для сохранения состояния соединения. Глобальный буфер пакета достаточно велик, чтобы поместить в себе пакет максимального размера. Когда пакет поступает через сеть, драйвер устройства помещает его в глобальный буфер, и вызывает стек TCP/IP. Если в пакете содержаться данные, то стек TCP/IP оповестит об этом соответствующее приложение. Поскольку данные в буфере будут перезаписаны следующим приходящим пакетом, то приложение должно либо сразу обработать данные, либо скопировать их в свой собственный буфер для последующей обработки. Буфер пакета не будет перезаписан, пока приложение обрабатывает данные. Пакеты, которые приходят в момент обработки приложения, должны быть поставлены в очередь либо сетевым устройством, либо драйвером устройства. Многие одночиповые контроллеры Ethernet имеют встроенные буферы достаточно большого размера, чтобы удержать как миниму 4 фрейма Ethernet максимального размера. Устройства, которые обрабатываются процессором, такие как порты RS-232, могут скопировать приходящие байты в отдельный буфер, пока приложение делает обработку. Если буфер заполнится, то приходящий пакет будет отброшен. Это приведет к ухудшению пропускной способности, но только когда параллельно работает несколько соединений. Причина в том, что uIP делает очень маленькое окно приема, что означает для каждого сетевого соединения наличие только одного сегмента TCP.

В uIP для заголовков TCP/IP исходящих данных используется тот же самый глобальный буфер пакета, который принимает входные пакеты. Если приложение посылает динамические данные, то оно может использовать части глобального буфера пакета, которые не используются как временое хранилище для заголовков. Для отправки данных приложение передает стеку указатель на данные и их длину. Заголовки TCP/IP записываются в глобальный буфер, и как только заголовки представлены, драйвер устройства передает заголовки и данные приложения через сеть. Для повторных передач данные не ставятся в очередь. Вместо этого приложение должно заново создать данные, если их нужно передать повторно.

Общее количество используемой uIP памяти зависит в большей степени от применений конкретного устройства, в котором работает реализация стека. Конфигурация памяти определяется как объемом трафика, который система должна пропустить, так и максимальным количеством допустимых одновременно соединений. Устройство, которое отправляет большие письма e-mail, одновременно поддерживает работу web-сервера с динамическими страницами, и обслуживает при этом несколько одновременно подключенных к серверу клиентов, поторебует больше памяти, чем просто сервер Telnet. Можно запустить реализацию uIP с малым расходом RAM 200 байт, но при условии очень низкого трафика и малого количества одновременных соединений.

Программный интерфейс (Application Program Interface, API)

API определяет, как программа приложения взаимодействует со стеком TCP/IP. Чаще всего для TCP/IP используется API сокетов BSD; такой API применяется на большинстве систем Unix, и оно значительно повлияло на реализацию Microsoft Windows WinSock API. Поскольку API сокетов использует сематику остановки и ожидания (stop-and-wait), то она подразумевает наличие нижележащей многопоточной операционной системы. Реализация управления задачами, переключения контекста и выделение места под стек - все эти задаяи слишком требовательны к ресурсам, чтобы их можно было напрямую запустить на архитектурах, с которыми работает uIP, так что для нашей цели интерфейс сокетов BSD не подойдет.

Стек uIP предоставляет для программистов 2 вида API: protosocket-ы (это сокеты, которые напоминают API сокетов BSD, но не требуют затрат на многопоточность), и "сырое" (raw API), основанное на событиях API, которое более низкоуровневое, чем протосокеты, но использует меньше памяти.

См. также:
Библиотека protosocket-ов.
Библиотека протопотоков (Protothreads)

uIP raw API

Сырое "raw" uIP API использует событийный интерфейс, где приложению предлагается обработать определенные события. Приложение, работающее поверх uIP, реализовано как функция C, которая вызывается uIP в ответ на определенные события. uIP вызывает приложение, когда поступают данные, когда данные были успешно доставлены на другой конец соединения, когда установлено новое соединение, или когда данные должны быть переданы повторно. Приложение также периодически опрашивается на предмет наличия новых данных. Приложение программы предоставляет только одну функцию обратного вызова (callback); это делается до приложений, имеющих дело с отображением различных сетевых служб на различные порты и соединения. Поскольку приложение может действовать в ответ на приходящие данные и запросы соединения, как только стек TCP/IP принял пакет, то можно достичь малого времени ответа даже на маломощных системах.

Стек uIP отличается от других стеков TCP/IP в том, что от приложения требуется помощь для организации повторных передач. Другие стеки TCP/IP буферизируют отправляемые данные, пока не будет известно, что данные были успешно доставлены на дальний конец соединения. Если данные требуется отправить повторно, такие стеки заботятся об этом сами, без оповещения приложения. Данные буферизируются в памяти в ожидании подверждения, даже если приложние могло бы быстро заново подготовить данные для повторной передачи.

Чтобы уменьшить использование памяти, стек uIP задействует вычислительные способности приложения, чтобы оно заново создало данные для переотправки. uIP не отслеживает содержимое пакета, после того как он был передан драйверу устойства, и требует от приложения активного участия в процессе выполнения повторной передачи. Когда uIP решила, что сегмент должен быть отправлен повторно, то она вызывает приложение с флагом, сигнализирующим о необходимости повторной передачи. Приложение проверят флаг ретрансмиссии и создает те же самые данные, которые были отправлены до этого. С точки зрения приложения выполнение ретрансмиссии не отличается от того, как данные передавались изначально. Таким образом, приложение может быть написано так, чтобы для отправки использовался тот же самый код, что и для повторной отправки. Также важно отметить, что хотя заботу о переотправке берет на себя приложение, именно стек отвечает за то, чтобы правильно определить необходимость повторной передачи. Так что код приложения не обязательно должен вырасти, несмотря на то что оно принимает участие в создании повторных передач.

События приложения (Application Events)

Приложение должно быть реализовано как функция на языке C: UIP_APPCALL(), которую uIP вызывает в ответ на любое событие. Каждое событие имеет функцию проверки, которая используется с целью отличит разные события друг от друга. Функции реализованы как макросы C, результат которых будет вычислен либо как 0, либо не 0. Имейте в виду, что определенные события случаются в связи с другими (например, новые данные могут поступить в то же самое время, как данные были подтверждены).

Указатель на соединение (Connection Pointer)

Когда стек uIP вызывает приложение, глобальная переменная uip_conn устанавливается как указатель на структуру uip_conn для текущего обрабатываемого соединения, и это называется "текущим соединением" (current connection). Могут использоваться поля в структуре uip_conn для текущего соединения, например для того чтобы отличить разные сервисы друг от друга, или для проверки, к какому адресу IP было произведено соединение. Обычное использование: инспекция uip_conn->lport (номер локального порта TCP), чтобы определить к какому сервису должно быть предоставлено соединение. Например, приложение может решить, что нужно работать как сервер HTTP, если значение uip_conn->lport равно 80, и работать как сервер TELNET, если значение равно 23.

Прием данных (Receiving Data)

Если проверка функции uIP uip_newdata() возвратила не 0, то дальний участник соединения (хост) послал новые данные. Указатель uip_appdata указывает на действительные данные. Размер данных определяется вызовом функции uip_datalen(). Данные не буферизируются uIP, и будут перезаписаны после возврата из функции приложения, так что приложение либо должно сразу обработать данные, либо поместить их с свой собственный буфер для последующей обработки.

Отправка данных (Sending Data)

При отправки данных uIP подстраивает длину отправляемых данных их приложения под доступное место в буфере и текущий размер окна TCP, заявленный приемником. Размер буфера задается опцией конфигурирования памяти на этапе компиляции. Так что может быть, что не все данные, отправленные приложением, будут получены приемником, и приложение может использовать функцию uip_mss(), чтобы увидеть, сколько данных было фактически отправлено стеком.

Приложение посылает данные с помощью функции uip_send(). Функция uip_send() принимает 2 аргумента; это указательн на данные для отправки, и длина данных. Если приложению нужно больще места в RAM для генерации данных, которые должны быть отправлены, то для этой цели может служить буфер пакета (на который указывает uip_appdata).

Приложение может отправить только один кусок данных за один вызов функции приложения для одного соединения, так что нельзя вызвать функцию uip_send() больше одного раза; будут отправлены данные только последнего вызова.

Повторная отправка данных (Retransmitting Data)

Ретрансмиссии происходят под управлением периодического таймера TCP. Каждый раз, когда вызывается периодический таймер, таймер ретрансмиссии для каждого соединения будет декрементироваться. Если счетчик дошел до 0, то должна быть сделана повторная отправка (ретрансмиссия). Стек uIP не отслеживает содержимое пакета после того, как пакет был послан драйвером устройства, и uIP требует от приложения повторного создания данных пакета для его ретрансмисии. Когда uIP решила, что сегмент должен быть отправлен повторно, retransmitted, функция приложения вызывается с установленным флагом uip_rexmit(), показывающим необходимость ретрансмиссии. Приложение должно проверить флаг uip_rexmit() и создать те же самые данные, которые были посланы ранее.

Закрытие соединений (Closing Connections)

Приложение закрывает текущее соединение функцией uip_close() из вызова приложения. Это приведет к тому, что произойдет чистое закрытие соединения. Чтобы показать серьезную ошибку (fatal error), приложение может захотеть оборвать (abort) соединение, и это можно сделать вызовом uip_abort().

Если соединение было закрыто на дальнем конце, то проверочаня фукнция uip_closed() вернет true. В этом случае приложение может сделать любую необходимую очистку.

Сообщения об ошибках

С соединением могут произойти 2 фатальные ошибки: либо соединение будет оборвано противоположной стороной (abort), либо много раз были переданы последние данные и соединение также оборвано. uIP сообщает о такх ошибках вызовом функции приложения. Приложение может вызвать две проверочные функции uip_aborted() и uip_timedout(), чтобы проверить, были ли эти ошибки.

Периодический опрос (Polling)

Когда соединение в состоянии ожидания (idle), uIP опрашивает приложение всякий раз, когда срабатывает периодический таймер. Приложение использует проверочную функцию uip_poll(), чтобы проверить, если ли опрос со стороны uIP.

Событие опроса (polling event) предназначено для 2 целей. Первая - периодически оповещать приложение, что соединение в состоянии ожидания (idle); это позволит приложению завершить соединения, которые находятся в состоянии ожидания слишком долго. Другое предназначение - позволить приложению отправить новые данные, если они были подготовлены. Приложение может отправить данные, только когда оно вызвано из uIP, так что poll event является единственным способом отправить данные, иначе соединение будет находиться в состоянии ожидания.

Прослушиваемые порты (Listening Ports)

uIP поддерживает список прослушиваемых портов TCP. Новый порт открывается для прослушивания функцией uip_listen(). Когда запрос на соединение достигает прослушиваемого порта, uIP создает новое соединение и вызывает функцию приложения. Проверочная функция uip_connected() вернет true, если было вызвано приложение, потому что было создано новое соединение.

Приложение может проверить поле lport структуры uip_conn, чтобы узнать, на какой порт было произведено соединение.

Открытие соединений (Opening Connections)

Новые соединения могут быть открыты из uIP с помощью функции uip_connect(). Эта функция выделяет новое соединение и устанавливает флаг в состоянии соединения, который откроет соединение TCP на указанные IP и порт в следующий раз, когда соединенеи будет опрошено стеком uIP. Функция uip_connect() вернет указатель на структуру uip_conn для нового соединения. Если больше нет свободных слотов для соединения, то функция вернет NULL.

Для упаковки адреса IP в два элемента 16-битного массива может использоваться функция uip_ipaddr(). 16-битный массив используется uIP для представления IP-адресов.

Ниже показаны два примера использования. Первый пример показывает, как открыть соединение на порт TCP 8080 дальнего хоста в текущем соединении. Если больше нет свободных слотов соединения TCP, чтобы новое соедиение было открыто, то функция uip_connect() вернет NULL и текущее соединение будет оборвано вызовом uip_abort().

void connect_example1_app(void) {
   if(uip_connect(uip_conn->ripaddr, HTONS(8080)) == NULL) {
      uip_abort();
   }
}   

Второй пример показывает, как открыть новое соединение по указанному адресу IP. В этом примере не будет сделано никаких проверок на ошибки.

void connect_example2(void) {
   u16_t ipaddr[2];

   uip_ipaddr(ipaddr, 192,168,0,1);
   uip_connect(ipaddr, HTONS(8080));
}

Примеры

Эта секция представляет несколько очень простых примеров приложений на основе uIP. Распространяемый пакет библиотеки uIP содержит несколько более сложных приложений.

Очень простое приложение

Пример показывает очень простое приложение. Оно прослушивает поступающие соединения на порт 1234. Когда соединение было установлено, приложение отвечает на любые посылаемые данные подтверждением "ok".

Реализация этого приложения показано ниже. Инициализация приложения происходит вызовом функции example1_init() и для uIP предоставлена функция обратного вызова (callback) с именем example1_app(). Для этого приложения должна быть задана конфигурационная переменная UIP_APPCALL на имя функции example1_app.

void example1_init(void) {
   uip_listen(HTONS(1234));
}

void example1_app(void) {
   if(uip_newdata() || uip_rexmit()) {
      uip_send("ok\n", 3);
   }
}

Функция инициализации вызывает функцию uIP uip_listen(), чтобы зарегистрировать порт прослушивания. Действительная функция приложения example1_app() использует проверочные функции uip_newdata() и uip_rexmit(), чтобы определить, почему функция была вызвана. Если приложение было вызвано по причине поступления новых данных от дальнего участника соединения (remote host), то оно ответит "ok". Если функция была вызвана из-за того, что данные сети были потеряны и их надо передать заново, то она также пошлет "ok". Обратите внимание, что этот пример показывает реализацию полноценного приложения uIP. Здесь не требуется участие приложения для всех типов событий, таких как uip_connected() или uip_timedout().

Более сложное приложение

Второй пример дает несколько более продвинутое приложение, чем предыдущее, и показывает, как импльзуется поле состояния приложения (application state) в структуре uip_conn.

это приложение работает подобным образом, как и предыдущее, оно также прослушивает порт на входящие соединения и отвечает на все посланные данные одиночным "ok". Большая разница в том, что это приложение печатает сообщение "Welcome!" (приглашение "добро пожаловать!"), когда соединение было только что установлено.

Выгладит это изменение как пустая мелочь, однако имеет большое значение как это реализовано в приложении. Причина возрастания сложности в том, что когда данные потерялись в сети, то приложение должно знать, какие именно данные надо передать заново. Если потерялось сообщение "Welcome!", то нужно передать его, а если потерялось "ok", то нужно передать его.

Приложение знает, что пока сообщение "Welcome!" не было подтверждено от remote host, оно может быть потеряно в сети. Поэтому как только хост пошлет подтверждение обратно, приложение удостоверится, что приглашение было принято, и в ответ на остальные потерянные данные нужно посылать сообщение "ok". Поэтому приложение может находиться в одном из двух состояний: либо в состоянии WELCOME-SENT, когда нужно посылать каждый раз "Welcome!", когда оно было послано, но не подтверждено, либо в состоянии WELCOME-ACKED, когда "Welcome!" было успешно принято и подтверждено.

Когда remote host подключился к приложению, то оно посылает сообщение "Welcome!", и устанавливает свое состояние в WELCOME-SENT. Когда приглашение было подтверждено, приложение переходит в состояние WELCOME-ACKED. Если затем приложение принимает любые новые данные от remote host, оно ответит отправкой сообщения "ok".

Если приложение запрашивается на повторную передачу последнего сообщения, то оно смотрит на свое текущее состояние. Если приложение в состоянии WELCOME-SENT, то оно ответит сообщением "Welcome!", потому что знает, что предыдущее сообщение не было подтверждено. Если текущее состояние WELCOME-ACKED, то приложение знает, что последнее сообщение было "ok", и отправит его.

Реализация этого приложения приведена ниже. Конфигурационные настройки приложения следуют после реализации.

struct example2_state {
   enum {WELCOME_SENT, WELCOME_ACKED} state;
};

void example2_init(void) {
   uip_listen(HTONS(2345));
}

void example2_app(void) {
   struct example2_state *s;

   s = (struct example2_state *)uip_conn->appstate;
   
   if(uip_connected()) {
      s->state = WELCOME_SENT;
      uip_send("Welcome!\n", 9);
      return;
   } 

   if(uip_acked() && s->state == WELCOME_SENT) {
      s->state = WELCOME_ACKED;
   }

   if(uip_newdata()) {
      uip_send("ok\n", 3);
   }

   if(uip_rexmit()) {
      switch(s->state) {
      case WELCOME_SENT:
         uip_send("Welcome!\n", 9);
         break;
      case WELCOME_ACKED:
         uip_send("ok\n", 3);
         break;
      }
   }
}

Конфигурационные настройки приложения следующие:

#define UIP_APPCALL       example2_app
#define UIP_APPSTATE_SIZE sizeof(struct example2_state)

Как отделить приложения друг от друга

Если в системе должно быть запущено несколько сетевых приложений, техника отличать их друг от друга заключается в использовании номера порта TCP либо на дальнем, либо на локальном конце соединения. Пример ниже показывает, как два выше приведенных примера могут работать вместе в одной программе.

void example3_init(void) {
   example1_init();
   example2_init();   
}

void example3_app(void) {
   switch(uip_conn->lport) {
   case HTONS(1234):
      example1_app();
      break;
   case HTONS(2345):
      example2_app();
      break;
   }
}

Использование TCP Flow Control

Этот пример показывает простое приложение, которое подключается к хосту, посылает запрос HTTP на загрузку файла и загружает его на медленное устройство памяти, такое как диск. Этот пример показывает, как использовать функции управления потоком (flow control) стека uIP.

void example4_init(void) {
   u16_t ipaddr[2];
   uip_ipaddr(ipaddr, 192,168,0,1);
   uip_connect(ipaddr, HTONS(80));
}

void example4_app(void) {
   if(uip_connected() || uip_rexmit()) {
      uip_send("GET /file HTTP/1.0\r\nServer:192.186.0.1\r\n\r\n",
               48);
      return;
   }

   if(uip_newdata()) {
      device_enqueue(uip_appdata, uip_datalen());
      if(device_queue_full()) {
         uip_stop();
      }
   }

   if(uip_poll() && uip_stopped()) {
      if(!device_queue_full()) {
         uip_restart();
      }
   }
}

Когда соединение установлено, на сервер отправляется HTTP-запрос. Так как это единственные данные, которые отправляет приложение, то оно всегда знает, какие данные нужно отправить в случае необходимости ретрансмиссии. Так можно эти два события скомбинировать, как и было сделано в этом примере.

Когда приложение приняло новые данные от remote host, оно посылает эти данные на медленное устройство хранения функцией device_enqueue(). Важно заметить, тчо этот пример подразумевает, что функция копирует данные в свои собственные буферы. Данные в буфере uip_appdata будут перезаписаны следующим пришедшим пакетом.

Если очередь (буферы) устройства заполнены, приложение останавливает получение данных от remote host вызовом функции uIP uip_stop(). Затем приложение может быть уверено, что новые данные не поступят, пока не будет вызвана функция uip_restart(). Используется событие опроса приложения (application polling event) для проверки состояния очереди устройства, и как только очередь перестала быть переполненной, то вызывается функция uip_restart(), и поток данных возобновляется.

Простой WEB-сервер

Этот пример показывает приложение очень простого файлового сервера, который слушает 2 порта, и использует номер порта, чтобы определить, какой файл будет отправлен. Если файлы правильно отформатированы, это простое приложение может быть использовано как простейший web-сервер со статичными страницами.

struct example5_state{
   char *dataptr;
   unsigned int dataleft;
};

void example5_init(void) {
   uip_listen(HTONS(80));
   uip_listen(HTONS(81));
}

void example5_app(void) {
   struct example5_state *s;
   s = (struct example5_state)uip_conn->appstate;
   
   if(uip_connected()) {
      switch(uip_conn->lport) {
      case HTONS(80):
         s->dataptr = data_port_80;
         s->dataleft = datalen_port_80;
         break;
      case HTONS(81):
         s->dataptr = data_port_81;
         s->dataleft = datalen_port_81;
         break;
      }
      uip_send(s->dataptr, s->dataleft);
      return;      
   }

   if(uip_acked()) {
      if(s->dataleft < uip_mss()) {
         uip_close();
         return;
      }
      s->dataptr += uip_conn->len;
      s->dataleft -= uip_conn->len;
      uip_send(s->dataptr, s->dataleft);      
   }
}

Состояние приложения (application state) содержит указатель на данные, которые должны быть отправления и размер данных, которые еще не отправлены. Когда remote host подключается к приложению, локальный номер порта используется для определения, какой файл надо отправить. Первый кусок данных отправляется вызовом uip_send(). uIP удостоверяется, что в действительности отправлено данных не больше, чем MSS байт, даже при том, что s->dataleft может быть больше MSS.

Приложение управляется приходом подтверждений. Когда данные были подтверждены, может быть отправлена новая порция данных. Если данные кончились, то соединение закрывается использованием функции uip_close().

Структурированный подход к разработке приложения

При написаении больших прогорамм с использованием uIP полезно использовать возможности uIP API структурированно. Следующий пример предоставляет структурный дизайн, который показывает как лучше всего реализоваывать реализацию сложного протокола. Программа поделена на функцию обработчика событий (uIP event handler), которая вызывает 7 разных функций: для обработки новх данных, работы с подтвержденными данными, отправки новых данных, разборок с установкой и закрытием соединения и для обработки ошибок. У всех функций есть понятные и удобные имена newdata(), acked(), senddata(), connected(), closed(), aborted() и timedout(), и их нужно написать в соответствии с реализуемым протоколом обмена.

Ниже показана функция обработчика событий стека (uIP event handler).

void example6_app(void) {
  if(uip_aborted()) {
    aborted();
  }
  if(uip_timedout()) {
    timedout();
  }
  if(uip_closed()) {
    closed();
  }
  if(uip_connected()) {
    connected();
  }
  if(uip_acked()) {
    acked();
  }
  if(uip_newdata()) {
    newdata();
  }
  if(uip_rexmit() ||
     uip_newdata() ||
     uip_acked() ||
     uip_connected() ||
     uip_poll()) {
    senddata();
  }
}

Функция начинается с обработки событий ошибки, которые могли произойти, путем проверки результатов uip_aborted() или uip_timedout(). Если ошибка произошла, то вызвается функция обработки ошибки. Аналогично если соединение было закрыто, то будет вызвана функция closed(), которая обработает событие закрытия соединения.

Далее функция проверяет, было ли соединение установлено только что (это так, если возврат из uip_connected() равен true). В этом случае будет вызвана функция connected(), и она выполнит все, что нужно сделать, когда соединение только что установлено - например, сделает инициализацию состояния приложения для соединения. Если в этом месте могут быть переданы данные, то должна быть вызвана функция senddata(), отправляющая данные.

Код ниже служит простым примером, как могут выглядеть функции обработчиков событий для этого приложения. Приложение просто ждет любых поступающих через соединение данных, и отвечает на эти данные отправкой сообщения "Hello world!". Чтобы показать, как разработать приложение с машиной состояний, это сообщение будет отправлено по частям, сначала "Hello", и затем "world!".

#define STATE_WAITING 0
#define STATE_HELLO   1
#define STATE_WORLD   2

struct example6_state {
  u8_t state;
  char *textptr;
  int  textlen;
};

static void aborted(void) {}
static void timedout(void) {}
static void closed(void) {}

static void connected(void) {
  struct example6_state *s = (struct example6_state *)uip_conn->appstate;

  s->state   = STATE_WAITING;
  s->textlen = 0;
}

static void newdata(void) {
  struct example6_state *s = (struct example6_state *)uip_conn->appstate;

  if(s->state == STATE_WAITING) {
    s->state   = STATE_HELLO;
    s->textptr = "Hello ";
    s->textlen = 6;
  }
}

static void acked(void) {
  struct example6_state *s = (struct example6_state *)uip_conn->appstate;
  
  s->textlen -= uip_conn->len;
  s->textptr += uip_conn->len;
  if(s->textlen == 0) {
    switch(s->state) {
    case STATE_HELLO:
      s->state   = STATE_WORLD;
      s->textptr = "world!\n";
      s->textlen = 7;
      break;
    case STATE_WORLD:
      uip_close();
      break;
    }
  }
}

static void senddata(void) {
  struct example6_state *s = (struct example6_state *)uip_conn->appstate;

  if(s->textlen > 0) {
    uip_send(s->textptr, s->textlen);
  }
}

Состояние приложения хранится в переменной "state", указатель "textptr" указывает на текстовое сообщение и переменная "textlen" содержит длину текстового сообщения. Переменная "state" может быть равной либо "STATE_WAITING", что означает ожидание приложением данных, поступабщих из сети, либо "STATE_HELLO", в этом состоянии приложение посылает часть сообщения "Hello", либо "STATE_WORLD", в котором приложение посылает вторую часть сообщения "world!".

Приложение не обрабатывает ошибки или события закрытия соединения, таким образом функции aborted(), timedout() и closed() реализованы как пустые.

Функция connected() будет вызвана, когда установилось соединение, и в этом случае переменная "state" равна "STATE_WAITING" и переменная "textlen" равна 0, значит никаких сообщений посылать не нужно.

Когда новые данные поступят через сеть, из функции обработчика событий будет вызвана функция newdata(). Функция newdata() проверит состояние приложения: если состояние "STATE_WAITING", то переключается в состояние "STATE_HELLO", и в соедиении регистрирует для отправки 6-байтное сообщение "Hello ". Это сообщение будет позже отправлено функцией senddata().

Функция acked() вызывается всякий раз, когда ранее отправленные данные были подтверждены принявшим их хостом. Эта функция acked() сначала уменьшает количество данных, подлежащих отправке, путем вычитания длины ранее отправленных данных (эта длина получена из "uip_conn->len") из значения переменной "textlen", и также соответственно подстраивает значение указателя "textptr". Если проверка показала, что значение переменной "textlen" стала равна 0, то это означает, что все данные были переданы, и эти переданные данные были приняты на дальнем конце, и если так, то состояние приложения поменяется. Если приложение было в состоянии "STATE_HELLO", то оно переключается в состояние "STATE_WORLD", и подготавливает для отправки 7-байтное сообщение "world!\n". Если приложение было в состоянии "STATE_WORLD", то соединение закрывается.

И в завершении функция senddata() берет на себя задачу отправки данных. Она вызывается из функции обработчика событий, когда были приняты новые данные, когда данные были подтверждены, когда было установлено новое соединение, когда соединение опрашивается по причине неактивности, или когда должна быть выполнена ретрансмиссия. Назначение функции senddata() - опционально отформатировать посылаемые данные, и вызвать uip_send() для действительной отправки данных. В этом частном примере функция просто вызывает uip_send() с нужными аргументами, если нужно отправить данные - после проверки, нужно ли их отправить, что показывается длиной переменной "textlen".

Важно отметить, что функция senddata() никогда не должна влиять на состояние приложения; состояние должно меняться только по результатам функций acked() и newdata().

Реализации протокола

Протоколы, входящие в семейство TCP/IP, разработаны с разделением по уровням, где каждый протокол выполняет отдельную функцию, и взаимодействие между слоями протоколов строго расписано. Хотя послойная реализация хорошо подходит для разработки протоколов, это не всегда лучший способ для их реализации. В uIP реализация протоколов сильно взаимосвязана, чтобы сохранить место под код.

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

IP --- Internet Protocol

Когда приходящие пакеты обрабатываются стеком uIP, слой IP первый, на котором проверяется пакет. Слой IP делает несколько простых проверок, таких как совпадает ли адрес назначения пакета (destination IP) локальному адресу IP, и проверка контрольной суммы заголовка IP. Поскольку здесь нет опций IP, которые строго нужно соблюдать (наподобие опций качества обслуживания QoS), и поскольку они очень редко используются, все опции принятых пакетов IP отбрасываются.

Пересборка фрагментов IP

Пересборка фрагментов IP реализована с использованием отдельного буфера, который содержит пересобираемый пакет. Приходящий фрагмент копируется в нужное место буфера, и используется битовая карта для отслеживания, какие фрагменты были приняты. Поскольку первый байт фрагмента IP выровнен на 8 байт, карта бит требует небольшого места в памяти. Когда фрагменты пересобраны, IP-пакет результата передается на слой транспорта. Если не были приняты все фрагменты в указанный промежуток времени, то пакет отбрасывается.

Текущая реализация имеет только один буфер для удержания пересобираемых пакетов, так что пересборка более чем одного пакета не поддерживается. Поскольку фрагментирование пакетов встречается редко, это должно быть разумным решением. Как бы то ни было, Вы можете можно довольно просто самостоятельно расширить реализацию пересборки для поддержки нескольких буферов, если это потребуется.

Broadcast-ы и Multicast-ы

В протоколе IP используется широковещательная (broadcast) и групповая (multicast) передача пакетов в локальной сети. Такие пакеты адресуются по специальным адресам broadcast и multicast. Broadcast используется главным образом во многих протоколах, основанных на UDP, такой как протокол обмена файлами Микрософт (Microsoft Windows file-sharing SMB, или его еще называют Samba). Multicast главным образом используется для доставки мультимедийного трафика, наподобие RTP. Протокол TCP является протоколом типа точка-точка, и он неиспользует пакеты broadcast или multicast. uIP в настоящее время поддерживает пакеты broadcast, а также отправку пакетов multicast. Присоединение к группам multicast (IGMP) и прием не локальных пакетов multicast в настоящий момент не поддерживается.

ICMP --- Internet Control Message Protocol

Протокол ICMP используется для сообщения о случаях мягких (программных?) ошибок и для опроса параметров хоста. Однако главное использование его - механизм эха, который задействован программой "ping".

Реализация ICMP в стеке uIP очень проста, поскольку она ограничена толко реализацией сообщений эха ICMP. Ответы на сообщения эха составляются простой сменой IP-адресов источника и получателя (source и destination) в приходящих запросах эха, и также делается перезапись в заголовке ICMP типа сообщения на тип Echo-Reply. Контольная сумма ICMP подстраивается с использованием стандартной техники (см. RFC1624).

Поскольку реализовано только сообщение ICMP echo, нет поддержки Path MTU discovery или сообщений перенаправления ICMP. Ни одна из этих возможностей не требуется для функциональной совместимости; эти возможности относятся только к механизмам улучшения производительности передачи трафика.

TCP --- Transmission Control Protocol

Реализация TCP в стеке uIP обрабатывается по приходу пакетов и по событиям таймера. Приходящие пакеты парсируются по TCP, и если пакет содержит данные, которые должны быть переданы приложению, то вызывается функция приложения. Если прищедший пакет подтверждает ранее отправленные данные, то обновляется состояние соединения и об этом оповещается приложение, что позволит ему передать новые данные.

Обработка входящих соединений (Listening Connections)

TCP позволяет установить соединение путем прослушивания входящих запросов на соединение. В стеке uIP прослушиваемое соединение идентифицируется по 16-битному номеру порта, и запрос входящего соединения проверяется по списку прослушиваемых соединений. Этот список прослушиваемых соединений динамический, и он может быть изменен приложениями в системе (во время работы кода).

Скользящее окно (Sliding Window)

Многие реализации TCP используют механизм sliding window для отправки данных. Несколько сегментов данных отправляются последовательно друг за другом, без ожидания подтверждения для каждого сегмента.

Алгоритм sliding window использует много 32-разрядных операций, и поскольку 32-битная арифметика довольно затратна для реализации на болшинстве 8-разрядных CPU, в стеке uIP эта технология не реализована. Также uIP не буферизирует отправляемые пакеты, а реализация sliding window, которая не буферизирует отправляемые пакеты, должна иметь сложную поддержку на уровне приложения. Вместо этого uIP позволяет только один сегмент TCP на подключение, который может быть неподтвержден в любой промежуток времени.

Важно отметить, что несмотря на то, что многие реализации TCP используют алгоритм sliding window, он не требуется по стандартам TCP. Так что удаление механизма sliding window никак не повлияет на совместимость.

Вычисление двухсторонней задержки (Round-Trip Time Estimation)

TCP постоянно оценивает текущее время Round-Trip (RTT) для каждого активного соединения, чтобы найти подходящую величину для таймаута ретрансмиссии.

Оценка RTT в uIP реализована с использованием периодического таймера TCP. Каждый раз, когда срабатывает периодический таймер, он инкрементирует счетчик для каждого соединения, у которого есть неподтвержденные сетевые данные (получение данных на дальнем конце пока не подтверждено). Когда подтверждение получено, текущее значение счетчика используется как данные RTT. Эти данные используются вместе со стандартной функцией TCP Van Jacobson-а для вычисления RTT. Алгоритм Karn-а используется для того, чтобы убедиться в том, что ретрансмиссии не искажают оценку RTT.

Повторные передачи (Retransmissions)

Ретрансмиссии также управляются периодическим таймером TCP. Каждый раз, когда вызывается обработчик интервала периодического таймера, декрементируется таймер ретрансмиссии для каждого соединения. Если счетчик таймера достиг 0, то должна быть выполнена ретрансмиссия.

Как уже много раз упоминалось, uIP не отслеживает содержимое пакетов после того, как пакет был передан драйверу устройства, так что uIP требует активного участия приложения для реализации ретрансмиссии. Когда uIP решила, что сегмент должен быть передан повторно, она вызывает приложение с установленным флагом, требующим ретрансмиссии. Приложение проверяет флаг ретрансмиссии, и выпускает те же самые данные, которые были посланы ранее. С точки зрения приложения выполнение ретрансмиссии не отличается от того, как данные передавались изначально. Таким образом, приложение может быть написано так, чтобы для отправки использовался тот же самый код, что и для повторной отправки. Также важно отметить, что хотя заботу о переотправке берет на себя приложение, именно стек отвечает за то, чтобы правильно определить необходимость повторной передачи. Так что код приложения не обязательно должен вырасти, несмотря на то что оно принимает участие в создании повторных передач.

Управление потоком данных (Flow Control)

Назначение механизма управления потоком TCP в том, чтобы позволить обмениваться данными между хостами, у которых разные возможности по доступной памяти. В каждом сегменте TCP получатель сегмента указывает размер своего доступного буфера. Отправитель TCP не должен отправлять данных больше, чем размер буфера, указанный получателем.

В uIP приложение не может отправить данных больше, чем принимающий хост может забуферизировать. И приложение не может отправить данных больше, чем это позволят принимающий хост. Если remote host вообще не может принять данные, стек инициирует механизм зондирования нулевого окна (zero window probing mechanism).

Управление загрузкой сети (Congestion Control)

Механизмы congestion control ограничивают количество одновременных TCP-сегментов в сети. Алгоритмы, используемые для congestion control, разработаны так, чтобы их можно было легко реализовать, и они требуют только нескольких строк кода.

Поскольку uIP обрабатывает только один рабочий сегмент TCP на одно соединение, то сумма обновременных сегментов не может быть еще более ограничена. Поэтому механизмы congestion control не нужны.

Данные, требующие быстрой доставки (Urgent Data)

Механим TCP urgent data предоставляет механизм оповещения между приложениями, который приложение может использовать для того, чтобы пометить части потока данных, которые требуют ускоренной доставки по сравнению с обычным потоком данных. Маркировка проходит до принимающего данные приложения, у котором, как подразумевается, срочность данных должна быть интерпритирована.

Во многих реализациях TCP, включая реализацию BSD, фича urgent data увеличивает сложность реализации, потому что требуется механизм асинхронного оповещения, или синхронного API. Стек uIP уже использует API, основанное на асинхронных событиях, поэтому реализация фичи urgent data не ведет к увеличению сложности.

Быстродействие (Performance)

В реализации TCP/IP на мощных системах время обработки тратится в основном на циклы вычисления контрольной суммы, операции копирования данных пакета и на переключение контекста. Операционные системы высокого класса часто имеют многоуровневую защиту, чтобы защитить данные ядра от процессов пользователя и процессы разных пользователей друг от друга. Поскольку стек TCP/IP работает в ядре, то данные должны быть скопированы между областью ядра и областью процесса пользователя, и как только данные скопированы, должно произойти переключение контекста. Быстродействие можно увеличить, если скомбинировать опекцию копирования с вычислением контрольной суммы. Так как высокопроизводительные системы обычно имеют в наличии множество активных соединений, то демультиплексирование пакетов также является затратной по вычислениям операцией.

В маленьких embedded-устройстван не требуется тратить вычислительные мощности на многоуровневую доменную защиту, и на запуска многопоточной операционной системы. Так что нет никакой необходимости копировать данные между стеком TCP/IP и программой приложения. С использованием API, основанном на событиях, не требуется переключение контекста между стеком TCP/IP и кодом приложений.

В таких ограниченных системах затраты на обработку TCP/IP состоят в копировании данных пакета из сетевого устройства в память хоста и в вычислении контрольной суммы. Кроме вычисления контрольной суммы и копирования обработка TCP приходящего пакета заключается только в обновлении нескольких счетчиков и флагов перед тем, как передать данные приложению. Так что оценить загрузку CPU нашей реализацией TCP/IP можно с помощью подстчета циклов CPU, требуемых на вычисление контрольной суммы и копирование пакета максимального размера.

Влияние задержанных подтверждений (Delayed Acknowledgments)

Многие приемники TCP реализуют алгоритм delayed acknowledgment, чтобы уменьшить количество только пакетов подтверждения. Приемник TCP, который использует этот алгоритм, отправит подтверждение только за другим полученным сегментом. Исли в определенное окно времени не было принято другого сегмента, то отправляется подтверждение на последний сегмент. Окно времени может иметь размер до 500 мс, но обычно это 200 мс.

Отправитель TCP наподобие uIP, который обрабатывает только один выделенный сегмент TCP, будет плохо взаимодействовать по такому алгоритму. Поскольку приемник получит каждый раз только один сегмент, то он будет ждать целых 500 мс, и только после этого отправит подтверждение. Это означает, что максимально возможная пропускная способность будет сильно ограничена из-за наличия времени ожидания подтверждения 500 мс.

Таким образом, уравнение для максимальной пропускной способности при отправке данных из uIP будет $p = s / (t + t_d)$, где $s$ размер сегмента и $t_d$ таймаут отложенного подтверждения, который обычно находится в пределах 200..500 мс. С размером сегмента в 1000, временем round-trip 40 мс и таймаутом отложенного подтверждения 200 мс, максимальная скорость передачи получится 4166 байт в секунду. Если отключить алгоритм отложенного подтверждения на приемнике, то максимальная скорость будет 25000 байт в секунду.

Однако следует отметить, что поскольку маленькие системы, на которых работает uIP, скорее всего не будут передавать большие объемы данных, то ухудшение пропускной способности из-за отложенного подтверждения не будет составлять большой проблемы. Небольшие количества посылаемых данных скорее всего попадут в один сегмент TCP, так что это не затронет ухудшение пропускной способности.

Когда uIP работает как получатель данных, то отложенное подтверждение не приводит к снижению пропускной способности.