一、自定义ProBuf解码器:处理半包问题,将Bytebuf数据包->POJO
* 1.读取长度,如果长度位数不够,则终止读取。 * 2.然后读取魔数,版本号等其他字段。 * 3.最后按照净长度读取内容。如果内容字节数不够,则恢复到之前的起始位置,然后终止读取。
二、自定义ProBuf编码器:处理半包问题,将POJO->Bytebuf数据包
* 1.写入字节码长度 * 2.写入其他字段,魔输,版本号等 * 3.写入POJO字节码内容
三、Protobuf消息格式设计
1.请求消息 2.应答消息 3.命令消息
四、登录流程
· 客户端编码:编码登录请求的Protobuf数据包编码。
· 服务器端解码:解码登录请求的Protobuf数据包解码。
· 服务器端编码:编码登录响应的Protobuf数据包编码。
· 客户端解码:解码登录响应的Protobuf数据包解码。
五、客户端
· ClientCommand模块:控制台命令收集器。
CommandContorller initCommandMap() 初始化菜单 startCommandThread() (1)connectFlag判断是否在连接,如果不在连接,启动连接线程,休眠命令线程。 (2)创建连接:设置连接监听器connectListener ----------------------------------- (1)获取通道线程,失败后10s重试连接 (2)成功后创建客户端会话session,绑定通道。 (3)成功后设置Connected。 (4)设置连接关闭监听器closeListener -------------------------------- (1)获取通道channel (2)根据SESSION_KEY获取通道中的session (3)关闭通道session.channel.close ·设置Connected=false ·关闭channel -------------------------------- (5)唤醒命令线程。 ----------------------------------- (3)根据输入指令处理业务(登录/登出/聊天)。 (4)发送POJO:根据业务场景将消息组装成Protobuf数据包。通过客户端发往服务器。 ClientSession 一、胶水类 (1)通过user,可以获得当前的用户信息。 (2)通过channel,可以向服务器端发送消息。 ·关闭通道:close() ·绑定通道:设置channel ·写数据帧:witeAndFlush(pkg) writeAndClose(pkg) 二、会话状态 (3)是否成功连接isConnected。 (4)是否成功登录isLogin。 三、绑定在通道上,将Session放入Channel。 · channel.attr=key:sessionKey value:this · 设置初始sessionId=-1 · 四、会话变量存储 map 五、登录成功 服务端登录成功,客户端处理如下: (1)获取session,设置SessionId (2)设置isLogin
· ProtobufBuilder模块:Protobuf数据包构造者。
· Sender模块:数据包发送器。LoginSender User + Session 1.生成Protobuf登录数据包。2.调用BaseSender发送。(从session中获取channel) ChatSender ChatMsg[user+time+from(userID)+FromNick]+[to(touid)+msgID+content+type]
· Handler模块:服务器响应处理器。
处理器 一、LoginResponceHandler处理器 (1)如果服务端登录成功,接到响应后保存session,设置登录成功状态。 (2)如果消息不是请求响应,传递到下一流水线。 (3)登录成功后,移除处理器,因为不需要在处理登录了。 同时,开启心跳处理器。 二、ChatMsgHandler处理器 (1)对消息类型类型进行判断,只处理聊天请求。 (2)如果是聊天消息,展示在控制台。
通道的容器属性
- Netty中的Channel通道类、HandlerContext上下文类。
- AttributeMap接口只有一个attr()方法,接收一个AttributeKey类型的key,返回一个Attribute类型的value。
- 按照Javadoc,AttributeMap实现必须是线程安全的。 channel实现了该接口。
public interface AttributeMap {
<T> Attribute<T> attr(AttributeKey<T> key);
}
- AttributeKey不是原始的key,而是一个key的包装类。保证了key的唯一性,在整个netty汇总,key必须唯一。
- Attribute不是原始的value,而是value的包装类。完成两个重要操作:设置-SET,取值-GET。
-
AttributeKey的创建: AttributeKey.valueOf(String) 创建完AttributeKey后,就可以通过通道完成key-value的设置和取值。 通过attr获取到Attribute,在设置值,常常是链式调用: channel.attr(AttributeKey) 获取Attribute后设置value: channel.attr(AttributeKey).set(会话实例) 取值: ServerSession session = ctx.channel().attr(SESSION_KEY).get(); 强调:AttributeKey一般定义为一个常量。
六、服务端
· Handler模块:客户端请求的处理。
LoginRequestHandler (1)非登录请求数据包传递到流水线下一站 (2)创建ServerSession [Session][1][sessionID] sessionID-uuid [Session][2][channel] channel (3)起异步任务:调度执行登录业务逻辑 为什么要再起异步任务,将handler和processer两个模块? 答: 在服务器端需要隔离EventLoop(Reactor)线程和业务线程。 业务线程处理时候,通常会涉及到一些比较耗时的处理。 如数据库操作,远程接口调用等。但是IO读写操作通常都是毫秒级。 也就是说,netty内部的IO操作和业务处理操作在时间上不在一个数量级。 netty的一个EventLoop实例会开启2倍CPU合数的内部线程。 通常情况下,一个netty服务器会有几万个连接通道。也就是说,这几个线程会负责几万个连接。 一个EventLoop中内部线程上任务是串行的。 如果一个业务处理器执行1000ms,最终结果是阻塞了其他几十万个连接的IO处理。会出现严重的性能问题。 如果隔离业务操作和EventLoop,如何实现? 答: 专门开辟一个独立的线程池,负责独立的异步任务处理队列。 对于耗时的业务操作封装成异步任务,并放入异步队列中去处理。 这样,服务端的性能会提升很多。
ChatRedirectHandler (1)判断是否为聊天请求 (2)判断是否登录,登录后可发消息 (3)开启异步消息转发,由ChatRedirectProcesser完成消息转发。
· Processer模块:以异步方式完成请求的业务逻辑处理。
LoginProcesser (1)密码验证,验证结果写入通道。 (2)验证通过,绑定服务端会话,加入在线用户列表。 -------------------- ·从ProtoMsg中获取user信息[uid+devId+token+nickName+platform] ·[Session][3][user] 放入user->session ·[Session][4][通道绑定] 通道绑定会话:channel.attr(SESSION_KEY).set(this) SessionMap加入SessionId:this ·设置服务端登录:isLogin = true --------------------
ChatRedirectProcesser (1)根据目标用户ID(userID),找出所有的服务器端的会话列表(session)。 (2)为每一个会话转发一份消息。 注:由于一个用户可能会有对个会话,所以发所有会话。
· Session模块:管理用户与通道的绑定关系。ServerSession 一、胶水类 每一个ServerSession对应一个客户端连接。 每一个ServerSession拥有一个Channel成员实例、一个User成员实例。 二、服务器会话 为了通道状态管理变得方便(连接channel和用户user),使用会话概念。 由于客户端和服务器分别有各自的通道,参数也不一致,因此使用两个会话类型:客户端会话,服务端会话。 三、导航关系 正向导航:会话-->通道 主要用于出站场景,通过会话将数据包写出到通道。 反向导航:通道-->会话 主要用于入站场景,通过通道获取会话,进行下一步业务处理。 四、唯一标识:sessionId
SessionMap 一、会话管理器 一台服务器需要接受几万/几十万的客户端连接。 每一条连接都对应一个ServerSession实例。 服务端需要对这些大量的ServerSession实例进行管理。 二、原理 内部有线程安全的ConcurrentHashMap的map。key:sessionId value:serversession
七、点对点单聊
客户端用户A登录 -->发送消息[userId=B:content] -->服务端接收-->服务端转发用户B-->客户端用户B收到消息
注:如果在不同客户端(桌面,移动端,网页),通过 userId 转发到多个会话。
客户端-ChatConsoleCommand public interface BaseCommand { void exec(Scanner scanner);//执行 String getKey();//命令KEY String getTip();//命令注释 }
八、心跳检测
网络假死现象:如果底层TCP连接已经断开,服务端并没有正常的关闭套接字,服务端会认为TCP连接仍然存在。客户端会显示已经断开。虽然客户端可以进行断线重连,但上一次连接状态在服务端被认为有效。服务端的资源(套接字上下文+接收发送缓冲区)得不到正确释放。每个连接会耗费CPU和内存。会使服务器越来越慢。
- 解决办法:客户端进行定时心跳检测,服务端进行定时空闲检测。
- 空闲检测:每隔一段时间,检测子通道是否有数据读写。如果有,正常。如果无,判定子通道为假死,关闭。
- 心跳检测:
-
* 服务端实现空闲检测,需要客户端定时发送心跳数据包(或者报文,或者消息)进行配合。 * 而且客户端发送心跳数据包的时间间隔需要远小于服务端的空闲检测时间间隔。 * 收到客户端心跳数据包后,可以会直接回复到客户端,让客户端也进行类似空闲检测。