基于 Netty 网络程式设计专案实战课程2
4业务开发 - 使用者注册/登入/个人资讯


4.1使用者登入功能 -后端开发
汇入 IdWorker.java 雪花算法 ID 生成器
初始化 IdWorker
@SpringBootApplication
@MapperScan(basePackages = "com.itheima.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 实现
@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, "登入错误");
}
}
UserService 界面定义
/**
*登入
*@param user
*@return
*/
User login(TbUser user);
编写 UserServiceImpl 实现
@Override
public User login(TbUser user) {
TbUserExample example = new TbUserExample(); TbUserExample.Criteria criteria = example.createCriteria(); criteria.andUsernameEqualTo(user.getUsername());
List userList = userMapper.selectByExample(example); if(userList != null && userList.size() == 1) {
TbUser userInDB = userList.get(0);
// MD5 加密认证
if(userInDB.getPassword().equals(DigestUtils.md5DigestAsHex(user.getPassword ().getBytes()))) {
return loadUserById(userInDB.getId());
}
else {
throw new RuntimeException("使用者名称或密码错误");
}
}
else {
throw new RuntimeException("使用者不存在");
}
}
4.2使用者登入功能 - 前端&测试
4.3注册功能 - 后端
UserController
@RequestMapping("/register")
public Result register(@RequestBody TbUser user) {
try {
userService.register(user);
return new Result(true, "注册成功");
} catch (RuntimeException e) {
return new Result(false, e.getMessage());
}
}
UserService 界面
void register(TbUser user);
UserServiceImpl 实现
@Override
public void register(TbUser user) {
// 1. 查询使用者是否存在
TbUserExample example = new TbUserExample(); TbUserExample.Criteria criteria = example.createCriteria(); criteria.andUsernameEqualTo(user.getUsername());
List 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()); user.setQrcode(""); user.setCreatetime(new Date()); userMapper.insert(user);
}
}
4.4注册功能 - 前端&测试
4.5FASTDFS - 档案服务器介绍与搭建
什么是 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
解压缩 fastdfs-image-server.zip
双击 vmx 档案,然后启动。
注意:遇到下列提示选择“我已移动该虚拟机器”!

IP 地址已经固定为 192.168.25.133,请设定你的仅主机网段为 25。
登入名为 root密码为 itcast
4.6FASTDFS - 整合 Spring Boot
汇入 ComponetImport.java 工具类
汇入 FastDFSClient.java、FileUtils.java 工具类
4.7个人资讯 - 后端照片上传功能开发
注入 FastDFS 相关 Bean
@Autowired
private Environment env; @Autowired
private FastDFSClient fastDFSClient;
编写 UserController update Handler 上传照片
@RequestMapping("/upload")
public Result upload(MultipartFile file, String userid) {
try {
// 上传
String url = fastDFSClient.uploadFace(file); String suffix = "_150x150.";
String[] pathList = url.split("\.");
String thumpImgUrl = pathList[0] + suffix + pathList[1];
// 更新使用者头像
User user = userService.updatePic(userid, url, thumpImgUrl); user.setPicNormal(env.getProperty("fdfs.httpurl") +
user.getPicNormal());
user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall());
return new Result(true, "上传成功", user);
} catch (IOException e) { e.printStackTrace();
return new Result(false, "上传失败");
}
编写 UserService
将新上传的图片储存到使用者资讯数据库中
/**
*更新使用者头像
*@param userid
*@param url
*@param thumpImgUrl
*/
User updatePic(String userid, String url, String thumpImgUrl);
编写 UserServiceImpl
@Override
public User updatePic(String userid, String url, String thumpImgUrl) { TbUser user = userMapper.selectByPrimaryKey(userid); user.setPicNormal(url);
user.setPicSmall(thumpImgUrl);;
userMapper.updateByPrimaryKey(user);
User userVo = new User();
BeanUtils.copyProperties(user, userVo);
return userVo;
}
4.8个人资讯 - 前端&测试头像上传


BeanUtils.copyProperties(user, userVo);
return userVo;
}
4.8个人资讯 - 前端&测试头像上传
4.9个人资讯 - 修改昵称后端实现
编写 UserController
@RequestMapping("/updateNickname")
public Result updateNickname(@RequestBody TbUser user) {
try {
userService.updateNickname(user.getId(), user.getNickname());
return new Result(true, "修改成功");
} catch (Exception e) {
e.printStackTrace();
return new Result(false, "修改失败");
}
}
UserSevice 界面
/**
*根据使用者id 更新使用者昵称
*@param userid
*@param nickname
*/
void updateNickname(String userid, String nickname);
UserServiceImpl 实现
@Override
public void updateNickname(String userid, String nickname) { System.out.println(userid);
TbUser user = userMapper.selectByPrimaryKey(userid); user.setNickname(nickname);
userMapper.updateByPrimaryKey(user);
}
4.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;
}
4.11个人资讯 - 修改昵称前端测试


4.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 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);
}
}
4.13个人资讯 - 二维码生成前端测试
5业务开发 - 发现页面与通讯录
5.1搜寻朋友 - 后端开发在搜寻朋友的时候需要进行以下判断:
1.不能新增自己为好友
2.如果搜寻的使用者已经是好友了,就不能再添加了
3. 如果已经申请过好友并且好友并没有处理这个请求了,也不能再申请。
前端页面展示

搜寻朋友其实就是使用者搜寻,所以我们只需要根据使用者名称将对应的使用者搜寻出来即可。
编写 UserController
@RequestMapping("/findUserById")
public User findUserById(String userid) { System.out.println(userid);
return userService.loadUserById(userid);
}
编写 UserService 界面
/**
*根据使用者 id 载入使用者资讯
*@param userid
*@return
*/
User findUserById(String userid);
编写 UserServiceImpl 实现
@Override
public User findUserById(String userid) {
TbUser tbUser = userMapper.selectByPrimaryKey(userid); User user = new User(); BeanUtils.copyProperties(tbUser, user);
if(StringUtils.isNotBlank(user.getPicNormal())) { user.setPicNormal(env.getProperty("fdfs.httpurl") +
user.getPicNormal());
}
if(StringUtils.isNotBlank(user.getPicSmall())) { user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall());
}
user.setQrcode(env.getProperty("fdfs.httpurl") + user.getQrcode());
return user;
}
5.2搜寻朋友 - 前端测试联调
5.3新增好友 - 传送好友请求后端开发
新增好友需要传送一个好友请求。
编写 FriendController
@RequestMapping("/sendRequest")
public Result sendRequest(@RequestBody TbFriendReq tbFriendReq) {
try {
friendService.sendRequest(tbFriendReq); return new Result(true, "传送请求成功");
}
catch (RuntimeException e) {
return new Result(false, e.getMessage());
}
catch (Exception e) { e.printStackTrace();
return new Result(false, "传送请求失败");
}
}
编写 FriendService
/**
* 传送好友请求
*/
void sendRequest(TbFriendReq friendReq);
编写 FriendServiceImpl 实现
@Override
public void sendRequest(TbFriendReq friendReq) {
// 判断使用者是否已经发起过好友申请
TbFriendReqExample example = new TbFriendReqExample(); TbFriendReqExample.Criteria criteria = example.createCriteria(); criteria.andFromUseridEqualTo(friendReq.getFromUserid()); criteria.andToUseridEqualTo(friendReq.getToUserid());
List friendReqList = friendReqMapper.selectByExample(example); if(friendReqList == null || friendReqList.size() == 0) {
friendReq.setId(idWorker.nextId()); friendReq.setCreatetime(new Date());
// 设定请求未处理
friendReq.setStatus(0);
friendReqMapper.insert(friendReq);
}
else {
throw new RuntimeException("您已经请求过了");
}
}
5.4新增好友 -前端测试
5.5展示好友请求 -后端开发
前端页面展示

编写 Controller
@RequestMapping("/findFriendReqByUserid")
public List findMyFriendReq(String userid) {
return friendService.findMyFriendReq(userid);
}
编写 FriendService
/**
*根据使用者 id 查询好友请求
*@param userid
*@return
*/
List findMyFriendReq(String userid);
@Override
public List findMyFriendReq(String userid) {
// 查询好友请求
TbFriendReqExample example = new TbFriendReqExample(); TbFriendReqExample.Criteria criteria = example.createCriteria(); criteria.andToUseridEqualTo(userid);
// 查询没有处理的好友请求
criteria.andStatusEqualTo(0);
List tbFriendReqList =
friendReqMapper.selectByExample(example);
List friendReqList = new ArrayList();
// 载入好友资讯
for (TbFriendReq tbFriendReq : tbFriendReqList) { TbUser tbUser =
userMapper.selectByPrimaryKey(tbFriendReq.getFromUserid()); FriendReq friendReq = new FriendReq(); BeanUtils.copyProperties(tbUser, friendReq); friendReq.setId(tbFriendReq.getId());
// 新增 HTTP 字首
friendReq.setPicSmall(env.getProperty("fdfs.httpurl") + friendReq.getPicSmall());
friendReq.setPicNormal(env.getProperty("fdfs.httpurl") + friendReq.getPicNormal());
friendReqList.add(friendReq);
}
return friendReqList;
}
5.6 展示好友请求 - 前端测试
5.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) {
// 设定请求状态为 1
TbFriendReq tbFriendReq = friendReqMapper.selectByPrimaryKey(reqid); tbFriendReq.setStatus(1); friendReqMapper.updateByPrimaryKey(tbFriendReq);
// 互相新增为好友
// 新增申请方好友
TbFriend friend1 = new TbFriend(); friend1.setId(idWorker.nextId()); friend1.setUserid(tbFriendReq.getFromUserid()); friend1.setFriendsId(tbFriendReq.getToUserid()); friend1.setCreatetime(new Date());
// 新增接受方好友
TbFriend friend2 = new TbFriend(); friend2.setId(idWorker.nextId()); friend2.setFriendsId(tbFriendReq.getFromUserid()); friend2.setUserid(tbFriendReq.getToUserid()); friend2.setCreatetime(new Date());
friendMapper.insert(friend1); friendMapper.insert(friend2);
// 传送讯息更新通讯录
// 获取传送好友请求方 Channel
Channel channel = UserChannelMap.get(tbFriendReq.getFromUserid());
if(channel != null){
Message message = new Message(); message.setType(4);
channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));
}
}
5.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) {
// 设定请求状态为 1
TbFriendReq tbFriendReq = friendReqMapper.selectByPrimaryKey(reqId); tbFriendReq.setStatus(1); friendReqMapper.updateByPrimaryKey(tbFriendReq);
}
5.9通讯录功能 - 后端
通讯录功能就是要根据当前登入使用者的 id,获取到使用者的好友列表。
前端页面效果

编写 FriendController
/**
*根据使用者 id 查询好友
*@param userid
*@return
*/
@RequestMapping("/findFriendsByUserid")
public List findFriendsByUserid(String userid) {
return friendService.findFriendsByUserid(userid);
}
编写 FriendService
/**
*根据使用者 id 查询好友
*@param userid
*@return
*/
List findFriendsByUserid(String userid);
编写 FriendServiceImpl
@Override
public List findFriendsByUserid(String userid) { TbFriendExample example = new TbFriendExample(); TbFriendExample.Criteria criteria = example.createCriteria(); criteria.andUseridEqualTo(userid);
List tbFriendList = friendMapper.selectByExample(example); List userList = new ArrayList();
for (TbFriend tbFriend : tbFriendList) {
TbUser tbUser = userMapper.selectByPrimaryKey(tbFriend.getFriendsId()); User user = new User();
BeanUtils.copyProperties(tbUser, user);
// 新增 HTTP 字首
user.setPicSmall(env.getProperty("fdfs.httpurl") + user.getPicSmall()); user.setPicNormal(env.getProperty("fdfs.httpurl") +
user.getPicNormal());
userList.add(user);
}
return userList;
}
6、业务开发 - 聊天业务
6.1聊天业务 - 使用者 id 关联 Netty 通道后端开发
要使用 netty 来进行两个客户端之间的通讯,需要提前建立好使用者 id 与 Netty 通道的关联。服务器端需要对讯息进行储存。
每一个 App 客户端登入的时候,就需要建立使用者 id 与通道的关联。
汇入 SpringUtil 工具类
此工具类主要用来在普通Java 类中获取 Spring 容器中的 bean
定义讯息实体类
public class Message implements Serializable{
private Integer type; // 讯息型别
private TbChatRecord chatRecord; // 讯息体
private String ext; // 扩充套件字段
// getter/setter
}
定义 UserChannelMap 用来储存使用者 id 与 Channel 通道关联
public class UserChannelMap {
public static HashMap 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 {
// 用于记录和管理所有客户端的 Channel
private static ChannelGroup clients = new
DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
// 1. 获取从客户端传输过来的讯息
String text = msg.text();
// 2. 判断讯息的型别,根据不同的讯息型别执行不同的处理
System.out.println(text);
Message message = JSON.parseObject(text, Message.class); Integer type = message.getType();
switch (type) {
case 0:
// 2.1 当 websocket 第一次 Open 的时候,初始化 channel,channeluserid
String userid = message.getChatRecord().getUserid();
关联到
// 储存 userid 对应的 channel
UserChannelMap.put(userid, channel);
for (Channel client : clients) {
System.out.println("客户端连线 id:" + client.id());
}
// 打印当前线上使用者
for(String uid : UserChannelMap.userChannelMap.keySet()) {
System.out.print("使用者 id:" + uid + " ");
System.out.println("Channelid:" + UserChannelMap.get(uid));
}
break;
case 1:
// 2.2 聊天记录储存到数据库,标记讯息的签收状态[未签收]
break;
case 2:
// 2.3 签收讯息,修改数据库中的讯息签收状态[已签收]
// 表示讯息 id 的列表
break;
case 3:
// 2.4 心跳型别的讯息
break;
}
}
/**
* 当客户端连线服务端之后(开启连线)
* 获取客户端的 channel,并且放入到 ChannelGroup 中去进行管理
* @param ctx
* @throws Exception
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// 将 channel 新增到客户端
clients.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// 当触发 handlerRemoved,ChannelGroup 会自动移除对应客户端的 channel clients.remove(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// 丢掷异常时移除通道cause.printStackTrace(); ctx.channel().close(); clients.remove(ctx.channel());
}
}
6.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();
}
6.3聊天业务 - 传送聊天讯息后端开发
将讯息传送到好友对应的Channel 通道,并将讯息记录储存到数据库中
编写 ChatHandler
获取 ChatRecordService 服务
Channel channel = ctx.channel();
ChatRecordService chatRecordService =(ChatRecordService) SpringUtil.getBean("chatRecordServiceImpl");
case 1:
// 2.2 聊天记录储存到数据库,标记讯息的签收状态[未签收] TbChatRecord chatRecord = message.getChatRecord(); String msgText = chatRecord.getMessage();
String friendid = chatRecord.getFriendid(); String userid1 = chatRecord.getUserid();
// 储存到数据库,并标记为未签收
String messageId = chatRecordService.insert(chatRecord); chatRecord.setId(messageId);
// 传送讯息
Channel channel1 = UserChannelMap.get(friendid);
if(channel1 != null) {
// 从 ChannelGroup 查询对应的额 Channel 是否存在Channel channel2 = clients.find(channel1.id()); if(channel2 != null) {
// 使用者线上,传送讯息到对应的通道
System.out.println("传送讯息到" + JSON.toJSONString(message)); channel2.writeAndFlush(new
TextWebSocketFrame(JSON.toJSONString(message)));
}
}
break;
编写 ChatRecordService 界面
/**
*储存聊天记录到服务器
*@param chatRecord
*/
String insert(TbChatRecord chatRecord);
编写 ChatRecordServiceImpl 实现
@Override
public String insert(TbChatRecord chatRecord) { chatRecord.setId(idWorker.nextId()); chatRecord.setHasRead(0);
chatRecord.setCreatetime(new Date()); chatRecord.setHasDelete(0);
chatRecordMapper.insert(chatRecord);
return chatRecord.getId();
}
6.4聊天业务 - 载入聊天记录功能
根据 userid 和 friendid 载入未读的聊天记录
编写 ChatRecordController
@RequestMapping("/findUnreadByUserIdAndFriendId")
public List findUnreadByUserIdAndFriendId(String userid, String friendid) {
return chatRecordService.findUnreadByUserIdAndFriendId(userid, friendid);
}
编写 ChatRecordService
/**
*根据使用者 ID 和朋友 ID 获取未读的讯息
*@param userid
*@param friendId
*@return
*/
List findUnreadByUserIdAndFriendId(String userid, String
friendId);
编写 ChatRecordServiceImpl 实现
@Override
public List findUnreadByUserIdAndFriendId(String userid, String friendid) {
TbChatRecordExample example = new TbChatRecordExample();
TbChatRecordExample.Criteria criteria1 = example.createCriteria();
criteria1.andUseridEqualTo(friendid); criteria1.andFriendidEqualTo(userid); criteria1.andHasReadEqualTo(0); criteria1.andHasDeleteEqualTo(0);
TbChatRecordExample.Criteria criteria2 = example.createCriteria(); criteria2.andUseridEqualTo(userid); criteria2.andFriendidEqualTo(friendid); criteria2.andHasReadEqualTo(0);
criteria2.andHasDeleteEqualTo(0);
example.or(criteria1); example.or(criteria2);
// 载入未读讯息
List chatRecordList =
chatRecordMapper.selectByExample(example);
// 将讯息标记为已读
for (TbChatRecord tbChatRecord : chatRecordList) { tbChatRecord.setHasRead(1); chatRecordMapper.updateByPrimaryKey(tbChatRecord);
}
return chatRecordList;
}
6.5聊天业务 - 已读/未读讯息状态标记
已读讯息
当用户接收到聊天讯息,且聊天视窗被开启,就会发送一条用来签收的讯息到Netty 服务器
使用者开启聊天视窗,载入所有聊天记录,此时会把发给他的所有讯息设定为已读
未读讯息
如果使用者没有开启聊天视窗,就认为讯息是未读的
ChatRecordController
@RequestMapping("/findUnreadByUserid")
public List findUnreadByUserid(String userid) {
try {
return chatRecordService.findUnreadByUserid(userid);
} catch (Exception e) { e.printStackTrace();
return new ArrayList();
}
}
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;
6.6聊天业务 - 未读讯息读取
在使用者第一次开启App 的时候,需要将所有的未读讯息载入到App
ChatRecordController
@RequestMapping("/findUnreadByUserid")
public List findUnreadByUserid(String userid) {
try {
return chatRecordService.findUnreadByUserid(userid);
} catch (Exception e) { e.printStackTrace();
return new ArrayList();
}
}
ChatRecordService
/**
*根据使用者 id,查询发给他的未读讯息记录
*@param userid 使用者 id
*@return 未读讯息列表
*/
List findUnreadByUserid(String userid);
ChatRecordServiceImpl
@Override
public List findUnreadByUserid(String userid) { TbChatRecordExample example = new TbChatRecordExample(); TbChatRecordExample.Criteria criteria = example.createCriteria();
// 设定查询发给 userid 的讯息criteria.andFriendidEqualTo(userid); criteria.andHasReadEqualTo(0);
return chatRecordMapper.selectByExample(example);
}
7业务开发 - 心跳机制
7.1Netty 心跳处理以及读写超时设定Netty 并不能监听到客户端设定为飞航模式时,自动关闭对应的通道资源。我们需要让 Netty 能够定期检测某个通道是否空闲,如果空闲超过一定的时间,就可以将对应客户端的通道资源关闭。

编写后端 Netty 心跳检查的Handler
/**
* 检测 Channel 的心跳 Handler
*/
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
// 客户端在一定的时间没有动作就会触发这个事件
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws
Exception {
// 用于触发使用者事件,包含读空闲/写空闲
if(evt instanceof IdleStateEvent) { IdleStateEvent event = (IdleStateEvent)evt;
if(event.state() == IdleState.READER_IDLE) {
System.out.println("读空闲...");
}
else if(event.state() == IdleState.WRITER_IDLE) { System.out.println("写空闲...");
}
else if(event.state() == IdleState.ALL_IDLE) { System.out.println("关闭客户端通道");
// 关闭通道,避免资源浪费
ctx.channel().close();
}
}
}
}
在通道初始化器中(WebSocketInitailizer)新增心跳检查
// 增加心跳事件支援
// 第一个引数: 读空闲 4 秒
// 第二个引数: 写空闲 8 秒
// 第三个引数: 读写空闲 12 秒
pipeline.addLast(new IdleStateHandler(4, 8, 12));
pipeline.addLast(new HeartBeatHandler());
7.2测试 Netty 心跳机制