百度做公司网站有用吗,西安网站手机网站建设,怎么用手机创建网页,国内做航模比较好的网站网关服务器 所谓网关#xff0c;其实就是维持玩家客户端的连接#xff0c;将玩家发的游戏请求转发到具体后端服务的服务器#xff0c;具有以下几个功能点#xff1a; 长期运行#xff0c;必须具有较高的稳定性和性能对外开放#xff0c;即客户端需要知道网关的IP和端口其实就是维持玩家客户端的连接将玩家发的游戏请求转发到具体后端服务的服务器具有以下几个功能点 长期运行必须具有较高的稳定性和性能对外开放即客户端需要知道网关的IP和端口才能连接上来多协议支持统一入口架构中可能存在很多后端服务如果没有一个统一入口则客户端需要知道每个后端服务的IP和端口请求转发由于统一了入口所以网关必须能将客户端的请求转发到准确的服务上需要提供路由无感更新由于玩家连接的是网关服务器只要连接不断更新后端服务器对玩家来说是无感知的或者感知很少根据实现方式不同业务无关对于游戏服务器网关不可避免的可能会有一点业务对于http请求来说micro框架本身已经实现了api网关可以参阅之前的博客 牌类游戏使用微服务重构笔记二: micro框架简介:micro toolkit 但是对于游戏服务器,一般都是需要长链接的需要我们自己实现 连接协议 网关本身应该是支持多协议的这里就以websocket举例说明我重构过程中的思路其他协议类似。首先选择提供websocket连接的库 推荐使用melody基于websocket库语法非常简单数行代码即可实现websocket服务器。我们的游戏需要websocket网关的原因在于客户端不支持HTTP2,不能和grpc服务器直连 package mainimport (github.com/micro/go-webgopkg.in/olahol/melody.v1lognet/http
)func main() {// New web serviceservice : web.NewService(web.Name(go.micro.api.gateway))// parse command lineservice.Init()// new melodym : melody.New()// Handle websocket connectionservice.HandleFunc(/, func(w http.ResponseWriter, r *http.Request) {_ m.HandleRequest(w, r)})// handle connection with new sessionm.HandleConnect(func(session *melody.Session) {})// handle disconnectionm.HandleDisconnect(func(session *melody.Session) {})// handle messagem.HandleMessage(func(session *melody.Session, bytes []byte) {})// run serviceif err : service.Run(); err ! nil {log.Fatal(Run: , err)}
}
复制代码请求转发 网关可以收取或发送数据并且数据结构比较统一都是[]byte这一点是不是很像grpc stream因此就可以使用protobuf的oneof特性来定义请求和响应可参照上期博客 牌类游戏使用微服务重构笔记六: 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; // pingPong pong 7;// pong}
}
复制代码对于req、notify都是客户端的无状态请求对应后端的无状态服务器这里仅需要实现自己的路由规则即可比如 message Req {string serviceName 1; // 服务名string method 2; // 方法名bytes args 3; // 参数google.protobuf.Timestamp timestamp 4; // 时间戳...
}
复制代码serviceName 调用rpc服务器的服务名method 调用rpc服务器的方法名args 调用参数timestamp 请求时间戳用于客户端对服务端响应做匹配识别模拟http请求req-rsp思路与micro toolkit的api网关类似(rpc 处理器)比较简单可参照之前的博客。 我们的项目对于此类请求都走http了并没有通过这个网关, 仅有一些基本的req比如authReq处理session认证。主要考虑是http简单、无状态、好维护再加上此类业务对实时性要求也不高。 grpc stream转发 游戏服务器一般都是有状态的、双向的、实时性要求较高req-rsp模式并不适合,就需要网关进行转发。每添加一种grpc后端服务器仅需要在oneof中添加一个stream来拓展 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;
}
复制代码这里根据客户端的路由请求的房间号和房间类型网关来选择正确的房间服务器甚至可能链接到旧版本的房间服务器上 选择正确的服务器后建立stream 双向流 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) {// 退出时关闭 streamdefer 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 cancelreturndefault:res, err : s.Recv()if err ! nil {// do something with recv errreturn}// send to session 这里可以通过oneof包装告知客户端是哪个stream发来的消息...}}
}(streamCtx, stream)
复制代码转发就比较简单了直接上代码 对于某个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}
}
复制代码当需要关闭某个stream时, 只需要调用对应的cancelFunc即可 if v, e : session.Get(cancelFunc); e {if c, ok : v.(context.CancelFunc); ok {c()}
}
复制代码使用oneOf的好处 由于接收请求的入口统一使用oneof就可以一路switch case,每添加一个req或者一种stream只需要添加一个case, 代码看起来还是比较简单、清爽的 func HandleMessageBinary(session *melody.Session, bytes []byte) {var msg pb.Messageif err : proto.Unmarshal(bytes, msg); err ! nil {// do somethingreturn}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!原因在于 micro源码在包装grpc时没有对关闭stream完善只有收到io.EOF的错误时才会关闭grpc的conn连接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 ClientConncloseErr : 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()
}
复制代码解决方法也比较简单,自己fork一份源码改一下关闭stream的时候同时关闭conn我们的业务是可以的因为在grpc stream客户端和服务端均实现收到err后关闭stream或者等作者更新用更科学的方式关闭 melody的session在get和set数据时会发生map的读写竞争而panic可以查看issue解决方法也比较简单一起学习 本人学习golang、micro、k8s、grpc、protobuf等知识的时间较短如果有理解错误的地方欢迎批评指正,可以加我微信一起探讨学习