Real-time сообщения
Сообщения о событиях, доставленные клиенту в кратчайшие сроки после возникновения
Решения
Centrifugo
Мотивация
>>> import hmac
>>> from hashlib import sha256
>>> h = hmac.new(b"secret", digestmod=sha256)
>>> h.update("42".encode())
>>> h.hexdigest()
'93c121e7aa437a1e01e3c512c6f0ce3c821a839025dca4408f85616de4aaee70'
HMAC example
Передаем с клиента "42" и получившуюся подпись, затем на стороне сервера проверяем подлинность полученных данных, повторив то же самое и сверив подписи
Генерируем на бекенде:
Особенности
- Language-agnostic
- Простая интеграция, не нужно переписывать бек
- Маcштабируется с помощью Redis
- Performance по сравнению с динамическими языками
- Восстановление пропущенных сообщений
- Presence информация, join/leave события
- Кроссплатформенность
- Бесплатно, MIT лицензия
Ограничения
- Только односторонний поток сообщений
- Нет нативной аутентификации и авторизации
- Нет тесной интеграции с бекендом и хуков (решаемо)
- Не подходит для динамичных мультиплеер-игр
- Большинство пользователей не знают Go
Библиотека Centrifuge
Зачем библиотека?
Ограничения
Только односторонний поток сообщенийНет нативной аутентификации и авторизацииНет тесной интеграции с бекендом и хуков (решаемо)Не подходит для динамичных мультиплеер-игрБольшинство пользователей не знают Go
Форматы протокола
JSON-streaming
{"id": 1, "method": "subscribe", "params": {"channel": "ch1"}}
{"id": 2, "method": "subscribe", "params": {"channel": "ch2"}}
Protobuf
0a 0a 32 08 08 07 10 00 18 1a 20 00 0a 08 22 06 08 02 10 19 18 03
0a 09 22 07 08 02 10 a2 03 18 20 0a 09 22 07 08 02 10 8d 02 ...
Protobuf vs JSON
BenchmarkJsonMarshal-8 500000 2980 ns/op 1223 B/op 9 allocs/op
BenchmarkJsonUnmarshal-8 500000 3120 ns/op 463 B/op 7 allocs/op
BenchmarkProtobufMarshal-8 2000000 901 ns/op 200 B/op 7 allocs/op
BenchmarkProtobufUnmarshal-8 2000000 692 ns/op 192 B/op 10 allocs/op
JSON: {"type":"ping","time":123456789} = 32 bytes
Protobuf: 0A 05 08 95 9A EF 3A = 7 bytes
Protobuf
Транспорты
Websocket
Но уже не из-за поддержки браузерами
SockJS
- xhr(xdr)-streaming
- eventsource
- iframe-eventsource
- iframe-htmlfile
- xhr(xdr)-polling
- iframe-xhr-polling
- jsonp-polling
SockJS fallbacks
GRPC
GRPC bidirectional streaming
service Centrifuge {
rpc Communicate(stream Command) returns (stream Reply) {}
}
message Command {
uint32 id = 1;
uint32 method = 2;
bytes params = 3;
}
message Reply {
uint32 id = 1;
Error error = 2;
bytes result = 3;
}
server {
listen 443 ssl http2;
ssl_certificate cert.pem;
ssl_certificate_key key.pem;
proxy_buffering off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Scheme $scheme;
proxy_set_header Host $http_host;
location /proto.Centrifuge {
grpc_pass grpc://localhost:8001;
}
location /connection/websocket {
proxy_pass http://localhost:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
Nginx: Websocket + GRPC
QUIC ?
Возможно когда-нибудь...
Prometheus
(+ Graphite в Centrifugo)
Метрики
Аутентификация
func authMiddleware(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
credentials := ¢rifuge.Credentials{
UserID: "42",
Exp: time.Now().Unix() + 600,
Info: []byte(`{"name": "Alexander"}`),
}
ctx = context.WithValue(
ctx,
centrifuge.CredentialsContextKey,
credentials,
)
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
})
}
Аутентификация
authInterceptor := func(
srv interface{},
ss grpc.ServerStream,
info *grpc.StreamServerInfo,
handler grpc.StreamHandler
) error {
ctx := ss.Context()
newCtx := context.WithValue(
ctx,
centrifuge.CredentialsContextKey,
¢rifuge.Credentials{
UserID: "42",
Exp: time.Now().Unix() + 10,
Info: []byte(`{"name": "Alexander"}`),
},
)
wrapped := grpc_middleware.WrapServerStream(ss)
wrapped.WrappedContext = newCtx
return handler(srv, wrapped)
}
Возможности протокола
PUB/SUB
var centrifuge = new Centrifuge("ws://example.com/websocket");
centrifuge.subscribe("channel", function(message){
console.log(message);
});
centrifuge.connect();
Асинхронное двунаправленное общение
var centrifuge = new Centrifuge("ws://example.com/websocket");
centrifuge.on("connect", function(){
centrifuge.send({"text": "hello"});
});
centrifuge.on("message", function(data) {
console.log(data);
});
centrifuge.connect();
RPC
var centrifuge = new Centrifuge("ws://example.com/websocket");
centrifuge.rpc({"method": "say_hi"}).then(function(data){
console.log(data);
});
centrifuge.connect();
PRESENCE
JOIN and LEAVE
RECOVER
PING-PONG
REFRESH
Оптимизации
- Производительный язык (Go)
- Слияние сообщений для экономии на syscall'ах
- Операции в Redis с помощью LUA для экономии RTT
- Buffer pool для JSON, Gogoprotobuf для Protobuf
- Неблокирующая отправка сообщения в канал
- Батчинг сообщений со стороны клиента
> XADD mystream * payload "hello"
1506871964177.0
> XRANGE mystream 1506871964175.0 +
1) 1) 1506871964177.0
2) 1) "payload"
2) "hello"
> XREAD BLOCK 5000 STREAMS mystream $
Redis STREAMs?
Почему все не так радужно?
Клиенты
Бенчмарки
Спасибо за внимание!
Присоединяйтесь!
Centrifugo and Centrifuge
By Emelin Alexander
Centrifugo and Centrifuge
- 2,849