1. Netty介绍与相关基础知识
1.1 Netty介绍
简介
Netty是由JBOSS提供的一个java开源框架。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。
(1) Netty提供了简单易用的API
(2) 基于事件驱动的编程方式来编写网络通信程序
(3) 更高的吞吐量
(4) 学习难度低
应用场景:
JavaEE: Dubbo
大数据:Apache Storm(Supervisor worker进程间的通信也是基于Netty来实现的)
1.2 BIO、NIO、AIO介绍与区别
阻塞与非阻塞
主要指的是访问IO的线程是否会阻塞(或者说是等待)
线程访问资源,该资源是否准备就绪的一种处理方式。
同步和异步
主要是指的数据的请求方式
同步和异步是指访问数据的一种机制
BIO
同步阻塞IO,Block IO,IO操作时会阻塞线程,并发处理能力低。
我们熟知的Socket编程就是BIO,一个socket连接一个处理线程(这个线程负责这个Socket连接的一系列数据传输操作)。阻塞的原因在于:操作系统允许的线程数量是有限的,多个socket申请与服务端建立连接时,服务端不能提供相应数量的处理线程,没有分配到处理线程的连接就会阻塞等待或被拒绝
NIO
同步非阻塞IO,None-Block IO
NIO是对BIO的改进,基于Reactor模型。我们知道,一个socket连接只有在特点时候才会发生数据传输IO操作,大部分时间这个“数据通道”是空闲的,但还是占用着线程。NIO作出的改进就是“一个请求一个线程”,在连接到服务端的众多socket中,只有需要进行IO操作的才能获取服务端的处理线程进行IO。这样就不会因为线程不够用而限制了socket的接入
AIO(NIO 2.0)
异步非阻塞IO
这种IO模型是由操作系统先完成了客户端请求处理再通知服务器去启动线程进行处理。AIO也称NIO2.0,在JDK7开始支持。
1.3 Netty Reactor模型 - 单线程模型、多线程模型、主从多线程模型介绍
1.3.1 单线程模型
用户发起IO请求到Reactor线程,Ractor线程将用户的IO请求放入到通道,然后再进行后续处理,处理完成后,Reactor线程重新获得控制权,继续其他客户端的处理,这种模型一个时间点只有一个任务在执行,这个任务执行完了,再去执行下一个任务。
- 但单线程的Reactor模型每一个用户事件都在一个线程中执行:
- 性能有极限,不能处理成百上千的事件
- 当负荷达到一定程度时,性能将会下降
- 某一个事件处理器发生故障,不能继续处理其他事件
1.3.2 Reactor多线程模型
Reactor多线程模型是由一组NIO线程来处理IO操作(之前是单个线程),所以在请求处理上会比上一中模型效率更高,可以处理更多的客户端请求。这种模式使用多个线程执行多个任务,任务可以同时执行,但是如果并发仍然很大,Reactor仍然无法处理大量的客户端请求
1.3.3 Reactor主从多线程模型
这种线程模型是Netty推荐使用的线程模型,这种模型适用于高并发场景,一组线程池接收请求,一组线程池处理IO。
1.4 Netty - 基于web socket简单聊天DEMO实现
后端编写
导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.xuwenfei</groupId>
<artifactId>netty_chat</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
</dependencies>
</project>
编写Netty Server
package com.xuwenfei.netty_chat;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class WebSocketNettyServer {
public static void main(String[] args) {
// 创建两个线程池
NioEventLoopGroup mainGrp = new NioEventLoopGroup(); // 主线程池
NioEventLoopGroup subGrp = new NioEventLoopGroup(); // 从线程池
try {
// 创建Netty服务器启动对象
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 初始化服务器启动对象
serverBootstrap
// 指定使用上面创建的两个线程池
.group(mainGrp, subGrp)
// 指定Netty通道类型
.channel(NioServerSocketChannel.class)
// 指定通道初始化器用来加载当Channel收到事件消息后,
// 如何进行业务处理
.childHandler(new WebSocketChannelInitializer());
// 绑定服务器端口,以同步的方式启动服务器
ChannelFuture future = serverBootstrap.bind(9090).sync();
// 等待服务器关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 优雅关闭服务器
mainGrp.shutdownGracefully();
subGrp.shutdownGracefully();
}
}
}
编写通道初始化器
public class WebSocketChannelInitializer extends ChannelInitializer<SocketChannel> {
// 初始化通道
// 在这个方法中去加载对应的ChannelHandler
protected void initChannel(SocketChannel ch) throws Exception {
// 获取管道,将一个一个的ChannelHandler添加到管道中
ChannelPipeline pipeline = ch.pipeline();
// 添加一个http的编解码器
pipeline.addLast(new HttpServerCodec());
// 添加一个用于支持大数据流的支持
pipeline.addLast(new ChunkedWriteHandler());
// 添加一个聚合器,这个聚合器主要是将HttpMessage聚合成FullHttpRequest/Response
pipeline.addLast(new HttpObjectAggregator(1024 * 64));
// 需要指定接收请求的路由
// 必须使用以ws后缀结尾的url才能访问
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
// 添加自定义的Handler
pipeline.addLast(new ChatHandler());
}
}
编写处理消息的ChannelHandler
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 用来保存所有的客户端连接
private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd hh:MM");
// 当Channel中有新的事件消息会自动调用
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 当接收到数据后会自动调用
// 获取客户端发送过来的文本消息
String text = msg.text();
System.out.println("接收到消息数据为:" + text);
for (Channel client : clients) {
// 将消息发送到所有的客户端
client.writeAndFlush(new TextWebSocketFrame(sdf.format(new Date()) + ":" + text));
}
}
// 当有新的客户端连接服务器之后,会自动调用这个方法
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 将新的通道加入到clients
clients.add(ctx.channel());
}
}
1.5 websocket以及前端代码编写
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借HTTP请求完成。Websocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了
前端编写
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>在线聊天室</title>
</head>
<body>
<input type="text" id="message">
<input type="button" value="发送消息" onclick="sendMsg()">
接收到的消息:
<p id="server_message" style="background-color: #AAAAAA"></p>
<script>
var websocket = null;
// 判断当前浏览器是否支持websocket
if (window.WebSocket) {
websocket = new WebSocket("ws://127.0.0.1:9090/ws");
websocket.onopen = function () {
console.log("建立连接.");
}
websocket.onclose = function () {
console.log("断开连接");
}
websocket.onmessage = function (e) {
console.log("接收到服务器消息:" + e.data);
var server_message = document.getElementById("server_message");
server_message.innerHTML += e.data + "<br/>";
}
} else {
alert("当前浏览器不支持web socket");
}
function sendMsg() {
var message = document.getElementById("message");
websocket.send(message.value);
}
</script>
</body>
</html>
1.6 MUI、HTML5+、HBuilder介绍
MUI介绍
官网地址:http://dev.dcloud.net.cn/mui/
MUI是一个轻量级的前端框架。MUI以iOS平台UI为基础,补充部分Android平台特有的UI控件。MUI不依赖任何第三方JS库,压缩后的JS和CSS文件仅有100+K和60+K,可以根据自己的需要,自定义去下载对应的模块。并且MUI编写的前端,可以打包成APK和IPA安装文件,在手机端运行。也就是,编写一套代码,就可以在Android、IOS下运行。
API地址:
http://dev.dcloud.net.cn/mui/ui/
H5+
H5+提供了对HTML5的增强,提供了40WAPI给程序员使用。使用H5+ API可以轻松开发二维码扫描、摄像头、地图位置、消息推送等功能
API地址:
http://www.html5plus.org/doc/zh_cn/accelerometer.html#
HBuilder
前端开发工具。本次项目所有的前端使用HBuilder开发。在项目开发完后,也会使用HBuilder来进行打包
Android/IOS的安装包。
1.7 MUI前端开发
1.7.1 创建项目/页面/添加MUI元素
创建MUI移动App项目
页面创建,添加组件
http://dev.dcloud.net.cn/mui/ui/#accordion
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
<title></title>
<script src="js/mui.min.js"></script>
<link href="css/mui.min.css" rel="stylesheet" />
</head>
<header class="mui-bar mui-bar-nav">
<h1 class="mui-title">登录页面</h1>
</header>
<div class="mui-content">
<form class="mui-input-group">
<div class="mui-input-row">
<label>用户名</label>
<input id="username" type="text" class="mui-input-clear" placeholder="请输入用户名" value="zhangsan">
</div>
<div class="mui-input-row">
<label>密码</label>
<input id="password" type="password" class="mui-input-password" placeholder="请输入密码">
</div>
<div class="mui-button-row">
<button id="confirm" type="button" class="mui-btn mui-btn-primary">确认</button>
<button type="button" class="mui-btn mui-btn-danger">取消</button>
</div>
</form>
</div>
1.7.2 获取页面元素/添加点击事件
获取页面元素
mui.plusReady(function() {
// 使用document.getElementById来获取Input组件数据
var username = document.getElementById("username");
var password = document.getElementById("password");
var confirm = document.getElementById("confirm");
// 绑定事件
confirm.addEventListener("tap", function() {
alert("按下按钮");
});
});
批量绑定页面元素的点击事件
mui(".mui-table-view").on('tap','.mui-table-view-cell',function(){
});
使用原生JS的事件绑定方式
// 绑定事件
confirm.addEventListener("tap", function() {
alert("按下按钮");
});
1.7.3 发起ajax请求
前端
当我们点击确认按钮的时候,将用户名和密码发送给后端服务器
// 发送ajax请求
mui.ajax('http://172.16.1.203:9000/login', {
data: {
username: username.value,
password: password.value
},
dataType: 'json', //服务器返回json格式数据
type: 'post', //HTTP请求类型
timeout: 10000, //超时时间设置为10秒;
headers: {
'Content-Type': 'application/json'
},
success: function(data) {
// 使用JSON.stringify可以将JSON对象转换为String字符串
console.log(JSON.stringify(data));
var jsonStr = JSON.stringify(data);
var jsonObj = JSON.parse(jsonStr);
if(data.success) {
var user = {
username: username.value,
password: password.value
}
// 将对象数据放入到缓存中,需要转换为字符串
plus.storage.setItem("user", JSON.stringify(user));
mui.openWindow({
url: 'login_success.html',
id:'login_succss.html'
});
}
else {
mui.openWindow({
url: 'login_failed.html',
id:'login_failed.html'
});
}
},
error: function(xhr, type, errorThrown) {
//异常处理;
console.log(type);
}
注:172.16.1.203,此Ip,为统一局域网下的ip,查看方式如下
后端
基于SpringBoot编写一个web应用,主要是用于接收ajax请求,响应一些数据到前端
@RestController
public class LoginController {
@RequestMapping("/login")
public Map login(@RequestBody User user) {
System.out.println(user);
Map map = new HashMap<String, Object>();
if("tom".equals(user.getUsername()) && "123".equals(user.getPassword())) {
map.put("success", true);
map.put("message", "登录成功");
}
else {
map.put("success", false);
map.put("message", "登录失败,请检查用户名和密码是否输入正确");
}
return map;
}
}
1.7.4 字符串转JSON对象以及JSON对象转字符串
将JSON对象转换为字符串
// 使用JSON.stringify可以将JSON对象转换为String字符串
console.log(JSON.stringify(data));
var jsonStr = JSON.stringify(data);
将字符串转换为JSON对象
var jsonObj = JSON.parse(jsonStr);
1.7.5 页面跳转
mui.openWindow({
url: 'login_success.html',
id:'login_succss.html'
});
1.7.6 App客户端缓存操作
大量的App很多时候都需要将服务器端响应的数据缓存到手机App本地。
http://www.html5plus.org/doc/zh_cn/storage.html
在App中缓存的数据,就是以key-value键值对来存放的。
**将数据放入到本地缓存中**
```html
if(data.success) {
var user = {
username: username.value,
password: password.value
}
// 将对象数据放入到缓存中,需要转换为字符串
plus.storage.setItem("user", JSON.stringify(user));
从本地缓存中读取数据
// 从storage本地缓存中获取对应的数据
var userStr = plus.storage.getItem("user");
2 构建项目
2.1 项目功能需求、技术架构介绍
功能需求
登录/注册
个人信息
搜索添加好友
好友聊天
技术架构
前端
开发工具:HBuilder
框架:MUI、H5+
下载地址:https://pan.baidu.com/s/1M4buI9bjLphfqMdr9KuNYQ 提取码:XWFN
后端
开发工具:IDEA
框架:Spring Boot、MyBatis、Spring MVC、FastDFS、Netty
数据库:mysql
2.2 使用模拟器进行测试
安装附件中的夜神Android模拟器
双击桌面图标启动模拟器
安装后找到模拟器的安装目录
到命令行中执行以下命令
nox_adb connect 127.0.0.1:62001
nox_adb devices
进入到Hbuilder安装目录下的tools/adbs目录
切换到命令行中执行以下命令
adb connect 127.0.0.1:62001
adb devices
打开HBuilder开始调试
2.3 前端 - HBuilder前端项目导入
从GitHub下载前端包Netty-Mui-Chat.zip解压,并导入到HBuilder中
下载地址:https://github.com/wenfei-alibaba/Netty-public.git
2.4 后端 - 导入数据库/SpringBoot项目/MyBatis逆向工程
从GitHub下载hchat.sql脚本在开发工具中执行
下载地址:https://github.com/wenfei-alibaba/Netty-public.git
数据库表结构介绍
tb_user用户表
tb_friend朋友表
tb_friend_req申请好友表
tb_friend_req申请好友表
tb_chat_record聊天记录表
使用MyBatis逆向工程生成代码
从GitHub下载generatorSqlmapCustom.zip解压导入到IDEA中,并配置项目所使用的JDK
下载地址:https://github.com/wenfei-alibaba/Netty-public.git
创建Spring Boot项目
从GitHub下载application.properties配置文件以及pom.xml
2.5 后端 - Spring Boot整合Netty搭建后台
spring boot整合Netty
从GitHub下载spring-netty.zip,然后将java文件导入到项目中
启动Spring Boot,导入HTML页面,使用浏览器打开测试Netty是否整合成功
3 业务开发 - 用户注册/登录/个人信息
3.1 用户登录功能 -后端开发
从GitHub下载Util.zip,解压后将雪花算法ID生成器,IdWorker.java文件导入到项目中
下载地址:https://github.com/wenfei-alibaba/Netty-public.git
初始化IdWorker
@SpringBootApplication
@MapperScan(basePackages = "com.xuwenfei.hchat.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Bean
public IdWorker idWorker() {
return new IdWorker(0, 0);
}
}
创建Result实体类
/**
* 将返回给客户端的数据封装到实体类中
*/
public class Result {
private boolean success; // 是否操作成功
private String message; // 返回消息
private Object result; // 返回附件的对象
public Result(boolean success, String message) {
this.success = success;
this.message = message;
}
public Result(boolean success, String message, Object result) {
this.success = success;
this.message = message;
this.result = result;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Object getResult() {
return result;
}
public void setResult(Object result) {
this.result = result;
}
创建返回给客户端的User实体类
/**
* 用来返回给客户端
*/
public class User {
private String id;
private String username;
private String picSmall;
private String picNormal;
private String nickname;
private String qrcode;
private String clientId;
private String sign;
private Date createtime;
private String phone;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPicSmall() {
return picSmall;
}
public void setPicSmall(String picSmall) {
this.picSmall = picSmall;
}
public String getPicNormal() {
return picNormal;
}
public void setPicNormal(String picNormal) {
this.picNormal = picNormal;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public String getQrcode() {
return qrcode;
}
public void setQrcode(String qrcode) {
this.qrcode = qrcode;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getSign() {
return sign;
}
public void setSign(String sign) {
this.sign = sign;
}
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", picSmall='" + picSmall + '\'' +
", picNormal='" + picNormal + '\'' +
'}';
}
UserController实现
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/findAll")
public List<TbUser> findAll() {
return userService.findAll();
}
@RequestMapping("/login")
public Result login(@RequestBody TbUser user) {
try {
User _user = userService.login(user.getUsername(), user.getPassword());
if(_user == null) {
return new Result(false, "登录失败,将检查用户名或者密码是否正确");
}
else {
return new Result(true, "登录成功", _user);
}
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "登录错误");
}
}
@RequestMapping("/register")
public Result register(@RequestBody TbUser user) {
try {
// 如果注册成功,不抛出抛出异常,如果注册失败抛出异常
userService.register(user);
return new Result(true, "注册成功");
} catch (RuntimeException e) {
e.printStackTrace();
return new Result(false, e.getMessage());
}
}
@RequestMapping("/upload")
public Result upload(MultipartFile file, String userid) {
try {
User user = userService.upload(file, userid);
if(user != null) {
System.out.println(user);
return new Result(true, "上传成功", user);
}
else {
return new Result(false, "上传失败");
}
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "上传失败");
}
}
@RequestMapping("/updateNickname")
public Result updateNickname(@RequestBody TbUser user) {
try {
userService.updateNickname(user.getId(), user.getNickname());
return new Result(true, "更新成功");
}
catch (RuntimeException e) {
return new Result(false, e.getMessage());
}
catch (Exception e) {
e.printStackTrace();
return new Result(false, "更新失败");
}
}
@RequestMapping("/findById")
public User findById(String userid) {
return userService.findById(userid);
}
@RequestMapping("/findByUsername")
public Result findByUsername(String userid, String friendUsername) {
try {
User user = userService.findByUsername(userid, friendUsername);
if(user != null) {
return new Result(true, "搜索成功", user);
}
else {
return new Result(false, "没有找到该用户");
}
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "搜索失败");
}
}
UserService接口定义
/**
* 用来登录检查,检查用户名和密码是否匹配
* @param username 用户名
* @param password 密码
* @return 如果登录成功返回用户对象,否则返回null
*/
User login(String username, String password);
编写UserServiceImpl实现
@Override
public User login(String username, String password) {
if(StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) {
TbUserExample example = new TbUserExample();
TbUserExample.Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(username);
List<TbUser> userList = userMapper.selectByExample(example);
if(userList != null && userList.size() == 1) {
// 对密码进行校验
String encodingPassword = DigestUtils.md5DigestAsHex(password.getBytes());
if(encodingPassword.equals(userList.get(0).getPassword())) {
User user = new User();
BeanUtils.copyProperties(userList.get(0), user);
return user;
}
}
}
return null;
}
3.2 用户登录功能 - 前端&测试
3.3 注册功能 - 后端
UserController
@RequestMapping("/register")
public Result register(@RequestBody TbUser user) {
try {
// 如果注册成功,不抛出抛出异常,如果注册失败抛出异常
userService.register(user);
return new Result(true, "注册成功");
} catch (RuntimeException e) {
e.printStackTrace();
return new Result(false, e.getMessage());
}
}
UserService接口
/**
* 注册用户,将用户信息保存到数据库中
* 如果抛出异常则,注册失败,否则,注册成功
* @param user 用户信息
*/
void register(TbUser user);
UserServiceImpl实现
@Override
public void register(TbUser user) {
try {
// 1. 判断这个用户名是否存在
TbUserExample example = new TbUserExample();
TbUserExample.Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(user.getUsername());
List<TbUser> userList = userMapper.selectByExample(example);
if(userList != null && userList.size() > 0) {
throw new RuntimeException("用户已存在");
}
// 2. 将用户信息保存到数据库中
// 使用雪花算法来生成唯一ID
user.setId(idWorker.nextId());
// 对密码进行MD5加密
user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()));
user.setPicSmall("");
user.setPicNormal("");
user.setNickname(user.getUsername());
// 生成二维码,并且将二维码的路径保存到数据库中
// 要生成二维码中的字符串
String qrcodeStr = "hichat://" + user.getUsername();
// 获取一个临时目录,用来保存临时的二维码图片
String tempDir = env.getProperty("hcat.tmpdir");
String qrCodeFilePath = tempDir + user.getUsername() + ".png";
qrCodeUtils.createQRCode(qrCodeFilePath, qrcodeStr);
// 将临时保存的二维码上传到FastDFS
String url = env.getProperty("fdfs.httpurl") +
fastDFSClient.uploadFile(new File(qrCodeFilePath));
user.setQrcode(url);
user.setCreatetime(new Date());
userMapper.insert(user);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("注册失败");
}
}
3.4 注册功能 - 前端&测试
3.5 FASTDFS - 文件服务器介绍与搭建
什么是FastDFS
FastDFS 是用 c 语言编写的一款开源的分布式文件系统。FastDFS 为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。
FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。
Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。
Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统 的文件系统来管理文件。可以将storage称为存储服务器。
服务端两个角色:
Tracker:管理集群,tracker 也可以实现集群。每个 tracker 节点地位平等。收集 Storage 集群的状态。
Storage:实际保存文件 Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。
在Linux中搭建FastDFS
下载地址:https://pan.baidu.com/s/1JPjB5dxVDUyM-QXbjc1v_Q 提取码:XWFN
解压缩fastdfs-image-server.zip
双击vmx文件,然后启动。
注意:遇到下列提示选择“我已移动该虚拟机”!
IP地址已经固定为192.168.25.133 ,请设置你的仅主机网段为25。
登录名为root 密码为itcast
3.6 FASTDFS - 整合Spring Boot
导入从github上下载的ComponetImport.java工具类
导入从github上下载的FastDFSClient.java、FileUtils.java工具类
3.7 个人信息 - 后端照片上传功能开发
注入FastDFS相关Bean
@Autowired
private Environment env;
@Autowired
private FastDFSClient fastDFSClient;
编写UserService
将新上传的图片保存到用户信息数据库中
/**
* 上传头像
* @param file 客户端上传的文件
* @param userid 用户id
* @return 如果上传成功,返回用户信息。否则返回null
*/
User upload(MultipartFile file, String userid);
编写UserServiceImpl
@Override
public User upload(MultipartFile file, String userid) {
try {
// 返回在FastDFS中的URL路径,这个路径是不带http://192.168.25.133/..
String url = fastDFSClient.uploadFace(file);
// 在FastDFS上传的时候,会自动生成一个缩略图
// 文件名_150x150.后缀
String[] fileNameList = url.split("\\.");
String fileName = fileNameList[0];
String ext = fileNameList[1];
String picSmallUrl = fileName + "_150x150." + ext;
String prefix = env.getProperty("fdfs.httpurl");
TbUser tbUser = userMapper.selectByPrimaryKey(userid);
// 设置头像大图片
tbUser.setPicNormal(prefix + url);
// 设置头像小图片
tbUser.setPicSmall(prefix + picSmallUrl);
// 将新的头像URL更新到数据库中
userMapper.updateByPrimaryKey(tbUser);
// 将用户信息返回到Controller
User user = new User();
BeanUtils.copyProperties(tbUser, user);
return user;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
3.8 个人信息 - 前端&测试头像上传
3.9 个人信息 - 修改昵称后端实现
编写UserController
@RequestMapping("/updateNickname")
public Result updateNickname(@RequestBody TbUser user) {
try {
userService.updateNickname(user.getId(), user.getNickname());
return new Result(true, "更新成功");
}
catch (RuntimeException e) {
return new Result(false, e.getMessage());
}
catch (Exception e) {
e.printStackTrace();
return new Result(false, "更新失败");
}
}
UserSevice接口
/**
* 更新昵称
* @param id 用户的id
* @param nickname 昵称
*/
void updateNickname(String id, String nickname);
UserServiceImpl实现
@Override
public void updateNickname(String id, String nickname) {
if(StringUtils.isNotBlank(nickname)) {
TbUser tbUser = userMapper.selectByPrimaryKey(id);
tbUser.setNickname(nickname);
userMapper.updateByPrimaryKey(tbUser);
}
else {
throw new RuntimeException("昵称不能为空");
}
}
3.10 个人信息 -重新加载用户信息后端实现
Controller
@RequestMapping("/findById")
public User findById(String userid) {
return userService.findById(userid);
}
UserService
/**
* 根据用户id查找用户信息
* @param userid 用户id
* @return 用户对象
*/
User findById(String userid);
UserServiceImpl
@Override
public User findById(String userid) {
TbUser tbUser = userMapper.selectByPrimaryKey(userid);
User user = new User();
BeanUtils.copyProperties(tbUser, user);
return user;
}
3.11 个人信息 - 修改昵称前端测试
3.12 个人信息 - 二维码生成后端编写
二维码是在用户注册的时候,就根据用户的用户名来自动生成一个二维码图片,并且保存到FastDFS中。
需要对注册的方法进行改造,在注册用户时,编写逻辑保存二维码。并将二维码图片的链接保存到数据库中。
二维码前端页面展示
导入二维码生成工具类
导入QRCodeUtils.java文件
UserServiceImpl
修改注册方法,在注册时,将使用二维码生成工具将二维码保存到FastDFS,并保存链接更新数据库
@Override
public void register(TbUser user) {
// 1. 查询用户是否存在
TbUserExample example = new TbUserExample();
TbUserExample.Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(user.getUsername());
List<TbUser> userList = userMapper.selectByExample(example);
// 1.1 如果存在抛出异常
if(userList != null && userList.size() > 0 ) {
throw new RuntimeException("用户名已经存在!");
}
else {
user.setId(idWorker.nextId());
// MD5加密保存
user.setPassword(DigestUtils.md5DigestAsHex(user.getPassword().getBytes()));
user.setPicSmall("");
user.setPicNormal("");
user.setNickname(user.getUsername());
// 获取临时目录
String tmpFolder = env.getProperty("hcat.tmpdir");
String qrCodeFile = tmpFolder + "/" + user.getUsername() + ".png";
qrCodeUtils.createQRCode(qrCodeFile, "user_code:" + user.getUsername());
try {
String url = fastDFSClient.uploadFile(new File(qrCodeFile));
user.setQrcode(url);
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException("上传文件失败");
}
user.setCreatetime(new Date());
userMapper.insert(user);
}
}
3.13 个人信息 - 二维码生成前端测试
4 业务开发 - 发现页面与通信录
4.1 搜索朋友 - 后端开发
在搜索朋友的时候需要进行以下判断:
- 不能添加自己为好友
- 如果搜索的用户已经是好友了,就不能再添加了
- 如果已经申请过好友并且好友并没有处理这个请求了,也不能再申请。
前端页面展示
搜索朋友其实就是用户搜索,所以我们只需要根据用户名将对应的用户搜索出来即可
编写UserController
@RequestMapping("/findByUsername")
public Result findByUsername(String userid, String friendUsername) {
try {
User user = userService.findByUsername(userid, friendUsername);
if(user != null) {
return new Result(true, "搜索成功", user);
}
else {
return new Result(false, "没有找到该用户");
}
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "搜索失败");
}
}
编写UserService接口
/**
* 根据用户名搜索用户(好友搜索)
* 在搜索用户的时候不需要进行校验
* @param userid 用户id
* @param friendUsername 好友的用户名
* @return 如果搜索到好友,就返回用户对象,否则返回null
*/
User findByUsername(String userid, String friendUsername);
编写UserServiceImpl实现
@Override
public User findByUsername(String userid, String friendUsername) {
TbUserExample example = new TbUserExample();
TbUserExample.Criteria criteria = example.createCriteria();
criteria.andUsernameEqualTo(friendUsername);
List<TbUser> userList = userMapper.selectByExample(example);
TbUser friend = userList.get(0);
User friendUser = new User();
BeanUtils.copyProperties(friend, friendUser);
return friendUser;
}
4.2 搜索朋友 - 前端测试联调
4.3 添加好友 - 发送好友请求后端开发
编写FriendController
@RequestMapping("sendRequest")
public Result sendRequest(@RequestBody TbFriendReq friendReq) {
try {
friendService.sendRequest(friendReq.getFromUserid(), friendReq.getToUserid());
return new Result(true, "已申请");
}
catch(RuntimeException e) {
return new Result(false, e.getMessage());
}
catch (Exception e) {
e.printStackTrace();
return new Result(false, "申请好友失败");
}
}
编写FriendService
/**
* 发送好友请求
* @param fromUserid 申请好友的用户id
* @param toUserid 要添加的好友id
*/
void sendRequest(String fromUserid, String toUserid);
编写FriendServiceImpl实现
@Override
public void sendRequest(String fromUserid, String toUserid) {
// 检查是否允许添加好友
TbUser friend = userMapper.selectByPrimaryKey(toUserid);
checkAllowToAddFriend(fromUserid, friend);
// 添加好友请求
TbFriendReq friendReq = new TbFriendReq();
friendReq.setId(idWorker.nextId());
friendReq.setFromUserid(fromUserid);
friendReq.setToUserid(toUserid);
friendReq.setCreatetime(new Date());
friendReq.setStatus(0);
friendReqMapper.insert(friendReq);
}
4.4 添加好友 -前端测试
4.5 展示好友请求 -后端开发
前端页面展示
编写Controller
@RequestMapping("/findFriendReqByUserid")
public List<FriendReq> findFriendReqByUserid(String userid) {
return friendService.findFriendReqByUserid(userid);
}
编写FriendService
/**
* 根据用户ID查询他对应的好友请求
* @param userid 当前登录的用户
* @return 请求好友的用户列表
*/
List<FriendReq> findFriendReqByUserid(String userid);
编写FriendServiceImpl实现
@Override
public List<FriendReq> findFriendReqByUserid(String userid) {
// 根据用户的id查询对应的好友请求
TbFriendReqExample example = new TbFriendReqExample();
TbFriendReqExample.Criteria criteria = example.createCriteria();
criteria.andToUseridEqualTo(userid);
criteria.andStatusEqualTo(0);
List<TbFriendReq> tbFriendReqList = friendReqMapper.selectByExample(example);
List<FriendReq> friendReqList = new ArrayList<FriendReq>();
// 根据好友请求,将发起好友请求的用户信息返回
for (TbFriendReq tbFriendReq : tbFriendReqList) {
TbUser tbUser = userMapper.selectByPrimaryKey(tbFriendReq.getFromUserid());
FriendReq friendReq = new FriendReq();
BeanUtils.copyProperties(tbUser, friendReq);
// 设置好友请求的id
friendReq.setId(tbFriendReq.getId());
friendReqList.add(friendReq);
}
return friendReqList;
}
4.6 展示好友请求 - 前端测试
4.7 添加好友 - 接受好友请求后端开发
添加好友需要双方互相添加。
例如:A接受B的好友申请,则将A成为B的好友,同时B也成为A的好友。
编写FriendController
@RequestMapping("/acceptFriendReq")
public Result acceptFriendReq(String reqid) {
try {
friendService.acceptFriendReq(reqid);
return new Result(true, "添加好友成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "添加好友失败");
}
}
编写FriendService
/**
* 接收好友请求
* @param reqid 好友请求的id
*/
void acceptFriendReq(String reqid);
编写FriendServiceImpl
@Override
public void acceptFriendReq(String reqid) {
System.out.println(reqid);
// 1. 将好友请求的status标志设置为1,表示已经处理了该好友请求
TbFriendReq friendReq = friendReqMapper.selectByPrimaryKey(reqid);
friendReq.setStatus(1);
friendReqMapper.updateByPrimaryKey(friendReq);
// 2. 互相添加好友,在tb_friend表中应该添加两条记录
TbFriend friend1 = new TbFriend();
friend1.setId(idWorker.nextId());
friend1.setUserid(friendReq.getFromUserid());
friend1.setFriendsId(friendReq.getToUserid());
friend1.setCreatetime(new Date());
TbFriend friend2 = new TbFriend();
friend2.setId(idWorker.nextId());
friend2.setUserid(friendReq.getToUserid());
friend2.setFriendsId(friendReq.getFromUserid());
friend2.setCreatetime(new Date());
friendMapper.insert(friend1);
friendMapper.insert(friend2);
}
4.8 添加好友 -拒绝添加好友后端开发
在用户选择忽略好友请求时,我们只需要将之前的好友请求状态(status)设置为1。无需添加好友。
编写FriendController
@RequestMapping("/ignoreFriendReq")
public Result ignoreFriendReq(String reqid) {
try {
friendService.ignoreFriendReq(reqid);
return new Result(true, "忽略好友请求成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "忽略好友请求失败");
}
}
编写FriendService接口
/**
* 忽略好友请求
* @param reqid 好友请求的id
*/
void ignoreFriendReq(String reqid);
编写FriendServiceImpl实现
@Override
public void ignoreFriendReq(String reqid) {
TbFriendReq tbFriendReq = friendReqMapper.selectByPrimaryKey(reqid);
tbFriendReq.setStatus(1);
friendReqMapper.updateByPrimaryKey(tbFriendReq);
}
4.9 通信录功能 - 后端
通信录功能就是要根据当前登录用户的id,获取到用户的好友列表。
前端页面效果
编写FriendController
@RequestMapping("/findFriendByUserid")
public List<User> findFriendByUserid(String userid) {
try {
return friendService.findFriendByUserid(userid);
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<User>();
}
}
编写FriendService
/**
* 查询我的好友
* @param userid 当前登录的用户id
* @return 好友列表
*/
List<User> findFriendByUserid(String userid);
编写FriendServiceImpl
@Override
public List<User> findFriendByUserid(String userid) {
// 根据userid查询tb_friend表中的数据
TbFriendExample friendExample = new TbFriendExample();
TbFriendExample.Criteria criteria = friendExample.createCriteria();
criteria.andUseridEqualTo(userid);
List<TbFriend> tbFriendList = friendMapper.selectByExample(friendExample);
List<User> friendList = new ArrayList<User>();
// 遍历好友列表,将好友的用户信息查询出来
for (TbFriend tbFriend : tbFriendList) {
// 根据好友的id将他对应的用户信息查询出来
TbUser tbUser = userMapper.selectByPrimaryKey(tbFriend.getFriendsId());
User friend = new User();
BeanUtils.copyProperties(tbUser, friend);
friendList.add(friend);
}
return friendList;
}
5 业务开发 - 聊天业务
5.1 聊天业务 - 用户id关联Netty通道后端开发
要使用netty来进行两个客户端之间的通信,需要提前建立好用户id与Netty通道的关联。服务器端需要对消息进行保存。
每一个App客户端登录的时候,就需要建立用户id与通道的关联。
导入SpringUtil工具类
此工具类主要用来在普通Java类中获取Spring容器中的bean
定义消息实体类
public class Message {
private Integer type; // 消息类型
private TbChatRecord chatRecord; // 聊天消息
private Object ext; // 扩展消息字段
public Integer getType() {
return type;
}
public void setType(Integer type) {
this.type = type;
}
public TbChatRecord getChatRecord() {
return chatRecord;
}
public void setChatRecord(TbChatRecord chatRecord) {
this.chatRecord = chatRecord;
}
public Object getExt() {
return ext;
}
public void setExt(Object ext) {
this.ext = ext;
}
}
定义UserChannelMap用来保存用户id与Channel通道关联
public class UserChannelMap {
public static HashMap<String, Channel> userChannelMap = new HashMap<>();
public static void put(String userid, Channel channel) {
userChannelMap.put(userid, channel);
}
public static Channel get(String userid) {
return userChannelMap.get(userid);
}
}
编写ChatHandller
用户在第一次登陆到手机App时,会自动发送一个type为0的消息,此时,需要建立用户与Channel通道的关联。后续,将会根据userid获取到Channel,给用户推送消息。
/**
* 处理消息的handler
* TextWebSocketFrame: 在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体
*/
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
// 用来保存所有的客户端连接
private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-mm-dd hh:MM");
// 当Channel中有新的事件消息会自动调用
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 当接收到数据后会自动调用
// 获取客户端发送过来的文本消息
String text = msg.text();
System.out.println("接收到消息数据为:" + text);
Message message = JSON.parseObject(text, Message.class);
// 通过SpringUtil工具类获取Spring上下文容器
ChatRecordService chatRecordService = SpringUtil.getBean(ChatRecordService.class);
switch (message.getType()) {
// 处理客户端连接的消息
case 0:
// 建立用户与通道的关联
String userid = message.getChatRecord().getUserid();
UserChannelMap.put(userid, ctx.channel());
System.out.println("建立用户:" + userid + "与通道" + ctx.channel().id() + "的关联");
UserChannelMap.print();
break;
// 处理客户端发送好友消息
case 1:
System.out.println("接收到用户消息");
// 将聊天消息保存到数据库
TbChatRecord chatRecord = message.getChatRecord();
chatRecordService.insert(chatRecord);
// 如果发送消息好友在线,可以直接将消息发送给好友
Channel channel = UserChannelMap.get(chatRecord.getFriendid());
if(channel != null) {
channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));
}
else {
// 如果不在线,暂时不发送
System.out.println("用户" + chatRecord.getFriendid() + "不在线");
}
break;
// 处理客户端的签收消息
case 2:
// 将消息记录设置为已读
chatRecordService.updateStatusHasRead(message.getChatRecord().getId());
break;
case 3:
// 接收心跳消息
System.out.println("接收到心跳消息:" + JSON.toJSONString(message));
break;
}
}
// 当有新的客户端连接服务器之后,会自动调用这个方法
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 将新的通道加入到clients
clients.add(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
ctx.channel().close();
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("关闭通道");
UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
UserChannelMap.print();
}
}
5.2 聊天业务 - 用户断开连接、连接异常取消关联通道
服务器端应该根据通道的ID,来取消用户id与通道的关联关系。
UserChannelMap类
/**
* 根据通道id移除用户与channel的关联
* @param channelId 通道的id
*/
public static void removeByChannelId(String channelId) {
if(!StringUtils.isNotBlank(channelId)) {
return;
}
for (String s : userChannelMap.keySet()) {
Channel channel = userChannelMap.get(s);
if(channelId.equals(channel.id().asLongText())) {
System.out.println("客户端连接断开,取消用户" + s + "与通道" + channelId + "的关联");
userChannelMap.remove(s);
break;
}
}
}
ChatHandler类
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
ctx.channel().close();
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
System.out.println("关闭通道");
UserChannelMap.removeByChannelId(ctx.channel().id().asLongText());
UserChannelMap.print();
}
5.3 聊天业务 - 发送聊天消息后端开发
将消息发送到好友对应的Channel通道,并将消息记录保存到数据库中
编写ChatHandler
获取ChatRecordService服务
// 通过SpringUtil工具类获取Spring上下文容器
ChatRecordService chatRecordService = SpringUtil.getBean(ChatRecordService.class);
switch (message.getType()) {
// 处理客户端连接的消息
case 0:
// 建立用户与通道的关联
String userid = message.getChatRecord().getUserid();
UserChannelMap.put(userid, ctx.channel());
System.out.println("建立用户:" + userid + "与通道" + ctx.channel().id() + "的关联");
UserChannelMap.print();
break;
// 处理客户端发送好友消息
case 1:
System.out.println("接收到用户消息");
// 将聊天消息保存到数据库
TbChatRecord chatRecord = message.getChatRecord();
chatRecordService.insert(chatRecord);
// 如果发送消息好友在线,可以直接将消息发送给好友
Channel channel = UserChannelMap.get(chatRecord.getFriendid());
if(channel != null) {
channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));
}
else {
// 如果不在线,暂时不发送
System.out.println("用户" + chatRecord.getFriendid() + "不在线");
}
break;
// 处理客户端的签收消息
case 2:
// 将消息记录设置为已读
chatRecordService.updateStatusHasRead(message.getChatRecord().getId());
break;
case 3:
// 接收心跳消息
System.out.println("接收到心跳消息:" + JSON.toJSONString(message));
break;
}
编写ChatRecordService接口
/**
* 将聊天记录保存到数据库中
* @param chatRecord
*/
void insert(TbChatRecord chatRecord);
编写ChatRecordServiceImpl实现
@Override
public void insert(TbChatRecord chatRecord) {
chatRecord.setId(idWorker.nextId());
chatRecord.setHasRead(0);
chatRecord.setCreatetime(new Date());
chatRecord.setHasDelete(0);
chatRecordMapper.insert(chatRecord);
}
5.4 聊天业务 - 加载聊天记录功能
根据userid和friendid加载未读的聊天记录
编写ChatRecordController
@RequestMapping("/findByUserIdAndFriendId")
public List<TbChatRecord> findByUserIdAndFriendId(String userid, String friendid) {
try {
return chatRecordService.findByUserIdAndFriendId(userid, friendid);
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<TbChatRecord>();
}
}
编写ChatRecordService
/**
* 根据用户id和好友id将聊天记录查询出来
* @param userid 用户id
* @param friendid 好友id
* @return 聊天记录列表
*/
List<TbChatRecord> findByUserIdAndFriendId(String userid, String friendid);
编写ChatRecordServiceImpl实现
@Override
public List<TbChatRecord> findByUserIdAndFriendId(String userid, String friendid) {
// 需要:
TbChatRecordExample example = new TbChatRecordExample();
TbChatRecordExample.Criteria criteria1 = example.createCriteria();
TbChatRecordExample.Criteria criteria2 = example.createCriteria();
// 将 userid -> friendid的聊天记录查询出来
criteria1.andUseridEqualTo(userid);
criteria1.andFriendidEqualTo(friendid);
criteria1.andHasDeleteEqualTo(0);
// 将 friendid -> userid的聊天记录查询出来
criteria2.andUseridEqualTo(friendid);
criteria2.andFriendidEqualTo(userid);
criteria2.andHasDeleteEqualTo(0);
example.or(criteria1);
example.or(criteria2);
// 将发给userid的所有聊天记录设置为已读
TbChatRecordExample exampleQuerySendToMe = new TbChatRecordExample();
TbChatRecordExample.Criteria criteriaQuerySendToMe = example.createCriteria();
criteriaQuerySendToMe.andFriendidEqualTo(userid);
criteriaQuerySendToMe.andHasReadEqualTo(0);
List<TbChatRecord> tbChatRecords = chatRecordMapper.selectByExample(exampleQuerySendToMe);
for (TbChatRecord tbChatRecord : tbChatRecords) {
tbChatRecord.setHasRead(1);
chatRecordMapper.updateByPrimaryKey(tbChatRecord);
}
return chatRecordMapper.selectByExample(example);
}
5.5 聊天业务 - 已读/未读消息状态标记
已读消息
当用户接收到聊天消息,且聊天窗口被打开,就会发送一条用来签收的消息到Netty服务器
用户打开聊天窗口,加载所有聊天记录,此时会把发给他的所有消息设置为已读
未读消息
如果用户没有打开聊天窗口,就认为消息是未读的
ChatRecordController
@RequestMapping("/findUnreadByUserid")
public List<TbChatRecord> findUnreadByUserid(String userid) {
try {
return chatRecordService.findUnreadByUserid(userid);
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<TbChatRecord>();
}
}
ChatRecordService
/**
* 设置消息为已读
* @param id 聊天记录的id
*/
void updateStatusHasRead(String id);
ChatRecordServiceImpl
@Override
public void updateStatusHasRead(String id) {
TbChatRecord tbChatRecord = chatRecordMapper.selectByPrimaryKey(id);
tbChatRecord.setHasRead(1);
chatRecordMapper.updateByPrimaryKey(tbChatRecord);
}
ChatHandler
// 处理客户端的签收消息
case 2:
// 将消息记录设置为已读
chatRecordService.updateStatusHasRead(message.getChatRecord().getId());
break;
5.6 聊天业务 - 未读消息读取
在用户第一次打开App的时候,需要将所有的未读消息加载到App
ChatRecordController
@RequestMapping("/findUnreadByUserid")
public List<TbChatRecord> findUnreadByUserid(String userid) {
try {
return chatRecordService.findUnreadByUserid(userid);
} catch (Exception e) {
e.printStackTrace();
return new ArrayList<TbChatRecord>();
}
}
ChatRecordService
/**
* 根据用户id,查询发给他的未读消息记录
* @param userid 用户id
* @return 未读消息列表
*/
List<TbChatRecord> findUnreadByUserid(String userid);
ChatRecordServiceImpl
@Override
public List<TbChatRecord> findUnreadByUserid(String userid) {
TbChatRecordExample example = new TbChatRecordExample();
TbChatRecordExample.Criteria criteria = example.createCriteria();
// 设置查询发给userid的消息
criteria.andFriendidEqualTo(userid);
criteria.andHasReadEqualTo(0);
return chatRecordMapper.selectByExample(example);
}
6 业务开发 - 心跳机制
6.1 Netty心跳处理以及读写超时设置
Netty并不能监听到客户端设置为飞行模式时,自动关闭对应的通道资源。我们需要让Netty能够定期检测某个通道是否空闲,如果空闲超过一定的时间,就可以将对应客户端的通道资源关闭。
编写后端Netty心跳检查的Handler
public class HearBeatHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent) {
IdleStateEvent idleStateEvent = (IdleStateEvent)evt;
if(idleStateEvent.state() == IdleState.READER_IDLE) {
System.out.println("读空闲事件触发...");
}
else if(idleStateEvent.state() == IdleState.WRITER_IDLE) {
System.out.println("写空闲事件触发...");
}
else if(idleStateEvent.state() == IdleState.ALL_IDLE) {
System.out.println("---------------");
System.out.println("读写空闲事件触发");
System.out.println("关闭通道资源");
ctx.channel().close();
}
}
}
}
在通道初始化器中(WebSocketInitailizer)添加心跳检查
// 添加Netty空闲超时检查的支持
// 1. 读空闲超时(超过一定的时间会发送对应的事件消息)
// 2. 写空闲超时
// 3. 读写空闲超时
pipeline.addLast(new IdleStateHandler(4, 8, 12));
pipeline.addLast(new HearBeatHandler());
// 添加自定义的handler
pipeline.addLast(new ChatHandler());