Формат кодограммы: ------------------ пакеты идут по следующему пути: пакет кем-то помещается в входную очередь (ETCP input_queue). Когда load balancer готов отправить следующий пакет он делает вызов etcp_request_pkt в etcp.c/h и etcp либо формирует следующую кодограмму или встаёт в состояние async ожидания (возвращая null): - либо wait ETCP input_queue (если нечего передавать) - либо wait SACK + wait timeout (если sack очередь полная и нет необслуженных запросов ретрансмиссий). после истечения wait timeout он помечает используя round-robin очередной пакет как "нужна ретрансмиссия". из async ожидания он может вызвать etcp_loadbalancer input_from_etcp и передать пакет на отправку. как формируется пакет: 1. берем пакет из списка неподтвержденных пакетов ожидающих отправку 2. если есть место в rwin (объём inflight данных) то берем очередной пакет из ETCP input_queue и его отправляем если пакет найден: 1. вызываем функцию формирования опциональных секций (ACK, channel timestamp) - они записываются в начало 2. в конец добавляем секцию 0x00 с данными пакета при отправке пакета помечаем в inflight списке с какого интерфейса он отправлен Функция прикрепления опциональных секций: - запрашивает выбор канала передачи. - добавляет накопившиеся ACK При приёме пакета (от etcp_connections): - последовательно сканируем секции и отдаём их на обработку нужным обработчикам: - ack: помечаем в inflight пакеты как подтверждённые и проставляем время подтверждения. также и ставим флаг в конце запустить etcp если он в wait timeout, т.е. wait_timeout!=NULL - timestamp: обновляем last RTT, пересчитываем RTT10,RTT100 (плавающим окном за последние 10/100 пакетов), и jitter=max(last 10)-min(last 10 packets) - [0x00] payload: добавляем пакет в нужное место сборочного linked-list (сверяем по ID если дубликат - игнорируем). после сканирования проверяем появились ли в linked-list собранные данные которые можно переместить в выходную очередь. и перемещаем если есть. пересканирование inflight: - вызывается по таймеру, таймер заводится при добавлении пакета в пустой inflight. - если время последней отправки > RTT10*k1+jitter*k2 (коэффициенты вынести в настройки utun instance) то пакет заново отправляется и заводим таймер отправки следующей ретрансмиссии (список отсортирован по времени) - если встречаем успешный ack, запоминаем это и его timestamp (назовем lp_ts). - если после этого далее в очереди находим пакет который не имеет переповторов и timestamp отправки раньше чем [ИСПРАВЛЕНО: фраза обрезана в оригинале; предположительно, "раньше чем lp_ts" на основе контекста; если нет, пометить как неполное] inflight - два списка (ll_queue): - список с неподтвержденными пакетами ожидающими ack (при запросе ретрансмиссии пакет перемещается в ожидающие отправку) - список с неподтвержденными пакетами ожидающими отправку (при отправке возвращается в ожидающие ack) доп. свойства пакетов: - указатель на ETC_LINK последней отправки - timestamp последней отправки - число отправок - число запросов переповтора Размер буфера inflight лимитируем как сумму по всем активным линкам optimal_inflight. optimal_inflight расчитывается из RTT и bandwidth линка. bandwidth по каждому линку адаптивно подстраиваем следующим алгоритмом: периодически инициируем burst_transmission (напоминаем ID пакета на которой она началась). после передаём пачку пакетов только по этому линку со следующими таймингами: - т.е. чуть задерживаем канал а потом выплёвываем пачку пакетов. и добавляем опцию [meas_ts][n] - где n - номер пакета в пачке приёмная сторона видя эту секцию должна отправить секцию [meas_resp] по всем пакетам, которые содержат локальный RECV_Timestamp фиксирующий время приёма каждого пакета с [meas_ts]. приняв эти response анализируем по разнице между pkt1-pkt2 не забита ли очередь отправки, а по дельте pkt2-3-4-5 оцениваем bandwidth. дальше по алгоритму (который потом оттюним) корректируем transmitter speed limit. **** Формат кодограмм для etcp.c/h **** Каждая кодограмма состоит из обязательного заголовка и опциональных секций: 1. Обязательный заголовок всего пакета (2 байта) добавляется при передаче, есть во всех пакетах: [Timestamp high][Timestamp low][flags] - Timestamp: время отправки в единицах 0.1 мс (циклическое). при переповторах время обновляется - flags: bit0 = up/down (recv_keepalive) шифруется строго ВСЁ включая ВСЕ заголовки (кроме INIT+publickey). timestamp вставляется ВСЕГДА ВО ВСЕ КОДОГРАММЫ 2. Опциональные секции (одна или несколько) добавляются в etcp.c: а) Подтверждения (ACK) - заголовок 0x01: [0x01][count][(id_hi,id_lo,ts_hi,ts_lo)×count][last_delivered_hi,lo][last_rx_hi,lo] - count: количество пар ID+timestamp (1-32) - last_delivered_id: последний ID, доставленный получателю - last_rx_id: последний полученный ID (для синхронизации прогресса) в) channel timestamp (добавляется после выбора канала передачи): [0x0F] [RET_Timestamp high][RET_Timestamp low] [RECV_Timestamp high][RECV_Timestamp low] - RET_Timestamp: это timestamp последнего принятого пакета по этому каналу ETCP_LINK плюс разница времени между принятием этого пакета и текущим временем. т.е. приёмная сторона по этому timestamp (и зная своё время) может просчитать время в пути туда + обратно - RECV_Timestamp: это timestamp времени приёма последнего принятого пакета (т.е. отправляем время "когда мы получили этот пакет по локальном урмени") - по нему другая сторона сможет просчитать относительное время задержки в одну сторону (время туда без обратно). если последний пакет по этому каналу принят сильно давно (прошло более 30000 единиц времени) то channel timestamp не добавляем. [СПОРНО: порог 30000 (3 сек) arbitrary; может потребовать настройки или обоснования; также риск clock skew между нодами] 3. Полезная нагрузка - заголовок 0x00 (одна секция и всегда последняя): [0x00] [ID 4 байта] [данные...] - Данные пакета - ID: 16-битный циклический номер пакета. [СПОРНО: 16-bit может привести к быстрому overflow в long-lived сессиях; рассмотреть 32-bit] Пакет может содержать несколько опциональных секций (например, ACK + данные). [СПОРНО: нет явных length полей для секций; parsing implicit, риск ошибок при variable длине; добавить length?] цель протокола: 1. контролировать состояние очередей (не накапливать данные где этого можно избежать), анализировать RTT и inflight - динамически подстраивать размер окна и ретрансмиссии, утилизируя сеть насколько это возможно но не допуская наполнения буферов в сети (что приводит к бестолковому увеличению RTT) Установка подключения для канала (реализуется в etcp_connections.c/h): каждое ETCP подключение может содержать несколько линков до endpoint (разные маршруты, каналы связи). При прохождении трафика через этот модуль подсчитываются метрики (для каждого канала отдельно): - RTT последнего пакета и время отправки последнего пакета - пакеты делятся на 3 категории: <300 байт, 300-699 байт, 700+ байт. для каждой категории отдельно считаются метрики: - количество отправленных в пакетах байтах - количество перезапросов (по каждому каналу отдельно) в пакетах и байтах. когда etcp модуль принял запрос ретрансмиссии он смотрит с какого интерфейса был отправлен этот пакет (это запоминается при отправке) и увеличивает счетчики. - обновление текущего количества неподтвержденных данных inflight (в пакетах и байтах) - сколько по этому каналу отправленных и неподтвержденных на текущий момент Планировщик/балансир каналов (etcp_loadbalancer.c/h): для каждого канала ограничение полосы с тестированием отклика: - полосу ограничиваем по объему данных - полному числу байт ethernet пакта (добавляем размер заголовков) Ограничение реализуем следующим образом: - в структуре есть счётчик времени (квантизация стандартная 0.1ms) по которое канал загружен данными. И суб-разряды (nanotime) этого счетчика 0.1ns - 0.1ms (считает по модулю 1000000 и переполнения добавляет в основной счетчик) чтобы при высоких скоростях не было потери точности (т.е. было ненулевое время передачи одного байта). Этим счётчиком контролируется полоса: при передаче к нему добавляется расчётное время передачи текущего ethernet пакета (используя текущий bandwidth считаем время передачи одного байта (float) и умножаем на число байт, переводим в таймбазу nanotime). А если при передаче время меньше current_time - delta_time (delta_time - константа в define, x0.1ms) то он устанавливается как current time-delta_time. (при неактивности обновляем время) счетчик нужен для контроля можно ли передавать следующий пакет ("расчётное время передачи"); [СПОРНО: сложность с float; риск precision issues] когда etcp пытается отправить пакет через линк, то если в линке не инициализировано подключение и он клиент - то он отбрасывает этот пакет и запускает процесс установки соединения. также процесс установки соединения инициируется при добавлении канала (в etcp_link_new) если это client. процесс установки соединения: - отправляем init запрос и выставляем таймаут (сохраняем его в struct ETCP_LINK) - по таймауту повторяем init запросы, ведем счетчик запросов init. - когда получаем init подтверждение (отправляется в etcp_connections_read_callback) снимаем таймаут и выставляем флаг struct ETCP_LINK initialized=1. При добавлении нового канала посылаем INIT 0x04 (без сброса), а при первичной инициализации подключения - 0x02 (это нужно, т.к. например перезапуск локального софта должен инициировать перезапуск удаленного соединения). **** Формат кодограмм для etcp_connections.c/h **** Кодограммы с этими секциями обрабатываются в etcp_connections (в этих кодограммах всегда только одна секция). в обязательном заголовке ID не используется, при передаче для порядка =0: [СПОРНО: заголовок в etcp — только 2 байта TS; здесь подразумевается ID? Уточнить единый формат] 1) Init запрос - заголовок 0x02 (со сбросом etcp сессии) или 0x04 (без сброса): [0x02/0x04] [my_node_id 64bit] [my mtu high] [my mtu low] [keepalive high] [keepalive low] [recovery high] [recovery low] [my link_id 1 байт] [my public key (64 байта, не шифруется)] - Инициирует новый connection для tcp instance. если tcp instance нет (первое подключение) - создаёт. Между нодами только одно подключение, но можно добавлять каналы. - link_id: локальный идентификатор канала (0-255), назначается отправителем для идентификации канала - Публичный ключ отправляется в конце пакета без шифрования, чтобы получатель мог установить его и расшифровать остальную часть пакета (т.к. инициатор соединения всегда имеет оригинальный peer public key в конфиге - по нему исключаем MITM) 2) Init подтверждение - заголовок 0x03/0x05 (ответы соответственно на коды 0x02 и 0x04): [0x03/0x05] [my_node_id 64bit] [my mtu high] [my mtu low] [my link_id 1 байт] [peer ipv4 4 байта] [peer port 2 байта] - Подтверждение инициализации (канал успешно создан, можно начинать обмен) - link_id: локальный идентификатор канала (0-255) отправителя ответа - peer ip.port: возвращаем ip:port с которого пришел init запрос (чтобы клиент узнал свой external ip:port если за nat) При получении init получатель пакета должен: - reset ETCP_LINK с этим ip_port если он есть - создать новый ETCP_CONN с этим node_id. если уже существует подключение с этим node_id - вызвать etcp_reset (функция сброса окон неподтвержденных данных и нумерации) приём кодограмм из UDP выглядит так: uasync select -> etcp_connection.c etcp_connections_read_callback: memory_pool_alloc, decrypt (or init) -> etcp_conn_input (etcp.c/h - сам tcp механизм) -> ETCP_CONN output_queue Keepalive: пакет без секций (только timestamp+flags) keepalive пакеты шлют и клиент и сервер с заданным интервалом если нет полезного трафика. Клиент: если все линки =down то начинается процедура восстановления связи: - клиент посылает init без сброса - сервер как получает init без сброса отправляет init подтверждение: со сбросом или без сброса в зависимости от состояния init его соединения (инициализировано - то без сброса).