cim github地址: https://github.com/crossoverJie/cim
- 第一篇: CIM-client 功能和设计分析
- 第二篇:CIM-router功能和设计分析
- 第三篇:CIM-server功能和设计分析
分析完了CIM-client,CIM-router后,最后分析下CIM-server就完整了。借用crossoverjie的架构图如下:
client与client通信都是通过router作为中介,相当于router作为中转站。一个client只要知道另外一个client与那个server连接起来,router就把消息发送该server。然后该server把消息写到client的channel里面去。
server端稍微简单点,直接进入主题。
1. 程序入口
public class CIMServerApplication implements CommandLineRunner{
private final static Logger LOGGER = LoggerFactory.getLogger(CIMServerApplication.class);
@Autowired
private AppConfiguration appConfiguration ;
@Value("${server.port}")
private int httpPort ;
// 正常启动
public static void main(String[] args) {
SpringApplication.run(CIMServerApplication.class, args);
LOGGER.info("启动 Server 成功");
}
// 把本地服务ip+prot 注册到ZK上
@Override
public void run(String... args) throws Exception {
//获得本机IP
String addr = InetAddress.getLocalHost().getHostAddress();
Thread thread = new Thread(new RegistryZK(addr, appConfiguration.getCimServerPort(),httpPort));
thread.setName("registry-zk");
thread.start() ;
}
}
public class RegistryZK implements Runnable {
@Override
public void run() {
//创建父节点
zKit.createRootNode();
//是否要将自己注册到 ZK
if (appConfiguration.isZkSwitch()){
String path = appConfiguration.getZkRoot() + "/ip-" + ip + ":" + cimServerPort + ":" + httpPort;
zKit.createNode(path);
logger.info("注册 zookeeper 成功,msg=[{}]", path);
}
}
}
- 以上主要是将自己注册到ZK中,作为服务被发现。
2. Server发送消息
server收到发送消息的router的请求,将http请求过来的消息发送给指定的client
@ApiOperation("服务端发送消息")
@RequestMapping(value = "sendMsg",method = RequestMethod.POST)
@ResponseBody
public BaseResponse sendMsg(@RequestBody SendMsgReqVO sendMsgReqVO){
BaseResponse res = new BaseResponse();
cimServer.sendMsg(sendMsgReqVO) ;
counterService.increment(Constants.COUNTER_SERVER_PUSH_COUNT);
SendMsgResVO sendMsgResVO = new SendMsgResVO() ;
sendMsgResVO.setMsg("OK") ;
res.setCode(StatusEnum.SUCCESS.getCode()) ;
res.setMessage(StatusEnum.SUCCESS.getMessage()) ;
res.setDataBody(sendMsgResVO) ;
return res ;
}
public void sendMsg(SendMsgReqVO sendMsgReqVO){
//获取到接受用户的channel
NioSocketChannel socketChannel = SessionSocketHolder.get(sendMsgReqVO.getUserId());
if (null == socketChannel) {
throw new NullPointerException("客户端[" + sendMsgReqVO.getUserId() + "]不在线!");
}
CIMRequestProto.CIMReqProtocol protocol = CIMRequestProto.CIMReqProtocol.newBuilder()
.setRequestId(sendMsgReqVO.getUserId())
.setReqMsg(sendMsgReqVO.getMsg())
.setType(Constants.CommandType.MSG)
.build();
ChannelFuture future = socketChannel.writeAndFlush(protocol);
future.addListener((ChannelFutureListener) channelFuture ->
LOGGER.info("服务端手动发送 Google Protocol 成功={}", sendMsgReqVO.toString()));
}
- 找到接受用户的channel,写入protocol就行。
3. channel和session的保存
public class SessionSocketHolder {
//userid ---> channel
private static final Map CHANNEL_MAP = new ConcurrentHashMap<>(16);
//userid --->username
private static final Map SESSION_MAP = new ConcurrentHashMap<>(16);
//保存用户消息
public static CIMUserInfo getUserId(NioSocketChannel nioSocketChannel){
for (Map.Entry entry : CHANNEL_MAP.entrySet()) {
NioSocketChannel value = entry.getValue();
if (nioSocketChannel == value){
Long key = entry.getKey();
String userName = SESSION_MAP.get(key);
CIMUserInfo info = new CIMUserInfo(key,userName) ;
return info ;
}
}
return null;
}
}
用两个map保存userid ---> channel和userid --->username的对应。这样方便快速查找。
4. CIMServerHandle
的处理
CIMServerHandle
主要处理client的登陆信息。
public class CIMServerHandle extends SimpleChannelInboundHandler {
/**
* 取消绑定
*
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
//可能出现业务判断离线后再次触发 channelInactive
CIMUserInfo userInfo = SessionSocketHolder.getUserId((NioSocketChannel) ctx.channel());
if (userInfo != null){
LOGGER.warn("[{}]触发 channelInactive 掉线!",userInfo.getUserName());
//remove SessionSocketHolder 里面保存的信息
userOffLine(userInfo, (NioSocketChannel) ctx.channel());
ctx.channel().close();
}
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
IdleStateEvent idleStateEvent = (IdleStateEvent) evt;
if (idleStateEvent.state() == IdleState.READER_IDLE) {
LOGGER.info("定时检测客户端端是否存活");
HeartBeatHandler heartBeatHandler = SpringBeanFactory.getBean(ServerHeartBeatHandlerImpl.class) ;
heartBeatHandler.process(ctx) ;
}
}
super.userEventTriggered(ctx, evt);
}
/**
* 用户下线
* @param userInfo
* @param channel
* @throws IOException
*/
private void userOffLine(CIMUserInfo userInfo, NioSocketChannel channel) throws IOException {
LOGGER.info("用户[{}]下线", userInfo.getUserName());
SessionSocketHolder.remove(channel);
SessionSocketHolder.removeSession(userInfo.getUserId());
//清除路由关系,清除router中保存的userid --> server的对应关系
clearRouteInfo(userInfo);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, CIMRequestProto.CIMReqProtocol msg) throws Exception {
LOGGER.info("收到msg={}", msg.toString());
if (msg.getType() == Constants.CommandType.LOGIN) {
//保存客户端与 Channel 之间的关系
SessionSocketHolder.put(msg.getRequestId(), (NioSocketChannel) ctx.channel());
SessionSocketHolder.saveSession(msg.getRequestId(), msg.getReqMsg());
LOGGER.info("客户端[{}]上线成功", msg.getReqMsg());
}
//心跳更新时间
if (msg.getType() == Constants.CommandType.PING){
NettyAttrUtil.updateReaderTime(ctx.channel(),System.currentTimeMillis());
//向客户端响应 pong 消息
CIMRequestProto.CIMReqProtocol heartBeat = SpringBeanFactory.getBean("heartBeat",
CIMRequestProto.CIMReqProtocol.class);
ctx.writeAndFlush(heartBeat).addListeners((ChannelFutureListener) future -> {
if (!future.isSuccess()) {
LOGGER.error("IO error,close Channel");
future.channel().close();
}
}) ;
}
}
}
- 主要接受client的注册,保存client的channel,方便server写入channe。
- 客户端的心跳是判断channel的当前时间-最后的读的时间是否大于给定的time,如果大于,则说明超时。则需要关闭客户端连接,清除userid--->channel,userid--->username的映射。然后通知router清楚userid--->server的映射。
总结
综上所述,server端的主要任务是完成注册,即保存userid--->channel的通道。待收到消息后,取出channel,往channel写入消息即可。在处理心跳的时候,当遇到读空闲的时候,判断当前时间-上次读时间是否大于预先设定的空闲时间,如果超了,则清除userid--->channel的缓存,userid--->username的缓存。并告知router下线。