看车二手车网站源码,江阴市住房和城乡建设局网站,宁波优化网站排名软件,游戏挂机云服务器整个工程包含三个部分#xff1a;
1、聊天服务器
聊天服务器的职责一句话解释#xff1a;负责接收所有用户发送的消息#xff0c;并将消息转发给目标用户。
聊天服务器没有任何界面#xff0c;但是却是IM中最重要的角色#xff0c;为表达敬意#xff0c;必须要给它放个…整个工程包含三个部分
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、聊天客户端
聊天客户端的职责一句话解释登陆给别人发聊天内容收其它人发给自己的聊天内容。
下面为方便演示我会打开两个客户端用两个不同用户登陆然后发消息。
3、Web管理控制台
目前只做了一个账户管理具体看图吧
概要设计 1、技术选型
1聊天服务端
聊天服务器与客户端通过TCP协议进行通信使用长连接、全双工通信模式基于经典通信框架Netty实现。
那么什么是长连接顾名思义客户端和服务器连上后会在这条连接上面反复收发消息连接不会断开。与长连接对应的当然就是短连接了短连接每次发消息之前都需要先建立连接然后发消息最后断开连接。显然即时聊天适合使用长连接。
那么什么又是全双工当长连接建立起来后在这条连接上既有上行的数据又有下行的数据这就叫全双工。那么对应的半双工、单工大家自行百度吧。
2Web管理控制台
Web管理端使用SpringBoot脚手架前端使用Layuimini一个基于Layui前端框架封装的前端框架后端使用SpringMVCJpaShiro。
3聊天客户端
使用SpringBootJavaFX做了一个极其简陋的客户端JavaFX是一个开发Java桌面程序的框架本人也是第一次使用代码中的写法都是网上查的这并不是本文的重点有兴趣的仔细百度吧。
4SpringBoot
以上三个组件全部以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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8;
这张表都在什么时候用到
1Web管理端登陆的时候 2聊天客户端将登陆请求发送到聊天服务端时聊天服务端进行用户认证 3聊天客户端的好友列表加载。
3、通信设计
本节将会是本文的核心内容之一主要描述通信报文协议格式、以及通信报文的交互场景。
1报文协议格式
下面这张图应该能说明99%了 图片 剩下的1%在这里说
a粘包问题TCP长连接中粘包是第一个需要解决的问题。通俗的讲粘包的意思是消息接收方往往收到的不是“整个”报文有时候比“整个”多一点有时候比“整个”少一点这样就导致接收方无法解析这个报文。那么上图中的头8个字节就为了解决这个问题接收方根据头8个字节标识的长度来获取到“整个”报文从而进行正常的业务处理
b2字节报文类型为了方便解析报文而设计。根据这两个字节将后面的json转成相应的实体以便进行后续处理
c变长报文体实际上就是json格式的串当然你可以自己设计报文格式我这里为了方便处理就直接放json了
d当然你可以把报文设计的更复杂、更专业比如加密、加签名等。
2报文交互场景
a登陆 图片
b发送消息-成功 图片
c发送消息-目标客户端不在线 图片
d发送消息-目标客户端在线但消息转发失败 图片
编码实现 前面说了那么多现在总得说点有用的。
1、先说说Netty
Netty是一个相当优秀的通信框架大多数的顶级开源框架中都有Netty的身影。具体它有多么优秀建议大家自行百度我不如百度说的好。我只从应用方面说说Netty。
应用过程中它最核心的东西叫handler我们可以简单理解它为消息处理器。收到的消息和出去的消息都会经过一系列的handler加工处理。收到的消息我们叫它入站消息发出去的消息我们叫它出站消息因此handler又分为出站handler和入站handler。收到的消息只会被入站handler处理发出去的消息只会被出站handler处理。
举个例子我们从网络上收到的消息是二进制的字节码我们的目标是将消息转换成java bean这样方便我们程序处理针对这个场景我设计这么几个入handler
1将字节转换成String的handler 2将String转成java bean的handler 3对java bean进行业务处理的handler。
发出去的消息呢我设计这么几个出站handler
1java bean 转成String的handler 2String转成byte的handler。
以上是关于handler的说明。
接下来再说一下Netty的异步。异步的意思是当你做完一个操作后不会立马得到操作结果而是有结果后Netty会通知你。通过下面的一段代码来说明 channel.writeAndFlush(sendMsgRequest).addListener(new GenericFutureListenerFuture? 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() { 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方法中的代码其他代码都是固定写法。那么什么叫固定写法呢通俗来讲就是可以Ctrlc、Ctrlv。
下面我们着重看initChannel方法里面的代码。这里面就是上面讲到的各种handler我们下面挨个讲这些handler都是干啥的。
1IdleStateHandler。这个是Netty内置的一个handler既是出站handler又是入站handler。它的作用一般是用来实现心跳监测。所谓心跳就是客户端和服务端建立连接后服务端要实时监控客户端的健康状态如果客户端挂了或者hung住了服务端及时释放相应的资源以及做出其他处理比如通知运维。所以在我们的场景中客户端需要定时上报自己的心跳如果服务端检测到一段时间内没收到客户端上报的心跳那么及时做出处理我们这里就是简单的将其连接断开并修改数据库中相应账户的在线状态。
现在开始说IdleStateHandler第一个参数叫读超时时间第二个参数叫写超时时间第三个参数叫读写超时时间第四个参数时时间单位秒。这个handler表达的意思是当25秒内没读到客户端的消息或者20秒内没往客户端发消息就会产生一个超时事件。那么这个超时事件我们该对他做什么处理呢请看下一条。
2HeartBeatHandler。结合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); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 }
3StringLengthFieldDecoder。这是个入站handler他的作用就是解决上面提到的粘包问题 public class StringLengthFieldDecoder extends LengthFieldBasedFrameDecoder { public StringLengthFieldDecoder() { super(1010241024,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; } 1 2 3 4 5 6 7 8 9 }
只需要集成Netty提供的LengthFieldBasedFrameDecoder 类并重写getUnadjustedFrameLength方法即可。
首先看构造方法中的5个参数。第一个表示能处理的包的最大长度第二三个参数应该结合起来理解表示长度字段从第几位开始长度的长度是多少也就是上面报文格式协议中的头8个字节第四个参数表示长度是否需要校正举例理解比如头8个字节解析出来的长度包体长度头8个字节的长度那么这里就需要校正8个字节我们的协议中长度只包含报文体因此这个参数填0最后一个参数表示接收到的报文是否要跳过一些字节本例中设置为8表示跳过头8个字节因此经过这个handler后我们收到的数据就只有报文本身了不再包含8个长度字节了。
再看getUnadjustedFrameLength方法其实就是将头8个字符串型的长度为转换成long型。重写完这个方法后Netty就知道如何收一个“完整”的数据包了。
4StringDecoder。这个是Netty自带的入站handler会将字节流以指定的编码解析成String。
5JsonDecoder。是我们自定义的一个入站handler目的是将json String转换成java bean以方便后续处理 public class JsonDecoder extends MessageToMessageDecoder { Override protected void decode(ChannelHandlerContext channelHandlerContext, String o, List 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; }
6BussMessageHandler。先看这个入站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); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 }
接下来还差线程池的处理逻辑也非常简单就是将任务封装成executor然后交给线程池处理 public class TaskDispatcher { private ThreadPoolExecutor threadPool;
public TaskDispatcher(){ int corePoolSize 15; int maxPoolSize 50; int keepAliveSeconds 30; int queueCapacity 1024; BlockingQueue 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); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 }
接下来看一下消息转发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 GenericFutureListenerFuture? 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); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 }
整体逻辑一获取要把消息发给那个账号二获取该账号对应的连接三在此连接上发送消息四获取消息发送结果将结果发给消息“发起者”。
下面是登陆处理的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 GenericFutureListenerFuture? super Void() { Override public void operationComplete(Future? super Void future) throws Exception { //登陆失败断开连接 if (!check){ logger.info(“用户{}登陆失败断开连接”,((LoginRequest) message).getUserName()); channel.disconnect(); } } }); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 }
登陆逻辑也不复杂登陆成功则更新用户在线状态并且无论登陆成功还是失败都会返一个登陆应答。同时如果登陆校验失败在返回应答成功后需要将链接断开。
7JsonEncoder。最后看这个唯一的出站handler服务端发出去的消息都会被出站handler处理他的职责就是将java bean转成我们之前定义的报文协议格式 public class JsonEncoder extends MessageToByteEncoder { 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”)); } }
8SessionManager。剩下最后一个东西没说这个是用来保存每个登陆成功账户的链接的底层是个mapkey为用户账户value为链接 public class SessionManager { private static ConcurrentHashMapString,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); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 }
到这里整个服务端的逻辑就走完了是不是很简单呢
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() { 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 GenericFutureListenerFuture? 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先看连接服务端的操作。首先发起连接连接成功后发送登陆报文。发起连接需要对成功和失败进行处理。发送登陆报文也需要对成功和失败进行处理。注意这里的成功失败只是代表当前操作的网络层面的成功失败这时候并不能获取服务端返回的应答中的业务层面的成功失败如果不理解这句话可以翻看前面讲过的“异步”相关内容。
2BussMessageHandler。整体流程还是跟服务端一样将受到的消息扔给线程池处理我们直接看处理消息的各个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()); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 }
接下来看客户端是怎么发聊天信息的 public void sendMessage(Message message) { channel.writeAndFlush(message).addListener(new GenericFutureListenerFuture? 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登陆和列表查询。
1Shiro登陆
首先定义一个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;}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 }
接下来把这个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; } 1 2 3 4 5 6 7 8 9 10 11 12 到现在为止Shiro配置好了下面看如何调起登陆 PostMapping(“/login”) ResponseBody public Result login(String username, String password, Boolean rememberMe) { Result 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系统中使用最频繁的操作。因此做一个通用性的封装非常有必要。以下代码不做过多讲解初级工程师到高级工程师就差这段代码了手动捂脸
aController RequestMapping(“/query”) ResponseBody public Result query(RequestParam MapString,Object params, String sort, String order, Integer pageIndex, Integer pageSize){ Page page userService.query(params,sort,order,pageIndex,pageSize); Result ret new Result(); ret.setData(page); return ret; }
bService Autowired private UserDao userDao; Autowired private QueryService queryService;
public Page query(MapString,Object params, String sort, String order, Integer pageIndex, Integer pageSize){ return queryService.query(userDao,params,sort,order,pageIndex,pageSize); } 1 2 3 public class QueryService { public com.easy.okim.common.model.Page query(JpaSpecificationExecutor dao, MapString,Object filters, String sort, String order, Integer pageIndex, Integer pageSize){ com.easy.okim.common.model.Page ret new com.easy.okim.common.model.Page(); MapString,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 page null; if (params.size() 0){ page dao.findAll(null,pageable); }else { Specification specification new Specification() { Override public Predicate toPredicate(Root root, CriteriaQuery? criteriaQuery, CriteriaBuilder builder) { List 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;} 1 2 3 4 5 6 7 8 9 10 11 12 13 }
cDao public interface UserDao extends JpaRepositoryUser,Long,JpaSpecificationExecutor { //啥都不用写继承Spring Data Jpa提供的类就行了 }