Погружение в lhttp — экспериментальный протокол чата поверх WebSocket. Узнайте, как он устроен, какие баги скрывает и стоит ли использовать сегодня. Попробуйте сами!
Вы когда-нибудь задумывались, как работали протоколы реального времени до появления Socket.IO? lhttp — это эксперимент 2016 года: протокол, похожий на HTTP, но работающий поверх WebSocket, предназначенный для создания масштабируемых IM-сервисов. В этой статье мы клонируем, скомпилируем и запустим десятилетний проект на Go, найдём баги и проверим его производительность. Спойлер: он всё ещё работает, но не без сюрпризов.
lhttp — это текстовый протокол, стилизованный под HTTP, который работает поверх постоянного WebSocket-соединения. В отличие от HTTP, соединение не закрывается после ответа, что позволяет серверу отправлять сообщения клиентам в любой момент. Когда серверов несколько, они общаются через NATS (точнее, через встроенную копию gnatsd). Каждый сервер подписывается на общую шину сообщений, поэтому клиент, подключённый к серверу 1, может получить сообщение от клиента сервера 3.
Пример протокола на проводе:
LHTTP/1.0 chat
content-type:json
publish:channel_jack
{"to":"jack","message":"hello jack"}Это команда, заголовки, пустая строка и тело — знакомо всем, кто когда-либо вручную отправлял HTTP-запросы через telnet.
Репозиторий использует Godeps и папку vendor/ с зависимостями, замороженными в 2016 году. В Godeps.json указана версия Go 1.6. Удивительно, но Go 1.24 собрал проект без единой ошибки:
export GOPATH=/some/path/gopath
mkdir -p "$GOPATH/src/github.com/fanux"
ln -s /path/to/lhttp "$GOPATH/src/github.com/fanux/lhttp"
cd "$GOPATH/src/github.com/fanux/lhttp"
GO111MODULE=off go build .Демо-сервер (websocketServer/test.go) и статический файловый сервер также скомпилировались без изменений. Единственная археологическая находка: в README указан порт 8081, а в исходниках — :8581. Ошибка в документации живёт уже почти десять лет.
Запускаем три процесса:
./gnatsd -p 4222 & # шина сообщений
./lhttp_server & # чат-сервер на :8581
./demo_server & # веб-интерфейс на :9090Подключаемся через Python и отправляем сырое сообщение:
import websocket
ws = websocket.create_connection("ws://localhost:8581/ws")
ws.send("LHTTP/1.0 chat\r\n\r\nHello from the blog experiment!")
print(ws.recv())
# Вывод: 'LHTTP/1.0 auth\r\ncontent-type:image/png\r\n\r\nHello from the blog experiment!'Отправили текст с командой chat, а получили ответ с командой auth и заголовком content-type:image/png. Очевидно, что обработчик ChatProcessor помечает каждый ответ как PNG — это отладочный артефакт, который не удалили перед релизом.
Проверяем подписку и публикацию:
# Клиент A подписывается на комнату
ws_a.send("LHTTP/1.0 subpub\r\nsubscribe:room1\r\n\r\n")
# Клиент B публикует сообщение
ws_b.send("LHTTP/1.0 subpub\r\npublish:room1\r\n\r\nHey room1, anyone here?")
# Клиент A получает:
print(ws_a.recv())
# 'LHTTP/1.0 subpub\r\npublish:room1\r\n\r\nHey room1, anyone here?'Фан-аут через NATS работает чисто — хороший строительный блок.
В демо-чате используется команда chat: при загрузке страницы клиент подписывается на канал chatroom, а при отправке публикует в него же. Воспроизведём это двумя соединениями:
jack.send("LHTTP/1.0 chat\r\nsubscribe:chatroom\r\n\r\n")
mike.send("LHTTP/1.0 chat\r\nsubscribe:chatroom\r\n\r\n")
jack.send("LHTTP/1.0 chat\r\npublish:chatroom\r\n\r\nyo mike, lhttp works!")Jack получает два сообщения, Mike — одно. Почему? Обработчик chat сначала отправляет персональный ответ (тот самый с PNG), а затем публикует сообщение в NATS. Поскольку Jack подписан на тот же канал, он получает копию собственного сообщения. Баг, который вылезет при первом же тестировании.
Проверяем, насколько быстр lhttp:
import time
start = time.time()
for i in range(10000):
ws.send(f"LHTTP/1.0 chat\r\n\r\nmsg-{i}")
for i in range(10000):
ws.recv()
print(f"Time: {time.time() - start:.3f} sec")
# Time: 0.558 sec (около 17900 msg/s)10 000 запросов-ответов через один WebSocket из однопоточного Python-клиента — 0.558 секунды. Для протокола, который разбирает строки посимвольно (без регулярок), это достойный результат.
Честно — вряд ли. Библиотека golang.org/x/net/websocket устарела, а в демо-версии есть баг. Но как исторический артефакт lhttp показывает, как решали задачу реального времени до стандартизации Socket.IO и JSON-фреймов. И да, приятно осознавать, что код 2016 года всё ещё компилируется современным Go.
Если вы пишете свой протокол или используете старый код — всегда тестируйте его в реальном окружении. Баги живут в документации и обработчиках годами. А если хотите попробовать lhttp сами — форкните репозиторий, запустите и найдите свои баги. Удачи!
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →