游戏已经上线了一年多了,现在正处于维护期,因此经常研究公司的底层架构和组件,研究透了后自己便会尝试自己写一个,而这个就是研究了相关组件后写的一个通讯组件啦,将通讯模块设计成组件的方案是支持导入到各个工程的,有需要可以自取。
demo访问地址: http://chatroom.lixifan.cn/index.html
源码地址: https://github.com/wiatingpub/chatRoom
技术栈
- springboot
- netty,通讯模块使用的是netty,采用的是websocket通讯协议
- docker,为了方便部署,这里采用的是docker的部署方式
- nginx,反向代理
- html&css&js,前段随便撸的一个东西,轻喷
设计亮点
- 通讯模块直接做成了组件,和实际工程分离,其他工程随导随用;
- 使用了协议分发机制,接收到客户端协议后会根据协议id自动找到对应facade下的method,通过反射触发;
- 新增了Api注解,工程启动时构建协议和对应method的映射,提供协议分发机制使用;
- 心跳机制监听,客户端定时发送ping协议包,如果异常断开,服务端一定时间没有接受到ping包后会断开连接;
界面表现
大概说说界面表现的东西,随便写的html页面。
1
登陆界面,这里开了两个端,输入昵称即可登陆,之后开始聊天
2下线后的表现如下
3下线后便监听到啦
ide如何启动
- idea导入chatRoom工程,点击File->Project Structure->点击Modules内的+导入lib下的socket组件
- 点击启动即可,监听的端口可以修改resources下的application.properties
- 客户端相关的放在chatRoomWeb下,直接访问即可
工程结构
image-20200614114451391协议设计
在proto协议包下可以看到有个抽象类AbstractMessage
image-20200614115619725这就是协议的基类,要求所有协议都需要去继承它,并且传入对应的协议id,而协议id由于是和具体工程绑定的东西,因此是放在工程目录下,我这边是采用了一个枚举实现,可以看到
image-20200614115820219协议分发机制的实现
Api 这是一个注解,主要作用是用于标注那些method要用来和客户端交互。
ApiInjectProcessor 协议分发机制的核心类,该核心类的主要作用是扫描工程中所有使用了Api注解的method,并将形式参数中的协议类取出组装成MessageBean后缓存起来,等后续分发器调用。
具体流程如下,首先先将源码贴出来下
image-20200614114650595可以看到该类继承了InstantiationAwareBeanPostProcessorAdapter,实际上InstantiationAwareBeanPostProcessorAdapter的作用在我旧的文章中有多处提及,在我们写组件的时候经常要用到的一个类,相当于在每个bean实例化过程中触发的一个埋点,可以看到我这边是实现了方法postProcessAfterInstantiation,该方法在bean实例化后被调用,调用的时候我这边会先去遍历类里边的方法,逐个判断是否实现了注解Api,如果有则遍历方法的形式参数,取出其中继承了AbstractMessage协议抽象类的参数,之后反射实例化该协议,并且和协议id、该方法所在的bean对象以及method自身组装成一个MessageBean后放入MessageDispatcher分发器中的id2MessageBeanMap中,提供后续调用。
MessageDispatcher 协议分发器,该类的作用是将对应的协议包通过反射的形式分发到对应的facade和method下执行,具体可以看dispatch方法
image-20200614121501536由于和客户端那边定下来的数据格式是以json形式进行的,因而我这边会先将拿到的数据转换成JSONObject的形式,目前使用的是fastjson,之后拿到里边字段为code的数据,通过该code从协议分发器中拿到对应的MessageBean并且通过反射的方式调用对应facade下的对应方法。
socket组件相关实现
直接开启动通讯组件的地方
image-20200614105606801Component注解就不多做说明了,看看SmartInitializingSingleton接口吧,该接口在实现组件方面非常受用,当所有单例bean都初始化完成以后,spring容器会回调该接口的方法afterSingletonsInstantiated,通过这种方式启动了通讯组件。
关于netty设置的参数直接看start接口的传参即可,各个参数都是从游戏中实际的情况出发设计的,有想了解的可以找我。
接下来看看我这边放入ChannelPipeline的情况,可以看到
image-20200614111516496HttpServerCodec 看源码不难发现,这是一个复杂类型的handler[CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>],通过其中的泛型也不难猜到这是一个对http协议包进行编码和解码的handler,而websocket本身就是一种建立在http协议上的一种机制,因此我们需要它;
HttpObjectAggregator 由于http协议get和post方式数据的存放方式是不一样的,而HttpServerCodec只能读取get请求,HttpObjectAggregator才可以处理post请求,因此我们需要它;
IdleStateHandler 主要用来处理心跳的一个机制,我这边的设计思路是服务器在60秒内内发现连接读空闲,则再移除对应的channel,当然了客户端要在60秒内定时推送ping数据过来,新手可能会问,是否只是new一个对象放入pipeline即可,no,too young too simple,这个handler主要作用是定时触发一个事件,而我们需要在其他handler内做监听,然后做我们的业务,这个后面再说;
AuthHandler 核心handler,可以看到我这里将ApplicationEventPublisher当做形参传了进去,ApplicationEventPublisher的主要作用了实现事件机制,由于AuthHandler 的实现有点意思,因此我这边将源码贴出详细说说
image-20200614111814629首先是继承了SimpleChannelInboundHandler接口,通过Inbound命名以及FullHttpRequest便可以知道该接口的作用是拦截客户端推送的http协议包,先看看channelRead0方法的实现,可以看到收到http协议请求后会调用ChannelManager.addChannel方法,该方法的实现如下
image-20200614112619190很明显,只是构建一个ChannelSession后放入sessionMap中保存,而为了支持并发,该map自然而然应该是并发包ConcurrentHashMap的形式,而ChannelManager便是管理channel的管理器,移除和添加以及激活都在该manager内进行。
现在看看具体方法userEventTriggered,上文提到的IdleStateHandler 便是在这里发挥了作用,在上文提到的60秒内如果对应客户端没有发数据包过来,则会调用ChannelManager.removeChannel方法,将该channel移除,并且抛出了一个LogoutEvent事件,我们可以反调该事件可以看到
image-20200614113006440有facade监听了该事件,并且在监听到后广播了一个协议出去,在上面界面的表现中也可以看到,当玩家离线后会有通知xxx已经下线,便是通过该种方式进行的。
WebSocketServerProtocolHandler 协议升级使用,也就是说将http协议升级成websocket协议,当升级成功后HttpServerCodec和HttpObjectAggregator都会被移除掉,继而替换成websocket相关的handler,因此我们需要它。
MessageHandler 核心handler,可以看到我这里又将ApplicationEventPublisher当做形参传了进去,
image-20200614113629192我们可以看到该handler是处理websocket协议包的,而最重要的方法是
image-20200614121312423该方法的作用便是通过协议包内的websocket数据调用协议分发器,让协议分发器通过对应的协议id激发对应的业务。
工程启动
直接启动ChatRoomApplication,该类配置了扫描com.nuofankj包下的类,启动后可以看到
image-20200614121853381对应协议已经录入协议分发器中啦,到此就可以通过web页面进行正常通讯了,至于web页面直接将chatRoomWeb包下的代码放到nginx或者apache下,然后访问即可。
image-20200614150536709
网友评论