前言
最近,学校刚考完试,闲着没事干,于是把老项目袂信又拿出来重新开发和优化。这是我大一做的一个项目,跟着慕课网的视频做的,那时候刚入门前端,很多东西都不懂,做着这个项目,大概了解到了前端的工作范围,界面的制作,数据的获取,数据的渲染,视图的更新,用户事件的监听,等等…
这也算是一个拿得出手的项目了,后端是我师兄开发的,后台接口开发好了,在这个基础上去把前端完成,这也算是当时进入工作室的考核了。前端用的是mui开发出来的5+App,可以跨端,大部分的原生接口也可以通过html5plus实现调用。就是界面是使用html页面的 webview 渲染,所以性能不太行。当然因为这个项目是全栈的,所以权衡利弊,使用前端来开发App,确实开发效率更高,而且实现了跨端,虽然在ios上只生成了越狱包…
那个时候,我还在聊天功能的基础上,加进去了地图,笔记本,打开文件,搜索(调用了百度的api),分享下载链接,聊天助手这些模块。不过现在看来,觉得还是把主要的聊天模块实现好就行,所以,我这次更新,也只改动了聊天模块。而且我这次更新的主要目的是要把后台给实现了,让聊天应用能重新跑起来。
开始动工
接触javaWeb
其实,我没什么Java的项目开发经验,就学了Java se 和 学了 springboot 的几个注解 , 在加上玩转了 2个 node 后端项目 , 对服务端 有一些 了解 ,所以,刚开始上手开发java 项目 ,还是会感觉吃力的,最直观的感受就是 java 的 项目 比起 js 为 主体的项目 (前端 也好,node也好) 的 模块化 要更细,更严谨,也可以说更加的繁琐,Java是有类型的,这点和js区别很大。最典型的例子就是 要返回一个对象给前端,如果是在node的话,可以直接采用对象字面量的形式,返回,而在java中,就需要先写个class,在class 中写各种 成员变量,给每个成员变量定义 get 和 set 方法,然后再主类中 new 出来这个对象,再调用这个对象的 set 方法 进行 赋值。当然,这样做的确也有好处,代码的规范性 可以避免减少很多错误,也会方便管理,我想这也算Java 这么流行的原理。
java web 的 分层很明确,分为 Control,Service,DAO 三层,DAO又分为mapper,pojo和vo,mapper用于数据库的CRUD,pojp简单数据库类型,vo 是CRUD返回的数据类型, Service 用于主要的业务逻辑处理,Control 用于向前端 提供接口。此外,还有划分些模块用于存放枚举和工具类,Application是项目的入口文件.
还有就是Spring的 IOC 和 AOP 思想,虽然我之前也有听说过了,但也是在项目中才慢慢体会到的,(因为时间问题,所以我直接跳过了spring 和 spring mvc 的 学习,毕竟写配置文件太累人了…),简单的来说,IOC 就是依赖注入,在springBoot 中 使用bean 注入的 类 ,在使用的时候是不需要 手动 去 new 的,因为 创建对象的工作是由spring 来实现的,spring 使用了 单例模式,对注入的类进行管理,这样,除了请求进来的参数对象是需要new出来的,和响应出去的对象是需要new返回出来的,其它对象都是可以通过spring框架实现共享的。这里,我在说下自己的个人理解,我觉得Spring,需要这么做的主要原因是Java没有函数这个概念,Java的逻辑写在类中的方法内,而类必须实例对象才能使用,而像Node,它的逻辑是写在函数中,函数也是对象,存放在堆区,所以Node很容易就实现了不同的响应和请求之间函数体对象(代码逻辑)的共享,而Spring,为了避免一个请求响应,创建一份代码逻辑的对象,所以需要使用单例模式,从而实现逻辑对象的共享。
至于AOP,我的理解是通过使用注解的方式,spring可以给类注入逻辑,从而实现对程序的权限拦截,运行监控等功能。比如control 层使用注解去规定是post 还是 get 时 调用此 方法, 这就是典型的权限拦截。至于底层的实现原理,是利用了Java的反射机制,通过反射检查出类的类型和方法,还有方法上的注解,从而执行响应的逻辑处理。
springBoot 框架 和 myBatis的集成
web 开发的主要框架是springBoot ,数据库操作则是使用了mybatis ,不过为了避免直接写sql语句,还是使用了个orm框架 tk-mybatis ,使用了springboot 集成 mybatis ,还是碰到了一些坑,主要还是我对java web 的 生态 不熟,不知道 java web的主要依赖是哪些 ,所以使用 maven 倒腾了 很久 ,最后的配置代码如下。
<!-- 分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>4.1.0</version> </dependency> <!-- 数据库处理 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency>// ORM 工具 <dependency> <groupId>tk.mybatis</groupId> <artifactId>mapper-spring-boot-starter</artifactId> <version>2.1.5</version> </dependency>// 用于生成dao层代码 <dependency> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-core</artifactId> <version>1.3.2</version> <scope>compile</scope> <optional>true</optional> </dependency>
其中的mysql 用于连接 mysql 数据库,mybatis 是在mysql 基础上的 一层封装 , generator 用于生成代码,pagehelper是个分页插件,tk.mybatis是个和mybatisPlus差不多的ORM 工具。此外idea 还有个plugin 的插件,用于实现 mapper 的 xml 和 java代码的跳转…
把这些工具都装上了之后,还需要再进行下数据库的配置,还有就是数据库和数据表的设计,这也是个学问,不过这篇文章就不涉及这些了,数据库和数据表设计好后,就是运行generator生成代码了。
因为使用了springboot,所以数据库的配置就写在applicatiog.properties这个文件就行,把连接的数据库的地址和账号,密码都写进去,还有一些springboot 集成 mybatis 的基础配置。然后还需要一个mybatis-generator 的 配置文件,配置数据库的信息,生成的mapper 和 pojo 的目录,要生成的数据库的表。然后执行 generator 生成代码。
// generator 的配置代码<generatorConfiguration> <context id="MysqlContext" targetRuntime="MyBatis3Simple" defaultModelType="flat"> <property name="beginningDelimiter" value="`"/> <property name="endingDelimiter" value="`"/> <plugin type="tk.mybatis.mapper.generator.MapperPlugin"> <property name="mappers" value="com.imooc.utils.MyMapper"/> </plugin>// 配置数据库信息 <jdbcConnection driverClass="com.mysql.jdbc.Driver" connectionURL="jdbc:mysql:" userId="" password=""> </jdbcConnection> <!-- 对于生成的pojo所在包 --> <javaModelGenerator targetPackage="com.imooc.pojo" targetProject="src/main/java"/><!-- 对于生成的mapper所在目录 --> <sqlMapGenerator targetPackage="mapper" targetProject="src/main/resources"/><!-- 配置mapper对应的java映射 --> <javaClientGenerator targetPackage="com.imooc.mapper" targetProject="src/main/java" type="XMLMAPPER"/>// 这是数据库对应的表<table tableName="users"></table><table tableName="my_friends"></table><table tableName="friends_request"></table><table tableName="chat_msg"></table> </context></generatorConfiguration>
接下来为了能够使用tk-mybatis,需要自定义个类继承至 tkmybatis , 然后把mapper 的 java 类 都 继承 至 自定义的类,这样就可以在 mapper 中 使用 tk-mybatis 的 方法了,这个在generator的配置文件中也要写上。
import tk.mybatis.mapper.common.Mapper;import tk.mybatis.mapper.common.MySqlMapper;public interface MyMapper<T> extends Mapper<T>, MySqlMapper<T> { //TODO //FIXME 特别注意,该接口不能被扫描到,否则会出错}
public interface ChatMsgMapper extends MyMapper<ChatMsg> {} // ChatMsg 是 pojo 类
到这里,mybatis 的集成 和 配置就基本完成了.再总结下springBoot中写sql语句的方式,可以使用注解,也可以在mapper的xml文件中写,最简单的还是直接使用tkmybatis提供的对象方法进行crud的操作,直接省略了大部分的sql语句。
最后,再总结下,JavaWeb 开发中常用到的 Maven依赖,
| 作用 | 技术 |
|---|---|
| 日志 | Log4j |
| 热部署 | devtools |
| 注解工具 | lombok |
| 测试 | JUnit |
| 数据库连接池 | Druid |
| 缓存连接 | Jedis |
| JSON解析 | Gson,fastjson,jackjson |
然后json 解析 就用了阿里的 fastJson ,最近 fastJson 报了bug 出来….
初识netty
因为这个项目的后端聊天模块是使用netty进行开发的,用netty实现的websocket。当然,我接触了一段时间的netty ,发现它可以实现的功能模块远不止websocket,甚至把tomcat的功能也实现了也没问题,netty 是一个java的网络库,就和libevent,libev,libuv这些网络库,只不过这些是c/c++实现的网络库,netty底层是使用了java 的 nio ,基于nio 进行了一层封装.至于java的io,Aio,Bio,Nio,我也只有一个简单的理解,Java的io 底层还是需要调用native模块的,所以,像linux下的epoll,poll,read,write 这类系统调用,还是需要通过native代码去实现的,然后java的io 在这些调用的基础上进行一层封装,例如Nio,可以理解为就是在linux系统上的多路复用io的一层封装,底层基于像epoll这类技术,通过多路复用io,就可以实现一个线程同时阻塞多个socket,一个线程同时监听多个socket的活动,所以netty的selector和channel也是基于这个。
然后就是nettty的网络模型,netty的异步io是指nioeventloop在使用selector 监听 多个 socket 的同时,会维护一个channel的队列,而channel上注册Handler,在socket活跃的时候,就会触发handler的执行。这和node的异步io模型也是有些区别的,node的事件处理逻辑是在主线程(js线程),而io处理是在从线程,eventloop 的主要作用是分发socket的连接断开和读写。而netty的eventloop的作用主要是用于分发io处理(socket的读写和事件的逻辑处理),具体线程的设置则要看reactor模型的设置(比如主线程用于接收io请求,从线程用于处理io请求的逻辑操作并读写数据)
还有一点要说明的是,java中也是有异步的,也是使用主从线程的模型,主线程用于分发任务,从线程用于执行任务,从而对主线程来说,任务的执行是异步的,任务执行完毕后后,从线程通知主线程,返回数据,或者通知主线程执行回调函数。java中提供了future和promise,这个promise和es6的promise还是有区别的,就不说了。
再顺便对比下tomcat,tomcat是java的应用服务器,tomcat 现在也支持nio了,使用nio后,它会使用io多路复用的模式去监听socket,当发现有请求到来的时候,它会接收连接,然后读取文本并且解析http协议,然后生成res,req,取出请求的servlet,从线程池取出线程,并在该线程中执行这个servlet,把res的内容发送到客户端连接。也就是说tomcat在执行io的逻辑处理的时候是使用多线程的,为每一个请求提供一个线程,而接收io这个过程可以使用单线程的io多路复用的模式。这种模型和netty的多线程模型基本一致,当然netty的reactor模型还有单线程模型和主从线程模型,单线程模型就是channel的处理和selector都在同一个线程,这让我想起了redis的单线程模型,主从线程模型就是使用2组线程池,一组线程池接受请求,一组线程池处理io请求.
netty 代码实战
用netty创建一个http服务器输出”hello word”,HelloServer用于启动netty,HelloServerInitializer是向里面注册channel,CustomHandler 是往channel的pipeline 中添加的handler.
public class HelloServer { public static void main(String[] args) throws InterruptedException { // 定义一对线程组, // 主线程组,用于接受客户端的连接,但是不做任何处理 EventLoopGroup mainGroup = new NioEventLoopGroup(); // 从线程组,负责做任务 EventLoopGroup workerGroup = new NioEventLoopGroup(); try{ // 服务器的创建,ServerBootstrap是一个启动类 ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(mainGroup, workerGroup) // 设置主从线程组 .channel(NioServerSocketChannel.class) // 设置nio的双向通道 .childHandler(new HelloServerInitializer()); // 子处理器,用于处理workerGroup // 启动8081 端口,启动服务器 ChannelFuture channelFuture = serverBootstrap.bind(8081).sync(); // 监听关闭的channel ,设置同步方式 channelFuture.channel().closeFuture().sync(); } finally { // 关闭线程池 mainGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }}
// 初始化器,channel 注册后,会执行里面的相应的初始化方法,使用拦截器处理管道public class HelloServerInitializer extends ChannelInitializer<SocketChannel> { protected void initChannel(SocketChannel socketChannel) throws Exception { // 获取管道 ChannelPipeline channelPipeline = socketChannel.pipeline(); // 通过管道,添加handler // HttpServerCodec 是由netty 自己提供的助手类,可以理解为拦截器 // 当请求到服务端,我们需要做解码,响应到客户端做编码 channelPipeline.addLast("HttpServerCodec",new HttpServerCodec()); // 添加自定义的助手类,返回 "hello netty" channelPipeline.addLast("customHandler",new CustomHandler()); }}
// 创建自定义助手类// 对于请求来讲,其实相当于[入站,入境]public class CustomHandler extends SimpleChannelInboundHandler<HttpObject> { @Override public void channelRegistered(ChannelHandlerContext ctx) throws Exception { System.out.println("channel 注册"); super.channelRegistered(ctx); } @Override public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { System.out.println("channel 移除"); super.channelUnregistered(ctx); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("channel 活跃"); super.channelActive(ctx); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println("channel 不活跃"); super.channelInactive(ctx); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { System.out.println("channel 读取完毕触发"); super.channelReadComplete(ctx); } @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { System.out.println("channel 用户事件触发"); super.userEventTriggered(ctx, evt); } @Override public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { System.out.println("channel 用户事件 更改"); super.channelWritabilityChanged(ctx); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("捕获到异常"); super.exceptionCaught(ctx, cause); } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { System.out.println("助手类添加"); super.handlerAdded(ctx); } @Override public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { System.out.println("助手类移除"); super.handlerRemoved(ctx); } protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception { // 获取channel Channel channel = ctx.channel(); if(msg instanceof HttpRequest){ System.out.println(channel.remoteAddress()); } // 定义发送的数据消息 ByteBuf content = Unpooled.copiedBuffer("hello netty~", CharsetUtil.UTF_8); //构建一个http response FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,content); // response 设置http 的头,版本信息,状态码,内容 buffer,然后把 response 用 上下文 ctx 写到客户端,这样handle 就处理完毕了 // content-type 设置返回的数据类型 // response 的 http 头类型 response.headers().set(HttpHeaderNames.CONTENT_TYPE,"text/plain"); response.headers().set(HttpHeaderNames.CONTENT_LENGTH,content.readableBytes()); // 把数据写到缓冲区,并刷到客户端 ctx.writeAndFlush(response); }}
头像图片上传模块
因为项目中使用到了图片上传到功能,所以我决定把七牛云加到我的项目中,刚开始我是想用fastFd+nginx搭建文件服务器的,但后来想想我服务器的带宽,还是算了。因为我前端用的是base64上传的图片,所以这里需要把base64的图片转化成file或者stream,之后上传,我转换成stream,但发现这样虽然能上传上去,但是返回的连接,也是stream,而不是直接png这种图片格式,无法直接渲染到页面上。所以我只能转换成file格式上传了,但是项目中还要使用到大小图的功能,得把file转换成stream才能进行压缩成缩略图…
@Autowired private QiNiuServiceImpl qiniuService ; private ImageUtils imageUtils ; @PostMapping("/uploadFaceBase64") public IMoocJSONResult uploadFile(@RequestBody userBO userBO) throws Exception { System.out.println("接收到上传的图片"); // 获取Base64的字符串 String base64File = userBO.getFaceData(); // System.out.println(base64File);// 将base64字符串转换file上传 File file = ImageUtils.base64ToFile(base64File,"./png/"+String.valueOf(System.currentTimeMillis()) + ".png"); String res = qiniuService.uploadFile(file); // 将 file 转为 inputstream InputStream in = new FileInputStream(file); // 生成缩略图 in = ImageUtils.thumbnailImage(in,120,120);// 将缩略图的inputStream转化为file,然后上传 file = ImageUtils.inputstreamtofile(in,"./png/"+String.valueOf(System.currentTimeMillis()) + ".png" ); String url = qiniuService.uploadFile(file); in.close(); Users users = new Users(); users.setId(userBO.getUserId()); users.setFaceImageBig(res); users.setFaceImage(url); // 更新完返回更新后用户的数据 Users result = userService.updateUserInfo(users); System.out.println("上传完成"); return IMoocJSONResult.ok(result); }
websocket的简单介绍
websocket是html5的协议,使用起来,还是挺简单的,包括4个生命周期和2个主动方法和4种状态。
| 生命周期 | 作用 |
|---|---|
| onoepn() | 连接建立时触发 |
| onmessage() | 用于监听来自服务端的消息 |
| onerror() | 连接出错时调用 |
| onclose() | 连接关闭时调用 |
| 方法 | 作用 |
| — | — |
| send() | 用于向服务端发送数据 |
| close() | 主动关闭websocket连接 |
| 状态 | 作用 |
| — | — |
| 0 | 连接尚未建立 |
| 1 | 连接已经建立,可以进行通信 |
| 2 | 连接正在进行关闭 |
| 3 | 连接已经关闭 |
websocket聊天内容的封装
为了更加容易进行客户端和服务端进行通信,于是在这个基础上,又进行了一层通信的消息内容的封装。封装的java类如下。
public class DataContent implements Serializable { private static final long serizalVersionUID = 8021381444738260454L; private ChatMsg chatMsg ; // 内容的主体 private Integer action ; // 动作类型 // 1.表示的是连接建立,放入map中 // 2.表示的是客户端发送消息 // 3.表示的是客户端接收到消息 // 4.表示的是客户端发送的心跳包 // 5. 表示的是服务端通知客户端更新消息人列表 private String extend ; // 批量接收的消息的ID public ChatMsg getChatMsg() { return chatMsg; } public void setChatMsg(ChatMsg chatMsg) { this.chatMsg = chatMsg; } public Integer getAction() { return action; } public void setAction(Integer action) { this.action = action; } public String getExtend() { return extend; } public void setExtend(String extend) { this.extend = extend; } }
public class ChatMsg { // 用于记录聊天消息的ID private String msgId; // 用于记录发送者的用户ID private String sendUserId; // 用于记录消息接受者的用户ID private String acceptUserId;// 用于记录聊天消息 private String msg;// 用于标记消息的已读和未读 private Integer signFlag;//用于记录消息的创建时间 private Date createTime;public String getMsgId() { return msgId; } public void setMsgId(String msgId) { this.msgId = msgId; } /** * @return send_user_id */ public String getSendUserId() { return sendUserId; } /** * @param sendUserId */ public void setSendUserId(String sendUserId) { this.sendUserId = sendUserId; } /** * @return accept_user_id */ public String getAcceptUserId() { return acceptUserId; } /** * @param acceptUserId */ public void setAcceptUserId(String acceptUserId) { this.acceptUserId = acceptUserId; } /** * @return msg */ public String getMsg() { return msg; } /** * @param msg */ public void setMsg(String msg) { this.msg = msg; } /** * 获取用于标记信息是否已读 * * @return sign_flag - 用于标记信息是否已读 */ public Integer getSignFlag() { return signFlag; } /** * 设置用于标记信息是否已读 * * @param signFlag 用于标记信息是否已读 */ public void setSignFlag(Integer signFlag) { this.signFlag = signFlag; } @Override public String toString() { return "ChatMsg{" + "msgId='" + msgId + '\'' + ", sendUserId='" + sendUserId + '\'' + ", acceptUserId='" + acceptUserId + '\'' + ", msg='" + msg + '\'' + ", signFlag=" + signFlag + ", createTime=" + createTime + '}'; } /** * @return create_time */ public Date getCreateTime() { return createTime; } /** * @param createTime */ public void setCreateTime(Date createTime) { this.createTime = createTime; }}
聊天模块的开发
项目中涉及到的聊天模块的功能逻辑还是很复杂的,但细致的功能划分就分为2个大的模块,一个是朋友列表的信息,一个是聊天的信息列表,2个模块都涉及到增删改查,当添加了好友请求后,客户端的好友列表要发生改变,当好友的个人信息发生了改变的时候,客户端的好友列表也要发生改变。聊天信息的增加则和用户在不在线有很大联系,如果用户在线,则直接把消息发送给客户端,否则则是先把消息存储在数据库中,标记为未读,待用户上线后,去服务端请求未读的消息,服务端把消息返回,并标记为已读。而在客户端用户对消息的已读和未读,则由客户端去控制,同时客户端还要根据已读和未读消息去渲染不同的聊天快照。而且在新消息来了之后,应该根据不同的联系人去进行分类,如果当前用户正在消息的联系人的聊天页面中,则直接将消息渲染到页面上。
关于服务端的消息推送机制,是每一个用户连接服务端后(connect阶段),就会使用ChannelGroup的channel保存起来,然后当用户退出连接时,就将用户的channel从ChannelGroup删掉(close阶段),这样所有在线用户的channel就都保存在一个ChannelGroup里了,但是这样还是没办法找到想要通信的人的channel,所以,得建立一种机制能够根据用户的id找到这个用户的channel,而用户自身的id和用户想要通信的人的用户id则从前端传过来,于是在ChannelGroup的基础上在使用一个map用来实现channel和用户的id的映射保存,客户端在onopen的第一个请求会发送自己的用户id,然后服务端把用户id和用户的channel保存到map中,这样的话,就能根据用户id,找到channel,再判断该channel在ChannelGroup中是否存在,(因为用户断开的时候并不会发送自己的用户id,所以没办法把映射删掉,所以在map中存在的也可能是以前连接的)来判断该用户是否在线了,如果在线的话,就用接收方的channel推送消息,不在线的话,就先把消息保存到数据库里,标记为未读。
为了避免客户端一直连接占用服务端的资源,于是又引入了个心跳包的机制,也就是在客户端设置个定时器,客户端每10秒就会向服务端发送一个websocket数据包,然后在服务端设置心跳包超时的时间是12秒,如果超过这个时间,那么服务端就会认为客户端程序已经被挂起了,于是会自动把websocket切断,从而减少资源的占用,不过客户端那里有个坑,后面再说。这样的话服务端的聊天模块就基本开发完毕了。
客户端切后台后的bug
客户端的逻辑是这样的,登陆进入主页之后,就会发送请求去 建立websocket连接,然后在websocket建立完毕后的onopen生命周期中,发送类型为1的请求数据包,让服务端把连接放到map中,然后启动一个定时器发送心跳包。如果出现数据包发不出去的情况就会判断当前的websocket是不是open状态,如果不是的话,就会重新建立websocket连接,设置重发定时器,然后把这个消息再重发出去,重发失败了又会去检查websocket的连接状态,再走一遍上面的流程。
这样逻辑也没多大问题,但是如果把应用切到后台运行,那么再切到后台的一段时间内,应用其实还是活着的,能够和服务端建立心跳包,但是过了一段时间,应用就会被系统冻结了,心跳包也就断了,服务端也就把websocket给关了,但是如果你不手动把在后台的应用杀死,过了一会,再重新打开程序,这时候程序就会出错了,因为启动后的程序的定时器还存在,于是还会去发送心跳包,然后心跳包发不出去,知道了websocket被断开了,于是会请求重新建立websocket,但是websocket 打开需要从连接状态变成open状态 ,到了open状态才能 发送消息,而此时重发定时器仍然在起作用 , 所以在websocket处于连接状态的时候 , 就想尝试去向服务器发送心跳包,这个时候是发不出的,于是客户端以为连接断了, 又会重新去建立一个新的websocket连接,然后按照设置重发定时器,时间到了重发,重发不出去再建立新的websocket连接,然后再重发的逻辑,最后陷入死循环,建立了一堆websocket,但消息就是发不出去。
为了解决这个bug,我最后的选择是在应用启动后websocket连接失败并关闭的onclose生命周期中把定时器给移除了,然后重新进行websocket初始化,同时在websocket初始化中进行判断,如果wensocket是处于状态0(连接状态)则退出初始化,同时对重发的定时器时间进行调整,终于成功踩坑。并设置只在websocket初始化的阶段去获取服务端的未读数据,不在心跳包的定时器内写获取服务端的未读消息的代码,避免出错。
mui使用事件委托
因为我的客户端的页面展示是基于webview,所以其实是个网页,在页面中加入了太多的事件监听函数,于是我在想能不能进行下优化,然后就想到了web的事件委托,但我用mui直接在body上进行监听又不行,于是查看了mui的文档,可以使用类似列表的方式进行批量的事件注册,于是我把原先的使用多个document注册监听事件改成下面这种模式。
mui("#about").on('tap','.mui-table-view-cell',function(){ //获取id var id = this.getAttribute("id"); switch (id){ case "kefu": ... break; case "fenxiang": ... break; case "check_update": ... break; case "shouhou": ... break; default: break; } })
springBoot 服务部署服务器
要把springBoot的服务部署到服务器主要有2种方法,一种是使用jar包的方式,一种是使用war包的方式,war包需要自己在服务器上安装tomcat,jar包就不需要了,因为springBoot自带了tomcat。于是我选择了用jar包部署,先用idae把项目打包出jar,然后放到服务器上,再服务器上使用命令启动就可以了。
nohup java -jar 项目名.jar &
这样就运行起来了,日志会打印到文件夹下的nohub.out这个文件。当然还有更高级的部署方式比如,jenkins+github+docker,不过那样对于我这个项目就太麻烦了,所以,还是简单点,手动部署就行,enen
结尾
到这里,就差不多把我这个项目遇到的坑和整体流程粗略讲完了,可能有些地方记录的不太详细…不管如何,这个项目也算我JavaWeb的第一个项目,前端也在以前的基础做了些简单的优化,在此做下记录,完结!