Ориентация главы

Глава 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):

1Слой сущностей (~150 строк своих)три структуры + операции контейнеров (вступление/удаление/поиск/управление кольцом)
2Слой протокола (~120 строк своих)движок приёма кадров + диспетчеризация op + генерация кадров (JSON Writer)
3Слой сессий (~180 строк своих)семь обработчиков рукопожатие/join/leave/say/dm/who/quit
4Слой упорядочивания (~60 строк своих)взятие номера/широковещание/досылка/курсор
5Сетевой слой (0 строк своих)Engine/Listener/Stream — целиком главы 64/67

Пятьсот строк реализуют чат-сервис с гарантиями упорядочивания и протоколом досылки — взаимная сверка с 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 (слой линейного разграничения):

examples/io/line/main.c源码 ↗























 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/io/line/main.c -lws2_32 -liphlpapi1: INFO server started2: WARN queue is busy3: ERROR request failed

Что только что произошло. (1) Два шага ReaderFromMemory→LineReaderTake и три состояния Next — тело chat_on_data chatd (Reader меняется на приёмный буфер соединения — событие данных Stream главы 67 есть сетевая форма Reader). (2) Заимствование представлений (каждая строка используется по прибытии) — освобождение после диспетчеризации, без кеширования — дисциплина «кадры не ночуют» главы 141. (3) Этот атом повторяется в трёх проектах (137/139/141) — переиспользование фундаментальных атомов — прямой урожай слоистого дизайна.

Вторая полная программа: генерация кадров широковещания

Вторая программа из examples/logging/file_json — образец для генерации JSON-кадров (один JSON-объект на строку как выходная форма):

examples/logging/file_json/main.c源码 ↗



















 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/logging/file_json/main.c -lws2_32 -liphlpapiwrote example_logger_json.log

Что только что произошло. (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) исполнена здесь: проект с самого начала сказал прямо, что он не производственный ответ.

Типичные ошибки

Ловушка 1: широковещание внутри блокировки:Ловушка 1: широковещание внутри блокировки

Симптом: отправка одного медленного клиента блокирует — вся комната встаёт (отправка покадрово до снятия блокировки).

Причина: перепутаны охваты блокировки «взять номер» и «доставить». Блокировка защищает только взятие номера (изменение счётчика и кольца); доставка (сетевые отправки) снаружи — медленная отправка никогда не блокирует чужое взятие номера.

错误写法
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);   /* отправка вне блокировки: взятие номера не блокируется */
Ловушка 2: гонка досылки с прямым широковещанием (новые кадры посреди досылки):Ловушка 2: гонка досылки с прямым широковещанием (новые кадры посреди досылки)

Симптом: досылка переподключения идёт, а широковещания новой речи тоже приходят — кадры клиента перемешиваются (старые досылаемые чередуются с новыми).

Причина: досылка проигрывает историю, широковещание толкает новые кадры — двое конкурентны без координации. Правильный порядок: досылка снимает внутри блокировки и повторяет (или буферизует новые кадры до завершения досылки) — гарантия: всё досылаемое раньше всего нового.

错误写法
replay_from(cursor);       /* 补齐开始 */
/* новые широковещания приходят одновременно — чередование */
正确写法
lock(R);
snapshot_ring(R, cursor, &pending);   /* снимок недостающих кадров внутри блокировки */
unlock(R);
send_pending(U, &pending);            /* досылка окончена */
resume_live(U);                        /* затем приём прямого эфира */
Ловушка 3: уничтожение структуры пользователя при разрыве:Ловушка 3: уничтожение структуры пользователя при разрыве

Симптом: после разрыва пользователя таблица членов комнаты разыменовывает висячий указатель — падение; или при переподключении старая идентичность не находится (курсор потерян).

Причина: разрыв принят за уничтожение. Разрыв есть переход в оффлайн (ресурсы соединения освобождены, идентичность сохранена) — уничтожение ждёт тайм-аута оффлайна. «Оффлайн-состояние» главы 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.

Вызов: полный chatd + нагрузочный аудит

Реализуйте все шесть 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 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки