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

Третий проект — TCP-чат (chatd): многокомнатный чат реального времени — пользователь подключается, входит в комнату и говорит, комната рассылает, личные сообщения, досылка пропущенных сообщений по номерам после переподключения. Проектно он стоит между configd (главы 139-140: долгоживущее состояние и конкурентная передача) и pushd (глава 144: веерная рассылка и конечные состояния) — чистейший образец переплетения трёх сущностей: состояние соединения × состояние комнаты × номера сообщений. Глава наследует шаблон пяти элементов решения главы 139 (проблема / варианты / решение / обоснование / проверка) — переиспользование проектной методологии опережает переиспользование кода. Шесть доменов проектирования: модель комнаты (форма связи комнаты и участников); протокол линии (разграничение сообщений в байтовом потоке TCP — повторный выбор глав 72/91); согласованность широковещания (номера и досылка — предыстория протокола номеров pushd); машина состояний соединения (жизненный цикл рукопожатие → вход → разговор → выход); граница хранения (водораздел онлайн-памяти и офлайн-истории); ёмкость и деградация (матрица стратегий для медленных участников и полных комнат).

141.1引入

Чат — классическая ловушка «выглядит просто»: разве не «получил сообщение — переслал всем в комнате»? За каждым «просто» реальной системы стоят решения: A говорит за секунду до входа B — должен ли B это видеть (семантика момента входа: граница видимости истории); между отправкой A и получением B из комнаты вышел C — как закрыть сессию C (конечное состояние сессии); двое говорят в одну секунду — какой порядок видит комната (право упорядочивания: кто решает порядок); B оборвал связь и переподключился — что с пропущенными сообщениями (протокол досылки: курсор номеров). Ошибка в ответе на любой вопрос превращается на продуктовом уровне в «сообщения теряются», «порядок путается», «призрачные сессии» — и все они — вопросы этапа проектирования, а не кодирования. Эта глава превращает каждый вопрос в явное решение — глава реализации (глава 142) исполнит каждое.

141.2概念

Требования и критерии приёмки

Пользовательские истории: UC1 соединение и рукопожатие (TCP-соединение → кадр рукопожатия с ником → сервер подтверждает или отказывает); UC2 вход/выход (кадры join/leave — после входа приходит хвост истории); UC3 говорение с рассылкой (кадр say → вся комната получает кадр рассылки с номером); UC4 личные сообщения (кадр dm → получает только адресат); UC5 досылка при переподключении (переподключение с последним номером → сервер досылает пропущенные кадры); UC6 список присутствующих (кадр who → список участников комнаты). Приёмка: (1) шесть UC с позитивными и негативными случаями; (2) упорядочивание рассылки: согласованный по всей комнате порядок при конкурентных репликах (номера, видимые всеми участниками одной комнаты, строго монотонны); (3) корректная досылка: кадры времени разрыва доставляются после переподключения все и без повторов; (4) конечное состояние сессии: после выхода/разрыва нет призрачных кадров (кадр закрытия приходит ровно один раз); (5) медленный участник не тормозит комнату (изоляция); (6) явные пути отказа для полной комнаты и конфликта ников; (7) OOM и по-точечные отказы без следов; (8) стабильность под нагрузкой 100 конкурентных × 10 комнат.

Решение 1: модель комнаты

Проблема: каковы отношения комнаты и участников, участника и соединения? Варианты: A комната = множество соединений (управляет соединениями напрямую); B комната = множество пользователей, пользователь независим от соединения (переподключение меняет соединение, не идентичность); C без сущности комнаты (сообщения разносятся таблицей маршрутизации). Решение: B. Основания: семантика переподключения (UC5 требует сохранять идентичность и курсор номеров — идентичность нельзя привязывать к соединению); маршрутизация личных сообщений (UC4 по пользователю, не по соединению); офлайн-история (объект досылки UC5 — пользователь, не соединение). A отвергнуто: обрыв соединения = потеря идентичности — переподключение становится новым пользователем (досылка невозможна). C отвергнуто: комната — продуктовое понятие (история и список участников — атрибуты комнаты) — таблица маршрутизации без сущности их не вмещает. Проверка: тест переподключения (UC5) проходит только при B. Форма данных: Map<房间名, 房间>; комната содержит Map<用户名, 用户> + кольцо сообщений (хвост истории) + счётчик номеров; пользователь содержит указатель текущего соединения (NULL = офлайн) + курсор.

Решение 2: протокол линии

Проблема: как разграничить сообщения в байтовом потоке TCP? Варианты: A голые текстовые строки (разделитель \n); B двоичный с префиксом длины; C JSON-строки (текстовая строка + JSON-нагрузка). Решение: C. Основания: читаемость человеком (обучение и отладка — telnet может говорить напрямую); структурность (тип кадра и поля через JSON — разбор главы 32 за один шаг); расширяемость (новые поля — ноль изменений протокола). A отвергнуто: поля пришлось бы экранировать самим (ник с пробелом — взрыв); B отвергнуто: отладка требует hexdump. Реализация разграничения: строчный разметчик главы 72 (разграничение \n) + JSON-разбор главы 32 — оба слоя — готовые темы. Проверка: матричный тест протокола (прямые и обратные случаи на каждый тип кадра). Это решение — зеркало pushd (там JSON поверх WS-кадров — здесь JSON поверх строк — нагрузка одна, слой разграничения следует за транспортом — слоистость превращает проектирование протокола в выбор одного слоя).

Решение 3: упорядочивание рассылки и согласованность

Проблема: кто определяет порядок конкурентных реплик? Варианты: A порядок отправителя (локальные номера); B одноточечное упорядочивание сервера (счётчик комнаты); C векторные часы (распределённое упорядочивание). Решение: B. Основание: комната — домен полного порядка внутри одного процесса (одна точка упорядочивания на комнату) — просто и достаточно; C готовит к распределённости (смысл лишь при многих инстансах — за пределами этого проекта). Механизм: кадр реплики прибыл → внутри блокировки комнаты берётся номер (счётчик++) → кадр рассылки несёт номер → доставка по номеру. Семантика согласованности: номера, видимые всеми участниками одной комнаты, строго монотонны и без дыр (дыра = потеря — запускает досылку); курсор участника записывает наибольший увиденный номер. Протокол досылки (UC5): кадр переподключения несёт курсор → сервер пересылает кадры из кольца истории начиная с курсор+1 → вне кольца (слишком старое) — ответ "too far behind" (клиент полностью обновляется). Проверка: аудит номеров при конкурентной нагрузке (каждый клиент проверяет монотонность без дыр — критерии (2)(3)).

Решение 4: машина состояний соединения

Рукопожатиекадр отказа (конфликт или некорректный ник — закрыть с явной причиной) →Закрыто

Гарантия конечного состояния: Close соединения ровно один раз (глава 67); жизненный цикл идентичности пользователя длиннее соединения (офлайн-счёт / очистка по тайм-ауту — следствие Решения 1). Линия обороны от призрачных кадров: кадр выхода отправляется самому пользователю после выгрузки очереди рассылки («ты вышел» — последний кадр) — последующие рассылки его больше не достигают (порядок «сначала удалить, потом рассылать»).

Решение 5: граница хранения

Проблема: где хранить сообщения и сколько? Варианты: A всё в памяти (кольцевой буфер); B всё на диске; C горячее в памяти + холодное на диске. Решение: A (кольцо в памяти) + объявление границы. Основание: ёмкостное предположение учебной формы (кольцо 1024 кадра на комнату — около 100 КБ на комнату); B/C — точки эволюции, не часть этого проекта (диск = последовательная запись главы 45 + воспроизведение при старте — путь расширения оставлен). Семантика кольца: новый кадр перезаписывает самый старый; досылка возможна только внутри кольца (вне кольца = too far behind). Проверка: тест закольцовывания (отправить больше длины кольца — самый старый перезаписан, номера по-прежнему монотонны).

Решение 6: ёмкость и деградация

Проблема: что делать с медленными участниками, полными комнатами и перегрузкой сервера? Матрица решений: медленный участник (отправка AGAIN дольше порога) → выселить с уведомлением (не безграничное ожидание — чат-версия трёх уровней pushd: семантика чата не позволяет терять кадры — потеря ломает «номера без дыр» — поэтому пропуск недоступен, только выселение); полная комната (предел участников) → кадр отказа во входе (с явной причиной); перегрузка (предел всех соединений) → отказ в рукопожатии. У каждого вида отказа есть свой кадр протокола (клиент понимает и может повторить) — путь отказа — гражданин первого класса. Проверка: тест с внедрением медленного участника (изоляция и семантика выселения — критерий (5)).

Разделение труда с pushd (глава 144)

chatd и pushd похожи формой (комнаты / рассылка / номера), но ограничения разные — таблица против смешения: транспорт (голый TCP против WebSocket — chatd строит рукопожатие и разграничение сам, pushd получает их даром); идентичность (пользователь независим от соединения против «соединение и есть идентичность» — переподключение с досылкой в chatd выталкивает наружу сущность пользователя); терпимость к потерям кадров (терять нельзя — номера без дыр против «терять можно, есть resync» — уровень пропуска pushd в chatd недоступен). Вывод: chatd учит «протокол с байтового нуля», pushd — «переиспользование высокоуровневых деталей» — один и тот же бизнес в двух транспортных слоях — тренировка слоистой чувствительности проектного взгляда.

141.3示例

Первая полная программа: атом разграничения протокола линии

Программа ниже из examples/io/line — исполняемый атом слоя разграничения протокола линии (строчные кадры \n):

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) Трёхстатный цикл разграничения строк — это движок приёма кадров chatd: LINE прибыл → JSON-разбор → диспетчеризация; END → закрытие соединения; ERROR → структурный разрыв. (2) Адаптация концов строк (все три распознаются) позволяет подключаться telnet/nc и любым клиентам — снисходительный вход (низший слой протокола снисходителен, верхний JSON строг — каждый слой на своём месте). (3) Этот атом тот же, что цитировался в главе 139 — два проекта делят один фундамент (прямое доказательство слоистого переиспользования).

Вторая полная программа: структурность нагрузки кадров

Вторая программа из examples/data/json — атом слоя нагрузки кадров (JSON):

examples/data/json/main.c源码 ↗





































 */

#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/data/json/main.c -lws2_32 -liphlpapiname = xrt{ "name": "xrt", "features": [ "json", "http" ]}member: codemember: ok{"code":200,"message":"OK"}

Что только что произошло. (1) DOM-разбор даёт доступ к полям (форма запроса member: code/ok) — прямой словарь диспетчеризации кадров chatd (маршрутизация по полю op — изоморфна on_message pushd). (2) Сериализационный круговорот (Writer производит корректный JSON) — форма генерации кадров рассылки. (3) Два атома (разграничение строк + JSON) вместе — весь протокол линии — репетиция реализации Решения 2: две готовые темы собирают один протокол — ноль собственного кода протокола.

Сводная таблица проектных решений

Одностраничный обзор шести решений (для ревью) — одна строка на домен, пять элементов сжаты в три колонки:

ДоменРешениеКлючевые основания и проверка
Модель комнатыпользователь независим от соединенияпереподключение сохраняет идентичность (UC5 проходит); двухслойный Map
Протокол линииразграничение строк + JSONчитаемость + структурность + ноль собственного разграничения
Упорядочиваниеодноточечный номер комнатыдомен полного порядка в одном процессе; номера монотонны без дыр
Машина состоянийшесть состояний, включая офлайнпериод идентичности длиннее соединения
Хранениекольцо в памяти 1024 кадраучебное ёмкостное предположение; диск — эволюция
Деградациядва уровня: выселение/отказ + кадры протокола«номера без дыр» запрещает пропуск; отказы понятны

Эта таблица — индекс сверки для главы реализации (глава 142): колонка «проверка» каждой строки — пункт тестового списка к моменту завершения реализации.

Методологическое сопоставление с проектной главой 138

Сопоставление двух исполнений шаблона проектных глав — проверка самой методологии: пять элементов решения в главе 138 развёрнуты тяжело (по разделу на решение), здесь сжаты в таблицу и разделы — степень детализации следует спорности решения (у настоящей множественности вариантов — разворот, у очевидного направления — сжатие — объём служит устранению двусмысленности); список приёмки прежде в обеих главах одинаков (шесть против восьми пунктов — здесь добавились ёмкость и нагрузка — у проекта с более высокой конкурентной плотностью приёмка естественно больше); сохранение отвергнутых альтернатив (в главе 138 шаблонизатор оставил точку расширения, здесь диск оставил точку эволюции — отвержение не уничтожает: когда основание исчезнет, путь назад остаётся). Три переносимых урока шаблона: шаблон — скелет, не таблица — порядок пяти элементов (проблема прежде) важнее формата; у каждого решения — проверка — решение без схемы проверки это желание, а не решение; решения взаимно проверяются («независимость идентичности» здесь определила форму «протокола досылки» — изолированные, каждое само по себе оптимальные решения могут противоречить в целом — главная работа проектного ревью и есть поиск таких пересечений).

141.4契约

  • Пять элементов решения: у каждого домена проектирования полны проблема/варианты/решение/обоснование/проверка — глава реализации исполняет построчно.
  • Модель комнаты: пользователь независим от соединения (переподключение сохраняет идентичность и курсор); комната = множество пользователей + кольцо сообщений + счётчик упорядочивания.
  • Протокол линии: разграничение строк (снисходительное) + JSON-нагрузка (строгая) — два готовых слоя.
  • Упорядочивание: одноточечное в комнате; номера строго монотонны без дыр; курсор записывает увиденное.
  • Протокол досылки: переподключение с курсором → досылка внутри кольца, вне кольца too far behind.
  • Конечное состояние сессии: после кадра выхода нет призрачных кадров (сначала удалить, потом рассылать); Close ровно один раз.
  • Жизненный цикл идентичности: длиннее соединения; офлайн-очистка по тайм-ауту (настраивается).
  • Граница хранения: кольцо в памяти (1024 кадра на комнату) + диск зарезервирован как эволюция.
  • Матрица деградации: медленного участника выселить (пропуск запрещён — номера без дыр) / полную комнату — отказ / при перегрузке — отказ в рукопожатии; у каждого отказа — кадр протокола.
  • Разделение с pushd: собственный протокол на голом TCP против переиспользования высокоуровневых деталей на WS — тренировочная пара одного бизнеса на двух транспортах.

Совместимость при эволюции протокола

Резервирование версионности протокола линии (упражнение в дальновидности на этапе проектирования): поле версии (кадр рукопожатия несёт "v":1 — сервер диспетчеризует разбор по версии — когда v2 добавит поля, клиент v1 не сломается: JSON-разбор естественно снисходителен к неизвестным полям — ещё один дивиденд выбора JSON); согласование возможностей (ответ на рукопожатие возвращает список возможностей сервера — клиент урезает функции по возможностям — who из UC6 может эволюционировать в постраничность, не ломая старых клиентов); путь деградации (неизвестный op — кадр "unknown op" без разрыва соединения — тот же рецепт, что снисходительность сообщений главы 140). Три пункта вместе — около двадцати строк проекта — покупают протоколу возможность эволюции вместо всеобщей синхронной смены версий при каждой правке. Срок жизни протоколов реальных IM-систем измеряется десятилетиями — совместимость — первое требование к протоколу, функциональность — второе.

Тестовые двойники и имитация клиентов

Тестовая стратегия chatd (определена на этапе проектирования, исполняется на этапе реализации): двойник клиента — скриптованный виртуальный пользователь (подключение → рукопожатие → вход → реплики / приём кадров / аудит курсора) — N двойников водит исполнитель главы 59; конфигурация формы сервера — нормальный / внедрение медленного / полная комната / закольцовывание — четыре серверных поведения тестовой матрицы; аудитор номеров — двойник при приёме кадра проверяет монотонность без дыр (у каждого клиента встроен проверщик критерия (2)); внедрение разрывов — двойник активно рвёт связь после заданного номера кадра (тест досылки) и пассивный разрыв (тест конечного состояния). Корректность двойников опирается на общую с настоящим клиентом библиотеку протокола (двойник — не вторая реализация — тестовая версия «нет второй реализации» главы 130).

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

Ловушка 1: идентичность привязана к соединению:Ловушка 1: идентичность привязана к соединению

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

Причина: соединение принято за идентичность. TCP-соединение — носитель сессии, а не сама сессия — семантика переподключения требует независимости идентичности от носителя (ядро Решения 1).

错误写法
struct user { xnetstream* Conn; ... };
/* обрыв Conn = пользователь исчез: курсор и связи с комнатой теряются */
正确写法
struct user {
	xnetstream* Conn;     /* NULL = оффлайн */
	uint64 Cursor;        /* самый поздний увиденный номер — независимо от соединения */
	...                   /* поля идентичности */
};
/* переподключение: новый Conn цепляется к старому user — курсор непрерывен */
Ловушка 2: порядок рассылки по порядку прибытия, а не по упорядочиванию:Ловушка 2: порядок рассылки по порядку прибытия, а не по упорядочиванию

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

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

错误写法
on_say(user, msg) {
	broadcast(room, msg);   /* прямая отправка в порядке прибытия: порядок вне блокировки не гарантирован */
}
正确写法
on_say(user, msg) {
	uint64 seq;
	lock(room); seq = room->Next++; unlock(room);
	broadcast(room, frame(seq, msg));   /* доставка с номером */
}
Ловушка 3: после выхода приходят рассылки (призрачные кадры):Ловушка 3: после выхода приходят рассылки (призрачные кадры)

Симптом: клиент после выхода всё ещё изредка получает сообщения комнаты — сессия закрыта нечисто; хуже — после переподключения загрязнение старыми кадрами.

Причина: перепутан порядок удаления участника и рассылки (сначала рассылка, потом удаление — эта партия рассылки всё ещё достигает вышедшего).

错误写法
broadcast(room, "user left");   /* сперва широковещание */
remove_member(room, user);      /* потом удаление: эта партия рассылки включает вышедшего */
正确写法
remove_member(room, user);      /* сперва удаление */
send_to(user, "you left");      /* последний кадр самому пользователю (ровно один раз) */
broadcast(room, "user left");   /* рассылка комнате не включает вышедшего */
Словарь типов кадров:Словарь типов кадров

Словарь типов кадров, финализированный проектом (протокольный авторитет главы реализации): восходящие (рукопожатие / join / leave / say / dm / who / quit — у каждого таблица обязательных полей); нисходящие (ok / err / broadcast / dm / history_tail / too_far / you_left / member_list — у каждого поля и правило появления номеров); двусторонняя возможность (квитанция unknown на неизвестный op). Словарь в форме трёхсегментной таблицы (тип кадра / направление / поля + ограничения) — идея «данных как данных» из файлов ожиданий главы 140, применённая к протоколу. Критерий приёмки: каждая последовательность кадров шести UC находит в словаре все свои типы; err ошибочных ветвей несёт локализуемый код причины.

Практические задания

Базовое: последовательности кадров шести UC

Выпишите для каждого из шести UC полную последовательность кадров (что отправляет клиент, что отвечает сервер — включая ошибочные ветви) — типы JSON-кадров таблицей. Критерии приёмки: у каждого UC по прямому и обратному случаю; поля кадров минимально полны (правила появления op / обязательных параметров / поля номера).

Продвинутое: диаграммы последовательности упорядочивания и досылки

Нарисуйте три последовательности: двое говорят конкурентно (взятие номера и доставка переплетаются — согласовано по всей комнате); переподключение после разрыва в 5 кадрах (курсор + кольцо); отказ досылки после закольцовывания (too far behind). Критерий приёмки: цепочку номеров каждой последовательности можно вывести вручную — без дыр и повторов.

Вызов: модель ёмкости

Оценка: 100 комнат × 50 пользователей × кольцо 1024 кадра × в среднем 200 байт/кадр — общий объём памяти; буфер на соединение при 1000 конкурентных соединениях (глава 65) — общая таблица бюджета. Дайте связь рекомендации по потолку ёмкости и порога отказа при перегрузке. Критерии приёмки: у каждого пункта бюджета есть источник (ссылка на главу); порог и бюджет согласованы.

Итоги

ТемаКратко
Шаблон проектапять элементов решения (проблема/варианты/решение/обоснование/проверка) — повторное использование шаблона главы 139
Модель комнатыпользователь независим от соединения; комната = множество пользователей + кольцо сообщений + упорядочиватель
Протокол линииразграничение строк (снисходительный вход) + JSON-нагрузка (строгая структура) — два готовых слоя
Упорядочиваниеодноточечный номер в комнате; номера монотонны без дыр; курсор записывает увиденное
Досылкапереподключение с курсором → досылка внутри кольца; вне кольца too far behind — полное обновление
Конечное состояние сессиисначала удалить, потом рассылать; завершающий кадр самому — ровно один раз
Период идентичностидлиннее соединения; офлайн-очистка по тайм-ауту
Хранениекольцо в памяти + диск как эволюция; закольцовывание перезаписывает старейшее
Деградациямедленного — выселить (пропуск запрещён) / полную — отказ / перегрузку — отказ; у отказа есть кадр
Разделение с pushdсобственный протокол на голом TCP против WebSocket с готовыми деталями — тренировочная пара одного бизнеса на двух транспортах

📖 Справочники API в этой главе:core (25 записей)value (159 записей)net (247 записей)

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