打造完美聊天体验:服务器、客户端、控制台。

摘要

我写这篇文章,本来只是为了挣点积分。但写完后,我发现这个东西值得分享,让更多人受益。如果我能得到一些学员MM的崇拜,那就更好了。我还想把我的收款二维码挂上去,每人1块钱,一天10000元,希望大家支持我。

正文

实战演练即时聊天工具,一文说搞清楚:闲聊网络服务器 闲聊手机客户端 Web管理方法控制面板。

一、序言

  坦白说,写这一玩意就是我上星期刚造成的念头,本想写完后把编码挂上来挣点積分也非常好。写完后发觉这东西非常值得写一篇文章,授人予鱼比不上授之以渔嘛(他们是那么说的吧),顺带挣点应届生学员MM的崇拜那么就更妙了。随后再挂一个收款二维码,一个人1块钱,一天10000本人支付,一个月三十万,一年360万。。。可了了不得,离一个亿的个人目标就差几十年了。

  不清楚博客园对说梦话是否有限定,有得话请告之,我能尽早删掉以上文本。

  那麼如今返回实际中,这篇博闻假如能有>两个评价,我事后会再出一个Netty有关的栏目。不然,也不出了。有些人会好奇心,为何把阀值界定成>2呢?不为什么,由于我毫无疑问会先我用媳妇的号留个言,随后用自身的号留个言。

  好啦,废话不多说了,后边也有许多事情呢,刷碗、煮饭、洗碗、跪搓衣。。。好啦,大破冲霄楼吧。

二、最后实际效果

  为何首先看最后实际效果?由于此时编码早已撸完了。更关键的是大家带上感观的总体目标去开展事后的剖析,能够 能够更好地了解。文章标题中提及了,全部工程项目包括三个一部分:

1、闲聊网络服务器

  闲聊网络服务器的岗位职责一句话表述:承担接受全部客户推送的信息,并将信息发送给总体目标客户。

  闲聊网络服务器沒有一切页面,可是则是IM中最重要的人物角色,为表述尊敬,务必要给它装个设计效果图:

 

2021-05-11 10:41:40.037  INFO 9392 --- [ntLoopGroup-3-1] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700900029,"messageType":"99"}
2021-05-11 10:41:50.049  INFO 9392 --- [ntLoopGroup-3-1] c.e.o.s.n.handler.BussMessageHandler     : 接到信息:{"time":1620700910045,"messageType":"14","sendUserName":"guodegang","recvUserName":"yuqian","sendMessage":"于老师你好"}
2021-05-11 10:41:50.055  INFO 9392 --- [ntLoopGroup-3-2] c.e.o.s.netty.executor.SendMsgExecutor   : 信息分享取得成功:{"time":1620700910052,"messageType":"14","sendUserName":"guodegang","recvUserName":"yuqian","sendMessage":"于老师你好"}
2021-05-11 10:41:54.068  INFO 9392 --- [ntLoopGroup-3-2] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700914064,"messageType":"99"}
2021-05-11 10:41:57.302  INFO 9392 --- [ntLoopGroup-3-2] c.e.o.s.n.handler.BussMessageHandler     : 接到信息:{"time":1620700917301,"messageType":"14","sendUserName":"yuqian","recvUserName":"guodegang","sendMessage":"郭老师你好"}
2021-05-11 10:41:57.304  INFO 9392 --- [ntLoopGroup-3-1] c.e.o.s.netty.executor.SendMsgExecutor   : 信息分享取得成功:{"time":1620700917303,"messageType":"14","sendUserName":"yuqian","recvUserName":"guodegang","sendMessage":"郭老师你好"}
2021-05-11 10:42:05.050  INFO 9392 --- [ntLoopGroup-3-1] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700925049,"messageType":"99"}
2021-05-11 10:42:12.309  INFO 9392 --- [ntLoopGroup-3-2] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700932304,"messageType":"99"}
2021-05-11 10:42:20.066  INFO 9392 --- [ntLoopGroup-3-1] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700940050,"messageType":"99"}
2021-05-11 10:42:27.311  INFO 9392 --- [ntLoopGroup-3-2] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700947309,"messageType":"99"}
2021-05-11 10:42:35.070  INFO 9392 --- [ntLoopGroup-3-1] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700955068,"messageType":"99"}
2021-05-11 10:42:42.316  INFO 9392 --- [ntLoopGroup-3-2] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700962312,"messageType":"99"}
2021-05-11 10:42:50.072  INFO 9392 --- [ntLoopGroup-3-1] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700970071,"messageType":"99"}
2021-05-11 10:42:57.316  INFO 9392 --- [ntLoopGroup-3-2] c.e.o.s.netty.handler.HeartBeatHandler   : server接到心跳包:{"time":1620700977315,"messageType":"99"}

  从设计效果图大家看到了一些內容:接到心跳包、接到信息,分享信息,这种內容后边会详尽解读。

2、闲聊手机客户端

  闲聊手机客户端的岗位职责一句话表述:登录,给他人发聊天内容,收其他人发送给自身的聊天内容。

  下边为便捷演试,我能开启2个手机客户端,用2个不一样用户登陆,随后发信息。

 

3、Web管理方法控制面板

  现阶段只干了一个账号管理,实际看。:

三、需求分析报告

  无(见第二章节目录)。

四、详细设计

1、技术选型

1)闲聊服务器端

  闲聊网络服务器与手机客户端根据TCP协议书开展通讯,应用长连接、双工通讯方式,根据經典通讯架构Netty完成。

  那麼什么叫长连接?说白了,手机客户端和网络服务器连之后,会在这里条联接上边不断收取和发送信息,联接不容易断掉。与长连接相匹配的自然便是短连接了,短连接每一次发信息以前都必须先创建联接,随后发信息,最终断开。显而易见,即时聊天工具合适应用长连接。

  那麼哪些也是双工?当长连接创建起來后,在这里条联接上不仅有上行下行的数据信息,又有下滑的数据信息,这就叫双工。那麼相匹配的半双工、单工,大伙儿自主百度吧。

2)Web管理方法控制面板

  Web管理方法端应用SpringBoot钢管脚手架,前面应用layuimini(一个根据Layui前端框架封裝的前端框架),后端开发应用SpringMVC Jpa Shiro。

3)闲聊手机客户端

  应用SpringBoot JavaFX,干了一个极为简单的手机客户端,JavaFX是一个开发设计Java桌面程序的架构,自己也是第一次应用,编码中的写法全是在网上查的,这并并不是文中的关键,有兴趣爱好的细心百度吧。

4)SpringBoot

  之上三个部件,所有以SpringBoot作为钢管脚手架开发设计。

5)编码搭建

  Maven。

2、概念模型设计

  大家只简易采用一张客户表,非常简单立即贴脚本制作:

CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '外键约束',
  `user_name` varchar(64) DEFAULT NULL COMMENT '登录名:登录账户',
  `pass_word` varchar(128) DEFAULT NULL COMMENT '登陆密码',
  `name` varchar(16) DEFAULT NULL COMMENT '呢称',
  `sex` char(1) DEFAULT NULL COMMENT '性別:1-男,2女',
  `status` bit(1) DEFAULT NULL COMMENT '客户情况:1-合理,0-失效',
  `online` bit(1) DEFAULT NULL COMMENT '在线状态:1-线上,0-线下',
  `salt` varchar(128) DEFAULT NULL COMMENT '登陆密码盐值',
  `admin` bit(1) DEFAULT NULL COMMENT '是不是管理人员(仅有管理人员才可以登陆Web端):1-是,0-否',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

  这张表都是在什么时候用到?

  1)Web管理方法端登录的情况下;2)闲聊手机客户端将登录要求发送至闲聊服务器端时,闲聊服务器端开展用户认证;3)闲聊手机客户端的朋友目录载入。

3、通讯设计方案

  这节可能是文中的具体内容之一,关键叙述通讯报文格式协议格式、及其通讯报文格式的互动情景。

1)报文格式协议格式

  下边这幅图应当能表明99%了:

  剩余的1%在这儿说:

  a)粘包难题,TCP长连接中,粘包是第一个必须处理的难题。简单的讲,粘包的意思是信息接受方通常接到的并不是“全部”报文格式,有时比“全部”多一点,有时比“全部”少一点,那样就造成接受方没法分析这一报文格式。那麼图中中的头八个字节数就为了更好地处理这个问题,接受方依据头八个字节数标志的长短来获得到“全部”报文格式,进而开展一切正常的业务流程解决;

  b)2字节报文格式种类,为了更好地便捷分析报文格式而设计方案。依据这两个字节数将后边的json转成相对应的实体线便于开展事后解决;

  c)拉长报健身培训事实上便是json文件格式的串,自然,你能自身设计方案协议类型,我这里为了更好地便捷解决就立即放json了;

  d)自然,你能把报文格式设计方案的更繁杂、更技术专业,例如数据加密、加签字等。

2)报文格式互动情景

  a)登录

  b)推送信息-取得成功

  c)推送信息-总体目标手机客户端不线上

  d)推送信息-总体目标手机客户端线上,但信息分享不成功

五、编号完成

  前边讲了那么多,如今总要说点有效的。

1、先说说Netty

  Netty是一个非常出色的通讯架构,大部分的顶尖开源框架上都有Netty的影子。实际它有多么的出色,提议大伙儿自主百度搜索,我比不上百度搜索说的好。我只从运用层面说说Netty。运用全过程中,它最关键的物品叫handler,我们可以简易了解它为信息CPU。接到的信息和出来 的信息都是会历经一系列的handler生产加工解决。接到的信息大家叫它入站信息,传出去的信息大家叫它出站信息,因而handler又分成出站handler和入站handler。接到的信息总是被入站handler解决,传出去的信息总是被出站handler解决。

  举个事例,大家从互联网上接到的信息是二进制的字节码,大家的总体目标是将信息转化成java bean,那样便捷大家程序执行,对于这一情景我设计方案那么好多个入站handler:

  1)将字节转换成String的handler;

  2)将String转成java bean的handler;

  3)对java bean开展业务流程解决的handler。

  传出去的信息呢,我设计方案那么好多个出站handler:

  1)java bean 转为String的handler;

  2)String转成byte的handler。

  之上是有关handler的表明。

  下面再聊一下Netty的多线程。多线程的意思是如果你做了一个实际操作后,不容易立刻获得实际操作結果,只是有結果后Netty会通告你。根据下边的一段编码来表明:

channel.writeAndFlush(sendMsgRequest).addListener(new GenericFutureListener<Future<? super Void>>() {
                @Override
                public void operationComplete(Future<? super Void> future) throws Exception {
                    if (future.isSuccess()){
                        logger.info("信息发送成功:{}",sendMsgRequest);
                    }else {
                        logger.info("信息推送不成功:{}",sendMsgRequest);
                    }
                }
            });

  上边的writeAndFlush实际操作没法马上回到結果,假如你关心結果,那麼为他加上一个listener,有結果之后在listener中回应。

  到这儿,百度搜索上找到的Netty有关的编码你基本上就看得懂了。

2、闲聊服务器端

  最先看主通道的编码

public void start(){
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup worker = new NioEventLoopGroup();
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(boss, worker)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .handler(new LoggingHandler(LogLevel.INFO))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        //心率
                        ch.pipeline().addLast(new IdleStateHandler(25, 20, 0, TimeUnit.SECONDS));
                        //收整包
                        ch.pipeline().addLast(new StringLengthFieldDecoder());
                        //转字符串数组
                        ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
                        //json转目标
                        ch.pipeline().addLast(new JsonDecoder());
                        //心率
                        ch.pipeline().addLast(new HeartBeatHandler());
                        //实体线转json
                        ch.pipeline().addLast(new JsonEncoder());
                        //信息解决
                        ch.pipeline().addLast(bussMessageHandler);
                    }
                });
        try {
            ChannelFuture f = serverBootstrap.bind(port).sync();
            f.channel().closeFuture().sync();
        }catch (InterruptedException e) {
            logger.error("服务项目运行不成功:{}", ExceptionUtils.getStackTrace(e));
        }finally {
            worker.shutdownGracefully();
            boss.shutdownGracefully();
        }
    }

  编码中除开initChannel方式 中的编码,别的编码全是固定不动书写。那麼什么是固定不动书写呢?简单而言便是能够 Ctrl c、Ctrl v。

  下边大家主要看initChannel方式 里边的编码。这里边便是上边提到的各种各样handler,大家下边逐个讲这种handler全是做什么的。

  1)IdleStateHandler。这个是Netty内嵌的一个handler,即是出站handler也是入站handler。它的功效一般是用于完成心率检测。说白了心率,便是手机客户端和服务器端创建联接后,服务器端要实时监控系统手机客户端的身心健康情况,假如手机客户端挂掉或是hung住了,服务器端立即释放出来相对应的資源,及其作出别的解决例如通告运维管理。因此 在大家的情景中,手机客户端必须按时汇报自身的心率,假如服务器端检验到一段时间内收走到手机客户端汇报的心率,那麼立即作出解决,大家这儿便是简易的将其联接断掉,并改动数据库查询中相对应帐户的在线状态。

  从现在起说IdleStateHandler,第一个主要参数叫读请求超时時间,第二个主要参数叫写请求超时時间,第三个主要参数叫读写能力请求超时時间,第四个主要参数时时间单位秒。这一handler表达的意思是当25秒内没念到手机客户端的信息,或是20秒内没往手机客户端发信息,便会造成一个请求超时事情。那麼这一请求超时事情大家该对他干什么解决呢,可以看下一条。

  2)HeartBeatHandler。融合a)一起看,当产生请求超时事情时,HeartBeatHandler会接到这一事情,并对它作出解决:第一将连接断掉;第二讲数据库查询中相对应的帐户升级为不在线状态。

public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
    private static Logger logger = LoggerFactory.getLogger(HeartBeatHandler.class);

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent)evt;
            if (event.state() == IdleState.READER_IDLE) {
                //读请求超时,应将联接断开
                InetSocketAddress socketAddress = (InetSocketAddress)ctx.channel().remoteAddress();
                String ip = socketAddress.getAddress().getHostAddress();
                ctx.channel().disconnect();
                logger.info("【{}】网络连接超时,断掉",ip);
                String userName = SessionManager.removeSession(ctx.channel());
                SpringContextUtil.getBean(UserService.class).updateOnlineStatus(userName,Boolean.FALSE);
            }else {
                super.userEventTriggered(ctx, evt);
            }
        }else {
            super.userEventTriggered(ctx, evt);
        }

    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof HeartBeat){
            //接到心跳包,不解决
            logger.info("server接到心跳包:{}",msg);
            return;
        }
        super.channelRead(ctx, msg);
    }
}
 

  3)StringLengthFieldDecoder。它是个入站handler,他的功效便是处理上边提及的粘包难题:

public class StringLengthFieldDecoder extends LengthFieldBasedFrameDecoder {
    public StringLengthFieldDecoder() {
        super(10*1024*1024,0,8,0,8);
    }


    @Override
    protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) {
        buf = buf.order(order);
        byte[] lenByte = new byte[length];
        buf.getBytes(offset, lenByte);
        String lenStr = new String(lenByte);
        Long len =  Long.valueOf(lenStr);
        return len;
    }
}

  只必须集成化Netty给予的LengthFieldBasedFrameDecoder 类,并调用getUnadjustedFrameLength方式 就可以。

  最先看构造函数中的五个主要参数。第一个表明能解决的包的较大长短;第二三个主要参数应当融合起來了解,表明长短字段名从第几个逐渐,长短的长短多少钱,也就是上边协议类型协议书中的头八个字节数;第四个主要参数表明长短是不是必须校准,举例说明了解,例如头八个字节数分析出去的长短=包体长短 头八个字节数的长短,那麼这儿就必须校准八个字节数,大家的协议书中长短只包括报健身培训,因而这一主要参数填0;最后一个主要参数,表明接受到的报文格式是不是要绕过一些字节数,本例中设定为8,表明跳过度八个字节数,因而历经这一handler后,大家接到的数据信息就仅有报文格式自身了,不会再包括八个长短字节数了。

  再看getUnadjustedFrameLength方式 ,实际上便是把头八个字符串数组型的长短为转化成long型。调用完这一方式 后,Netty就了解怎样收一个“详细”的数据文件了。

  4)StringDecoder。这个是Netty内置的入站handler,会将字节流以特定的编号分析成String。

  5)JsonDecoder。是大家自定的一个入站handler,目地是将json String转化成java bean,以便捷事后解决:

public class JsonDecoder extends MessageToMessageDecoder<String> {
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext, String o, List<Object> list) throws Exception {
        Message msg = MessageEnDeCoder.decode(o);
        list.add(msg);
    }

}

  这儿会启用大家自定的一个编码解码协助类开展变换:

public static Message decode(String message){
        if (StringUtils.isEmpty(message) || message.length() < 2){
            return null;
        }
        String type = message.substring(0,2);
        message = message.substring(2);
        if (type.equals(LoginRequest)){
            return JsonUtil.jsonToObject(message,LoginRequest.class);
        }else if (type.equals(LoginResponse)){
            return JsonUtil.jsonToObject(message,LoginResponse.class);
        }else if (type.equals(LogoutRequest)){
            return JsonUtil.jsonToObject(message,LogoutRequest.class);
        }else if (type.equals(LogoutResponse)){
            return JsonUtil.jsonToObject(message,LogoutResponse.class);
        }else if (type.equals(SendMsgRequest)){
            return JsonUtil.jsonToObject(message,SendMsgRequest.class);
        }else if (type.equals(SendMsgResponse)){
            return JsonUtil.jsonToObject(message,SendMsgResponse.class);
        }else if (type.equals(HeartBeat)){
            return JsonUtil.jsonToObject(message,HeartBeat.class);
        }
        return null;
    }

  6)BussMessageHandler。首先看这一入站handler,是大家的一个业务流程解决主通道,他的关键工作中便是将信息分发送给线程池去解决,此外还负荷一个小情景,当手机客户端积极断掉时,必须将相对应的帐户数据库查询中情况升级为不线上。

public class BussMessageHandler extends ChannelInboundHandlerAdapter {
    private static Logger logger = LoggerFactory.getLogger(BussMessageHandler.class);

    @Autowired
    private TaskDispatcher taskDispatcher;

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        logger.info("接到信息:{}",msg);
        if (msg instanceof Message){
            taskDispatcher.submit(ctx.channel(),(Message)msg);
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //手机客户端联接断掉
        InetSocketAddress socketAddress = (InetSocketAddress)ctx.channel().remoteAddress();
        String ip = socketAddress.getAddress().getHostAddress();
        logger.info("手机客户端断掉:{}",ip);
        String userName = SessionManager.removeSession(ctx.channel());
        SpringContextUtil.getBean(UserService.class).updateOnlineStatus(userName,Boolean.FALSE);
        super.channelInactive(ctx);
    }
}

  下面还差线程池的解决逻辑性,也比较简单,便是将每日任务封裝成executor随后交到线程池解决:

public class TaskDispatcher {
    private ThreadPoolExecutor threadPool;

    public TaskDispatcher(){
        int corePoolSize = 15;
        int maxPoolSize = 50;
        int keepAliveSeconds = 30;
        int queueCapacity = 1024;
        BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(queueCapacity);
        this.threadPool = new ThreadPoolExecutor(
                corePoolSize, maxPoolSize, keepAliveSeconds, TimeUnit.SECONDS,
                queue);
    }

    public void submit(Channel channel, Message msg){
        ExecutorBase executor = null;
        String messageType = msg.getMessageType();
        if (messageType.equals(MessageEnDeCoder.LoginRequest)){
            executor = new LoginExecutor(channel,msg);
        }
        if (messageType.equalsIgnoreCase(MessageEnDeCoder.SendMsgRequest)){
            executor = new SendMsgExecutor(channel,msg);
        }
        if (executor != null){
            this.threadPool.submit(executor);
        }
    }
}
 

  下面看一下信息分享executor是怎么做的:

public class SendMsgExecutor extends ExecutorBase {
    private static Logger logger = LoggerFactory.getLogger(SendMsgExecutor.class);

    public SendMsgExecutor(Channel channel, Message message) {
        super(channel, message);
    }

    @Override
    public void run() {
        SendMsgResponse response = new SendMsgResponse();
        response.setMessageType(MessageEnDeCoder.SendMsgResponse);
        response.setTime(new Date());
        SendMsgRequest request = (SendMsgRequest)message;
        String recvUserName = request.getRecvUserName();
        String sendContent = request.getSendMessage();
        Channel recvChannel = SessionManager.getSession(recvUserName);
        if (recvChannel != null){
            SendMsgRequest sendMsgRequest = new SendMsgRequest();
            sendMsgRequest.setTime(new Date());
            sendMsgRequest.setMessageType(MessageEnDeCoder.SendMsgRequest);
            sendMsgRequest.setRecvUserName(recvUserName);
            sendMsgRequest.setSendMessage(sendContent);
            sendMsgRequest.setSendUserName(request.getSendUserName());
            recvChannel.writeAndFlush(sendMsgRequest).addListener(new GenericFutureListener<Future<? super Void>>() {
                @Override
                public void operationComplete(Future<? super Void> future) throws Exception {
                    if (future.isSuccess()){
                        logger.info("信息分享取得成功:{}",sendMsgRequest);
                        response.setResultCode("0000");
                        response.setResultMessage(String.format("发送给客户[%s]信息取得成功",recvUserName));
                        channel.writeAndFlush(response);
                    }else {
                        logger.error(ExceptionUtils.getStackTrace(future.cause()));
                        logger.info("信息分享不成功:{}",sendMsgRequest);
                        response.setResultCode("9999");
                        response.setResultMessage(String.format("发送给客户[%s]信息不成功",recvUserName));
                        channel.writeAndFlush(response);
                    }
                }
            });
        }else {
            logger.info("客户{}不线上,信息分享不成功",recvUserName);
            response.setResultCode("9999");
            response.setResultMessage(String.format("客户[%s]不线上",recvUserName));
            channel.writeAndFlush(response);
        }
    }
}

  总体逻辑性:一获得要把信息发送给那一个账户;二获得该账户相匹配的联接;三在这里联接上推送信息;四获得信息推送結果,将結果发送给信息“发动者”。

  下边是登录解决的executor:

public class LoginExecutor extends ExecutorBase {
    private static Logger logger = LoggerFactory.getLogger(LoginExecutor.class);

    public LoginExecutor(Channel channel, Message message) {
        super(channel, message);
    }
    @Override
    public void run() {
        LoginRequest request = (LoginRequest)message;
        String userName = request.getUserName();
        String password = request.getPassword();
        UserService userService = SpringContextUtil.getBean(UserService.class);
        boolean check = userService.checkLogin(userName,password);
        LoginResponse response = new LoginResponse();
        response.setUserName(userName);
        response.setMessageType(MessageEnDeCoder.LoginResponse);
        response.setTime(new Date());
        response.setResultCode(check?"0000":"9999");
        response.setResultMessage(check?"登录取得成功":"登录不成功,登录名或登陆密码错");
        if (check){
            userService.updateOnlineStatus(userName,Boolean.TRUE);
            SessionManager.addSession(userName,channel);
        }
        channel.writeAndFlush(response).addListener(new GenericFutureListener<Future<? super Void>>() {
            @Override
            public void operationComplete(Future<? super Void> future) throws Exception {
                //登录不成功,断开
                if (!check){
                    logger.info("客户{}登录不成功,断开",((LoginRequest) message).getUserName());
                    channel.disconnect();
                }
            }
        });
    }
}

  登录逻辑性都不繁杂,登录取得成功则升级客户在线状态,而且不管登录取得成功或是不成功,都是会返一个登录回复。另外,假如登录校检不成功,在回到回复取得成功后,必须将连接断掉。

  7)JsonEncoder。最终看这个唯一的转站handler,服务器端传出去的信息都是会被出站handler解决,他的岗位职责便是将java bean转为大家以前界定的报文格式协议格式:

public class JsonEncoder extends MessageToByteEncoder<Message> {
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext, Message message, ByteBuf byteBuf) throws Exception {
        String msgStr = MessageEnDeCoder.encode(message);
        int length = msgStr.getBytes(Charset.forName("UTF-8")).length;
        String str = String.valueOf(length);
        String lenStr = StringUtils.leftPad(str,8,'0');
        msgStr = lenStr   msgStr;
        byteBuf.writeBytes(msgStr.getBytes("UTF-8"));
    }
}

  8)SessionManager。剩余最后一个物品没说,这个是用于储存每一个登录取得成功帐户的连接的,最底层是个map,key为管理员账户,value为连接:

public class SessionManager {
    private static ConcurrentHashMap<String,Channel> sessionMap = new ConcurrentHashMap<>();

    public static void addSession(String userName,Channel channel){
        sessionMap.put(userName,channel);
    }

    public static String removeSession(String userName){
        sessionMap.remove(userName);
        return userName;
    }

    public static String removeSession(Channel channel){
        for (String key:sessionMap.keySet()){
            if (channel.id().asLongText().equalsIgnoreCase(sessionMap.get(key).id().asLongText())){
                sessionMap.remove(key);
                return key;
            }
        }
        return null;
    }

    public static Channel getSession(String userName){
        return sessionMap.get(userName);
    }
}

  到这儿,全部服务器端的逻辑性就走完后,是否,非常简单呢!

3、闲聊手机客户端

  手机客户端中页面有关的物品是根据JavaFX架构做的,这一我是第一次用,因此 不准备讲这方面,怕欺诈大伙儿。关键或是讲Netty做为手机客户端是怎样跟服务器端通讯的。

  依照国际惯例,或是先铺出主通道:

public void login(String userName,String password) throws Exception {
        Bootstrap clientBootstrap = new Bootstrap();
        EventLoopGroup clientGroup = new NioEventLoopGroup();
        try {
            clientBootstrap.group(clientGroup)
                    .channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS,10000);
            clientBootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ch.pipeline().addLast(new IdleStateHandler(20, 15, 0, TimeUnit.SECONDS));
                    ch.pipeline().addLast(new StringLengthFieldDecoder());
                    ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
                    ch.pipeline().addLast(new JsonDecoder());
                    ch.pipeline().addLast(new JsonEncoder());
                    ch.pipeline().addLast(bussMessageHandler);
                    ch.pipeline().addLast(new HeartBeatHandler());
                }
            });
            ChannelFuture future = clientBootstrap.connect(server,port).sync();
            if (future.isSuccess()){
                channel = (SocketChannel)future.channel();
                LoginRequest request = new LoginRequest();
                request.setTime(new Date());
                request.setUserName(userName);
                request.setPassword(password);
                request.setMessageType(MessageEnDeCoder.LoginRequest);
                channel.writeAndFlush(request).addListener(new GenericFutureListener<Future<? super Void>>() {
                    @Override
                    public void operationComplete(Future<? super Void> future) throws Exception {
                        if (future.isSuccess()){
                            logger.info("登录信息发送成功");
                        }else {
                            logger.info("登录信息推送不成功:{}", ExceptionUtils.getStackTrace(future.cause()));
                            Platform.runLater(new Runnable() {
                                @Override
                                public void run() {
                                    LoginController.setLoginResult("网络错误,登录信息推送不成功");
                                }
                            });
                        }
                    }
                });
            }else {
                clientGroup.shutdownGracefully();
                throw new RuntimeException("网络错误");
            }
        }catch (Exception e){
            clientGroup.shutdownGracefully();
            throw new RuntimeException("网络错误");
        }
    }

  对这一段编码,大家关键关心这几个方面:一全部handler的复位;二connect服务器端。

  全部handler中,除开bussMessageHandler是手机客户端独有的外,别的的handler在服务器端章节目录早已讲过去了,不会再过多阐释。

  1)首先看联接服务器端的实际操作。最先进行联接,联接取得成功后推送登录报文格式。进行联接必须对取得成功和不成功开展解决。推送登录报文格式也必须对取得成功和不成功开展解决。留意,这儿的成功与失败仅仅意味着当今实际操作的互联网方面的成功与失败,此刻并不可以获得服务器端回到的回复中的业务流程方面的成功与失败,假如不理解他们,能够 翻阅前边讲过的“多线程”相关内容。

  2)BussMessageHandler。总体步骤或是跟服务器端一样,将遭受的信息丢给线程池解决,大家直接看解决信息的每个executor。

  首先看手机客户端传出登录要求后,接到登录回复信息后是怎么处理的(这一段编码能够 融合1)的內容一起了解):

public class LoginRespExecutor extends ExecutorBase {
    private static Logger logger = LoggerFactory.getLogger(LoginRespExecutor.class);

    public LoginRespExecutor(Channel channel, Message message) {
        super(channel, message);
    }

    @Override
    public void run() {
        LoginResponse response = (LoginResponse)message;
        logger.info("登录結果:{}->{}",response.getResultCode(),response.getResultMessage());
        if (!response.getResultCode().equals("0000")){
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    LoginController.setLoginResult("登录不成功,登录名或密码错误");
                }
            });
        }else {
            LoginController.setCurUserName(response.getUserName());
            ClientApplication.getScene().setRoot(SpringContextUtil.getBean(MainView.class).getView());
        }
    }
}

  下面看手机客户端是怎么发闲聊信息内容的:

public void sendMessage(Message message) {
        channel.writeAndFlush(message).addListener(new GenericFutureListener<Future<? super Void>>() {
            @Override
            public void operationComplete(Future<? super Void> future) throws Exception {
                SendMsgRequest send = (SendMsgRequest)message;
                if (future.isSuccess()){
                    Platform.runLater(new Runnable() {          @Override
                        public void run() {
                            MainController.setMessageHistory(String.format("[我]在[%s]发送给[%s]的信息[%s],发送成功",
                                    DateFormatUtils.format(send.getTime(),"yyyy-MM-dd HH:mm:ss"),send.getRecvUserName(),send.getSendMessage()));
                        }
                    });
                }else {
                    Platform.runLater(new Runnable() {
                        @Override
                        public void run() {
                            MainController.setMessageHistory(String.format("[我]在[%s]发送给[%s]的信息[%s],推送不成功",
                                    DateFormatUtils.format(send.getTime(),"yyyy-MM-dd HH:mm:ss"),send.getRecvUserName(),send.getSendMessage()));
                        }
                    });
                }
            }
        });
    }

  事实上,到这儿通讯有关的编码早已贴好了。剩余的全是页面解决有关的编码,不会再贴了。

  手机客户端,是否,比较简单!

4、Web管理方法端

  Web管理方法端能够 说成更没一切科技含量,便是Shiro登录验证、目录增删。删改改没有什么好说的,下边关键说一下Shiro登录和目录查看。

  1)Shiro登录

  最先界定一个Realm,对于这个是什么定义,自主百度吧,这儿并并不是文中关键:

public class UserDbRealm extends AuthorizingRealm {
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String username = upToken.getUsername();
        String password = "";
        if (upToken.getPassword() != null)
        {
            password = new String(upToken.getPassword());
        }
        // TODO: 2021/5/13 校检用户名密码,不通过则抛验证出现异常就可以 
        ShiroUser user = new ShiroUser();
        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, password, getName());
        return info;
    }
}

  下面把这个Realm申请注册成Spring Bean,另外界定过虑链:

    @Bean
    public Realm realm() {
        UserDbRealm realm = new UserDbRealm();
        realm.setAuthorizationCachingEnabled(true);
        realm.setCacheManager(cacheManager());
        return realm;
    }
    @Bean
    public ShiroFilterChainDefinition shiroFilterChainDefinition() {
        DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition();
        chainDefinition.addPathDefinition("/css/**", "anon");
        chainDefinition.addPathDefinition("/img/**", "anon");
        chainDefinition.addPathDefinition("/js/**", "anon");
        chainDefinition.addPathDefinition("/logout", "logout");
        chainDefinition.addPathDefinition("/login", "anon");
        chainDefinition.addPathDefinition("/captchaImage", "anon");
        chainDefinition.addPathDefinition("/**", "authc");
        return chainDefinition;
    }

  到现在截止,Shiro配备好啦,下边看怎样调起登录:

    @PostMapping("/login")
    @ResponseBody
    public Result<String> login(String username, String password, Boolean rememberMe)
    {
        Result<String> ret = new Result<>();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        Subject subject = SecurityUtils.getSubject();
        try
        {
            subject.login(token);
            return ret;
        }
        catch (AuthenticationException e)
        {
            String msg = "客户或密码错误";
            if (StringUtils.isNotEmpty(e.getMessage()))
            {
                msg = e.getMessage();
            }
            ret.setCode(Result.FAIL);
            ret.setMessage(msg);
            return ret;
        }
    }

  登录编码就那么开心的完成了。

  2)目录查看

  查是个非常简单的实际操作,可是则是全部web系统软件中应用最经常的实际操作。因而,做一个实用性的封裝,十分必须。下列编码不做太多解读,初级工程师到工程师职称,就差这一段编码了(手动式捂着脸):

  a)Controller

    @RequestMapping("/query")
    @ResponseBody
    public Result<Page<User>> query(@RequestParam Map<String,Object> params, String sort, String order, Integer pageIndex, Integer pageSize){
        Page<User> page = userService.query(params,sort,order,pageIndex,pageSize);
        Result<Page<User>> ret = new Result<>();
        ret.setData(page);
        return ret;
    }

  b)Service

    @Autowired
    private UserDao userDao;
    @Autowired
    private QueryService queryService;

    public Page<User> query(Map<String,Object> params, String sort, String order, Integer pageIndex, Integer pageSize){
        return queryService.query(userDao,params,sort,order,pageIndex,pageSize);
    }
public class QueryService {
    public <T> com.easy.okim.common.model.Page<T> query(JpaSpecificationExecutor<T> dao, Map<String,Object> filters, String sort, String order, Integer pageIndex, Integer pageSize){
        com.easy.okim.common.model.Page<T> ret = new com.easy.okim.common.model.Page<T>();
        Map<String,Object> params = new HashMap<>();
        if (filters != null){
            filters.remove("sort");
            filters.remove("order");
            filters.remove("pageIndex");
            filters.remove("pageSize");
            for (String key:filters.keySet()){
                Object value = filters.get(key);
                if (value != null && StringUtils.isNotEmpty(value.toString())){
                    params.put(key,value);
                }
            }
        }
        Pageable pageable = null;
        pageIndex = pageIndex - 1;
        if (StringUtils.isEmpty(sort)){
            pageable = PageRequest.of(pageIndex,pageSize);
        }else {
            Sort s = Sort.by(Sort.Direction.ASC,sort);
            if (StringUtils.isNotEmpty(order) && order.equalsIgnoreCase("desc")){
                s = Sort.by(Sort.Direction.DESC,sort);
            }
            pageable = PageRequest.of(pageIndex,pageSize,s);
        }
        Page<T> page = null;
        if (params.size() ==0){
            page = dao.findAll(null,pageable);
        }else {
            Specification<T> specification = new Specification<T>() {
                @Override
                public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder builder) {
                    List<Predicate> predicates = new ArrayList<>();
                    for (String filter : params.keySet()) {
                        Object value = params.get(filter);
                        if (value == null || StringUtils.isEmpty(value.toString())) {
                            continue;
                        }
                        String field = filter;
                        String operator = "=";
                        String[] arr = filter.split("\\|");
                        if (arr.length == 2) {
                            field = arr[0];
                            operator = arr[1];
                        }
                        if (arr.length == 3) {
                            field = arr[0];
                            operator = arr[1];
                            String type = arr[2];
                            if (type.equalsIgnoreCase("boolean")){
                                value = Boolean.parseBoolean(value.toString());
                            }else if (type.equalsIgnoreCase("integer")){
                                value = Integer.parseInt(value.toString());
                            }else if (type.equalsIgnoreCase("long")){
                                value = Long.parseLong(value.toString());
                            }
                        }
                        String[] names = StringUtils.split(field, ".");
                        Path expression = root.get(names[0]);
                        for (int i = 1; i < names.length; i  ) {
                            expression = expression.get(names[i]);
                        }
                        // logic operator
                        switch (operator) {
                            case "=":
                                predicates.add(builder.equal(expression, value));
                                break;
                            case "!=":
                                predicates.add(builder.notEqual(expression, value));
                                break;
                            case "like":
                                predicates.add(builder.like(expression, "%"   value   "%"));
                                break;
                            case ">":
                                predicates.add(builder.greaterThan(expression, (Comparable) value));
                                break;
                            case "<":
                                predicates.add(builder.lessThan(expression, (Comparable) value));
                                break;
                            case ">=":
                                predicates.add(builder.greaterThanOrEqualTo(expression, (Comparable) value));
                                break;
                            case "<=":
                                predicates.add(builder.lessThanOrEqualTo(expression, (Comparable) value));
                                break;
                            case "isnull":
                                predicates.add(builder.isNull(expression));
                                break;
                            case "isnotnull":
                                predicates.add(builder.isNotNull(expression));
                                break;
                            case "in":
                                CriteriaBuilder.In in = builder.in(expression);
                                String[] arr1 = StringUtils.split(filter.toString(), ",");
                                for (String e : arr1) {
                                    in.value(e);
                                }
                                predicates.add(in);
                                break;
                        }
                    }

                    // 将全部标准用 and 协同起來
                    if (!predicates.isEmpty()) {
                        return builder.and(predicates.toArray(new Predicate[predicates.size()]));
                    }
                    return builder.conjunction();
                }
            };
            page = dao.findAll(specification,pageable);
        }
        ret.setTotal(page.getTotalElements());
        ret.setRows(page.getContent());
        return ret;
    }
}

  c)Dao

public interface UserDao extends JpaRepository<User,Long>,JpaSpecificationExecutor<User> {
    //啥都无需写,承继Spring Data Jpa给予的类就可以了
}

五、总结

  尽管文章标题起的有一些信口开河了,但內容也的确全是切切实实的干货知识,期待文中能对大伙儿有一些协助,源码工程项目不准备贴了,期待你可以跟随文章内容自身手敲一遍。

  开始说的收款二维码,仅仅说笑,假如你好想支付,请私聊我索要收款二维码,额度不设限制的嘿嘿。

  热烈欢迎阅读文章,热烈欢迎转截,转截请标明来源,求你了。

关注不迷路

扫码下方二维码,关注宇凡盒子公众号,免费获取最新技术内幕!

温馨提示:如果您访问和下载本站资源,表示您已同意只将下载文件用于研究、学习而非其他用途。
文章版权声明 1、本网站名称:宇凡盒子
2、本站文章未经许可,禁止转载!
3、如果文章内容介绍中无特别注明,本网站压缩包解压需要密码统一是:yufanbox.com
4、本站仅供资源信息交流学习,不保证资源的可用及完整性,不提供安装使用及技术服务。点此了解
5、如果您发现本站分享的资源侵犯了您的权益,请及时通知我们,我们会在接到通知后及时处理!提交入口
0

评论0

请先

站点公告

🚀 【宇凡盒子】全网资源库转储中心

👉 注册即送VIP权限👈

👻 全站资源免费下载✅,欢迎注册!

记得 【收藏】+【关注】 谢谢!~~~

立即注册
没有账号?注册  忘记密码?

社交账号快速登录