转载

撸了一个通用的聊天室,召唤一起开发

游戏已经上线了一年多了,现在正处于维护期,因此经常研究公司的底层架构和组件,研究透了后自己便会尝试自己写一个,而这个就是研究了相关组件后写的一个通讯组件啦,将通讯模块设计成组件的方案是支持导入到各个工程的,有需要可以自取。

demo访问地址: http://chatroom.lixifan.cn/index.html

源码地址: https://github.com/wiatingpub...

技术栈

  • springboot
  • netty,通讯模块使用的是netty,采用的是websocket通讯协议
  • docker,为了方便部署,这里采用的是docker的部署方式
  • nginx,反向代理
  • html&css&js,前段随便撸的一个东西,轻喷

设计亮点

  • 通讯模块直接做成了组件,和实际工程分离,其他工程随导随用;
  • 使用了协议分发机制,接收到客户端协议后会根据协议id自动找到对应facade下的method,通过反射触发;
  • 新增了Api注解,工程启动时构建协议和对应method的映射,提供协议分发机制使用;
  • 心跳机制监听,客户端定时发送ping协议包,如果异常断开,服务端一定时间没有接受到ping包后会断开连接;

界面表现

大概说说界面表现的东西,随便写的html页面。

撸了一个通用的聊天室,召唤一起开发

登陆界面,这里开了两个端,输入昵称即可登陆,之后开始聊天

撸了一个通用的聊天室,召唤一起开发

下线后的表现如下

撸了一个通用的聊天室,召唤一起开发

下线后便监听到啦

ide如何启动

  • idea导入chatRoom工程,点击File->Project Structure->点击Modules内的+导入lib下的socket组件
  • 点击启动即可,监听的端口可以修改resources下的application.properties
  • 客户端相关的放在chatRoomWeb下,直接访问即可

工程结构

撸了一个通用的聊天室,召唤一起开发

协议设计

在proto协议包下可以看到有个抽象类AbstractMessage

撸了一个通用的聊天室,召唤一起开发

这就是协议的基类,要求所有协议都需要去继承它,并且传入对应的协议id,而协议id由于是和具体工程绑定的东西,因此是放在工程目录下,我这边是采用了一个枚举实现,可以看到

撸了一个通用的聊天室,召唤一起开发

协议分发机制的实现

Api这是一个注解,主要作用是用于标注那些method要用来和客户端交互。

ApiInjectProcessor协议分发机制的核心类,该核心类的主要作用是扫描工程中所有使用了Api注解的method,并将形式参数中的协议类取出组装成MessageBean后缓存起来,等后续分发器调用。

具体流程如下,首先先将源码贴出来下

撸了一个通用的聊天室,召唤一起开发

可以看到该类继承了InstantiationAwareBeanPostProcessorAdapter,实际上InstantiationAwareBeanPostProcessorAdapter的作用在我旧的文章中有多处提及,在我们写组件的时候经常要用到的一个类,相当于在每个bean实例化过程中触发的一个埋点,可以看到我这边是实现了方法 postProcessAfterInstantiation ,该方法在bean实例化后被调用,调用的时候我这边会先去遍历类里边的方法,逐个判断是否实现了注解Api,如果有则遍历方法的形式参数,取出其中继承了AbstractMessage协议抽象类的参数,之后反射实例化该协议,并且和协议id、该方法所在的bean对象以及method自身组装成一个MessageBean后放入MessageDispatcher分发器中的id2MessageBeanMap中,提供后续调用。

MessageDispatcher协议分发器,该类的作用是将对应的协议包通过反射的形式分发到对应的facade和method下执行,具体可以看dispatch方法

撸了一个通用的聊天室,召唤一起开发

由于和客户端那边定下来的数据格式是以json形式进行的,因而我这边会先将拿到的数据转换成JSONObject的形式,目前使用的是fastjson,之后拿到里边字段为code的数据,通过该code从协议分发器中拿到对应的MessageBean并且通过反射的方式调用对应facade下的对应方法。

socket组件相关实现

直接开启动通讯组件的地方

撸了一个通用的聊天室,召唤一起开发

Component注解就不多做说明了,看看SmartInitializingSingleton接口吧,该接口在实现组件方面非常受用,当所有单例bean都初始化完成以后,spring容器会回调该接口的方法afterSingletonsInstantiated,通过这种方式启动了通讯组件。

关于netty设置的参数直接看start接口的传参即可,各个参数都是从游戏中实际的情况出发设计的,有想了解的可以找我。

接下来看看我这边放入ChannelPipeline的情况,可以看到

撸了一个通用的聊天室,召唤一起开发

HttpServerCodec看源码不难发现,这是一个复杂类型的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 的实现有点意思,因此我这边将源码贴出详细说说

撸了一个通用的聊天室,召唤一起开发

首先是继承了SimpleChannelInboundHandler接口,通过Inbound命名以及FullHttpRequest便可以知道该接口的作用是拦截客户端推送的http协议包,先看看 channelRead0 方法的实现,可以看到收到http协议请求后会调用 ChannelManager.addChannel 方法,该方法的实现如下

撸了一个通用的聊天室,召唤一起开发

很明显,只是构建一个ChannelSession后放入sessionMap中保存,而为了支持并发,该map自然而然应该是并发包ConcurrentHashMap的形式,而ChannelManager便是管理channel的管理器,移除和添加以及激活都在该manager内进行。

现在看看具体方法 userEventTriggered ,上文提到的 IdleStateHandler 便是在这里发挥了作用,在上文提到的60秒内如果对应客户端没有发数据包过来,则会调用ChannelManager.removeChannel方法,将该channel移除,并且抛出了一个LogoutEvent事件,我们可以反调该事件可以看到

撸了一个通用的聊天室,召唤一起开发

有facade监听了该事件,并且在监听到后广播了一个协议出去,在上面界面的表现中也可以看到,当玩家离线后会有通知xxx已经下线,便是通过该种方式进行的。

WebSocketServerProtocolHandler协议升级使用,也就是说将http协议升级成websocket协议,当升级成功后HttpServerCodec和HttpObjectAggregator都会被移除掉,继而替换成websocket相关的handler,因此我们需要它。

MessageHandler核心handler,可以看到我这里又将ApplicationEventPublisher当做形参传了进去,

撸了一个通用的聊天室,召唤一起开发

我们可以看到该handler是处理websocket协议包的,而最重要的方法是

撸了一个通用的聊天室,召唤一起开发

该方法的作用便是通过协议包内的websocket数据调用协议分发器,让协议分发器通过对应的协议id激发对应的业务。

工程启动

直接启动ChatRoomApplication,该类配置了扫描 com.nuofankj 包下的类,启动后可以看到

撸了一个通用的聊天室,召唤一起开发

对应协议已经录入协议分发器中啦,到此就可以通过web页面进行正常通讯了,至于web页面直接将chatRoomWeb包下的代码放到nginx或者apache下,然后访问即可。

撸了一个通用的聊天室,召唤一起开发

原文  https://segmentfault.com/a/1190000023015226
正文到此结束
Loading...