上一篇(游戏服务器之网关)说了一些网关大致功能,这次说说具体的实现.
网关需要与客户端保证连接。这里网关使用Netty4来做为网络通信框架。它也是目前在Java游戏服务器开发中,长连接使用最多的框架。
1,管理与客户端的连接
客户端连接到网关之后,并且验证过之后,我们需要把连接的channel和用户绑定起来,这样方便使用用户id查询到它和客户端的连接,就可以给客户端返回消息了。因为是需要管理所有的客户端连接。所以会涉及到多线程的操作。在每个连接验证成功之后,会在当前连接的channel之中添加用户id和channel的映射到一个map集合中。简单点这个map可以是ConcurrentHashMap,它是线程安全的。但是如果并发理大的话,由于ConcurrentHashMap使用了锁会产生大量的上下文切换。所以我们这里采用无锁的单线程模式实现。即同一个用户id的操作都会放到同一个线程中去执行,而不加锁。
2,消息的定义
这里说的消息定义是指客户端与服务器长连接的情况下,使用消息进行数据交互。一个消息代表客户端发出的一个请求,或服务器返回给客户端的一个响应。
由于服务器由网关和逻辑服务组成,所以消息又分为外部消息和内部消息,和客户端交互的叫外部消息,在服务器内部交互的叫内部消息。
外部请求消息的组成
包头信息:
- 消息Id:消息的唯一id,用于区分每个消息的逻辑意义,知道这个消息是干什么的
- 消息发送时间
- 是否压缩
- 是否加密
- 服务类型,这个主要用于分布式服务。
- crc32校验码,用于保证消息的完整性。
- 消息序列Id: 每个用户登陆之后,从给服务器发送第一条消息起,开始累计计数,叫序列id,Id主要用于服务器判断消息的唯一性,对消息做等幂处理。
包体信息
- 消息体,向服务器发送的请求内容,这个部分可以根据自己的业务需要自行设计,可以是json,也可以是由protobuf序列化组成的。而且这部分信息可以加密码,压缩。
外部返回消息组成
- 消息序列Id: 每个用户登陆之后,从给服务器发送第一条消息起,开始累计计数,叫序列id,Id主要用于服务器判断消息的唯一性,对消息做等幂处理。
- 消息Id:消息的唯一id,处理于区分每个消息
- 服务类型:返回消息的服务类型。
- 服务器发送时间
- 是否压缩
- 错误码:如果服务器处理的消息有错误,以错误码的形式返回给客户端,在这里定义返回的错误码
- 消息体,向服务器发送的请求内容,这个部分是由protobuf或json序列化组成的。
3,消息编码与解码
通信协议就是一份约定。在网络通信中,所有的数据都是以二进制的形式传输的,不管是客户端还是服务器端,在收到二进制的消息之后,都需要把二进制转化为明文的形式,以方便在代码里面使用。把明文数据转化为二进制的过程叫序列化的过程,把二进制转化为明文的过程中反序列化的过程,也叫编码和解码。由于我们使用的是netty的tcp协议,它是一种流协议,没有明确的界限,所以我们需要知道我们每次传输的数据的大小,每次收到消息后,只有得到整个包才能正解的反序列化出来。这就需要处理好断包和粘包的总理了。不过在服务器端,netty已帮我们实现好了,我们只需要配置一下就可以了。
请求协议编码解码格式:
数据总长度(short(2)) + 消息序列号(int(4)) + 消息发送时间(long(8)) +消息id(ishort(2)) + 服务类型(short(2)) + 是否压缩(1)+ 是否加密(1) + crc32 (32) + 消息体
数据总长度:等于所有的位置占用字节数的总和 = 2 + 4 + 8 + 2 + 2 + 1 + 1 + 32 + 消息体长度
返回协议的编码解码格式:
数据总长度(short(2)) + 消息序列号(int(4)) + 消息id(short(2)) + 服务类型(short(2))+ 服务器发送时间(8)+ 是否压缩(1) + 错误码(short(2)) + 消息体
数据总长度:等于所有的位置占用字节数的总和 = 2 + 4 + 2 + 2 + 8+ 1 + 2 + 消息体长度
网关消息向服务器转发
网关与业务服务之间是一种一对多的关系,当网关收到客户端消息的时候,需要把它转发或叫路由到业务服务上面,而且为了使业务服务可以动态扩展,当多个业务服务启动或关闭时,网关都应该能感应到。所以需要有一个服务发现的服务。为了统一框架,我们采用springcloud中的Eureka做为服务的注册和发现。每个服务都有一个服务类型和服务id,两者组成一个唯的标识符标记一个唯一服务。服务类型表示服务提供哪些功能,比如活动服务,但是有时候为了动态扩展,当服务压力过大时,一活动服务不能满足要求,可能需要启动两台活动服务,为了区别服务实例,需要一个服务id。所以同个服务类型,可以由多个服务id的服务组成,这样可以做动态负载。
为了提高性能,网关与业务服务之间采用异步的通信方式,而且为了防止客户端请求负载的波动,需要缓存部分请求。所以可以使用消息队列负责网关与服务器之间的通信,使用现成的消息中间件,可以减少对底层网络的关注,减少网络层的开发时间,减少bug的出现。消息队列还可以缓存消息,起到消峰的作用。还可以使用消息的订阅、发布功能,方便的解决网关与业务服务一对多的问题。
当网关收到客户端的消息时,需要知道消息是属于哪个服务类型的,根据服务类型,找到这个服务类型所有的服务实例,然后根据角色id% 实例数量路由消息到一个服务实例上。我使用阿里的Rockketmq做为消息中间件。网关向业务服务发送消息就是publish一个消息,需要一个publsh topic和唯一的tag, 这个tag可以标识一个唯一的消息,它由:prefix + messageId + serverType + serverId ,而业务服务在启动的时候,会监听这个服务处理的消息,监听同样的topic和tag。
业务服务向网关发送消息
当业务服务处理完请求之后,需要给网关返回结果,再由网关把结果返回给客户端。网关监听一个公开的topic ,它的tag就是网关实例Id。 这样就算有多个网关的话,也可以准确的返回消息。所以业务服务收到的消息中要携带消息来源的服务实例id.这样业务服务就可以publish topic到网关了。
内部消息的通信协议
网关与业务服务之间交互的消息格式编码与解码:
- roleId (long)
- userId (long)
- 消息序列号(int)
- 消息Id (short)
- 服务类型 (short)
- 消息发送时间(long)
- 消息来源的服务实例Id (short)
- 消息要到达的服务实例id (short)
- 错误码(short)
- ip字符串长度
- 客户端的ip地址 (string)
- 消息体(byte[])
消息的封装和具体实现在GitHub:https://github.com/youxijishu/xinyue-game-frame/tree/master/game-frame-server/game-network-message
3,消息转发和接收