madf: (Default)
[personal profile] madf
Болюча тема.
Неможливість чітко визначити обсяги даних це одна із тих причин, через які я не дуже люблю текстові протоколи. Існує кілька варіантів визначити кінець передачі. Наприклад, по спеціальному токену, явним вказуванням обсягу даних чи закриванням з’єднання. Стандарт HTTP/1.0 каже що для кожного повідомлення що містить тіло треба вказувати заголовок Content-Length що містить довжину тіла у байтах. Але часто тіло повідомлення генерується динамічно і без буферизації визначити його довжину неможливо. Стандарт HTTP/1.1 вводить ще один спосіб визначення довжини блоку даних: chunked encoding. Суть його у тому що перед передачею блоку даних передавати рядок із числом що визначає довжину блоку. Особливо цей спосіб корисний для потокової передачі даних. Точне значення довжини блоку даних дозволяє визначити проблеми під час передачі.
У теорії все виглядає красиво, доки не переходиш до практики.
1. Ніколи не знаєш скільки даних тобі треба прочитати ("1\r\n", "10\r\n", "fffffffff\r\n" - все це валідні варіанти довжини chunk).
2. Ніколи не знаєш скільки байтів ти прочитаєш насправді. read може читати і по одному байту за раз.
3. Намагаючись отримати довжину chunk можна отримати одразу кілька chunk, і не обов’язково цілих ("1\r\na\r\n1\r\nb\r\n1").
4. Ніхто не зупинить мене якщо я захочу передати 1 Тб одним chunk.
Самий тупий варіант - читати по одному байту за раз. Це закриває всі описані проблеми, але може бути неефективним (уже хоча б через те що різко збільшується кількість системних викликів і перемикань контексту). Читання ж у фіксований буфер потребує постійного ковзання по буферу і склеювання даних. Довжина chunk може не влізти у буфер, а може навпаки влізти туди із ще парою chunk'ів. Абсолютно нормальна ситуація коли у кінці буферу знаходиться початок блоку даних. Дуже просто виділити великий буфер під увесь chunk і читати у нього доки всі дані не будуть прочитані. Але що як я загоню терабайт даних одним chunk?
У першій реалізації NTRIP GNSS Caster, яка успішно працює уже більше року, у мене була дуже ефективна реалізації розбору chunked encoding, константна по пам’яті. Хоча це і був мій перший серйозний проект з використанням Boost.Asio, але страшних ляпів я не наробив. Не тормозить (правда, там немає де тормозити - на розбір іде всього з пів сотні потоків по ~1 kbps кожний), не тече (займає ~70 kb після кількох місяців uptime). Проблема у тому що реалізація прибита кривими іржавими цвяхами до гнилої архітектури.
Коли прийшов час розвивати проект (а саме - додавати до нього сервіс prepaid VRS) виявилось що ця гнила архітектура не дозволяє зробити це без проблем. Слава богу, мені вистачило досвіду зробити проект достатньо модульним щоб не переписувати його цілком. Під рефакторинг попадає всього один клас.
Почав я з простого - написав прототип NTRIP-client з урахуванням усіх попередніх помилок. Простий як дошка, навіть без підтримки того самого chunked encoding. Зайняло це у мене від сили годин 8, з усіма "рюшечками". Це дозволило не тільки перевірити саму задумку, але і "увійти в курс справи" - згадати все забуте за рік. Наступним став NTRIP-relay, у якому я відокремив загальний код NTRIP-client і NTRIP-server (насправді це не сервер а, скоріше, джерело даних, але із текстом стандарту не посперечаєшся) у NTRIP-connection. В результаті вони зменшились до пари десятків рядків коду (реалізації одного віртуального метода). Звісно, реалізація зберігає асинхронність привнесену самою бібліотекою.
Останні пару днів я займався "артпідготовкою" до реалізації chunked encoding у рамках NTRIP-connection. Власне, для NTRIP-server уся реалізація звелася до "обгортання" блоку данних довжиною chunk і "\r\n". Спочатку я визначив всі негативні моменти у попередній реалізації. Це було не важко - великий handler-диспетчер що реалізовував FSM у вигляді купи вкладених if. Якщо із розміром я б ще погодився (трохи менше сотні рядків із debug-вставками), то складність і неочевидність вирішило його долю.
Я довго думав про те, як красиво його реалізувати не вдаючись до "монстрячества" і все ніяк не міг узгодити простоту з проблемами описаними на початку посту. Здавалось, без явної реалізації FSM нічого не вийде. Тоді я вирішив подивитись як люди таке реалізовують. C-шні бібліотеки мені не підходили, там усе було очевидно (у curl я все-таки зазирнув). Глянув на cpp-netlib і pion-net. Вони обидві працюють через Boost.Asio і навіть збирались об’єднуватись у спільний проект. В результаті це призвело до того що робота з chunked encoding у них реалізована однаково з точністю до імен змінних - через FSM. Я уже збирався забити і робити FSM і собі, як наштовхнувся на цей код: asio http client without istream.
Спочатку я подумав що він ніяк не верішує проблему "велетенських chunk" - streambuf розростеться і схаває усю пам’ять. Та потім я пильно переглянув документацію на нього і дійшов двох висновків.
1. streambuf не розростеться.
2. З chunk більше 64 кб він втратить "надлишкові" дані.
Хоча boost::streambuf і менш ефективний ніж звичайні буфери, зате він надзвичайно спрощує роботу із текстовим протоколом. Я дивився його реалізацію, там більш-менш розумний підхід до використання внутрішнього буферу. Зараз я переписав NTRIP-connection на роботу із boost::streambuf замість простого буферу і реалізація читання chunked encoding зайняла всього дві невеликих функції. Навіть обробка крайових варіантів (переповнення streambuf) вийшла простою як двері.
Скажу чесно, код ще не тестував, і мені цікаво як він поведе себе на великих chunk. Буде дуже сумно вертати все в зад. Завтра подивимось. Якщо все буде добре - покажу код.

PS: сьогодні колеги поставили ще одну станцію - у Донецьку. Тепер у системі 16 базових станцій (правда, із них 4 - ретранслятори RTK від СКНЗУ і "сирих даних" по ним немає): TNT-TPI GNSS Network. Здається, обігнали найближчих конкурентів - ZAKPOS.

Profile

madf: (Default)
madf

April 2018

S M T W T F S
1234567
891011121314
15161718192021
22232425262728
2930     

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Jan. 6th, 2026 11:39 am
Powered by Dreamwidth Studios