You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
273 lines
16 KiB
273 lines
16 KiB
# План реализации ETCP (Extended Transmission Control Protocol) |
|
|
|
## Обзор |
|
Протокол поверх UDP с восстановлением порядка, повторными передачами, метриками RTT/jitter и ограничением полосы. Использует две очереди: сортированный linked-list для принятых пакетов и ll_queue для выходных данных. |
|
|
|
## Структуры данных |
|
|
|
### struct epkt (etcp.h) |
|
```c |
|
typedef struct epkt epkt_t; |
|
|
|
struct epkt { |
|
// Очереди |
|
ll_queue_t* tx_queue; // Очередь пакетов на передачу |
|
ll_queue_t* output_queue; // Выходная очередь (собранные данные) |
|
struct rx_packet* rx_list; // Сортированный linked-list принятых пакетов |
|
|
|
// Метрики |
|
uint16_t rtt_last; // Последний RTT (timebase 0.1ms) |
|
uint16_t rtt_avg_10; // Среднее RTT за последние 10 пакетов |
|
uint16_t rtt_avg_100; // Среднее RTT за последние 100 пакетов |
|
uint16_t jitter; // Jitter (усреднённый) |
|
uint16_t bandwidth; // Текущая полоса пропускания (байт/таймбазу) |
|
uint32_t bytes_sent_total; // Всего отправлено байт |
|
uint16_t last_sent_timestamp; // Timestamp последней отправки |
|
uint32_t bytes_allowed; // Расчётное число байт, которое можно отправить |
|
|
|
// Состояние |
|
uint16_t next_tx_id; // Следующий ID для передачи |
|
uint16_t last_rx_id; // Последний принятый ID (для ACK) |
|
uint16_t last_delivered_id; // Последний доставленный в output_queue ID |
|
|
|
// Таймеры |
|
void* next_tx_timer; // Таймер следующей передачи |
|
void* retransmit_timer; // Таймер повторных передач |
|
|
|
// Callback'и |
|
void (*tx_callback)(epkt_t*, uint8_t*, uint16_t); // Отправка в UDP |
|
void* tx_callback_arg; |
|
|
|
// Буферы для метрик |
|
uint16_t rtt_history[100]; // История RTT для усреднения |
|
uint8_t rtt_history_idx; |
|
uint8_t rtt_history_count; |
|
|
|
// Накопленные timestamp'ы для отчётов |
|
uint16_t pending_ack_ids[32]; // ID пакетов, для которых нужно отправить ACK |
|
uint16_t pending_ack_timestamps[32]; // Соответствующие timestamp'ы |
|
uint8_t pending_ack_count; |
|
}; |
|
``` |
|
|
|
### struct rx_packet (внутренняя) |
|
```c |
|
struct rx_packet { |
|
struct rx_packet* next; |
|
uint16_t id; |
|
uint16_t timestamp; |
|
uint8_t* data; |
|
uint16_t data_len; |
|
// uint8_t has_payload; // 1 если содержит payload (hdr=0) - пакеты без payload сюда не попадаютю просто парсим сразу метрици и всё. |
|
}; |
|
``` |
|
|
|
## Формат пакета |
|
``` |
|
<id:2> <timestamp:2> [<hdr:1> <metrics_data>]* <hdr=0> <payload> |
|
``` |
|
|
|
### Заголовки (hdr): |
|
- `0x00`: payload (данные начинаются со следующего байта) |
|
- `0x01`: отчёт о timestamp принятого пакета (4 байта: id:2 + timestamp:2) |
|
- `0x10`-`0x2F`: перезапрос пакетов + ACK: |
|
- hdr & 0x0F = количество записей (1-32) |
|
- Далее N*2 байт: ID пакетов для повторной передачи |
|
- Последние 2 байта: номер последнего доставленного пакета (ACK) |
|
|
|
### Пакет только с метриками: |
|
Если очередь передачи пуста, отправляется пакет с id=0 и без hdr=0+payload. |
|
|
|
## Алгоритмы |
|
|
|
### 1. Инициализация |
|
- Создать очереди tx_queue и output_queue через queue_new() |
|
- Инициализировать метрики нулями |
|
- Установить bandwidth по умолчанию (например, 100000 байт/таймбазу) |
|
|
|
### 2. Приём пакетов (etcp_rx_input) |
|
1. Парсинг: |
|
- Читаем id, timestamp |
|
- Пока есть данные, читаем hdr: |
|
- hdr=0x00: запоминаем payload |
|
- hdr=0x01: обрабатываем отчёт о timestamp (обновляем метрики) |
|
- hdr=0x10-0x2F: обрабатываем перезапрос (добавляем ID в очередь повторной передачи) |
|
2. Для пакетов с hdr!=0 вызываем upd_metric_for_transmitter: |
|
- rtt_last = текущее время - timestamp |
|
- Обновляем rtt_avg_10 и rtt_avg_100 (скользящее среднее) |
|
- jitter += (abs(rtt_avg_10 - rtt_last) - jitter) * 0.1 |
|
3. Добавляем пакет в сортированный rx_list: |
|
- Ищем позицию по id (сравнение через (int16_t)(id1-id2)) |
|
- Пропускаем дубликаты |
|
- Вставляем в нужное место |
|
4. Перемещаем непрерывную последовательность в output_queue: |
|
- Начиная с last_delivered_id+1, проверяем наличие пакетов в rx_list |
|
- При нахождении непрерывной цепочки перемещаем payload в output_queue |
|
- Освобождаем rx_packet структуры |
|
5. Накопление ACK: |
|
- Для каждого принятого пакета сохраняем id и timestamp в pending_ack_* |
|
- При следующей отправке включаем эти ACK в пакет |
|
|
|
### 3. Передача пакетов |
|
1. Очередь tx_queue содержит данные для отправки |
|
2. Функция tx_process вызывается по таймеру или при добавлении в пустую очередь: |
|
- Проверяем ограничение полосы: |
|
- delta_time = текущее_время - last_sent_timestamp |
|
- bytes_allowed += delta_time * bandwidth |
|
- Если bytes_allowed < размер_пакета, планируем таймер и выходим |
|
- Формируем пакет: |
|
- Базовый заголовок: next_tx_id++, текущий timestamp |
|
- Добавляем pending_ack (hdr=0x01 для каждого) |
|
- Добавляем перезапросы если нужно (на основе метрик) |
|
- Добавляем payload из tx_queue (hdr=0x00) |
|
- Если payload нет и есть метрики - отправляем пакет с id=0 |
|
- Отправляем через tx_callback |
|
- Обновляем bytes_sent_total, last_sent_timestamp, bytes_allowed |
|
- Сохраняем пакет в список отправленных (для возможной ретрансмиссии) |
|
3. Повторные передачи: |
|
- Для каждого отправленного пакета отслеживаем время отправки |
|
- Если (текущее_время - время_отправки) > rtt_avg_10*1.2 + jitter*2 |
|
- Добавляем ID в очередь повторной передачи |
|
|
|
### 4. Ограничение полосы |
|
- bandwidth: константа (байт/таймбазу), timebase = 0.1ms |
|
- При инициализации: bytes_allowed = 0, last_sent_timestamp = текущее_время |
|
- При отправке: |
|
- current_time = uasync время |
|
- delta = (int16_t)(current_time - last_sent_timestamp) (циклическое) |
|
- bytes_allowed += delta * bandwidth |
|
- Если bytes_allowed >= размер_пакета: |
|
- bytes_allowed -= размер_пакета |
|
- last_sent_timestamp = current_time |
|
- Отправляем пакет |
|
- Иначе: |
|
- wait_time = (размер_пакета - bytes_allowed) / bandwidth |
|
- Устанавливаем таймер на wait_time |
|
|
|
## API функции |
|
|
|
### Основные: |
|
```c |
|
epkt_t* etcp_init(void); |
|
void etcp_free(epkt_t* epkt); |
|
void etcp_set_callback(epkt_t* epkt, void (*cb)(epkt_t*, uint8_t*, uint16_t), void* arg); |
|
int etcp_rx_input(epkt_t* epkt, uint8_t* pkt, uint16_t len); |
|
int etcp_tx_queue_size(epkt_t* epkt); |
|
``` |
|
|
|
### Вспомогательные: |
|
```c |
|
void etcp_set_bandwidth(epkt_t* epkt, uint16_t bandwidth); |
|
uint16_t etcp_get_rtt(epkt_t* epkt); |
|
uint16_t etcp_get_jitter(epkt_t* epkt); |
|
ll_queue_t* etcp_get_output_queue(epkt_t* epkt); // Для извлечения данных |
|
int etcp_tx_put(epkt_t* epkt, uint8_t* data, uint16_t len); // Добавить данные на передачу |
|
``` |
|
|
|
## Интеграция с проектом |
|
|
|
### Зависимости: |
|
- `ll_queue.c/.h` - для очередей |
|
- `u_async.h` - для таймеров |
|
- `stdint.h`, `stdlib.h`, `string.h` - стандартные библиотеки |
|
|
|
### Таймеры: |
|
- Использовать `uasync_set_timeout` для: |
|
- Планирования следующей передачи (при ограничении полосы) |
|
- Повторных передач |
|
- Очистки старых пакетов в rx_list |
|
- Все callback'и должны быть быстрыми, не блокирующими |
|
|
|
### Обработка циклических значений: |
|
- ID: uint16_t, сравнение через `(int16_t)(a - b)` |
|
- Timestamp: uint16_t, timebase 0.1us, сравнение аналогично |
|
- При вычислении дельты времени учитывать переполнение |
|
|
|
## План тестирования |
|
|
|
1. **Unit-тесты для парсинга:** |
|
- Корректность разбора различных hdr |
|
- Обработка циклических ID |
|
|
|
2. **Тесты очередей:** |
|
- Сортировка в rx_list |
|
- Перемещение в output_queue |
|
- Обработка дубликатов |
|
|
|
3. **Тесты метрик:** |
|
- Расчет RTT (скользящее среднее) |
|
- Расчет jitter |
|
- Обновление bandwidth |
|
|
|
4. **Интеграционный тест:** |
|
- Два экземпляра ETCP, обмен данными через эмуляцию UDP |
|
- Проверка восстановления порядка при потере пакетов |
|
- Проверка ограничения полосы |
|
|
|
5. **Тест производительности:** |
|
- Минимальные задержки |
|
- Корректность работы при высокой нагрузке |
|
|
|
## Последовательность реализации |
|
|
|
1. Создать etcp.h с определениями структур и API |
|
2. Реализовать etcp.c в следующем порядке: |
|
a) Базовая структура и init/free |
|
b) Внутренние функции для работы с rx_list |
|
c) Парсинг пакетов (etcp_rx_input) |
|
d) Функции метрик (upd_metric_for_transmitter) |
|
e) Механизм передачи (tx_process, ограничение полосы) |
|
f) Повторные передачи |
|
g) Интеграция с таймерами u_async |
|
3. Создать тестовую программу test_etcp.c |
|
4. Протестировать, исправить ошибки |
|
5. Интегрировать в Makefile |
|
|
|
## Риски и неопределённости |
|
|
|
1. **Переполнение буферов:** Нужны лиматиры на размер rx_list и pending_ack |
|
3. **Производительность:** Сортированный linked-list может быть медленным при большом количестве пакетов. Возможно, нужна оптимизация. - не должно быть много пакетов в списке перезапросов |
|
4. **Интеграция с существующим кодом:** Проверить совместимость стиля кодирования и соглашений об именовании. |
|
|
|
## Дополнительные вопросы |
|
|
|
1. Нужны ли callback'и для событий (доставка данных, изменение метрик)? - нет, но нужна структура из которой эти метрики можно считывать. |
|
2. Как обрабатывать очень старые пакеты в rx_list (таймаут)? - никак. надо сделать reset_connection - он обнуляет все очереди, метрики итд. |
|
3. Как определять начальный bandwidth и адаптировать его? - пока константа. адаптировать позже. |
|
|
|
## Статус реализации (13.01.2026) |
|
|
|
### Реализовано: |
|
1. Структуры epkt, rx_packet, sent_packet |
|
2. API функции: etcp_init, etcp_free, etcp_set_callback, etcp_rx_input, etcp_tx_queue_size, etcp_tx_put, etcp_get_output_queue, etcp_set_bandwidth, etcp_get_rtt, etcp_get_jitter |
|
3. Парсинг пакетов с поддержкой заголовков 0x00 (payload), 0x01 (timestamp report), 0x10-0x2F (retransmission request) |
|
4. Сортированный rx_list с учётом циклических ID (сравнение через (int16_t)(id1-id2)) |
|
5. Перемещение непрерывной последовательности пакетов в output_queue |
|
6. Ограничение полосы пропускания (bandwidth, bytes_allowed) |
|
7. Отправка пакетов с данными и метриками, включая пакеты только с метриками (id=0) |
|
8. Накопление ACK для полученных пакетов и отправка их в следующий пакет |
|
9. Повторные передачи на основе RTT и jitter |
|
10. Обновление метрик RTT (скользящее среднее за 10 и 100 пакетов) и jitter |
|
11. Интеграция с u_async для таймеров передачи и проверки повторных отправок |
|
12. Unit-тесты, покрывающие базовую функциональность (инициализация, передача, приём, реordering) |
|
|
|
### Особенности реализации: |
|
- Timebase: 0.1ms (совместимость с u_async) |
|
- Bandwidth по умолчанию: 10000 байт/таймбазу |
|
- Максимальное количество pending ACK: 32 |
|
- Максимальное количество pending retransmit: 32 |
|
- RTT история: 100 измерений |
|
|
|
### Ограничения и упрощения: |
|
- Отсутствует таймаут для старых пакетов в rx_list |
|
- Отсутствует адаптация bandwidth |
|
- get_current_timestamp использует статический счётчик (для тестов) |
|
- Нет обработки reset_connection |
|
- Нет защиты от переполнения буферов при очень большом количестве пакетов |
|
|
|
### Планируемые улучшения: |
|
1. Реализация reset_connection для сброса состояния |
|
2. Использование системного времени для get_current_timestamp |
|
3. Добавление таймаута для rx_list |
|
4. Оптимизация производительности при большом количестве пакетов |
|
5. Адаптация bandwidth на основе метрик
|
|
|