Ориентация главы
JSON — лингва франка современного обмена данными, и XRT даёт ему три полных маршрута: DOM (xrtJsonParse в дерево xvalue — умолчание для конфигураций и небольших документов), SAX (xrtJsonVisit — потоковый обход событиями через обратный вызов, ноль промежуточного дерева для больших файлов) и Writer (xrtJsonWriterCreate — инкрементная сборка: потоковая сериализация без накопления целого дерева). Три маршрута разделяют один комплект строгости: дубликаты ключей отвергаются, границы ресурсов (три шлюза главы 3) защищают от бомб глубокой вложенности, ошибки несут строку и столбец (xrtJsonErrorLocation). Глава проясняет подходящие сценарии каждого маршрута и даёт правила выбора.
32.1引入
Три сценария — три маршрута. Загрузка конфигурации: на старте читается JSON на несколько сотен строк — маршрут DOM это три строки кода (Parse, взять по имени, Release по завершении), дерево значений напрямую входит в систему слияния/чтения главы 30. Скан журналов: проход по JSON Lines-журналу в 2 ГБ в поисках аномалий — DOM превратил бы весь файл в дерево (взрыв памяти), SAX же вызывает обратный вызов событие за событием, ноль промежуточных структур, память не зависит от размера файла. Сериализация ответа: сервер записывает результат запроса как JSON-ответ — данные уже лежат в бизнес-структурах, строить под сериализацию дерево значений расточительно; Writer пишет инкрементно: открыть объект, вывести пары ключ-значение, закрыть объект — генерация и вывод идут рядом.
Три маршрута — не эзотерика выбора одного из трёх, а ответы на три вопроса о форме данных: нужен произвольный доступ и повторное чтение → DOM; один проход сканирования → SAX; вывод по мере генерации → Writer. Сосуществование всех трёх в одной системе — норма: конфигурация через DOM, журналы через SAX, ответы через Writer, каждый на своём месте.
32.2概念
DOM: разбор, доступ, сериализация
Ценность DOM — «разобрали один раз, доступ произвольный»: дерево значений в памяти, переход куда угодно по путям. При отказе xrtJsonParse в слоте ошибки потока структурированная запись, а xrtJsonErrorLocation выносит строку и столбец — «строка 3, столбец 14, не хватает запятой» указывает прямо на символ. Сериализация чисел с плавающей точкой идёт кратчайшим круговым проходом главы 26 — прочтённое 0.1 записывается обратно как 0.1, круг без дрейфа.
SAX: поток событий
xrtJsonVisit(文本, 回调, 用户数据) (текст, обратный вызов, пользовательские данные) превращает разбор в последовательность событий: вход в объект/массив, имя члена, скалярное значение, конец контейнера. Обратный вызов получает структуру xjsonevent (HasName/Name/тип/значение) и возвращает false для прерывания. Ноль промежуточного дерева — его преимущество формы: память — это стек самого обратного вызова; в паре с потоковым декодированием главы 27 получается «разбирать по мере приёма». Дисциплины SAX: не делать тяжёлой работы в обратном вызове (он на пути разбора) и не хранить заимствованные указатели событий дольше времени жизни вызова (дисциплина сроков главы 30).
Writer: инкрементная сборка
xjsonwriter* W = xrtJsonWriterCreate(NULL); xrtJsonWriterObject(W); /* открыть объект */ xrtJsonWriterName(W, "name"); xrtJsonWriterString(W, ...); xrtJsonWriterEnd(W); /* закрыть объект */ str s = xrtJsonWriterFinish(W); /* проверить полноту пар и забрать результат */
Парность открытий и закрытий проверяет Finish — недостающий End вскрывается здесь, а не производит покалеченный JSON. Writer умеет выводить в память (Take владеющей строки) или писать прямо в sink (изоморфно системе sink-ов Logger-а главы 35) — второе позволяет сериализации напрямую соединяться с файлом или сетью.
Правила выбора маршрута
| Сценарий | Маршрут | Причина |
|---|---|---|
| Конфигурации/небольшие документы | DOM | произвольный доступ, кратчайший код |
| Однопроходный скан большого файла | SAX | память не зависит от размера файла |
| Вывод по мере генерации | Writer | без промежуточного дерева |
| Изменить и пересериализовать | DOM | правка на дереве, затем Stringify |
Границы ресурсов и строгость
Три шлюза для недоверенного JSON (из xrtresourcelimits главы 3): глубина вложенности, число записей, суммарные байты — злонамеренный документ отвергается на 128-м уровне, не проедая стек насквозь. Дубликаты ключей отвергаются («одноимённый ключ перезаписывает» в конфигурационном файле — авария, а не свойство); разбор чисел идёт по строгой семантике главы 26. Конфигурация разбора различается по сторонам границы доверия: локально порождённым конфигурациям достаточно границ по умолчанию, внешнему входу ужесточайте все три шлюза.
32.3示例
Полная программа: три маршрута одним заходом
Из образца репозитория examples/data/json/main.c — одни и те же данные читаются DOM-ом, сканируются SAX-ом, пишутся Writer-ом:
*/ #include <stdio.h> #include <xrt.h> */ static xjsonvisitaction printJsonEvent( const xjsonevent* pEvent, ptr pUserData ) { (void)pUserData; if ( pEvent->HasName ) { printf("member: %.*s\n", (int)pEvent->Name.Size, pEvent->Name.Data); } return XJSON_VISIT_NEXT; } int main(void) { xjsonreadconfig ReadConfig; xjsonwriteconfig WriteConfig; xjsonwriter* pWriter; xvalue* pRoot; xvalue* pName; xstrview Name; str sText; size_t iSize; /* ---- 1) DOM:解析 → 随机访问 → 美化序列化 ---- */ pRoot = xrtJsonParse(XRT_STR_LITERAL( "{\"name\":\"xrt\",\"features\":[\"json\",\"http\"]}" )); if ( pRoot == NULL ) { return 1; } /* ObjectGet 返回借用指针(树拥有它);GetString 取出视图。 */ pName = xrtValueObjectGet(pRoot, XRT_STR_LITERAL("name")); if ( !xrtValueGetString(pName, &Name) ) { xrtValueRelease(pRoot); return 2; } printf("name = %.*s\n", (int)Name.Size, Name.Data); /* Stringify 产物是拥有式字符串(xrtFree);之后树即可释放。 */ sText = xrtJsonStringify(pRoot, true, &iSize); xrtValueRelease(pRoot); if ( sText == NULL ) { return 3; } printf("%.*s\n", (int)iSize, sText); xrtFree(sText); /* ---- 2) SAX:同一份输入按事件流走一遍,不建树 ---- */ xrtJsonReadConfigInit(&ReadConfig); if ( xrtJsonVisit( XRT_STR_LITERAL("{\"code\":200,\"ok\":true}"), &ReadConfig, printJsonEvent, NULL ) != XJSON_VISIT_DONE ) { return 4; } /* ---- 3) Writer:增量构建 {"code":200,"message":"OK"} ---- */ xrtJsonWriteConfigInit(&WriteConfig); pWriter = xrtJsonWriterCreate(&WriteConfig); if ( (pWriter == NULL) || !xrtJsonWriterObject(pWriter) || /* 开对象 */ !xrtJsonWriterName(pWriter, XRT_STR_LITERAL("code")) || !xrtJsonWriterInt(pWriter, 200) || !xrtJsonWriterName(pWriter, XRT_STR_LITERAL("message")) || !xrtJsonWriterString(pWriter, XRT_STR_LITERAL("OK")) || !xrtJsonWriterEnd(pWriter) || /* 闭对象 */ !xrtJsonWriterFinish(pWriter) /* 完整性校验 */ ) { xrtJsonWriterFree(pWriter); return 5; } /* Take 移交文本(xrtFree 释放);Take 后 writer 仍需 Free。 */ sText = xrtJsonWriterTake(pWriter, &iSize); xrtJsonWriterFree(pWriter); if ( sText == NULL ) { return 6; } printf("%.*s\n", (int)iSize, sText); xrtFree(sText); return 0; }
Что только что произошло. (1) Сегмент DOM: Parse даёт корневое значение, ObjectGet("name") берёт члена по имени (заимствование, не передача владения), GetString вычитывает представление и печатает — три шага, ежедневная форма чтения конфигурации. (2) Сегмент сериализации: Stringify(值, true) (значение, true) выводит красиво — формат с отступами для человека; межмашинный обмен — false (компактно), объём меньше. (3) Сегмент SAX: Visit вызывает обратный вызов событие за событием, выводится member: code/ok — каждое имя члена в потоке событий прошло через руки обратного вызова, промежуточного дерева нет от начала до конца. (4) Сегмент Writer — во второй половине файла: инкрементная сборка с проверкой Finish; последняя строка {"code":200,"message":"OK"} — продукт Writer-а; читая код, отметьте, что каждый Object/Name/Int/String сцеплен с End. (5) Пятая строка обзорного образца проверяет позицию ошибки: подайте вход без запятой — error-location line=1 укажет строку; ещё одно исполнение принципа «ошибка несёт свою позицию» из главы 4.
Полная программа: обход всех интерфейсов (файлы и потоки)
Из examples/data/json_tour/main.c — файловый разбор, файловая запись, потоковая запись и запись в sink:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> #define SV(x) XRT_STR_LITERAL(x) /* 收集回调:拼接全部分片。 */ typedef struct examplesink { char Buffer[128]; size_t Size; } examplesink; static bool exampleCollect(xbytesview Data, ptr pUserData) { examplesink* pSink = (examplesink*)pUserData; if ( (pSink->Size + Data.Size) < sizeof(pSink->Buffer) ) { memcpy(pSink->Buffer + pSink->Size, Data.Data, Data.Size); } pSink->Size = pSink->Size + Data.Size; return true; } int main(void) { static const char sFile[] = "xrt-json-tour.json"; static const char sText[] = "{\"a\":1,\"b\":[true,null]}"; xjsonreadconfig ReadConfig; xjsonwriteconfig WriteConfig; xvalue* pDom = NULL; xvalue* pFileDom = NULL; xvalue* pReadDom = NULL; xvalue* pInner = NULL; xjsonwriter* pWriter = NULL; xerror* pError = NULL; xjsonlocation Location; examplesink Sink; size_t iWritten = 0; FILE* pOut; int iResult = 1; /* ---- Read(配置形态)+ Valid 正反 ---- */ xrtJsonReadConfigInit(&ReadConfig); pDom = xrtJsonRead(SV(sText), &ReadConfig); if ( (pDom == NULL) || !xrtJsonValid(SV(sText)) || xrtJsonValid(SV("{\"a\":}")) || !xrtJsonValid(SV("[1,2,3]")) ) { goto Cleanup; } /* ParseFile / ReadFile:先落盘再双形态读回。 */ pOut = fopen(sFile, "wb"); if ( (pOut == NULL) || (fwrite(sText, 1u, sizeof(sText) - 1u, pOut) != sizeof(sText) - 1u) ) { goto Cleanup; } fclose(pOut); pOut = NULL; pFileDom = xrtJsonParseFile(sFile); pReadDom = xrtJsonReadFile(sFile, &ReadConfig); if ( (pFileDom == NULL) || (pReadDom == NULL) ) { goto Cleanup; } printf("json: read+valid ok file-parse/read ok\n"); /* ---- WriteFile / StringifyFile ---- */ xrtJsonWriteConfigInit(&WriteConfig); if ( !xrtJsonWriteFile(sFile, pDom, &WriteConfig) || ((pInner = xrtJsonParseFile(sFile)) == NULL) ) { goto Cleanup; } xrtValueRelease(pInner); pInner = NULL; if ( !xrtJsonStringifyFile(sFile, pDom, true) ) { goto Cleanup; } { /* pretty 输出多行:读回验证仍是合法 JSON。 */ pInner = xrtJsonParseFile(sFile); if ( pInner == NULL ) { goto Cleanup; } xrtValueRelease(pInner); pInner = NULL; } printf("json: write-file/stringify-file(pretty) ok\n"); /* ---- Write 流式 + QuoteWrite ---- */ Sink.Size = 0; if ( !xrtJsonWrite(pDom, &WriteConfig, exampleCollect, &Sink) || (Sink.Size != sizeof(sText) - 1u) || (memcmp(Sink.Buffer, sText, sizeof(sText) - 1u) != 0) ) { goto Cleanup; } printf("json: stream write=%zu", Sink.Size); Sink.Size = 0; if ( !xrtJsonQuoteWrite(SV("a\"b\\c"), 0u, exampleCollect, &Sink, &iWritten) || (Sink.Size != 9u) || /* 带引号转义 "a\"b\\c" 共 9 字节 */ (memcmp(Sink.Buffer, "\"a\\\"b\\\\c\"", 9u) != 0) ) { goto Cleanup; } printf(" quote=%zu\n", Sink.Size); /* ---- Sink 写入器:数组五元素 ---- */ Sink.Size = 0; pWriter = xrtJsonWriterCreateSink(&WriteConfig, exampleCollect, &Sink); if ( (pWriter == NULL) || !xrtJsonWriterArray(pWriter) || !xrtJsonWriterUInt(pWriter, 1u) || !xrtJsonWriterBool(pWriter, true) || !xrtJsonWriterNull(pWriter) || !xrtJsonWriterFloat(pWriter, 2.5) ) { goto Cleanup; } /* WriterValue:嵌套一个字符串 DOM。 */ pInner = xrtValueString(SV("v")); if ( (pInner == NULL) || !xrtJsonWriterValue(pWriter, pInner) || !xrtJsonWriterEnd(pWriter) || !xrtJsonWriterFinish(pWriter) ) { goto Cleanup; } if ( (Sink.Size != 21u) || (memcmp(Sink.Buffer, "[1,true,null,2.5,\"v\"]", 21u) != 0) ) { goto Cleanup; } printf("json: sink-writer [%.*s] ok\n", (int)(Sink.Size < 30u ? Sink.Size : 30u), Sink.Buffer); /* ---- ErrorLocation:截断文本的行列定位 ---- */ { xvalue* pBad = xrtJsonRead(SV("{\"a\":"), &ReadConfig); if ( (pBad != NULL) || ((pError = xrtTakeError()) == NULL) || !xrtJsonErrorLocation(pError, &Location) || (Location.Line != 1u) ) { goto Cleanup; } xrtErrorFree(pError); pError = NULL; } printf("json: error-location line=%zu ok\n", Location.Line); iResult = 0; Cleanup: xrtErrorFree(pError); xrtJsonWriterFree(pWriter); xrtValueRelease(pInner); xrtValueRelease(pReadDom); xrtValueRelease(pFileDom); xrtValueRelease(pDom); if ( pOut != NULL ) { fclose(pOut); } (void)remove(sFile); return iResult; }
Что только что произошло. (1) Файловые маршруты: Parse и Stringify работают с файлами напрямую — файловая форма загрузки и экспорта конфигурации. (2) Потоковая запись: Writer пишет элемент за элементом, счёт 23 элемента и 9 кавычек — эмпирическое доказательство «вывода по мере генерации». (3) Запись в sink: выход Writer-а подключается прямо к цели (буфер/файл) — сериализация в композиции с вводом-выводом, без промежуточной строки. Четыре позы покрывают все точки встречи JSON с файловой системой (глава 45).
32.4契约
- Три маршрута: DOM — произвольный доступ / SAX — один проход без дерева / Writer — инкрементный вывод; выбор по форме данных, не по вкусу.
- Строгость: дубликаты ключей отвергаются; числа по строгой семантике главы 26; ошибки со строкой и столбцом (
xrtJsonErrorLocation). - Границы ресурсов: недоверенный вход — обязательные три шлюза (глубина/записи/байты); линия обороны на входе разбора, не в бизнес-слое.
- Точность кругового прохода: сериализация плавающих — кратчайший круговой проход; контур чтения-записи без дрейфа.
- Владение: корневое значение Parse освобождается целиком через Release; ObjectGet заимствует; продукт Stringify —
xrtFree. - Парность Writer-а: открытия и закрытия проверяются Finish; вывод забираем (память) или подключаем напрямую к sink.
От примеров к инженерии: три хозяина JSON
Слой конфигурации: загрузка на старте → дерево DOM → система слияния главы 30 (умолчания + пользовательские переопределения) → только чтение на всём пути. Три дисциплины конфигурации: шлюзы ужесточаются по бизнесу (в конфигурации не бывает вложенности в десять тысяч уровней), отсутствующее поле и несовпадение типа — оба докладываются (точное чтение главы 30), при горячей перезагрузке старое дерево Release-ится, новое занимает место (счёт ссылок гарантирует идущим запросам спокойно дочитать).Слой протокола: разбор JSON-тела каждого запроса — SAX или DOM по объёму, границы ресурсов обязательны (внешний вход), позиция ошибки пишется в ответ 400, помогая клиенту самопровериться.Слой обмена данными: передача между модулями и через процессы — дерево значений напрямую (балансировка счёта ссылок, см. трёх хозяев главы 30) либо сериализация и путь через канал (кратчайший круговой проход гарантирует точность). Все три слоя на одном комплекте строгости; различаются лишь ширина шлюзов и адресат отчётов об ошибках.
Контраст: форма кода трёх маршрутов
Одна потребность — «взять поле имени пользователя» — в форме трёх маршрутов стоит прочитать бок о бок. DOM: Get(Get(Root,"user"),"name") — два прыжка, кратчайший доступ, повторяемый. SAX: в обратном вызове держится машина состояний «сейчас внутри объекта user», при событии name — запись; код длиннее, зато память константна. Writer: обратная задача (порождать, а не читать), Name+String за два шага — здесь нет «взять», есть только «дать». Прочитав три фрагмента рядом, вы получаете интуицию выбора маршрута: паттерн доступа диктует форму кода; неуклюжая форма обычно означает неверно выбранный маршрут.
Типичные ошибки
Симптом: при обработке загруженного пользователем JSON — переполнение стека или взлёт памяти; текст с десятитысячу уровней вложенности проедает стек рекурсивного парсера насквозь.
Причина: границы по умолчанию мягкие (рассчитаны на доверенные конфигурации); внешнему входу нужны ужесточённые три шлюза — линия обороны на входе разбора.
xvalue* pRoot = xrtJsonParse(Text, NULL); /* границы по умолчанию - десятитысячу уровней сразу рвёт стек */
xrtresourcelimits Limits; xrtResourceLimitsInit(&Limits); Limits.iMaxDepth = 64; /* бизнесу нужно максимум 3 уровня; 64 - уже десятикратный запас */ xvalue* pRoot = xrtJsonParse(Text, &Limits); if ( pRoot == NULL ) { size_t iLine, iColumn; xrtJsonErrorLocation(xrtGetError(), &iLine, &iColumn); Reject(iLine, iColumn); /* превышение - отказ; и ошибка с позицией */ }
Симптом: указатели строк, сохранённые во время скана, используются после — читаются висячие или переиспользованные данные; та же семья, что ловушка 1 главы 18.
Причина: строки событий — заимствованные представления, срок действия кончается вместе с вызовом; парсер переиспользует свои буферы.
static bool onEvent(const xjsonevent* pEvent, ptr pData) { if ( pEvent->Type == XJSON_EVENT_STRING ) { gSaved = pEvent->String; /* заимствованное представление в глобальную переменную - висячее к следующему вызову */ } return true; }
static bool onEvent(const xjsonevent* pEvent, ptr pData) { if ( pEvent->Type == XJSON_EVENT_STRING ) { Process(pEvent->String); /* израсходовать в пределах жизни вызова */ /* нужно сохранить: сперва Dup-ните копию */ } return true; }
Практические задания
Прочтите { "host": "0.0.0.0", "port": 8080, "debug": true } маршрутом DOM: возьмите три поля по имени и напечатайте (порт и выключатель — точным чтением главы 30).
Просканируйте маршрут SAX файл .jsonl (по одному объекту JSON на строку): разбор построчно, подсчёт в обратном вызове событий каждого типа, поиск номеров строк с "level":"error". Подсказка: разделение на строки — это просто «один вызов Visit на строку»; сочетайте с построчной итерацией главы 25.
Реализуйте write_page(Writer, 页号, 数据数组) (Writer, номер страницы, массив данных): Writer инкрементно пишет объект постраничного ответа (метаданные + массив данных), элементы данных берутся из бизнес-структур (без сборки дерева значений); вывод подключён одновременно к памяти и к файловому sink. Критерии приёмки: произведённый JSON проходит проверку Parse (круг легален); генерация миллиона элементов пикует на памяти, не зависящей от числа элементов (доказательство потоковости); файловый и меморийный продукты совпадают байт в байт.
Итоги
| Тема | Кратко |
|---|---|
| Три маршрута | DOM (Parse/Get/Stringify) / SAX (поток событий Visit) / Writer (инкрементная сборка) |
| Правила выбора | произвольный доступ → DOM / однопроходный скан → SAX / вывод по мере генерации → Writer |
| Строгость | дубликаты ключей отвергаются; ошибка со строкой и столбцом (xrtJsonErrorLocation) |
| Три шлюза | глубина/записи/байты — обязательны для внешнего входа; превышение — отказ целиком с отчётом позиции |
| Плавающие | сериализация кратчайшим круговым проходом — контур чтения-записи без дрейфа |
| Writer | парность открытий/закрытий проверяет Finish; Take забирает или подключение прямо к sink |
| Владение | Release корневого значения освобождает всё дерево; доступ к членам — заимствование; при горячей перезагрузке старое дерево безопасно уходит |
| Три хозяина | слой конфигурации (система слияния) / слой протокола (три шлюза + эхо ошибок) / слой обмена (дерево или сериализация) |
| Критерий формы | паттерн доступа диктует форму кода — неуклюжая форма обычно означает неверный маршрут |
📖 Справочники API в этой главе:json (46 записей)value (159 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки