сервер шлюза
На самом деле, так называемые шлюзы, на самом деле, поддерживают плеер Client Connections, игрок отправит запрос на отправку удельных серверов сервера, имеет следующие функции:
- Долгосрочная работа, должна иметь высокую стабильность и производительность
- Открытый для внешнего мира, то есть клиенту нужно знать IP и порт шлюза для подключения.
- Поддержка нескольких протоколов
- Единая запись, в архитектуре может быть много внутренних служб, если единой записи нет, клиенту необходимо знать IP и порт каждой внутренней службы.
- Переадресация запросов, поскольку портал унифицирован, шлюз должен иметь возможность перенаправить запрос клиента в нужную службу, и необходимо обеспечить маршрутизацию.
- Обновление без распознавания, так как плеер подключен к серверу-шлюзу, пока соединение непрерывное; обновление внутреннего сервера не имеет смысла для игрока или имеет очень мало смысла (в зависимости от метода реализации)
- Business Agnostic (для игровых серверов шлюзов там неизбежно может быть немного бизнесом)
Для HTTP-запросов сам Micro Framework реализовал шлюз API, вы можете обратиться к предыдущему блогу
А вот для игровых серверов вообще требуются длинные ссылки, и нам нужно их реализовать самим
договор о подключении
Сам шлюз должен поддерживать несколько протоколов.Здесь я использую websocket в качестве примера, чтобы проиллюстрировать идею моего процесса реконструкции.Другие протоколы аналогичны. Сначала выберите библиотеку, обеспечивающую подключение через веб-сокет. Рекомендуется использовать.melody, на основеwebsocketБиблиотека, синтаксис очень прост, и сервер веб-сокетов можно реализовать с помощью нескольких строк кода. Причина, по которой нашей игре нужен шлюз веб-сокета, заключается в том, что клиент не поддерживает HTTP2 и не может напрямую подключаться к серверу grpc.
package main
import (
"github.com/micro/go-web"
"gopkg.in/olahol/melody.v1"
"log"
"net/http"
)
func main() {
// New web service
service := web.NewService(web.Name("go.micro.api.gateway"))
// parse command line
service.Init()
// new melody
m := melody.New()
// Handle websocket connection
service.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
_ = m.HandleRequest(w, r)
})
// handle connection with new session
m.HandleConnect(func(session *melody.Session) {
})
// handle disconnection
m.HandleDisconnect(func(session *melody.Session) {
})
// handle message
m.HandleMessage(func(session *melody.Session, bytes []byte) {
})
// run service
if err := service.Run(); err != nil {
log.Fatal("Run: ", err)
}
}
Переадресация запроса
Шлюз может получать или отправлять данные, а структура данных относительно однородна.[]byte
, разве это не похожеgrpc stream
, так что вы можете использоватьprotobuf
изoneof
Функции для определения запросов и ответов, см. предыдущий блог
Карточные игры с использованием заметки рефакторинга микросервисов (6): Protobuf скалолазание
определениеgateway.basic.proto
, классифицировать сообщения, полученные/отправленные шлюзом
message Message {
oneof message {
Req req = 1; // 客户端请求 要求响应
Rsp rsp = 2; // 服务端响应
Notify notify = 3; // 客户端推送 不要求响应
Event event = 4; // 服务端推送
Stream stream = 5; // 双向流请求
Ping ping = 6; // ping
Pong pong = 7;// pong
}
}
заreq
,notify
Все они являются запросами без сохранения состояния от клиента, соответствующими серверу без сохранения состояния на бэкэнде.Здесь вам нужно только реализовать свои собственные правила маршрутизации, такие как
message Req {
string serviceName = 1; // 服务名
string method = 2; // 方法名
bytes args = 3; // 参数
google.protobuf.Timestamp timestamp = 4; // 时间戳
...
}
- serviceName вызывает имя службы сервера rpc
- имя метода для вызова rpc-сервера
- Args Параметры вызова
- временная метка запроса, используемая клиентом для сопоставления и идентификации ответа сервера, имитирующего HTTP-запрос
req-rsp
идеи иmicro toolkit
Шлюз API аналогичен (процессор rpc), который относительно прост, вы можете обратиться к предыдущему блогу.
Наш проект использует http для таких запросов, и не проходит через этот шлюз, только некоторые основныеreq
,НапримерauthReq
иметь дело сsession
Сертификация. Основное соображение заключается в том, что HTTP прост, не имеет состояния, хорошо обслуживается, плюс к таким службам не предъявляются высокие требования к работе в реальном времени.
переадресация потока grpc
Игровые серверы, как правило, сохраняют состояние, являются двунаправленными и предъявляют высокие требования к работе в реальном времени.req-rsp
Режим не подходит, и шлюз нужно пробрасывать. Каждый раз, когда вы добавляете внутренний сервер grpc, вам нужно толькоoneof
Добавьте поток для расширения
message Stream {
oneof stream {
room.basic.Message roomMessage = 1; // 房间服务器
game.basic.Message gameMessage = 2; // 游戏服务器
mate.basic.Message mateMessage = 3; // 匹配服务器
}
}
И вам нужно определить соответствующий запрос маршрутизации для обработки того, на какой внутренний сервер перенаправлять (другие реализации могут не потребоваться), здесь потребуется немного бизнеса, например
message JoinRoomStreamReq {
room.basic.RoomType roomType = 1;
string roomNo = 2;
}
Здесь в соответствии с номером комнаты и типом комнаты, запрошенными маршрутизацией клиента, шлюз выбирает правильный сервер комнаты (и может даже ссылаться на более старую версию сервера комнаты).
После выбора правильного сервера установите поток двунаправленного потока
address := "xxxxxxx" // 选择后的服务器地址
ctx := context.Background() // 顶层context
m := make(map[string]string) // some metadata
streamCtx, cancelFunc := context.WithCancel(ctx) // 复制一个子context
// 建立stream 双向流
stream, err := xxxClient.Stream(metadata.NewContext(streamCtx, m), client.WithAddress(address))
// 存储在session上
session.Set("stream", stream)
session.Set("cancelFunc", cancelFunc)
// 启动一个goroutine 收取stream消息并转发
go func(c context.Context, s pb.xxxxxStream) {
// 退出时关闭 stream
defer func() {
session.Set("stream", nil)
session.Set("cancelFunc", nil)
if err := s.Close(); err != nil {
// do something with close err
}
}()
for {
select {
case <-c.Done():
// do something with ctx cancel
return
default:
res, err := s.Recv()
if err != nil {
// do something with recv err
return
}
// send to session 这里可以通过oneof包装告知客户端是哪个stream发来的消息
...
}
}
}(streamCtx, stream)
Пересылка относительно проста, иди прямо к коду
запрос на поток
message Stream {
oneof stream {
room.basic.Message roomMessage = 1; // 房间服务器
game.basic.Message gameMessage = 2; // 游戏服务器
mate.basic.Message mateMessage = 3; // 匹配服务器
}
}
Добавить код переадресации
s, exits := session.Get("stream")
if !exits {
return
}
if stream, ok := s.(pb.xxxxStream); ok {
err := stream.Send(message)
if err != nil {
log.Println("send err:", err)
return
}
}
Когда вам нужно закрыть поток, вам нужно только вызвать соответствующийcancelFunc
Просто
if v, e := session.Get("cancelFunc"); e {
if c, ok := v.(context.CancelFunc); ok {
c()
}
}
Преимущества использования oneOf
Поскольку унифицированный полученный запрос на вход, используяoneof
весь путьswitch case
, каждый добавляяreq
илиstream
Просто добавьте случай, код выглядит относительно простым и освежающим.
func HandleMessageBinary(session *melody.Session, bytes []byte) {
var msg pb.Message
if err := proto.Unmarshal(bytes, &msg); err != nil {
// do something
return
}
defer func() {
err := recover()
if err != nil {
// do something with panic
}
}()
switch x := msg.Message.(type) {
case *pb.Message_Req:
handleReq(session, x.Req)
case *pb.Message_Stream:
handleStream(session, x.Stream)
case *pb.Message_Ping:
handlePing(session, x.Ping)
default:
log.Println("unknown req type")
}
}
func handleStream(session *melody.Session, message *pb.Stream) {
switch x := message.Stream.(type) {
case *pb.Stream_RoomMessage:
handleRoomStream(session, x.RoomMessage)
case *pb.Stream_GameMessage:
handleGameStream(session, x.GameMessage)
case *pb.Stream_MateMessage:
handleMateStream(session, x.MateMessage)
}
}
Горячее обновление
Очень важно чтобы игра обновлялась постоянно без остановки Мои идеи будут представлены в будущем блоге можете обратить внимание на волну хехе
яма!
- Такой шлюз вроде бы и не проблема, но какое-то время работал
pprof
Наблюдение найдетgoroutine
и память медленно растут, т.е.goroutine leak!
, причина в том, что исходник микро не завершил поток закрытия при упаковке grpc, только получилio.EOF
Conn-соединение grpc будет закрыто только при возникновении ошибки
func (g *grpcStream) Recv(msg interface{}) (err error) {
defer g.setError(err)
if err = g.stream.RecvMsg(msg); err != nil {
if err == io.EOF {
// #202 - inconsistent gRPC stream behavior
// the only way to tell if the stream is done is when we get a EOF on the Recv
// here we should close the underlying gRPC ClientConn
closeErr := g.conn.Close()
if closeErr != nil {
err = closeErr
}
}
}
return
}
и есть TODO
// Close the gRPC send stream
// #202 - inconsistent gRPC stream behavior
// The underlying gRPC stream should not be closed here since the
// stream should still be able to receive after this function call
// TODO: should the conn be closed in another way?
func (g *grpcStream) Close() error {
return g.stream.CloseSend()
}
Решение также относительно простое: разветвить исходный код и изменить его на close conn одновременно с закрытием потока (наше дело возможно, потому что и клиент потока grpc, и сервер реализуют закрытый поток после получения ошибки), или ждать для автора, чтобы обновить Близко более научным способом
- сессия мелодии идет
get
иset
При чтении и записи данных будет соревнование по чтению и записи карты и паника, вы можете просмотреть ееissue, решение проще
учиться вместе
Недолго изучаю golang, micro, k8s, grpc, protobuf и другие знания.Если возникнут какие-то недопонимания прошу покритиковать и поправить меня.Вы можете добавить меня в WeChat для обсуждения и обучения вместе.