前言
上文分析了我们这款SLG的架构,本章着重讲解我们的网络通信架构,由上文的功能分析我们可以得知,游戏的所有功能基本上属于非及时的通信机制,所以依靠HTTP短连接就能够基本满足游戏的通信需求。
当然,我们先撇开国战部分不说,因为国战部分我们正在优化开发最新版本,之前我们做的版本是想通过异步战斗的机制达到实时战斗效果,通过使用HTTP的请求机制,加上前端画面表现,让玩家感觉到即时战斗的感觉,既能在地图上能看到其他玩家的行进队列,又能进入城池多国混战。可惜的是,让异步战斗来实现实时战斗的效果,会产生很多问题,最终因为机制的问题而商议出必须优化一版再上线。所以目前所有的功能均通过HTTP实现,如果后期国战需要使用TCP长连接可以单独对国战部分使用TCP长连接实现。
通信框架
使用Netty
在开始设计通信机制时,就需要选择合适的通信框架,当然,我们也可以自己动手写底层通信的实现,不过在前人已有成熟框架的情况下,我们大而不必重复造轮子,由于在通信方面,我们并没有太多的个性化需求,因此基本上成熟的通信框架都能满足目前所需。选择框架,无非就是看它们的底层架构是否符合需求、资料是否齐全、API文档是否详细、以及成熟案例有多少。上文提到,可以使用HTTP通信协议的框架中,有Servlet、Spring、Struts、Mina和Netty等常见的通信框架,在这其中我选择了Netty,Servlet、Spring和Struts属于同一系列,他们的底层都是Servlet的实现,在Servlet3.0以前均是BIO通信模式,而Mina和Netty均属于基于Java NIO的通信框架,由于通信机制的不同,基于NIO的通信程序比基于BIO的通信程序能承受更多的并发连接,而在后者的框架选择中,其实并没有太多的谁好与不好,Mina和Netty底层都是Java NIO的封装,并且两者的底层框架也是大致一样(其作者其实就是一个人),选择Netty更多的是因为Netty的有更多的资料可查,遇到问题可能会更容易解决,并且我个人在同时使用过Mina和Netty的情况下,认为Netty的API更友好,使用起来更方便(个人感觉哈)。综合种种原因,我选择了Netty作为我的底层通信框架。
Netty的特点
选择了Netty,我们就应该明白Netty的一些特点,Netty具有以下特点:
1.异步、非阻塞、基于事件驱动的NIO框架
2.支持多种传输层通信协议,包括TCP、UDP等
3.开发异步HTTP服务端和客户端应用程序
4.提供对多种应用层协议的支持,包括TCP私有协议、HTTP协议、WebSocket协议、文件传输等
5.默认提供多种编解码能力,包括Java序列化、Google的ProtoBuf、二进制编解码、Jboss marshalling、文本字符串、base64、简单XML等,这些编解码框架可以被用户直接使用
6.提供形式多样的编解码基础类库,可以非常方便的实现私有协议栈编解码框架的二次定制和开发
7.经典的ChannelFuture-listener机制,所有的异步IO操作都可以设置listener进行监听和获取操作结果
8.基于ChannelPipeline-ChannelHandler的责任链模式,可以方便的自定义业务拦截器用于业务逻辑定制
9.安全性:支持SSL、HTTPS
10.可靠性:流量×××、读写超时控制机制、缓冲区最大容量限制、资源的优雅释放等
11.简洁的API和启动辅助类,简化开发难度,减少代码量
NIO
Netty是基于NIO的通信框架,为什么要使用NIO而不是用传统的BIO通信机制呢,因为在BIO的线程模型上,存在着致命缺陷,由于线程模型问题,接入用户数与服务端创造线程数是1:1的关系,也就是说每一个用户从接入断开连接,服务端都要创造一个与之对应的线程做处理,一旦并发用户数增多,再好配置的服务器也有可能会有因为线程开销问题造成服务器崩溃宕机的情况。除此之外,BIO的所有IO操作都是同步的,当IO线程处理业务逻辑时,也会出现同步阻塞,其他请求都要进入阻塞状态。
相反,NIO的通信机制可以很好地解决BIO的线程开销问题,NIO采用Reactor通信模式,一个Reactor线程聚合一个多路复用Selector,这个Selector可同时注册、监听、轮回上百个Channel请求,这种情况下,一个IO线程就可以处理N个客户端的同时接入,接入用户数与线程数为N:1的关系,并且IO总数有限,不会出现频繁上下文切换,提高了CPU利用率,并且所有的 IO 操作都是异步的,即使业务线程直接进行IO操作,也不会被同步阻塞,系统不再依赖外部的网络环境和外部应用程序的处理性能
Netty架构
Netty采用经典的MVC三层架构:
1.第一层:Reactor通信调度层,它由一系列辅助类组成,包括Reactor线程NioEventLoop 以及其父类、NioSocketChannel/NioServerSocketChannel 以及其父类、ByteBuffer 以及由其衍生出来的各种 Buffer、Unsafe 以及其衍生出的各种内部子类等。
2.第二层:职责链ChannelPipeLine,它负责调度事件在职责链中的传播,支持动态的编排职责链,职责链可以选择性的拦截自己关心的事件,对于其它IO操作和事件忽略,Handler同时支持inbound和outbound事件
3.第三层:业务逻辑编排层,业务逻辑编排层通常有两类:一类是纯粹的业务逻辑编排,还有一类是应用层协议插件,用于协议相关的编解码和链路管理,例如CMPP协议插件
基于Netty实现的HTTP Server
Netty其实更适合使用创建TCP长连接的Server,但是其也提供了HTTP的实现封装,我们也可以很容易的实现基于Netty的HTTP服务器。Netty实现HTTP服务器主要通过HttpResponseEncoder和HttpRequestDecoder来进行HTTP请求的解码以及HTTP响应的编码,通过HttpRequest和HttpResponse接口来实现对请求的解析以及对响应的构造。本节先描述整个处理流程,然后通过源码进行分享。
处理流程
使用Netty实现的HTTP Server的处理流程如下:
1.HttpServer接收到客户端的HttpRequest,打开Channel连接
2.pipeline中的HttpInHandler调用channelRead方法读取Channel中的ChannelHandlerContext和Object
3.channelRead中调用实现类HttpInHandlerImp中的处理,将请求按照Get或Post方式进行解析,并将数据转为ProtoMessage,然后转交给MsgHandler处理
4.MsgHandler将其封装为Message类添加到userid哈希的消息处理队列中,并对队列中的消息调用handle进行游戏的逻辑处理
5.在逻辑处理中,调用HttpInHandler的writeJSON方法构造并返回HttpResponse响应消息
6.HttpOutHandler截取消息并打印log日志
7.HttpResponse响应消息返回给客户端并断开Channel连接
整个流程的流程图如下:
HttpServer
HttpServer中负责创造并启动Netty实例,并绑定我们的逻辑Handler到pipeline,使请求进入我们自己的逻辑处理
1 | package com.kidbear._36.net.http; |
代码中首先使用NioEventLoopGroup构造boss线程和work线程,然后构造ServerBootstrap,来设置Server的一些属性,包括在pipeline中添加Http的编码解码以及逻辑处理相关类。通过调用该类的start方法即可启动此HTTP服务器,其中端口在配置文件中配置好,启动时从配置文件读取。
HttpInHandler
Http请求的处理器,绑定在pipeLine中,负责请求的解析与逻辑处理,代码如下:
1 | package com.kidbear._36.net.http; |
其中的实现方法我都将其分离出来为单独的类来处理,我这样做主要为了我以后能通过JSP热修复Bug(以后会讲到,通过JSP热加载的原理实现线上项目的热修复),分离出来的实现类代码如下:
1 | package com.kidbear._36.net.http; |
以上代码实现了使用Netty中封装的Http请求的解析类对消息进行Get或Post解析,并使用了Http相应的构造类对返回消息进行Http消息格式的构造。
MsgHandler
以上代码包含了Netty中的Get请求和Post请求的解析处理,请求消息以及响应消息的XXTea加密解密等。其中,服务器接受到请求后,会将请求交给一个消息处理类进行具体的消息处理,消息处理器MsgHandler的代码如下:
1 | package com.kidbear._36.net; |
以上代码包含handleMsg、handle和addMsg方法,msgMap中包含每个用户的userid哈希对应的消息处理队列,原本我的设想是在服务器启动时,调用MsgHandler的run方法启动消息处理,无限循环的遍历msgMap,来处理所有玩家的消息处理队列,请求接入时,直接添加消息到msgMap的相应玩家的消息队列,然后由这个run方法中的线程来处理所有的消息,后来考虑到效率问题,改为直接在HttpInHandler中调用handleMsg方法,直接处理消息请求。每个玩家分配一个消息队列来进行处理主要是为了考虑到单个玩家的并发请求的情况。hash使用ConcurrentMap主要是考虑到这个Map的并发使用情景,使用ConcurrentMap的桶锁机制可以让它在并发情境中有更高的处理效率。
Message
MsgHandle中使用的Message类是对消息的封装包括ProtoMessage和ChannelHandlerContext,代码如下:
1 | package com.kidbear._36.net; |
ProtoMessage
ProtoMessage是通信中对消息格式的封装,消息格式定义为:”{typeid:1,userid:1,data:{}}”,typeid代表游戏中接口的协议号,userid代表玩家id,data代表具体传输的数据,其代码如下:
1 | package com.kidbear._36.net; |
HttpOutHandler
绑定在pipeLine中,负责处理相应消息,其实响应消息的处理在HttpInHandler的writeJSON方法中已经完成,使用DefaultFullHttpResponse对响应消息进行Http格式构造,然后调用ChannelHandlerContext的write方法直接write到消息管道中,并且在完成消息传输后自动关闭管道。而HttpOutHandler则只是截取响应消息并进行log打印输出一下,然后继续调用super发送出去,其接口及实现类代码如下:
HttpOutHandler:
1 | package com.kidbear._36.net.http; |
HttpOutHandlerImp:
1 | package com.kidbear._36.net.http; |
总结
本章内容介绍我们的这款游戏的网络通信的处理方式,总体来说,对目前的策划需求,以及目前的用户量来说,这个通信框架已经能满足,但客观的说,这个网络架构还是存在很多问题的,比如通信使用JSON字符串,使得通信数据的大小没有得到很好地处理,如果使用ProtoBuffer这样高效的二进制数据传输会有更小的数据传输量。另外,通信完全采用Http通信,使得游戏中一些需要实时展示的效果只能通过请求——响应式来获取最新数据,比如游戏中的邮件、战报等功能,只能通过客户端的不断请求来获取到最新消息,实时效果通过非实时通信来实现,会有很多冗余的请求,浪费带宽资源,如果以后玩家数量太多,对网络通信这块,我们肯定还会再进行优化。
下章内容,我们会对游戏中的数据缓存与存储进行介绍。
原文:http://hjcenry.github.io/2016/08/27/SLG%E6%89%8B%E6%B8%B8Java%E6%9C%8D%E5%8A%A1%E5%99%A8%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%BC%80%E5%8F%91%E2%80%94%E2%80%94%E7%BD%91%E7%BB%9C%E9%80%9A%E4%BF%A1/
转载于:https://blog.51cto.com/shamrock/1852851