IRCv3: уроки обратно совместимого дизайна протокола
IRCv3 модернизирует протокол 1988 года и не ломает совместимость. Уроки согласования возможностей, тегов сообщений и эволюции протокола с обратной совместимостью.

В 1988 году финский студент университета написал протокол для чата, который умещался на нескольких страницах. Почти четыре десятилетия спустя этот же протокол — IRC — всё ещё передаёт миллионы сообщений в день, а рабочая группа IRCv3 тихо учит остальных, как развивать протокол, не выжигая дотла уже установленную базу. Если вам когда-либо приходилось версионировать API, на который полагаются незнакомые люди, IRCv3 — кейс, который стоит изучить и перенять.
Я время от времени писал боты и простые клиенты для IRC на протяжении многих лет, и меня постоянно удивляет, насколько современно может ощущаться работа с ним, когда обе стороны говорят на новых расширениях. История сообщений, которую отдаёт сервер. Нормальная аутентификация. Временные метки, которые переживают баунсер. И при этом клиент, написанный в 1995 году, всё ещё может подключиться к тому же серверу и спокойно общаться. Такое сочетание возникает не случайно — это результат продуманных решений дизайна и нескольких болезненных ошибок, которые сообществу пришлось обходить.
Первородный грех: возможности, о которых нельзя спросить
Базовый протокол IRC, закреплённый в RFC 1459 и позже в RFC 2812, не давал клиенту никакого механизма, чтобы узнать у сервера, что тот поддерживает. Ни одного. Вы подключались, отправляли NICK и USER, а дальше гадали. Хотите узнать, поддерживает ли сервер какой-то режим или расширение? Отправьте команду и посмотрите, придёт ли ошибка. Звучит забавно, пока вы не поддерживаете клиентскую библиотеку, которой приходится учитывать особенности поведения пяти разных серверных реализаций, каждая из которых слегка по-своему трактует двусмысленные места спецификации.
Проблема здесь не теоретическая. Разработчики писали клиенты, которые проверяли возможности, отправляя команды и разбирая числовые коды ошибок — хрупкая конструкция, которая ломалась, стоило серверу поменять текст ошибки. Боты заходили на каналы с неверными предположениями и засыпали всё сообщениями об ошибках. Это разрыв между спецификацией и реализацией в его естественной среде: когда спецификация не даёт механизма обнаружения, каждый разработчик изобретает свой, и делает это плохо.
Решением IRCv3 стало согласование возможностей, и оно настолько простое, что почти неловко. При регистрации соединения клиент отправляет CAP LS, а сервер отвечает списком именованных возможностей, которые он поддерживает, например sasl, server-time или message-tags. Затем клиент отправляет CAP REQ ровно с тем подмножеством, которое ему нужно, сервер подтверждает, и обе стороны продолжают работу с общим и явным пониманием того, что поддерживает сессия. Легаси-клиент, который никогда не отправляет CAP, получает старое поведение. Легаси-сервер, который не понимает CAP, возвращает ошибку, которую клиент может безопасно проигнорировать и перейти к запасному варианту.
-> CAP LS 302
<- CAP * LS :sasl=PLAIN,EXTERNAL server-time message-tags echo-message
-> CAP REQ :sasl server-time
<- CAP * ACK :sasl server-time
-> AUTHENTICATE PLAIN
<- AUTHENTICATE +
-> AUTHENTICATE dXNlcgB1c2VyAHBhc3N3b3Jk
<- 900 jane jane!jane@host jane :You are now logged in
Этот урок выходит далеко за пределы IRC. Если вы сегодня проектируете любой протокол или API, заложите механизм согласования в первую версию, даже если пока не о чем договариваться. Цена — один обмен сообщениями; выигрыш в том, что каждая будущая функция становится опциональной, а не ломающим изменением.
Урок: расширяемость важнее новых команд
Вторая большая идея IRCv3 — теги сообщений, и она появилась из реальной ошибки: люди постоянно пытались добавлять функции, определяя новые команды или втискивая семантику в существующие параметры сообщений. Формат IRC печально известен своей лаконичностью: префикс, команда и список завершающих параметров. Места, куда можно было бы прикрепить метаданные, не было. Поэтому, когда кому-то нужны были временные метки для воспроизводимой истории или пометка того, какой аккаунт отправил сообщение, старый ответ заключался в изобретении нового числового кода или искажении завершающего текста. Оба подхода ломали старых клиентов или заставляли каждый парсер обрастать особыми случаями.
Решение — добавить новую необязательную секцию в начало сообщения: @key=value;key2=value2 перед префиксом. Важно, что серверы отправляют теги только клиентам, которые согласовали возможность message-tags, так что старые клиенты никогда не увидят байт, который не могут разобрать. Вот как на самом деле выглядит сообщение с временной меткой в протоколе:
@time=2024-03-14T09:26:00.000Z;account=jane :jane!jane@host PRIVMSG #rust :borrow checker finally clicked for me
Это тот же архитектурный ход, что и HTTP-заголовки, метаданные gRPC или толерантность protobuf к неизвестным полям: зарезервировать общую точку расширения, и функции перестанут требовать хирургического вмешательства в протокол. Расширение server-time, которое решает давнюю проблему баунсеров, когда все воспроизведённые сообщения выглядят так, будто пришли только что, — это буквально один тег. Черновик +reply, позволяющий клиентам связывать ответы с конкретными сообщениями, — ещё один тег. Никаких новых команд. Никаких новых числовых кодов. Никакого «дня икс» для обновления версий.
Я сам совершал противоположную ошибку. Несколько лет назад во внутренней системе pub/sub мы добавили функцию, перегрузив поле JSON магическим префиксом, потому что изменение формата конверта «было слишком дорого». Восемь месяцев спустя мы поддерживали регулярное выражение на трёх языках, чтобы распаковывать эту магию, а парсер одной из команд воспринимал магическую строку как буквальные данные, что привело к запоминающемуся инциденту. Простое, понятное, явное поле метаданных стоило бы нам одного дня. Магическая строка стоила кварталов. Теги сообщений IRCv3 — именно то, что такой системе следовало иметь с самого начала.

Урок: убрать пароль в открытом виде как обходной путь
Спросите любого, кто пользовался IRC-клиентом в 2000-х, как работал логин, и получите один и тот же ответ: вы писали /msg NickServ IDENTIFY password. Учётные данные уходили как обычное сообщение в чате, оставались в логах и иногда оказывались в публичном канале, когда клиент путал команду — это был такой обычный обряд посвящения, что стал мемом. Аутентификация была прикручена на уровне приложения, потому что в протоколе её понятия не было.
IRCv3 исправил это по-настоящему, стандартизировав аутентификацию SASL во время регистрации. Вместо того чтобы общаться с ботом после подключения, клиент согласует возможность sasl и проходит аутентификацию в рамках рукопожатия, ещё до того, как у него появится никнейм, который кто-либо видит. Сервер знает, кто вы, уже из первого сообщения, и это тянет за собой всё остальное: расширение account-tag может прикреплять имя аккаунта к каждому сообщению, боты могут принимать решения о правах без собственных эвристик разбора NickServ, а клиенты могут строить сценарии регистрации, не требующие чтения стены текста NOTICE.
Более широкий урок: когда обходной путь становится повсеместным, он перестаёт быть обходным путём и превращается в ваш де-факто протокол — без какой-либо продуманности дизайна. NickServ не был ни в одном RFC, но он формировал два десятилетия поведения клиентов, включая их режимы отказа. Стандартизация того, что люди уже делали (аутентификация при подключении), через настоящий механизм (SASL, который включает PLAIN, EXTERNAL для клиентских сертификатов и место для других) превратила фольклор в инфраструктуру. Если все ваши пользователи проделывают одну и ту же неуклюжую процедуру, чтобы что-то сделать, это ваш следующий пункт спецификации, а не вопрос для FAQ поддержки.
Урок: история — это проблема протокола, а не клиента
На протяжении большей части жизни IRC протокол был строго живым: если ваш клиент не был подключён в момент отправки сообщения, для вас этого сообщения не существовало. Ответом сообщества стал баунсер — постоянно подключённый прокси (классический пример — ZNC), который работал на VPS, держал вашу сессию и воспроизводил пропущенное. Баунсеры работали, но это была ещё одна инфраструктура для запуска, ещё одни учётные данные для управления, а опыт воспроизведения был ужасен: всё приходило с меткой времени воспроизведения и вразнобой с тем, что вы видели вживую.
IRCv3 атаковал эту проблему слоями. Сначала server-time сделал воспроизводимую историю пригодной, передавая реальную метку времени получения. Затем черновик chathistory дал клиентам стандартный способ запрашивать диапазоны истории, а серверы вроде Ergo начали хранить историю каналов напрямую. Результат, который многие обнаружили, поднимая небольшой сервер Ergo для друзей, поразителен: подключитесь с телефона после недели офлайн, и сервер воспроизведёт всё с корректными метками времени — без баунсера, без возни с VPS. Это самый большой скачок в качестве жизни, который когда-либо был у IRC, и он пришёл из протокола, а не из хитрого клиентского трюка.
Стоит серьёзно отнестись к контраргументу, и его выдвигают ежедневные пользователи IRC: большая часть воспринимаемой корявости IRC — на стороне клиента, и хороший опыт сегодня можно получить с приличным клиентом и баунсером. Это верно, но такой аргумент подтверждает мысль с другой стороны. Каждый автор клиента независимо переписывал склейку скроллбэка, отслеживание упоминаний и дедупликацию повторов, допуская слегка разные ошибки. Перенос сложной части в протокол и сервер означает, что её реализуют один раз, правильно, и все клиенты получают выгоду. Мы постоянно принимаем такое же решение в веб-разработке: переносим инварианты в платформу (вспомните, сколько кода приложений исчезло, когда в браузерах появился нативный fetch или когда TLS стал обязательным минимумом), а не позволяем тысяче кодовых баз наполовину решать задачу.
Урок: опциональные функции порождают расходящиеся реальности
Теперь о промахе, который IRCv3 пока не до конца решил, и он важнее всего для тех, кто проектирует расширяемые системы. Поскольку каждое расширение опционально и согласуется независимо, два пользователя в одном канале буквально могут видеть разные разговоры. Один клиент получает сообщения с тегами +reply и отображает их как ветки ответов; другой показывает те же сообщения плоскими строками без контекста. Один пользователь видит реакции-эмодзи как аккуратные значки, прикреплённые к сообщению; другой не видит ничего вовсе или получает невнятную запасную строку. Таблица поддержки IRCv3 клиентами — это сетка из «да» и «нет», и ваш опыт зависит от того, в каком столбце вы находитесь.
Это тёмная сторона мягкой деградации. Обратная совместимость гарантировала, что ничего не сломается, но ничто в модели не гарантирует согласованности. Slack и Discord, что бы вы о них ни думали, никогда не сталкиваются с этой проблемой: платформа так плотно контролирует обе стороны, что реакция — это реакция для всех. IRCv3 выбрал эволюцию вместо контроля, и цена — постоянно неравномерный ландшафт функций, где черновики спецификаций иногда остаются черновиками годами.
Если вы строите что-то расширяемое, закладывайте это в бюджет с первого дня. Некоторые способы смягчения, к которым пришли IRCv3 и похожие экосистемы:
- Проектируйте деградацию осознанно. Тег ответа должен откатываться к читаемой цитате, а не исчезать. Если запасной вариант непригоден, функция не готова к выпуску как опциональная.
- Публикуйте таблицу поддержки и держите её честной. Таблицы клиентов и серверов IRCv3 позволяют пользователям делать осознанный выбор; молчание порождает обвинения, направленные не на того виновника.
- Определите базовый уровень и продвигайте его. Пронумерованные релизы (3.1, 3.2) объединяют возможности, так что клиенты могут ориентироваться на «поддержку 3.2», а не на одиннадцать отдельных флагов. Дайте экосистеме ориентир, к которому можно сходиться.
- Завершайте свои черновики. Черновик, который попадает на серверы, но так и не финализируется — история каналов однажды едва избежала такой участи, — хуже отсутствия черновика, потому что расщепляет поведение, не фиксируя его.
Мы видели вариант этого в статье о том, почему совместное редактирование сложнее, чем кажется: как только участники могут расходиться во мнениях о форме общего состояния, нужны явные правила сходимости, а не просто добрые намерения. Опциональность по умолчанию — разумная стратегия для возможностей, но видимая пользователю семантика требует плана для тех, кто оказался по другую сторону таблицы.

Что это значит для вашего следующего API или протокола
Снимите ностальгию по чату, и IRCv3 окажется компактным руководством по эволюции чего угодно с сетевым форматом. Вот чек-лист, который я бы передал любой команде, которая начинает протокол, схему событий или даже публичный REST API:
- Сначала — согласование. Обмен возможностями в первой версии, даже с пустым списком. Добавлять обнаружение задним числом в десять раз сложнее.
- Зарезервируйте точку расширения. Теги, заголовки, карты метаданных — один общий слот, к которому могут крепиться будущие функции, не трогая базовую грамматику.
- Делайте новое поведение опциональным для каждого соединения. Старые клиенты должны получать побайтово идентичное старое поведение. Серверы IRCv3 никогда не отправляют тег клиенту, который о нём не просил.
- Стандартизируйте фольклор. Любой хак, к которому сошлись ваши пользователи (аутентификация в стиле NickServ, воспроизведение в стиле баунсера), — это спецификация, которую ждёт правильное написание.
- Планируйте таблицу поддержки. Опциональные функции расходятся; решите, как выглядит деградированный опыт, и публикуйте, кто что поддерживает.
- Объединяйте прогресс в именованные вехи. Отдельные флаги нужны реализаторам; пронумерованные релизы — людям.
Что я нахожу по-настоящему впечатляющим, так это сдержанность. IRCv3 не гнался за сквозным шифрованием, перепроектированием федерации или каким-либо грандиозным переписыванием. Он выбрал маленькие, острые проблемы — обнаружение, метаданные, аутентификацию, время — и решил их так, чтобы клиент 1995 года мог их просто игнорировать. Поэтому IRC — практически единственный выживший из своего поколения интернет-протоколов, который всё ещё в основном работает так, как задумано, и поэтому выходные-проект сегодня может дать группе друзей синхронизацию истории, TLS и настоящие аккаунты на VPS за 5 долларов.
Обратная совместимость — это не отсутствие изменений. Это дисциплина в том, чтобы каждое изменение было тем, что прошлое может безопасно проигнорировать.
В следующий раз, когда вас потянет версионировать API, поменяв путь с /v1 на /v2 и заставив каждого потребителя мигрировать, вспомните протокол, который позволил клиенту эпохи модемов и современному мобильному приложению делить один и тот же канал. Согласовывайте возможности. Прикрепляйте метаданные вместо изобретения команд. Держите базовый уровень прочным и позвольте потолку расти по собственному графику. IRCv3 доказывает, что можно иметь протокол, который уважает своих предшественников и при этом учится новым трюкам — а если чат-система из 1988 года на такое способна, у вашего трёхлетнего микросервиса нет оправданий.


