CIM-server功能和设计分析

cim github地址: https://github.com/crossoverJie/cim

  • 第一篇: CIM-client 功能和设计分析
  • 第二篇:CIM-router功能和设计分析
  • 第三篇:CIM-server功能和设计分析
    分析完了CIM-client,CIM-router后,最后分析下CIM-server就完整了。借用crossoverjie的架构图如下:
    CIM-server功能和设计分析_第1张图片
    image.png

    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下线。

你可能感兴趣的:(CIM-server功能和设计分析)