Возможности и преимущества#
Матрица поддержки#
Все три версии протокола используют один и тот же API запросов: client.Do / client.DoStream с переиспользуемым client.Response, которым владеет вызывающий код. Сжатие тоже общее: ответы декодируются во всех четырёх кодировках — gzip, deflate, br и zstd (клиент отправляет accept-encoding: gzip, deflate, br, zstd), а Request.CompressBody сжимает тела запросов — см. Сжатие ниже. Различается лишь то, какую часть протокола открывает каждый транспорт.
| Протокол | Реализация | Конструкторы | Параллельные запросы в одном соединении | Пул соединений | Service discovery | Ключевые возможности |
|---|---|---|---|---|---|---|
| HTTP/1.1 | С нуля | NewH1Client, NewH1PoolClient, NewManagedH1Client | Нет — один обмен на соединение в каждый момент (без pipelining) | NewH1PoolClient (пул с эксклюзивной выдачей: MaxConnsPerHost и есть параллелизм запросов) | NewManagedH1Client (Resolver + Selector) | Keep-alive и переиспользование соединений; тела запросов передаются потоком (Request.BodyReader, chunked, если длина заранее неизвестна), но ответы всегда буферизуются в Response.Body — DoStream и BodyStream возвращают ошибку; цель ALPN-фолбэка: TransportALPN автоматически выбирает HTTP/1.1, если сервер не предлагает h2 |
| HTTP/2 | RFC 7540 + HPACK (RFC 7541), с нуля | NewSingleConnClient, NewPoolClient, NewManagedClient | Да — мультиплексирование потоков, ограниченное MAX_CONCURRENT_STREAMS | NewPoolClient (пул на хост, выбор наименее загруженного соединения, вытеснение простаивающих) | NewManagedClient (Resolver + Selector) | DoStream и трейлеры запроса; управление потоком (flow control); динамические SETTINGS; корректное завершение по GOAWAY; PING keepalive; server push (PUSH_PROMISE); приоритеты запросов; extended CONNECT (RFC 8441, WebSockets поверх H2); CONTINUATION; прокси-диалеры HTTP CONNECT; h2c prior knowledge |
| HTTP/3 | RFC 9114 + QUIC (RFC 9000/9001/9002) + QPACK (RFC 9204), с нуля | NewH3Client, NewH3PoolClient, NewManagedH3Client | Да — параллельные запросы в рамках одного QUIC-соединения | NewH3PoolClient (пул из нескольких соединений) | NewManagedH3Client | DoStream; динамический QPACK в обоих направлениях (кодирование и декодирование); все AEAD-шифры TLS 1.3 (AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305); контроль перегрузки — NewReno по умолчанию, BBR по выбору; на Linux: пакетная отправка через GSO, пакетный приём через GRO, ограниченная коалесценция ACK |
У HTTP/1.1 тот же набор конструкторов, что и у двух других версий, но его пул устроен иначе. В HTTP/1.1 нет мультиплексирования: одно соединение — это строго последовательные запросы, поэтому без пула клиент вообще не может генерировать нагрузку по HTTP/1.1. Пул выдаёт соединения в эксклюзивное пользование — один обмен на соединение в каждый момент, — так что MaxConnsPerHost и есть параллелизм запросов; MaxStreamsPerConn не применяется. Запрос, заставший все соединения занятыми, ждёт освобождения одного из них — ожидание ограничено контекстом запроса. Соединения поддерживаются живыми и переиспользуются; Connection: close, мёртвое соединение или ошибка обмена приводят к сбросу соединения и повторному подключению. Pipelining сознательно не реализован. Диалер не должен предлагать ALPN-токен h2 — используйте обычный TCP-диалер или TLS-диалер, у которого в NextProtos только "http/1.1".
Включение BBR для HTTP/3:
client.ClientOptions{
Transport: client.TransportH3,
H3ConnOptions: []quic.ConnOption{quic.WithCongestionControl(quic.CCBBR)},
}Сжатие#
Сжатие работает одинаково поверх HTTP/1.1, HTTP/2 и HTTP/3.
Ответы. Клиент отправляет accept-encoding: gzip, deflate, br, zstd и декодирует все четыре кодировки, используя пулы ридеров. Заголовок accept-encoding, заданный вызывающим кодом, имеет приоритет; Request.DisableDecompression отключает и заголовок, и декодирование. Защита от decompression bomb отклоняет тела, распухающие сверх MaxDecompressedSize (по умолчанию 10 MiB), с ошибкой ErrBodyTooLarge; окно zstd ограничено 8 MiB. Сопоставление Content-Encoding нечувствительно к регистру (RFC 9110 §8.4.1).
Запросы. Установите Request.CompressBody — клиент сожмёт тело и сам выставит content-encoding:
var resp client.Response
err := c.Do(ctx, &client.Request{
Method: "POST", Path: "/ingest",
Body: payload,
CompressBody: client.EncodingZstd, // client sets content-encoding itself
}, &resp)Принимаются EncodingGzip, EncodingDeflate, EncodingBrotli и EncodingZstd. Нулевое значение, EncodingIdentity, отправляет тело как есть — те, кто не включает сжатие, не платят ничего (0 аллокаций). Выставленный вручную content-encoding по-прежнему означает «это тело уже закодировано», и тело не трогается (RFC 9110 §8.4 — Content-Encoding описывает тело, а не предписывает действие). Одновременно заданные CompressBody и ручной content-encoding дают ошибку ErrConflictingContentEncoding. content-length для буферизованного тела — это сжатый размер; для потокового тела он опускается (HTTP/1.1 тогда использует chunked transfer-encoding).
Зачем poseidon#
Один клиент — три версии протокола. HTTP/1.1, HTTP/2 и HTTP/3 через единый API Do/DoStream. В стандартной библиотеке Go нет HTTP/3; большинство стеков прикручивают его отдельной библиотекой с отдельным API. Здесь переключить нагрузочный тест с h2 на h3 — это замена конструктора, а не переписывание.
Никакого стороннего протокольного кода. Каждый протокольный стек написан с нуля, в этом модуле: QUIC (RFC 9000/9001/9002), HTTP/3 (RFC 9114), QPACK (RFC 9204), фрейминг HTTP/2 (RFC 7540) с HPACK (RFC 7541) и HTTP/1.1. Ни quic-go, ни nghttp2, ни net/http, ни cgo. Хендшейк TLS 1.3 выполняет стандартный crypto/tls. Четыре прямые зависимости — это криптографические и компрессионные примитивы, взятые сознательно, а не написанные заново: golang.org/x/net, golang.org/x/crypto (защита пакетов ChaCha20-Poly1305), github.com/andybalholm/brotli и github.com/klauspost/compress (zstd). Переписывать Poly1305 или Brotli своими руками — риск для безопасности без всякой выгоды: Brotli требует статический словарь на 122 KB плюс 121 трансформацию, zstd от klauspost имеет за плечами годы фаззинга, а декомпрессор — первоочередная поверхность атаки. Граница проходит так: весь протокольный код наш и аудируется в одном модуле; криптографические и компрессионные примитивы заимствованы, потому что именно здесь заимствование — более безопасное инженерное решение.
Кодек без аллокаций. Весь проводной кодек работает на 0 B/op, 0 allocs/op для обеих версий протокола — HTTP/2 (кодирование и декодирование фреймов и HPACK) и HTTP/3 (разбор и сериализация QUIC-фреймов и заголовков пакетов, фреймы HTTP/3, секции полей QPACK) — и bench-гейт в CI роняет сборку при регрессии. При высоких частотах запросов в нагрузочном генераторе аллокации на каждый фрейм напрямую превращаются в давление на GC; этот кодек не добавляет ничего. Пакеты frame, hpack и qpack можно использовать отдельно. Честная оговорка: путь отправки QUIC-пакетов (сборка и шифрование исходящего пакета) аллоцирует мало, но не ноль — ноль аллокаций относится к кодеку, а не ко всему запросу.
Тонкий контроль. Прямой доступ к потокам, окнам flow control, SETTINGS, политике пулинга, контролю перегрузки (NewReno или BBR) и пейсингу — рычаги, которые net/http прячет за своим транспортом. Если инструменту нужно держать окно закрытым, зафиксировать число параллельных потоков или измерить эффект контроллера перегрузки — эти рычаги открыты.
Встроенные возможности для нагрузочного тестирования. Пул соединений, DNS service discovery (Resolver/Selector), опциональные ограниченные повторы идемпотентных запросов, ограничение частоты по алгоритму token bucket (WithRateLimit), хуки жизненного цикла (Client.Hooks) и метрики (Client.MetricsSnapshot(), Client.PoolStats()). Всё это общее для HTTP/1.1, HTTP/2 и HTTP/3 — настраивается один раз, а не для каждого протокола.
Проверено на соответствие спецификациям. Около 200 conformance-тестов, привязанных к конкретным разделам RFC и закреплённых в CI. Interop-матрица HTTP/3 из трёх серверов (Caddy/quic-go, nginx/C, aioquic/Python) работает поверх настоящего UDP. Парсеры проводного формата фаззятся. Весь набор тестов выполняется под -race.
Сравнение с net/http#
net/http — стандартный клиент «всё включено». Он без настройки обрабатывает редиректы, куки, прокси из окружения и согласование HTTP/1.1 + HTTP/2. poseidon меняет это удобство на контроль: добавляет HTTP/3, кодек без аллокаций и инструментарий для нагрузочного тестирования, но взамен требует создавать клиентов под каждую цель и самостоятельно управлять ответами. Нужен универсальный веб-клиент — берите net/http. Пишете нагрузочные генераторы или нужен HTTP/3 с тонким контролем — берите poseidon.
Сравнение с quic-go#
quic-go — зрелая и широко используемая библиотека QUIC и HTTP/3, покрывающая и сервер, и клиент. poseidon реализует QUIC заново, чтобы весь протокольный код оставался в этом модуле, и сосредоточен на нагрузочном тестировании. Он моложе и уже по охвату: это HTTP-клиент. Пакет quic при этом открывает серверную роль (Listen / Accept, приём потоков) — она существует, чтобы у клиента был настоящий пир в тестах, — но HTTP/3-сервера поверх неё нет. Если нужен проверенный в бою QUIC-стек или HTTP/3-сервер — устоявшийся выбор это quic-go.
Вне рамок 1.0#
Следующее сознательно не входит в этот релиз:
- 0-RTT / возобновление сессии. Клиент никогда его не инициирует.
- Миграция QUIC-соединения. Не инициируется.
- HTTP/3 server push. Не задействуется.
Если пир предлагает что-то из этого, клиент просто не вступает в обмен — ничего не ломается. Неподдерживаемый шифронабор TLS завершается чисто, типизированной ошибкой ErrCryptoSuite; без зависаний и без паник.