Ориентация главы
У HTTP/1.1 есть механизм «перевоплощения»: заголовок Upgrade предлагает сменить протокол, сервер отвечает 101 Switching Protocols, и в том же TCP-соединении начинается новый язык. WebSocket — самый знаменитый пользователь механизма: кадры, сообщения и потоки глав 95~97 всё начинаются с одного успешного обновления. Глава разбирает обе стороны: сторону предложений (xrtHttpUpgradeFieldCursorInit/FieldNext итерируют клиентские предложения, xrtHttpUpgradeWrite пишет «поддержанное мной подмножество» как канонический ответ — не только для WebSocket; обновления вроде h2c употребляют то же); сторону рукопожатия WebSocket (построение и сверка запроса/ответа в websocket/upgrade, каноническое по RFC вычисление xrtWsAccept, согласование подпротоколов xrtWsProtocolSelect). У значения Accept есть стандартный тестовый вектор RFC 6455 — совпадение с ним и есть интероперабельное доказательство правильности реализации.
94.1引入
Почему WebSocket начинается с HTTP? Инженерный ответ: порты 80/443 уже открыты, прокси и межсетевые экраны понимают HTTP, аутентификация и маршрутизация могут переиспользовать инфраструктуру HTTP. Протокольный ответ: обновление позволяет провести «переговоры» на языке, который знают обе стороны, — клиент предлагает (хочу говорить websocket, подпротоколы вот эти), сервер выбирает (101 + принимаемые ключевые параметры), и лишь по завершении переговоров язык меняется; любая сторона недовольна — продолжают говорить HTTP (обычный ответ, без переключения).
Три детали семантики обновления стоит установить заранее: предложения могут повторяться (несколько полей Upgrade, списки через запятую — правила семейств полей главы 92); имена протоколов регистронезависимы (WebSocket/websocket — одно и то же); версия может прилагаться (форма protocol/version вроде HTTP/2.0). Всё это обрабатывают итераторы слоя обновления — опять история «не режьте руками».
94.2概念
Сторона предложений: итерация и ответ
Выход xrtHttpUpgradeFieldNext — xhttpupgradeitem: представление Protocol + опциональное представление Version — итератор объединил повторяемые поля, разрезал запятые, нормализовал регистр (при сравнении). xrtHttpUpgradeWrite(支持列表, 数量, 输出, 容量, &长度) (список поддержки, количество, вывод, ёмкость, &длина) обратно порождает каноническое значение — атомарность ёмкости общебиблиотечная.Семантика ответа: ответ обновления пишет только «выбранные протоколы» (не весь список поддержки); ответ 101 Switching Protocols обязан ещё и повторить Connection: Upgrade — построение ответа выполняется WebSocket-вариантом слоя обновления (следующий раздел) или собирается вручную.
Рукопожатие WebSocket: запрос, ключ и сверка ответа
WebSocket-обновление накладывает на общий механизм специализированные поля: клиент шлёт Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key (Base64 от 16 случайных байтов) + опционально Sec-WebSocket-Protocol (список подпротоколов); сервер обязан ответить Sec-WebSocket-Accept — вычисленным по канону RFC 6455: Base64(SHA1(Key + 魔法串)) (ключ + магическая строка). Это вычисление — не «проверка секрета» (в ключе нет тайны), а доказательство, что сервер действительно знает протокол: реализация, вычислившая верный Accept, неизбежно реализовала спецификацию. xrtWsAccept(Key, 输出, 容量) (Key, вывод, ёмкость) делает это за один шаг, выводя 28 символов Base64.
Построение запроса (демонстрирует examples/websocket/upgrade): полный заголовок запроса GET 路径 HTTP/1.1 + Host + Upgrade/Connection + Key/Version(+Protocol); сверка ответа (response check binds key+protocol из upgrade_tour): сверка обязана быть привязана к ключу этой сессии — повторенный противоположной стороной Accept сравнивается со значением, вычисленным из вашего отправленного ключа (против подмены сессии посредником), а выбранный подпротокол обязан быть в вашем списке предложений.
Согласование подпротоколов
Sec-WebSocket-Protocol — выбор протокола прикладного уровня (нечто вроде chat, superchat, binary): xrtWsProtocolSelect(客户端提议, 服务端支持, &选中) (предложения клиента, поддержка сервера, &выбранный) берёт первый по порядку клиентских предпочтений, поддержанный и сервером — порядок чувствителен (выражение приоритетов клиента). Без пересечения — отказ (рукопожатие продолжается без подпротокола либо приложение отказывает — стратегия ваша).
Место обновления в конвейере
Пять слоёв последовательно — каждый слой это API предыдущих глав. Такова блочная структура тома 9: обновление — «последнее дело HTTP и первое дело WebSocket».
94.3示例
Первая полная программа: итерация предложений и генерация ответа
Программа ниже из examples/http/upgrade/main.c — полная обработка повторяемых полей, списков через запятую и форм версий:
#include <stdio.h> #include <xrt/http_upgrade.h> */ /* 解析重复字段并规范生成一个 Upgrade 选择列表。 */ int main(void) { static const xhttpfield Fields[] = { { XRT_STR_INIT("Upgrade"), XRT_STR_INIT("h2c, websocket") }, { XRT_STR_INIT("upgrade"), XRT_STR_INIT("HTTP/2.0") } }; static const xhttpupgradeitem Offered[] = { { XRT_STR_INIT("websocket"), { NULL, 0 } }, { XRT_STR_INIT("HTTP"), XRT_STR_INIT("2.0") } }; xhttpupgradefieldcursor Cursor; xhttpupgradeitem Upgrade; xhttpnext Next; char sOutput[64]; size_t iSize; xrtHttpUpgradeFieldCursorInit(&Cursor); while ( (Next = xrtHttpUpgradeFieldNext( Fields, 2u, &Cursor, &Upgrade )) == XHTTP_NEXT_ITEM ) { printf( "protocol: %.*s", (int)Upgrade.Protocol.Size, Upgrade.Protocol.Data ); if ( Upgrade.Version.Size != 0 ) { printf( "/%.*s", (int)Upgrade.Version.Size, Upgrade.Version.Data ); } putchar('\n'); } if ( (Next == XHTTP_NEXT_ERROR) || !xrtHttpUpgradeWrite( Offered, 2u, sOutput, sizeof(sOutput), &iSize ) ) { return 1; } printf("field: %.*s\n", (int)iSize, sOutput); return 0; }
Что только что произошло. (1) Вход — два поля Upgrade (имена в разном регистре, одно со списком через запятую) — итератор объединяет их в три предложения: h2c, websocket, HTTP/2.0 (форма protocol/version показана раздельно).Никакой ручной резки — повторяемые поля, запятые, регистр целиком обрабатывают FieldCursorInit/FieldNext. (2) UpgradeWrite пишет «поддержанное подмножество» (websocket, HTTP/2.0) как каноническое значение websocket, HTTP/2.0 — ответ пишет только выбранное множество, порядок из предоставленного списка. (3) Этот образец — чисто полевой уровень (без сети), юнит-тестовая форма семантики обновления; вставленный между RequestParse и построением ответа 101, он и есть полный серверный путь обновления.
Вторая полная программа: вектор Accept и согласование подпротоколов
Вторая программа из examples/websocket/handshake/main.c — стандартные векторы RFC и согласование по предпочтениям:
#include <stdio.h> #include <xrt.h> */ /* 展示无需网络对象的 RFC Accept 计算与子协议协商。 */ int main(void) { char Accept[XWS_ACCEPT_CAPACITY]; xstrview Selected; if ( !xrtWsAccept( XRT_STR_LITERAL("dGhlIHNhbXBsZSBub25jZQ=="), Accept, sizeof(Accept) ) ) { return 1; } if ( !xrtWsProtocolSelect( XRT_STR_LITERAL("chat, superchat"), XRT_STR_LITERAL("superchat, binary"), &Selected ) ) { return 2; } printf( "accept=%s protocol=%.*s\n", Accept, (int)Selected.Size, Selected.Data != NULL ? Selected.Data : "" ); return 0; }
Что только что произошло. (1) Входной nonce dGhlIHNhbXBsZSBub25jZQ== — канонический образец RFC 6455 §1.3; вычисленный xrtWsAccept-ом s3pPLMBiTxaQ9kYGzzhZRbK+xOo= посимвольно совпадает со стандартным вектором — совпадение и есть интероперабельное доказательство правильности реализации (векторы RFC главы 76, применённые вновь). (2) ProtocolSelect("chat, superchat", "superchat, binary") выбирает superchat — в порядке клиентских предпочтений (chat первым) сервер не знает chat; superchat — первый, поддержанный обеими сторонами. (3) Ноль сетевых объектов: вычислительный слой рукопожатия полностью автономен — тесты, прокси и отладчики употребляют его напрямую. Сопроводительные examples/websocket/upgrade (построение полного запроса рукопожатия) и upgrade_tour (четыре строки самопроверки: конфигурация/сверка запроса/привязка ответа/стыковка конфигурации потока) покрывают обе стороны полной API-поверхности рукопожатия.
94.4契约
- Семантика предложений: поля Upgrade повторяемы, значения — списки через запятую, имена регистронезависимы; итератор выдаёт структурированные предложения
protocol/version. - Семантика ответа:
UpgradeWriteпишет только выбранное подмножество; ответ 101 обязан повторитьConnection: Upgrade; атомарность ёмкости. - Вычисление Accept:
Base64(SHA1(Key + RFC 魔法串))(ключ + магическая строка RFC); ключ — Base64 от 16 случайных байтов (24 символа); само вычисление не секретно — это доказательство соответствия спецификации. - Привязка сверки ответа: клиент обязан проверить, что Accept соответствует отправленному им ключу, а выбранный протокол — из его предложений; против подмены.
- Согласование подпротоколов: порядок клиентских предпочтений ∩ поддержка сервера, берётся первый по клиентскому порядку; обработка пустого пересечения — политика приложения.
- Путь неудачного обновления: без совпадения — обычный HTTP-ответ (без переключения): обновление это предложение, а не требование.
- Обрезка:
HTTP_UPGRADEнезависимо; рукопожатие WebSocket зависит отCRYPTO_SHA1(вычисление Accept) и Base64. - Граница композиции: вверх RequestParse/FieldTokenFind (главы 90/92); вниз WsStreamAttach (глава 96).
Типичные ошибки
Симптом: клиент пропускает Accept, потому что «похоже на формат» (28 символов Base64) — промежуточное устройство пересоздаёт рукопожатие, и соединение принадлежит уже другой сессии.
Причина: сверка Accept — утверждение равенства: 服务端回的 Accept == 我用我的 Key 算的值 (возвращённый сервером Accept == значение, вычисленное из моего ключа). Проверка формата — не сверка.
if ( looks_like_base64(AcceptField, 28) ) { proceed(); /* "формат похож" - и хватит: пара ключ+Accept от человека-в-середине тоже пройдёт */ }
char Mine[XWS_ACCEPT_CAPACITY]; if ( !xrtWsAccept(MyKey, Mine, sizeof(Mine)) || !equal(AcceptField, Mine) ) { abort_handshake(); /* утверждение равенства провалено: это не моя сессия */ }
Симптом: сервер выбирает binary по порядку своего списка поддержки, хотя клиент явно хотел chat — результат согласования нарушает клиентский приоритет, поведение расходится.
Причина: RFC 6455 требует уважать порядок предложений клиента — он встроен в ProtocolSelect; свой цикл «обойти серверный список в поисках первого общего» переворачивает смысл.
for ( each server_proto ) { /* серверский порядок: нарушение протокола */ for ( each client_proto ) { if ( equal ) return server_proto; } }
xstrview Selected; if ( xrtWsProtocolSelect(ClientOffered, ServerSupported, &Selected) ) { /* клиентский порядок ∩ поддержка сервера: семантика протокола встроена */ }
Симптом: запрос только с Upgrade: websocket, но Connection: keep-alive — соединение переключили; клиент не просил обновления, состояние соединения перепутано.
Причина: обновление — совместная семантика двух полей: Upgrade говорит, на что переключиться, Connection: Upgrade — «я прошу переключить». Глядеть только на одно — решать за клиента.
if ( has_field(Fields, "Upgrade") ) { switch_protocol(); /* Connection не сказал upgrade: ошибочное переключение */ }
if ( xrtHttpFieldTokenFind(Fields, iCount, XRT_STR_LITERAL("upgrade")) && /* Connection содержит upgrade */ has_upgrade_websocket(Fields) ) { /* Upgrade предлагает websocket */ switch_protocol(); }
Практические задания
Прогоните образцы websocket/upgrade и handshake; командной строкой OpenSSL посчитайте Accept для того же ключа и сверьте с образцом. Критерии приёмки: Accept совпадает с вектором RFC; построенный запрос содержит все обязательные поля.
На массиве полей реализуйте is_upgrade_request(Fields, 数量, &提议协议) (Fields, количество, &предложенный протокол): Connection содержит upgrade (поиск токена) + итерация предложений Upgrade, возвращает — обновление ли и первое предложение. Проверьте на четырёх входах: стандартное обновление, без Connection, пустой список Upgrade, предложение не-websocket. Критерии приёмки: четыре вердикта сходятся с ручной выкладкой; всюду API семейства главы 92, ноль ручной резки.
Реализуйте accept_handshake(请求字段, 支持的子协议, 输出, 容量, &长度) (поля запроса, поддержанные подпротоколы, вывод, ёмкость, &длина): извлечь ключ, вычислить Accept, согласовать подпротокол, сгенерировать полный ответ 101 (строка состояния + поля Upgrade/Connection/Accept/Protocol). Проверьте сквозным образом запрос, построенный websocket/upgrade. Критерии приёмки: ответ проходит сверку upgrade_tour; без подходящего подпротокола деградация по политике (без поля Protocol); нехватка ёмкости — ноль записей.
Итоги
| Тема | Кратко |
|---|---|
| Универсальное обновление | предложения повторяемы/через запятую/регистронезависимы; итерация FieldCursorInit+FieldNext; ответ UpgradeWrite пишет только выбранное |
| Семантика 101 | ответ повторяет Connection: Upgrade; без совпадения - обычный HTTP-ответ (предложение, не требование) |
| Спецполя WS | Upgrade: websocket + Connection + Sec-WebSocket-Key + Protocol (опционально) |
| Вычисление Accept | Base64(SHA1(Key+魔法)) (ключ+магия); вектор RFC s3pPLMBiTxaQ9kYGzzhZRbK+xOo= |
| Сверка ответа | утверждение равенства с привязкой к собственному ключу + выбранный протокол в пределах предложений - проверка формата не сверка |
| Согласование подпротоколов | клиентский порядок ∩ поддержка сервера, первый по клиенту; пустое пересечение - политика приложения |
| Совместное определение | Connection содержит upgrade И Upgrade содержит предложение - оба поля обязательны |
| Место в конвейере | последнее дело HTTP, первое дело WebSocket; пять слоёв сцепляют главы 90->95 |
| Обрезка | HTTP_UPGRADE независимо; рукопожатие WS зависит от SHA1+Base64 |
📖 Справочники API в этой главе:http_upgrade (13 записей)websocket_upgrade (13 записей)websocket (86 записей)http (72 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки