ГлавнаяБлогРазбираем lhttp: протокол чата 2016 года, который до сих пор работает
Python

Разбираем lhttp: протокол чата 2016 года, который до сих пор работает

Погружение в lhttp — экспериментальный протокол чата поверх WebSocket. Узнайте, как он устроен, какие баги скрывает и стоит ли использовать сегодня. Попробуйте сами!

Al
Редакция Algolitalgolit.ru
10 мин чтения13 июля 2026 г.

Что такое lhttp и зачем это читать

Вы когда-нибудь задумывались, как работали протоколы реального времени до появления Socket.IO? lhttp — это эксперимент 2016 года: протокол, похожий на HTTP, но работающий поверх WebSocket, предназначенный для создания масштабируемых IM-сервисов. В этой статье мы клонируем, скомпилируем и запустим десятилетний проект на Go, найдём баги и проверим его производительность. Спойлер: он всё ещё работает, но не без сюрпризов.

Архитектура lhttp: как это устроено

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.

Сборка десятилетнего Go-проекта

Репозиторий использует 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 — это отладочный артефакт, который не удалили перед релизом.

Pub/sub работает как часы

Проверяем подписку и публикацию:

# Клиент 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 подписан на тот же канал, он получает копию собственного сообщения. Баг, который вылезет при первом же тестировании.

Производительность: 17 900 сообщений в секунду

Проверяем, насколько быстр 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 секунды. Для протокола, который разбирает строки посимвольно (без регулярок), это достойный результат.

Стоит ли использовать lhttp сегодня?

Честно — вряд ли. Библиотека golang.org/x/net/websocket устарела, а в демо-версии есть баг. Но как исторический артефакт lhttp показывает, как решали задачу реального времени до стандартизации Socket.IO и JSON-фреймов. И да, приятно осознавать, что код 2016 года всё ещё компилируется современным Go.

Практический вывод

Если вы пишете свой протокол или используете старый код — всегда тестируйте его в реальном окружении. Баги живут в документации и обработчиках годами. А если хотите попробовать lhttp сами — форкните репозиторий, запустите и найдите свои баги. Удачи!

#lhttp#WebSocket#Go#протоколы#история
Al
Редакция Algolit

Пишем про алгоритмы, подготовку к собеседованиям и карьеру в IT — так, чтобы было понятно и полезно.

Хочешь закрепить знания на практике?

Решай задачи на Algolit — интерактивная платформа для обучения

Начать бесплатно →