Примечания по рефакторингу карточной игры с использованием микросервисов (8): сервер игрового шлюза

Go

сервер шлюза

На самом деле, так называемые шлюзы, на самом деле, поддерживают плеер Client Connections, игрок отправит запрос на отправку удельных серверов сервера, имеет следующие функции:

  • Долгосрочная работа, должна иметь высокую стабильность и производительность
  • Открытый для внешнего мира, то есть клиенту нужно знать IP и порт шлюза для подключения.
  • Поддержка нескольких протоколов
  • Единая запись, в архитектуре может быть много внутренних служб, если единой записи нет, клиенту необходимо знать IP и порт каждой внутренней службы.
  • Переадресация запросов, поскольку портал унифицирован, шлюз должен иметь возможность перенаправить запрос клиента в нужную службу, и необходимо обеспечить маршрутизацию.
  • Обновление без распознавания, так как плеер подключен к серверу-шлюзу, пока соединение непрерывное; обновление внутреннего сервера не имеет смысла для игрока или имеет очень мало смысла (в зависимости от метода реализации)
  • Business Agnostic (для игровых серверов шлюзов там неизбежно может быть немного бизнесом)

Для HTTP-запросов сам Micro Framework реализовал шлюз API, вы можете обратиться к предыдущему блогу

Примечания по рефакторингу с использованием микросервисов в карточных играх (2): Введение в Micro Framework: micro toolkit

А вот для игровых серверов вообще требуются длинные ссылки, и нам нужно их реализовать самим

договор о подключении

Сам шлюз должен поддерживать несколько протоколов.Здесь я использую 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.EOFConn-соединение 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 для обсуждения и обучения вместе.