简介
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 来实现的)
用户发起 IO 请求到 Reactor 线程
Ractor 线程将用户的 IO 请求放入到通道,然后再进行后续处理
处理完成后,Reactor 线程重新获得控制权,继续其他客户端的处理
这种模型一个时间点只有一个任务在执行,这个任务执行完了,再去执行下一个任务。
Reactor 多线程模型是由一组 NIO 线程来处理 IO 操作(之前是单个线程),所以在请求处理上会比上一中模型效率更高,可以处理更多的客户端请求。
这种模式使用多个线程执行多个任务,任务可以同时执行
但是如果并发仍然很大,Reactor 仍然无法处理大量的客户端请求
这种线程模型是 Netty 推荐使用的线程模型
这种模型适用于高并发场景,一组线程池接收请求,一组线程池处理 IO。
后端编写
导入依赖
<dependencies>
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.25.Finalversion>
dependency>
dependencies>
编写 Netty Server
public class WebSocketNettyServer {
public static void main(String[] args) {
//创建两个线程池
NioEventLoopGroup mainGrp = new NioEventLoopGroup();//创建主线程池
NioEventLoopGroup subGrp = new NioEventLoopGroup();//创建从线程池
//创建Netty服务器启动对象
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
//初始化服务器启动对象
serverBootstrap
//指定使用上面的两个线程池
.group(mainGrp,subGrp)
//指定Netty通道
.channel(NioServerSocketChannel.class)
//指定通道初始化器用来加载在当Channel收到信息后,如何进行业务的处理
.childHandler(new WebSocketChannelInitial());
//绑定服务器端口,以同步的方式启动服务器
ChannelFuture future = serverBootstrap.bind(9090).sync();
//等待服务器关闭
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//关闭服务器
mainGrp.shutdownGracefully();
subGrp.shutdownGracefully();
}
}
}
编写通道初始化器
public class WebSocketChannelInitial extends ChannelInitializer<SocketChannel> {
//初始化通道,在这个方法中加载对应的ChannelHandler
@Override
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
/**
* 处理消息的 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);
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());
}
}
2.5 websocket 以及前端代码编写
WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需
要借助 HTTP 请求完成。Websocket 是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手,
握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
前端编写
<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 + "
";
}
}
else {
alert("当前浏览器不支持web socket");
}
function sendMsg() {
var message = document.getElementById("message");
websocket.send(message.value);
}
script>
body>
html>
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 的安装包。
http://www.dcloud.io/
<body>
<form class="mui-input-group">
<div class="mui-input-row">
<label>用户名label>
<input type="text" class="mui-input-clear" placeholder="请输入用户名">
div>
<div class="mui-input-row">
<label>密码label>
<input type="password" class="mui-input-password" placeholder="请输入密码">
div>
<div class="mui-button-row">
<button type="button" class="mui-btn mui-btn-primary">确认button>
<button type="button" class="mui-btn mui-btn-danger">取消button>
div>
form>
body>
http://dev.dcloud.net.cn/mui/ui/#accordion
mui.init();//这是mui必须的js
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(){
//获取id
var id = this.getAttribute("id");
//传值给详情页面,通知加载新数据
mui.fire(detail,'getDetail',{id:id});
//打开新闻详情
mui.openWindow({
id:'detail',
url:'detail.html'
});
})
使用原生 JS 的事件绑定方式
// 绑定事件
confirm.addEventListener("tap", function() {
alert("按下按钮");
});
前端
当我们点击确认按钮的时候,将用户名和密码发送给后端服务器
// 发送ajax请求
mui.ajax('http://192.168.1.106: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) {
// 可以使用console.log打印数据,一般用于调试
console.log(data);
},
error: function(xhr, type, errorThrown) {
//异常处理;
console.log(type);
}
});
后端
基于 SpringBoot 编写一个 web 应用,主要是用于接收 ajax 请求,响应一些数据到前端
@RestController
public class DemoController1 {
@RequestMapping("login")
public Map getLogin(@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;
}
}
将 JSON 对象转换为字符串
// 使用JSON.stringify可以将JSON对象转换为String字符串
console.log(JSON.stringify(data));
将字符串转换为 JSON 对象
var jsonObj = JSON.parse(jsonStr);
mui.openWindow({
url: 'login_succss.html',
id:'login_succss.html'
});
大量的 App 很多时候都需要将服务器端响应的数据缓存到手机 App 本地。
http://www.html5plus.org/doc/zh_cn/storage.html
在 App 中缓存的数据,就是以 key-value 键值对来存放的。
将数据放入到本地缓存中
var user = {
username: username.value,
password: password.value
}
// 将对象数据放入到缓存中,需要转换为字符串
plus.storage.setItem("user", JSON.stringify(user));
从本地缓存中读取数据
// 从storage本地缓存中获取对应的数据
var userStr = plus.storage.getItem("user");
安装附件中的夜神 Android 模拟器(nox_setup_v6.2.3.8_full.exe)
双击桌面图标启动模拟器
安装后找到模拟器的安装目录
到命令行中执行以下命令
nox_adb connect 127.0.0.1:62001
nox_adb devices
进入到 Hbuilder 安装目录下的 tools/adbs 目录
切换到命令行中执行以下命令
adb connect 127.0.0.1:62001
adb devices
将资料中的 heima-chat.zip 解压,并导入到 HBuilder 中。
导入数据库
将资料中的 hchat.sql 脚本在开发工具中执行
数据库表结构介绍
tb_user 用户表
tb_friend 朋友表
tb_friend_req 申请好友表
tb_chat_record 聊天记录表
使用 MyBatis 逆向工程生成代码
将资料中的 generatorSqlmapCustom 项目导入到 IDEA 中,并配置项目所使用的 JDK
创建 Spring Boot 项目
拷贝资料 pom.xml 依赖
拷贝资料中的 application.properties 配置文件
完成之后测试从数据库中拿数据:
spring boot 整合 Netty
导入资料中配置文件中的 spring-netty 文件夹中的 java 文件
启动 Spring Boot,导入 HTML 页面,使用浏览器打开测试 Netty 是否整合成功
导入 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;
get/set
}
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 (Exception e) {
e.printStackTrace();
return new Result(false,e.getMessage());
}
}
}
UserService 接口定义
public interface UserService {
//返回数据库中所有的用户
List<TbUser> findAll();
//用来检查,检查用户名和密码是否匹配,如果成功返回用户对象,否则返回null
User login(String username, String password);
//注册用户,将用户信息保存到数据库中,
//如果抛出异常,注册失败,否则注册成功
void register(TbUser user);
}
编写 UserServiceImpl 实现
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private TbUserMapper userMapper;
@Autowired
private IdWorker idWorker;
@Override
public List<TbUser> findAll() {
return userMapper.selectByExample(null);
}
@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();
try {
BeanUtils.copyProperties(userList.get(0),user);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return user;
}
}
}
return null;
}
@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);
if(userList != null && userList.size() > 1){
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());
user.setQrcode("");
user.setCreatetime(new Date());
userMapper.insert(user);
}
}
UserController
写在上边的register中了。
UserService 接口
void register(TbUser user);
UserServiceImpl 实现
看上边的那段代码。
什么是 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
导入 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
将新上传的图片保存到用户信息数据库中
/**
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 接口
/**
/**
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 搜索朋友 - 后端开发
在搜索朋友的时候需要进行以下判断:
不能添加自己为好友
如果搜索的用户已经是好友了,就不能再添加了
如果已经申请过好友并且好友并没有处理这个请求了,也不能再申请。
前端页面展示
搜索朋友其实就是用户搜索,所以我们只需要根据用户名将对应的用户搜索出来即可。
编写 UserController
@RequestMapping("/findUserById")
public User findUserById(String userid) {
System.out.println(userid);
return userService.loadUserById(userid);
}
编写 UserService 接口
/**
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
/**
else {
throw new RuntimeException(“您已经请求过了”);
}
}
5.4 添加好友 -前端测试
5.5 展示好友请求 -后端开发
前端页面展示
编写 Controller
@RequestMapping("/findFriendReqByUserid")
public List findMyFriendReq(String userid) {
return friendService.findMyFriendReq(userid);
}
编写 FriendService
/**
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
/**
// 互相添加为好友
// 添加申请方好友
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 接口
/**
编写 FriendController
/**
*/
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
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,给用户推送消息。
/**
// 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,channel 关联到
userid
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 + “\n\n”);
System.out.println(“Channelid:” + UserChannelMap.get(uid));
}
break;
case 1:
// 2.2 聊天记录保存到数据库,标记消息的签收状态[未签收]
break;
case 2:
// 2.3 签收消息,修改数据库中的消息签收状态[已签收]
// 表示消息 id 的列表
break;
case 3:
// 2.4 心跳类型的消息
break;
}
}
/**
@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 类
/**
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 接口
/**
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
/**
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
/**
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
/**
7 业务开发 - 心跳机制
7.1 Netty 心跳处理以及读写超时设置
Netty 并不能监听到客户端设置为飞行模式时,自动关闭对应的通道资源。我们需要让 Netty 能够定期检测某个通
道是否空闲,如果空闲超过一定的时间,就可以将对应客户端的通道资源关闭。
编写后端 Netty 心跳检查的 Handler
/**
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 心跳机制