Ориентация главы
Глава 141 зафиксировала шесть проектных решений (модель комнаты / линейный протокол / упорядочивание / машина состояний / хранение / деградация); эта глава — глава реализации chatd — та же методология, что у главы 140 (реализация configd): решения приземляются построчно, приёмка сверяется построчно, пересмотры оставляют след. Рельеф реализации: слой сущностей (три структуры — пользователь/комната/кольцо сообщений — и их контейнеры; решение 1); слой протокола (приём/диспетчеризация/отправка кадров строка+JSON; решение 2); слой сессий (машина состояний рукопожатие→вход→речь→выход; решение 4); упорядочивание и досылка (взятие номера/широковещание/курсор/закольцовка; решения 3+5); уборка и деградация (гарантии конечных состояний/выселение/отказ; решения 4+6). Венец главы — нагрузочный аудит: машинная форма критерия 8, доказательство согласованности номеров по всей комнате на 100 виртуальных пользователях × 10 комнатах — семантика «номера без дыр» проектной главы, проверяемая покадрово под конкурентностью.
142.1引入
Реализация начинается с словаря кадров (не с сокетов) — словарь типов из упражнения 4 главы 141 есть протокольный авторитет: семь восходящих типов (рукопожатие/join/leave/say/dm/who/quit) и девять нисходящих (ok/err/broadcast/dm/history_tail/too_far/you_left/member_list/unknown). Сначала словарь, потом диспетчер — протокол консультируем с первого дня: каждая ветвь on_frame соответствует строке словаря, ревью читает их бок о бок. Скелет сущностей раньше сети: структуры пользователя/комнаты и операции контейнеров (вступление/удаление/поиск) — чистая логика данных, юнит-тестируемая без единого сокета (дивиденд слоистого тестирования главы 133); сетевой слой (Engine/Listener/Stream) подключается последним — к тому моменту бизнес-логика уже под защитой тестов. Этот порядок совпадает с «снизу вверх» configd — сначала чистые слои данных — общий ритм обеих глав реализации.
142.2概念
Карта модулей реализации
Слоистая карта chatd (изоморфна карте границ главы 140):
Пятьсот строк реализуют чат-сервис с гарантиями упорядочивания и протоколом досылки — взаимная сверка с configd (280 строк) и webserv (меньше 400): объём кода XRT-проекта пропорционален бизнес-сложности (сущности + протокол + упорядочивание chatd — три настоящих бизнес-блока) — системная сложность сети и конкурентности целиком впитана библиотечными слоями. Эта цифра — последняя группа доказательств «собирают, а не пишут» главы 145.
Слой сущностей: три структуры и контейнеры
struct chuser { char Name[32]; /* идентичность (независима от соединения — решение 1) */ xnetstream* Stream; /* NULL = оффлайн */ uint64 Cursor; /* максимальный увиденный номер */ struct chroom* Room; /* текущая комната (NULL = холл) */ }; struct chroom { char Name[32]; uint64 NextSeq; /* счётчик упорядочивания (решение 3) */ struct chmsg Ring[1024]; /* кольцо сообщений (решение 5) */ size_t RingHead; /* позиция старейшего */ void* Members; /* таблица членов (map главы 18) */ }; struct chmsg { uint64 Seq; char From[32]; char Text[256]; };
Выбор контейнеров: таблица членов — 成员表 map (поиск по имени — горячий путь who/join, глава 18); таблица комнат — один глобальный map (того же семейства). Реализация кольца: массив + указатель головы (Ring[RingHead] — старейший) — при перезаписи голова сдвигается вперёд; досылка проигрывает по порядку от курсора до хвоста кольца. Эти три структуры — всё состояние chatd — никаких скрытых копий (SetData соединения вешает указатель пользователя — контекстный механизм главы 67).
Слой протокола: сторона генерации кадров
Кроме приёма кадров есть и отправка (девять нисходящих типов) — форма генерации: JSON Writer (глава 32) производит по строке на кадр. Функции-шаблоны кадров (по маленькой функции на тип — broadcast/dm/err/ok делят скелет): заполнить op, заполнить поля нагрузки, (семейство broadcast) заполнить seq, Writer Finish даёт законный JSON — атомарность гарантирует Writer (полукадр за дверь не выходит). Путь отправки: текст кадра → xrtNetStreamSend (строчная буферизация + \n) — симметрично линейному разграничению принимающей стороны (оба конца одного протокола пользуются одной парой примитивов). Единый формат кадра ошибки: {"op":"err","code":N,"why":"..."} — code есть перечисление (BADJSON/NOOP/NICK_TAKEN/ROOM_FULL/NOT_IN_ROOM/...), а why — человеку — машина судит по code, человек читает why (модель ошибок главы 4 в издании кадра).
Слой протокола: приём и диспетчеризация кадров
Движок приёма = линейный разметчик (глава 72) + разбор JSON (глава 32):
/* событие читаемости соединения: строка пришла -> JSON -> диспетчеризация по op */ static void chat_on_data(xnetstream* Stream, ptr Ctx) { chuser* U = ctx_user(Ctx); xlineview Line; while ( xrtLineReaderNext(U->Lines, &Line) == XLINE_NEXT_LINE ) { xvalue* Msg = NULL; xvalue* Op = NULL; xstrview Op; if ( (Msg = chat_parse_json(Line)) == NULL ) { chat_send_err(U, CH_E_BADJSON); continue; /* испорченный кадр: ошибка в ответ, соединение живёт */ } Op = xrtValueObjectGet(Msg, XRT_STR_LITERAL("op")); if ( (Op == NULL) || !xrtValueGetString(Op, &Op) ) { chat_send_err(U, CH_E_NOOP); } else if ( xrtStrEqual(Op, XRT_STR_LITERAL("hello")) ) { chat_do_hello(U, Msg); } else if ( xrtStrEqual(Op, XRT_STR_LITERAL("join")) ) { chat_do_join(U, Msg); } /* ... say/dm/who/quit изоморфны ... */ xrtValueRelease(Msg); } }
Граница снисходительности и строгости (протокольное решение главы 141): линейный слой снисходителен (плохая строка не влияет на соединение), слой JSON полуснисходителен (плохой JSON — ошибка, но не разрыв), бизнес-слой строг (конфликт ников/полная комната — отказ; но протокольным кадром, не разрывом соединения). Форма диспетчеризации изоморфна on_message pushd — два проекта делят один паттерн на слое протокола (выучил раз — применяешь дважды; сложный процент последовательности проектов).
Упорядочивание и широковещание: номер-доставка-курсор
static void chat_do_say(chuser* U, xvalue* Msg) { chroom* R = U->Room; chmsg* M; if ( R == NULL ) { chat_send_err(U, CH_E_NOT_IN_ROOM); return; } /* взятие номера: одноточечное упорядочивание внутри блокировки (решение 3) */ lock(R); M = &R->Ring[R->RingTail]; M->Seq = ++R->NextSeq; copy_from(M, U->Name, Msg); R->RingTail = (R->RingTail + 1) % 1024; unlock(R); /* доставка: широковещание с номером + продвижение курсора говорящего */ chat_broadcast(R, M); U->Cursor = M->Seq; }
Гарантия порядка широковещания: кадры несут Seq — курсоры членов продвигаются при получении; путь досылки (переподключение): hello несёт cursor → внутри комнаты повтор от cursor+1 до хвоста кольца → после полной досылки ответ ok; курсор старше головы кольца → too_far (клиент обновляется целиком). Непрерывность номеров при закольцовке: перезапись старейшего кадра никогда не останавливает монотонность NextSeq — окно досылки = ёмкость кольца (разрывы в пределах 1024 кадров восстанавливаются — проектное предположение ёмкости исполнено).
Три детали переходов машины состояний
Три перехода, которые легче всего смазать при реализации: неудача рукопожатия → закрытие (после кадра отказа активно Close — не ждать, пока клиент сам оборвёт: явная позиция сервера против протокольных ошибок; слив очереди отправки перед закрытием гарантирует семантика Stream — глава 67); выход и повторный вход в ту же комнату (сущность пользователя переиспользуется — курсор продолжается (решение 1 исполнено снова: выход не чистит курсор — повторный вход продолжает с хвоста истории)); кадр quit и обрыв TCP обрабатываются эквивалентно (graceful-выход и аварийный разрыв идут одним путём очистки — разница лишь в том, отправлять ли you_left — один путь очистки, два входа — стандартный приём против расщепления состояния).
Машина состояний сессии и конечные состояния
Рукопожатие (ник корректен и уникален → регистрация пользователя; конфликт → err + закрытие): чувствительность порядка join/leave (реализация ловушки 3 главы 141): сперва удалить участника → отправить самому you_left (последний кадр) → широковещание комнате member_left. Разрыв соединения (событие Close уровня TCP): пользователь входит в состояние оффлайна (Stream=NULL, курсор сохранён) — не уничтожение (ресурс досылки при переподключении); настоящая очистка только по тайм-ауту оффлайна (настраиваемому). Серверное выселение (медленный участник — решение 6): отправка держит AGAIN дольше порога → кадр выселения → Close — семантика «номера без дыр» запрещает ступень «пропуск» (ключевое отличие от pushd — реализационная грань таблицы разделения главы 141).
Детали исполнения нагрузочного теста
Конкретная форма прогона 100 двойников × 10 комнат на стороне реализации: двойники водятся в процессе (исполнитель главы 59 со 100 задачами — каждый двойник цикл машины состояний: случайные действия (речь 70% / слушание 20% / dm 10%) + межкадровые интервалы (распределение Пуассона, имитирующее человеческий ритм — случайность главы 11) + аудитор (проверка Seq==Cursor+1 при приёме)); сервер в том же процессе или subprocess (локальная петля — настоящая сеть, но управляемая); инжектор (назначенные двойники рвут связь/замедляются на назначенном номере кадра — внедрение отказов, невидимое серверу); вердикт (через 10 минут: все аудиты без нарушений + стыковка досылки корректна + счёт выселений совпадает с инъекциями + stats без утечек). Аргумент достоверности этой топологии: различие петли TCP и настоящей сети (задержки/потери) не влияет на проверку корректности (аудит номеров — логическое свойство, не временное) — выводы о производительности требуют настоящей сети (это послеразвертачное дело — сравнение базовых линий главы 136). Корректностный стресс в CI, производительностный стресс в среде — два класса измерений расходятся по домам.
Ретроспектива: завершённое состояние главы реализации
Реализация chatd завершена — учебный замысел последовательности проектов закрывается здесь (webserv уже сделал финал всей книги в главе 144 — эта глава восполняет недостающее звено последовательности): лестница ограничений шести проектов представлена целиком — масштаб данных (logstat) → жизненный цикл (configd) → идентичность и упорядочивание соединений (chatd) → удалённая неопределённость (xdl) → веер и конечные состояния (pushd) → полнофункциональный синтез (webserv). Уникальный вклад chatd — «упорядочивание»: данные остальных проектов либо неупорядочены (инструменты), либо однописательные (конфигурация), либо теряемы (пуш) — только чат делает согласованный по всей комнате порядок многопартийной конкурентной записи ядром корректности — это ближайший к «базе данных» проект по сложности (одноточечный упорядочиватель = миниатюрный WAL, кольцо = циклический журнал, досылка = воспроизведение журнала). Двадцать инженерных аксиом проверены в chatd: все применимы, две (сверка конечных состояний / веер проектируется заранее) снова в главных ролях — устойчивость списка аксиом установлена шестым повтором.
Нагрузочный аудит: машинная форма критерия 8
Позиция аудитора: каждый виртуальный клиент (дизайн двойников главы 141) несёт собственную проверку при приёме — Seq == Cursor+1 (строгое поэтапное возрастание) — нарушение докладывается (какая комната, где раскрылась дыра). Топология нагрузки: 100 двойников на 10 комнатах, случайная речь 10 минут, 10 точек разрыва, 5 медленных клиентов. Вывод аудита: цепочки номеров всех комнат полны (без дыр), курсоры состыкованы после досылки, после выселения медленных остальные комнаты не задеты. Этот аудит — предельная форма семантики «номера без дыр» проектной главы — не анализ, а перечислительная проверка в рантайме (каждый кадр проверен) — тестовая философия главы 133, применённая конкурентно.
Сверка с проектной главой
Восемь критериев приёмки построчно: (1) шесть UC, прямые и обратные (словарь кадров + случаи слоя диспетчеризации всё зелёное); (2) упорядочивание согласовано (нагрузочный аудит — монотонность по всей комнате); (3) досылка корректна (тесты с внедрением разрывов: после переподключения курсор стыкуется без повторов и потерь); (4) конечные состояния чисты (тесты призрачных кадров после выхода/разрыва — ноль призраков); (5) медленные участники изолированы (внедрение медленного: номера остальных не задеты); (6) отказ полной комнаты/конфликта ника (тесты ёмкости: кадры отказа с причинами); (7) OOM без остатка (по точкам: любая неудача не меняет состояния пользователя/комнаты); (8) стресс стабилен (аудит 100×10 минут всё зелёный). Все восемь пройдены — chatd готов к поставке.
142.3示例
Первая полная программа: атом движка приёма кадров
Программа ниже из examples/io/line — учебный атом движка приёма кадров chatd (слой линейного разграничения):
*/ #include <stdio.h> #include <xrt.h> int main(void) { /* 混合行尾 + 末行无换行的日志样本。 */ static const char sLog[] = "INFO server started\r\n" "WARN queue is busy\n" "ERROR request failed"; xreader* pReader; xlinereader* pLines; xlineview Line; /* 借用:到下一次 Next 前有效 */ xlinenext Next; size_t iCount = 0u; int iResult = 1; */ pReader = xrtReaderFromMemory(XRT_BYTES_LITERAL(sLog)); pLines = xrtLineReaderTake(&pReader, 1024u); if ( pLines == NULL ) { xrtReaderDestroy(pReader); return 1; } /* 逐行消费:每行是借用视图,处理完再取下一行。 */ while ( (Next = xrtLineReaderNext(pLines, &Line)) == XLINE_NEXT_LINE ) { printf("%zu: %.*s\n", ++iCount, (int)Line.Text.Size, Line.Text.Data); } if ( Next == XLINE_NEXT_END ) { iResult = 0; } /* 接管模式:一个 Destroy 连底层 Reader 一起关闭。 */ if ( !xrtLineReaderDestroy(pLines) ) { iResult = 1; } return iResult; }
Что только что произошло. (1) Два шага ReaderFromMemory→LineReaderTake и три состояния Next — тело chat_on_data chatd (Reader меняется на приёмный буфер соединения — событие данных Stream главы 67 есть сетевая форма Reader). (2) Заимствование представлений (каждая строка используется по прибытии) — освобождение после диспетчеризации, без кеширования — дисциплина «кадры не ночуют» главы 141. (3) Этот атом повторяется в трёх проектах (137/139/141) — переиспользование фундаментальных атомов — прямой урожай слоистого дизайна.
Вторая полная программа: генерация кадров широковещания
Вторая программа из examples/logging/file_json — образец для генерации JSON-кадров (один JSON-объект на строку как выходная форма):
*/ #include <xrt.h> #include <stdio.h> int main(void) { xlogfileoptions Options; xlogger* pLogger; xlogfield Field; if ( !xrtLogFileOptionsInit(&Options, "example_logger_json.log") ) { return 1; } Options.MaxBytes = 1024u * 1024u; /* 1MB 轮转 */ Options.BackupCount = 3u; pLogger = xrtLogCreate(XRT_STR_LITERAL("example"), XLOG_INFO); Field = xrtLogFieldUInt(XRT_STR_LITERAL("request_id"), 42u); if ( (pLogger == NULL) || !xrtLogAddJsonFile(pLogger, &Options, NULL) || /* 一行挂载 */ (xrtLogFields( pLogger, XLOG_INFO, XRT_STR_LITERAL("JSON file sink ready"), &Field, 1u ) != XLOG_RESULT_WRITTEN) ) { xrtLogFree(pLogger); return 2; } xrtLogFree(pLogger); printf("wrote example_logger_json.log\n"); return 0; }
Что только что произошло. (1) Форма выхода JSON Lines (один полный объект на строку) изоморфна нисходящим кадрам chatd — broadcast/dm/err, по JSON-строке на кадр. (2) Механизм fields журналирования (структурные поля при записи) соответствует полям нагрузки чат-кадров — одна потребность сериализации в разных системах (журнал — кадры для машин, чат — кадры для клиентов). (3) Генерация кадров chatd идёт напрямую через Writer главы 32 ({"op":"broadcast","seq":N,...}) — этот образец её журнальный родственник.
Протокол пересмотра относительно проектной главы
Один пересмотр и два уточнения проекта главы 141 в ходе реализации (исполнение пункта «решения можно пересматривать»): пересмотр — политика блокировки досылки меняется с «держать блокировку комнаты во время досылки» на «снимок внутри блокировки, повтор снаружи» (проект не уточнял охват блокировки — реализация обнаружила, что повтор под блокировкой блокирует взятие номеров всей комнатой — паттерн снимка получает оба); уточнение одно — порог медленного участника уточняется с «продолжительно AGAIN» до «N подряд доставок AGAIN плюс тайм-аут T» (настраиваемая квантификация — продукт калибровки нагрузкой); уточнение два — тайм-аут оффлайна уточняется с «настраиваемого» до умолчания 300 секунд (продуктовое предположение окна переподключения — разумное умолчание по обычаю IM). Ни одно из трёх не трогает ядерных решений (модель комнаты/упорядочивание/номера без дыр) — периферийные параметры проекта калибруются реальностью при реализации — в этом ценность «решения и параметры разделены»: ядро стабильно, периферия регулируема.
Разбор реализации: исходник протокола досылки
Досылка (стыковка курсора переподключения) — тончайший путь chatd — разбор ключевого сегмента:
/* рукопожатие переподключения: с cursor -> снимок и повтор -> возврат к прямому эфиру */ static void chat_do_resume(chuser* U, chroom* R, uint64 Cursor) { chmsg Pending[1024]; size_t n = 0; if ( R == NULL ) { chat_send_err(U, CH_E_NOT_IN_ROOM); return; } lock(R); if ( (R->NextSeq - Cursor) > 1024u ) { unlock(R); chat_send(U, "{"op":"too_far"}"); return; /* старше головы кольца: полное обновление */ } /* снимок внутри блокировки: кадры Cursor+1..NextSeq копируются наружу */ { uint64 s; for ( s = Cursor + 1u; s <= R->NextSeq; s++ ) { Pending[n++] = ring_at(R, s); } } unlock(R); /* снимок готов — блокировка снята */ /* повтор вне блокировки: медленный клиент не блокирует комнату */ { size_t i; for ( i = 0; i < n; i++ ) { chat_send_frame(U, &Pending[i]); } } U->Cursor = R->NextSeq; /* стыковка курсора */ chat_send_ok(U); }
Пункты разбора. (1) Переполненческая безопасность (NextSeq - Cursor) > 1024: NextSeq только растёт, Cursor ≤ NextSeq выполняется всегда (курсор указывает лишь на виденные кадры) — разность неотрицательна. (2) Массив снимка лежит за пределом стека (1024 × 300 байт ≈ 300 КБ — выделять в куче или повторять партиями) — учебная форма набрасывает только логику; настоящая реализация повторяет партиями (по 64 кадра, оберегая стек/пики памяти). (3) Продвижение курсора — после повтора — ещё один разрыв посреди досылки в следующий раз начинает её заново (идемпотентность — досылка без побочных эффектов). (4) ring_at(R, s) берёт кадр по номеру (случайный доступ кольца — циклическая индексная арифметика).
Список тестовых активов
Четырёхквадрантные тесты chatd (определение поставки, изоморфное главе 139): позитив (test_chat.c — по позитивному случаю на каждый из шести UC + по одному на каждый из шестнадцати типов словаря кадров); гонки (test_chat_threads.c — аудит порядка конкурентной речи (многопоточно 1000 кадров, затем цепочки номеров по всей комнате монотонны) + чередование досылки (речь продолжается во время разрыва/переподключения) + проверки призрачных кадров (ноль кадров после выхода)); негатив (конфликт ников/полная комната/плохой JSON/неизвестный op/недостающие поля — каждый возвращает err, соединение живёт); OOM (test_chat_oom.c — операции сущностей по точкам: при неудаче состояние комнаты/пользователя не меняется). Библиотека двойников (test_chat_client.c — драйвер виртуальных пользователей + встроенный аудитор номеров) делит с испытуемым код протокола — без второй реализации. Всё висит на поле tests манифеста — build.py --suite chatd по обеим колеям.
142.4契约
- Три структуры сущностей: пользователь (идентичность + обнуляемое соединение + курсор) / комната (упорядочиватель + кольцо + таблица членов) / сообщение (номер + отправитель + текст) — всё состояние, без копий.
- Контейнеры: таблица членов и таблица комнат map-ы (поиск по имени) — глава 18.
- Движок приёма: линейное разграничение (трёхстатный цикл) + JSON (ObjectGet берёт op) — граница снисходительности по главе 140.
- Форма диспетчеризации: строковая маршрутизация по op — изоморфна pushd (переиспользование паттерна).
- Взятие номера:
++NextSeqвнутри блокировки, одноточечное упорядочивание; кадры несут Seq; курсоры участников продвигаются при получении. - Досылка:
helloсcursor→ снимок внутри блокировки, повтор снаружи; старше головы кольца → too_far, обновление целиком. - Семантика кольца: 1024 кадра на комнату; перезаписывает старейшее, но номера остаются монотонными; окно досылки = ёмкость кольца (разрывы за окном — too_far).
- Порядок join/leave: сперва удаление → you_left самому → широковещание комнате — ноль призрачных кадров.
- Семантика разрыва: вход в оффлайн-состояние (курсор сохранён); очистка только по тайм-ауту — идентичность живёт дольше соединения.
- Медленные участники: выселение (без ступени пропуска) — семантика «номера без дыр» запрещает пропуск.
Форма поставки и эксплуатация
Поставка chatd (сервисная форма, общая с pushd/webserv): отдельный процесс (ссылка на статическую библиотеку или один заголовок — адрес прослушивания/ёмкость комнат/длина кольца/порог медленности/тайм-аут оффлайна читаются из окружения — пять параметров закрывают всю настраиваемую грань); эксплуатационный интерфейс (управляющие TCP-команды или локальные сигналы: список комнат/число членов/уровни номеров/число соединений — минимальная форма эксплуатационной точки); наблюдаемость (скорость номеров по комнатам/счёт соединений/счёт выселений периодической печатью — JSON Lines в тот же поток журнала); семантика перезапуска (сервис в памяти — перезапуск теряет комнаты (учебная граница); клиенты переподключаются + полное обновление — бесклиентные-по-состоянию клиенты есть источник устойчивости к перезапуску). Против эксплуатационной грани pushd: изоморфно (счётчики + списки + выселения) — эксплуатационная грань сервисных проектов сходится в один паттерн — урок главы 144 исполнен снова.
Продуктовый компромисс ёмкости кольца
Продуктовая семантика длины кольца 1024 заслуживает последнего взгляда: она решает окно досылки (разрыв свыше 1024 кадров — too_far) — активная комната (100 msg/s) имеет окно всего 10 секунд — слабосетевые пользователи постоянно обновляются целиком. Пространство подстройки: длина кольца ↑ (память ↑: 300 Б на кадр × 1024 = 300 КБ на комнату — кольцо ×10 = 3 МБ на комнату × 100 комнат = 300 МБ — неосуществимо); история на диске (точка эволюции решения 5: кольцо + диск двумя слоями — досылка сперва в память, потом на диск — проблема ёмкости становится проблемой IO); снимок + дельта (при полном обновлении отправлять сжатый снимок, а не покадрово — стоимость обновления падает). Общий урок трёх направлений: кольцо в памяти — правильный выбор учебной формы и стартовая точка, но не конечная станция производственной формы — честность объявления границы (глава 141) исполнена здесь: проект с самого начала сказал прямо, что он не производственный ответ.
Типичные ошибки
Симптом: отправка одного медленного клиента блокирует — вся комната встаёт (отправка покадрово до снятия блокировки).
Причина: перепутаны охваты блокировки «взять номер» и «доставить». Блокировка защищает только взятие номера (изменение счётчика и кольца); доставка (сетевые отправки) снаружи — медленная отправка никогда не блокирует чужое взятие номера.
lock(R); seq = ++R->NextSeq; store(R, seq, msg); broadcast(R, seq, msg); /* отправка в блокировке: медленный тащит всех */ unlock(R);
lock(R); seq = ++R->NextSeq; store(R, seq, msg); unlock(R); broadcast(R, seq, msg); /* отправка вне блокировки: взятие номера не блокируется */
Симптом: досылка переподключения идёт, а широковещания новой речи тоже приходят — кадры клиента перемешиваются (старые досылаемые чередуются с новыми).
Причина: досылка проигрывает историю, широковещание толкает новые кадры — двое конкурентны без координации. Правильный порядок: досылка снимает внутри блокировки и повторяет (или буферизует новые кадры до завершения досылки) — гарантия: всё досылаемое раньше всего нового.
replay_from(cursor); /* 补齐开始 */ /* новые широковещания приходят одновременно — чередование */
lock(R); snapshot_ring(R, cursor, &pending); /* снимок недостающих кадров внутри блокировки */ unlock(R); send_pending(U, &pending); /* досылка окончена */ resume_live(U); /* затем приём прямого эфира */
Симптом: после разрыва пользователя таблица членов комнаты разыменовывает висячий указатель — падение; или при переподключении старая идентичность не находится (курсор потерян).
Причина: разрыв принят за уничтожение. Разрыв есть переход в оффлайн (ресурсы соединения освобождены, идентичность сохранена) — уничтожение ждёт тайм-аута оффлайна. «Оффлайн-состояние» главы 141 соответствует в реализации не уничтожению.
on_close(U) { destroy_user(U); } /* разрыв = уничтожение: курсор и связи с комнатой теряются */
on_close(U) { U->Stream = NULL; /* оффлайн: соединение пусто, идентичность и курсор сохранены */ schedule_timeout(U); /* настоящая очистка только по тайм-ауту */ }
Полная цепь кадра say от прибытия до получения всей комнатой (сторона широковещания обработчика say, дополнение):
/* доставка: всей комнате (вне блокировки) + продвижение курсоров */ static void chat_broadcast(chroom* R, const chmsg* M) { size_t n = member_snapshot(R, NULL, 0); /* 数成员 */ chuser** Members = malloc(n * sizeof(void*)); if ( Members == NULL ) { return; } 重连补齐兜底(序号不丢) */ member_snapshot(R, Members, n); /* стабильный снимок */ for ( size_t i = 0; i < n; i++ ) { chuser* U = Members[i]; if ( (U->Stream == NULL) || !chat_send_frame(U, M) ) { /* оффлайн: курсор + досылка; неудача отправки онлайн: учесть медленного */ mark_slow(U); continue; } U->Cursor = M->Seq; /* курсор получателя продвигается */ } free(Members); }
Пункты разбора. (1) Снимок членов двухфазен (сперва посчитать, потом взять — миниатюра паттерна главы 112: снимок таблицы членов комнаты под блокировкой). (2) Семантика подстраховки OOM: кадр уже в кольце (do_say сохраняет до широковещания) — неудача на стороне отправки не теряет данных (досылка переподключения проигрывает из кольца) — кольцо есть слой персистентности широковещания. (3) mark_slow при неудаче отправки: счёт медленных накапливается до порога и запускает выселение (квантификация решения 6). (4) Широкещание не держит блокировку комнаты (позитивная форма ловушки 1).
Практические задания
Реализуйте словарь типов из упражнения 4 главы 141 как функцию диспетчеризации (по ветке на каждый из шестнадцати типов) + прямые и обратные случаи на каждый тип. Критерии приёмки: обратные все возвращают err/unknown без разрыва; ошибки недостающих полей локализуемы.
Инжектор разрывов (двойники рвут после кадра N) + переподключение с курсором: проверьте порядок досылаемых кадров, границу too_far (разрыв свыше 1024 кадров), корректность буферизации речи во время досылки. Критерии приёмки: все три случая верны — машинное доказательство критерия 3.
Реализуйте все шесть UC + словарь кадров + упорядочивание + досылку + конечные состояния + выселение медленных; нагрузка 100 двойников × 10 комнат × 10 минут (аудитор проверяет монотонность покадрово). Критерии приёмки: все восемь критериев пройдены; аудит ноль дыр ноль призраков; stats ноль утечек (критерий 7).
Та же одностраничная сверка, что у главы 140: (1) случаи шести UC всё зелёное (тесты диспетчеризации слоя протокола); (2) упорядочивание согласовано (нагрузочный аудит без нарушений); (3) досылка корректна (трёхстатные тесты внедрения разрывов); (4) конечные состояния чисты (ноль призрачных кадров); (5) медленные изолированы (внедрение медленного, остальные не замечают); (6) отказ полной комнаты/ника (тесты ёмкости и конфликта); (7) OOM без остатка (сущности по точкам); (8) стресс стабилен (аудит 100×10 минут). Все восемь отмечены, у каждого свой раздел-доказательство — ведомость, которую вклеивают в PR.
С главой реализации chatd все восемь подглав тома 13 (137–144) на месте — шестипроектная последовательность полна: каждая глава идёт по структуре «приёмка прежде → выбор/решения → разбор реализации → сверка → ретроспектива», список уроков каждой главы растёт и повторяется в следующей. Вдоль этой последовательности читатель прошёл шесть системных ограничений (масштаб / жизненный цикл / идентичность и упорядочивание / удалённость / веер / полный стек) — каждое ограничение по-настоящему обработано хотя бы в одном проекте. Это завершённое состояние тома 13 как закрывающего тома книги — не конечная точка знания, а стартовая точка способности его комбинировать.
Таблица семантики полей трёх структур (для справки при реализации): пользователь (Name — ключ идентичности / Stream — обнуляемое соединение = оффлайн / Cursor — последний увиденный номер / Room — указатель текущей комнаты); комната (Name — ключ комнаты / NextSeq — счётчик упорядочивания, только растёт / Ring — циклический массив на 1024 / RingHead — позиция старейшего / Members — дескриптор таблицы членов); сообщение (Seq — номер внутри комнаты / From — имя говорящего / Text — текст). Три инварианта: Cursor ≤ NextSeq той же комнаты (получение не забегает вперёд); Seq внутри кольца непрерывен (перезапись начинается со старейшего — непрерывность обещана только внутри окна); ключ таблицы членов = имя пользователя (повтор имени = тот же пользователь — глобальная уникальность ников есть предпосылка рукопожатия).
Итоги
| Тема | Кратко |
|---|---|
| Ритм реализации | словарь кадров → сущности → протокол → сессия → стресс (сперва чистые данные) |
| Три структуры | пользователь (обнуляемое соединение + курсор) / комната (упорядочивание + кольцо + таблица членов) / сообщение |
| Приём кадров | линейный трёхстатный цикл + маршрутизация по op — изоморфное переиспользование с диспетчером pushd |
| Взятие номера | ++NextSeq внутри блокировки; кадры с номерами; доставка снаружи |
| Досылка | повтор от cursor+1; too_far — полное обновление; снимок внутри блокировки против чередования |
| Семантика кольца | 1024 кадра на комнату; перезапись старейшего при монотонных номерах; окно досылки = ёмкость кольца |
| Порядок join/leave | сперва удаление → you_left самому → широковещание комнате — ноль призрачных кадров |
| Семантика разрыва | вход в оффлайн (не уничтожение); настоящая очистка только по тайм-ауту |
| Медленные | выселение (без ступени пропуска) — «номера без дыр» запрещают пропуск |
| Нагрузочный аудит | двойники проверяют Seq==Cursor+1 покадрово — перечислительное рантайм-доказательство конкурентной корректности |
📖 Справочники API в этой главе:value (159 записей)net (247 записей)core (25 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки