Netty网络编程聊天项目
Netty网络编程聊天项目
后端编写
导入依赖
<dependencies><dependency><groupId>io.netty</groupId><artifactId>netty-all</artifactId><version>4.1.15.Final</version></dependency></dependencies>
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.1</version><configuration><target>1.8</target><source>1.8</source></configuration></plugin></plugins></build>
编写Netty Server
public class WebsocketServer {public static void main(String[] args) throws InterruptedException {// 初始化主线程池(boss线程池)NioEventLoopGroup mainGroup = new NioEventLoopGroup();// 初始化从线程池(worker线程池)NioEventLoopGroup subGroup = new NioEventLoopGroup();try {// 创建服务器启动器ServerBootstrap b = new ServerBootstrap();// 指定使用主线程池和从线程池b.group(mainGroup, subGroup)// 指定使用Nio通道类型.channel(NioServerSocketChannel.class)// 指定通道初始化器加载通道处理器.childHandler(new WsServerInitializer());// 绑定端口号启动服务器,并等待服务器启动// ChannelFuture是Netty的回调消息ChannelFuture future = b.bind(9090).sync();// 等待服务器socket关闭future.channel().closeFuture().sync();} finally {// 优雅关闭boos线程池和worker线程池mainGroup.shutdownGracefully();subGroup.shutdownGracefully();}}
}
编写通道初始化器
public class WsServerInitializer extends ChannelInitializer<SocketChannel> {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline = ch.pipeline();// ------------------// 用于支持Http协议// websocket基于http协议,需要有http的编解码器pipeline.addLast(new HttpServerCodec());// 对写大数据流的支持pipeline.addLast(new ChunkedWriteHandler());// 添加对HTTP请求和响应的聚合器:只要使用Netty进行Http编程都需要使用// 对HttpMessage进行聚合,聚合成FullHttpRequest或者FullHttpResponse// 在netty编程中都会使用到Handlerpipeline.addLast(new HttpObjectAggregator(1024 * 64));// ---------支持Web Socket -----------------// websocket服务器处理的协议,用于指定给客户端连接访问的路由: /ws// 本handler会帮你处理一些握手动作: handshaking(close, ping, pong) ping + pong = 心跳// 对于websocket来讲,都是以frames进行传输的,不同的数据类型对应的frames也不同pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));// 添加自定义的handlerpipeline.addLast(new ChatHandler());}
}
编写处理消息的ChannelHandler
/*** 处理消息的handler* TextWebSocketFrame: 在netty中,是用于为websocket专门处理文本的对象,frame是消息的载体*/
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {// 用于记录和管理所有客户端的Channelprivate static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);@Overrideprotected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {// 获取从客户端传输过来的消息String text = msg.text();System.out.println("接收到的数据:" + text);// 将接收到消息发送到所有客户端for(Channel channel : clients) {// 注意所有的websocket数据都应该以TextWebSocketFrame进行封装channel.writeAndFlush(new TextWebSocketFrame("[服务器接收到消息:]"+ LocalDateTime.now() + ",消息为:" + text));}}/*** 当客户端连接服务端之后(打开连接)* 获取客户端的channel,并且放入到ChannelGroup中去进行管理* @param ctx* @throws Exception*/@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception {// 将channel添加到客户端clients.add(ctx.channel());}@Overridepublic void handlerRemoved(ChannelHandlerContext ctx) throws Exception {// 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channel//clients.remove(ctx.channel());// asLongText()——唯一的ID// asShortText()——短ID(有可能会重复)System.out.println("客户端断开, channel对应的长id为:" + ctx.channel().id().asLongText());System.out.println("客户端断开, channel对应的短id为:" + ctx.channel().id().asShortText());}
}
- websocket以及前端代码编写
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。Websocket是应用层第七层上的一个应用层协议,它必须依赖 HTTP 协议进行一次握手,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
前端编写
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title></title></head><body><div>发送消息</div><input type="text" id="msgContent" /><input type="button" value="点击发送" onclick="CHAT.chat()"/><div>接收消息:</div><div id="recMsg" style="background-color: gainsboro;"></div><script type="application/javascript">window.CHAT = {socket: null,init: function() {// 判断浏览器是否支持websocketif(window.WebSocket) {// 支持WebScoekt// 连接创建socket,注意要添加ws后缀CHAT.socket = new WebSocket("ws://127.0.0.1:9001/ws");CHAT.socket.onopen = function() {console.log("连接建立成功");};CHAT.socket.onclose = function() {console.log("连接关闭")};CHAT.socket.onerror = function() {console.log("发生错误");};CHAT.socket.onmessage = function(e) {console.log("接收到消息:" + e.data);var recMsg = document.getElementById("recMsg");var html = recMsg.innerHTML;recMsg.innerHTML = html + "<br/>" + e.data;};}else {alert("浏览器不支持websocket协议");}},chat: function() {var msg = document.getElementById("msgContent");CHAT.socket.send(msg.value);}}CHAT.init();</script></body>
</html>
- 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的安装包。
http://www.dcloud.io/
- MUI前端开发
- 创建项目/页面/添加MUI元素
创建MUI移动App项目
页面创建,添加组件
<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 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></div>
http://dev.dcloud.net.cn/mui/ui/#accordion
- 获取页面元素/添加点击事件
获取页面元素
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("按下按钮");});
- 发起ajax请求
前端
当我们点击确认按钮的时候,将用户名和密码发送给后端服务器
// 发送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 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;}
}字符串转JSON对象以及JSON对象转字符串
将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很多时候都需要将服务器端响应的数据缓存到手机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");
- 构建项目
- 项目功能需求、技术架构介绍
功能需求
登录/注册
个人信息
搜索添加好友
好友聊天
技术架构
前端
开发工具:HBuilder
框架:MUI、H5+
后端
开发工具:IDEA
框架:Spring Boot、MyBatis、Spring MVC、FastDFS、Netty
数据库:mysql
- 使用模拟器进行测试
安装附件中的夜神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
打开HBuilder开始调试
- 前端 - HBuilder前端项目导入
将资料中的heima-chat.zip解压,并导入到HBuilder中。
- 后端 - 导入数据库/SpringBoot项目/MyBatis逆向工程
导入数据库
将资料中的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 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);}@Beanpublic 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;}@Overridepublic 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<TbUser> 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("用户不存在");}
}
- 用户登录功能 - 前端&测试
- 注册功能 - 后端
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<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());user.setQrcode(""); user.setCreatetime(new Date());userMapper.insert(user);}
}
- 注册功能 - 前端&测试
- 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
解压缩fastdfs-image-server.zip
双击vmx文件,然后启动。
注意:遇到下列提示选择“我已移动该虚拟机”!
IP地址已经固定为192.168.25.133 ,请设置你的仅主机网段为25。
登录名为root 密码为itcast
- FASTDFS - 整合Spring Boot
导入ComponetImport.java工具类
导入FastDFSClient.java、FileUtils.java工具类
- 个人信息 - 后端照片上传功能开发
注入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;
}
- 个人信息 - 前端&测试头像上传
- 个人信息 - 修改昵称后端实现
编写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);
}
- 个人信息 -重新加载用户信息后端实现
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;
}
- 个人信息 - 修改昵称前端测试
- 个人信息 - 二维码生成后端编写
二维码是在用户注册的时候,就根据用户的用户名来自动生成一个二维码图片,并且保存到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);}
}
- 个人信息 - 二维码生成前端测试
- 业务开发 - 发现页面与通信录
- 搜索朋友 - 后端开发
在搜索朋友的时候需要进行以下判断:
- 不能添加自己为好友
- 如果搜索的用户已经是好友了,就不能再添加了
- 如果已经申请过好友并且好友并没有处理这个请求了,也不能再申请。
前端页面展示
搜索朋友其实就是用户搜索,所以我们只需要根据用户名将对应的用户搜索出来即可。
编写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;
}
- 搜索朋友 - 前端测试联调
- 添加好友 - 发送好友请求后端开发
添加好友需要发送一个好友请求。
编写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<TbFriendReq> 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("您已经请求过了");}
}
- 添加好友 -前端测试
- 展示好友请求 -后端开发
前端页面展示
编写Controller
@RequestMapping("/findFriendReqByUserid")
public List<FriendReq> findMyFriendReq(String userid) {return friendService.findMyFriendReq(userid);
}
编写FriendService
/*** 根据用户id查找好友请求* @param userid* @return*/
List<FriendReq> findMyFriendReq(String userid);
编写FriendServiceImpl实现
@Override
public List<FriendReq> findMyFriendReq(String userid) {// 查询好友请求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);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;
}
- 展示好友请求 - 前端测试
- 添加好友 - 接受好友请求后端开发
添加好友需要双方互相添加。
例如: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) {// 设置请求状态为1TbFriendReq 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);// 发送消息更新通信录// 获取发送好友请求方ChannelChannel channel = UserChannelMap.get(tbFriendReq.getFromUserid());if(channel != null){Message message = new Message();message.setType(4);channel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(message)));}
}
- 添加好友 -拒绝添加好友后端开发
在用户选择忽略好友请求时,我们只需要将之前的好友请求状态(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) {// 设置请求状态为1TbFriendReq tbFriendReq = friendReqMapper.selectByPrimaryKey(reqId);tbFriendReq.setStatus(1);friendReqMapper.updateByPrimaryKey(tbFriendReq);
}
- 通信录功能 - 后端
通信录功能就是要根据当前登录用户的id,获取到用户的好友列表。
前端页面效果
编写FriendController
/*** 根据用户id查询好友* @param userid* @return*/
@RequestMapping("/findFriendsByUserid")
public List<User> findFriendsByUserid(String userid) {return friendService.findFriendsByUserid(userid);
}
编写FriendService
/*** 根据用户id查找好友* @param userid* @return*/
List<User> findFriendsByUserid(String userid);
编写FriendServiceImpl
@Override
public List<User> findFriendsByUserid(String userid) {TbFriendExample example = new TbFriendExample();TbFriendExample.Criteria criteria = example.createCriteria();criteria.andUseridEqualTo(userid);List<TbFriend> tbFriendList = friendMapper.selectByExample(example);List<User> userList = new ArrayList<User>();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;
}
- 业务开发 - 聊天业务
- 聊天业务 - 用户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<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> {// 用于记录和管理所有客户端的Channelprivate static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);@Overrideprotected 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,channel关联到useridString userid = message.getChatRecord().getUserid();// 保存userid对应的channelUserChannelMap.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;}}
/*** 当客户端连接服务端之后(打开连接)* 获取客户端的channel,并且放入到ChannelGroup中去进行管理* @param ctx* @throws Exception*/@Overridepublic void handlerAdded(ChannelHandlerContext ctx) throws Exception {// 将channel添加到客户端clients.add(ctx.channel());}@Overridepublic void handlerRemoved(ChannelHandlerContext ctx) throws Exception {// 当触发handlerRemoved,ChannelGroup会自动移除对应客户端的channelclients.remove(ctx.channel());}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {// 抛出异常时移除通道cause.printStackTrace();ctx.channel().close();clients.remove(ctx.channel());}
}
- 聊天业务 - 用户断开连接、连接异常取消关联通道
服务器端应该根据通道的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();
}
- 聊天业务 - 发送聊天消息后端开发
将消息发送到好友对应的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();
}
- 聊天业务 - 加载聊天记录功能
根据userid和friendid加载未读的聊天记录
编写ChatRecordController
@RequestMapping("/findUnreadByUserIdAndFriendId")
public List<TbChatRecord> findUnreadByUserIdAndFriendId(String userid, String friendid) {return chatRecordService.findUnreadByUserIdAndFriendId(userid, friendid);
}
编写ChatRecordService
/*** 根据用户ID和朋友ID获取未读的消息* @param userid* @param friendId* @return*/
List<TbChatRecord> findUnreadByUserIdAndFriendId(String userid, String friendId);
编写ChatRecordServiceImpl实现
@Override
public List<TbChatRecord> 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<TbChatRecord> chatRecordList = chatRecordMapper.selectByExample(example);// 将消息标记为已读for (TbChatRecord tbChatRecord : chatRecordList) {tbChatRecord.setHasRead(1);chatRecordMapper.updateByPrimaryKey(tbChatRecord);}return chatRecordList;
}
- 聊天业务 - 已读/未读消息状态标记
已读消息
当用户接收到聊天消息,且聊天窗口被打开,就会发送一条用来签收的消息到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;
- 聊天业务 - 未读消息读取
在用户第一次打开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);
}
- 业务开发 - 心跳机制
- Netty心跳处理以及读写超时设置
Netty并不能监听到客户端设置为飞行模式时,自动关闭对应的通道资源。我们需要让Netty能够定期检测某个通道是否空闲,如果空闲超过一定的时间,就可以将对应客户端的通道资源关闭。
编写后端Netty心跳检查的Handler
/*** 检测Channel的心跳Handler*/
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {// 客户端在一定的时间没有动作就会触发这个事件@Overridepublic 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());
代码示例下载:
链接:https://pan.baidu.com/s/1Y72ItY7XrcimUHDKuWJgiA
提取码:4sw6
Netty网络编程聊天项目相关推荐
- 基于 Netty 网络编程项目实战课程
一 基于 Netty 网络编程项目实战课程 1项目介绍 2Netty 介绍与相关基础知识 2.1Netty 介绍 简介 Netty 是由 JBOSS 提供的一个 java 开源框架.Netty 提供异 ...
- Netty网络编程实战2,使用Netty开发聊天室功能
目录 一.服务端 1.主程序类 2.自定义初始化器 3.自定义处理器 二.客户端 1.主程序类 2.自定义初始化器 3.自定义处理器 三.启动服务端.客户端 1.服务端:你好,我是服务端,哪吒编程 2 ...
- Netty网络编程第三卷
Netty网络编程第三卷 三. Netty 进阶 1. 粘包与半包 1.1 粘包现象 1.2 半包现象 1.3 现象分析 MSS 限制 Nagle 算法 1.4 解决方案 方法1,短链接 方法2,固定 ...
- Netty网络编程第八卷
Netty网络编程第八卷 整体架构 ByteBuf Channel EventLoop和EventLoopGroup ChannelFuture ChannelHandler和ChannelPipel ...
- java网络编程-聊天室
目录 V01 # 聊天室客户端(V1) # 聊天室服务端(V1) V02 # 聊天室客户端(V2) # 聊天室服务端(V2) V03 V04 # 聊天室客户端(V4) # 聊天室服务端(V4) V05 ...
- Socket网络编程--聊天程序(8)
上一节已经完成了对用户的身份验证了,既然有了验证,那么接下来就能对不同的客户端进行区分了,所以这一节讲实现私聊功能.就是通过服务器对客户端的数据进行转发到特定的用户上, 实现私聊功能的聊天程序 实现的 ...
- java 网络编程 聊天_Java——网络编程(实现基于命令行的多人聊天室)
目录: 1.ISO和TCP/IP分层模型 2.IP协议 3.TCP/UDP协议 4.基于TCP的网络编程 5.基于UDP的网络编程 6.基于TCP的多线程的聊天室的实现 1.ISO和TCP/IP分层模 ...
- Python编程:从入门到实践+爬虫开发与项目实战+网络编程基础+项目开发实战
给还在苦苦自学Python的小伙伴们分享一波学习教程~有了它们,至少能节省50%的时间,少走一半的弯路. 书不在多,而在于精~ <Python编程:从入门到实践>豆瓣评分9.2 本书是针对 ...
- 【netty篇】- 第0章netty网络编程必备知识[持续更新中]~
一.三大组件简介 Channel与Buffer Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer).通道表示打开到 IO 设备(例如:文件.套接字)的连接.若需要使用 NI ...
最新文章
- 数据库单表数据过亿_最受欢迎的三大数据库,你用过吗?
- WPF游戏,使用move游戏开发
- Flask 中的数据库迁移
- MySQL innodb_page_size
- clock函数的时间单位_PAT B1026:程序运行时间
- 利用数组求前n个质数
- 错误解决 “No module named ‘pytest‘“
- 深入理解Java的反射与动态代理
- 随机森林评估特征重要性
- 这三款提升工作效率的小工具,你都用过吗?
- MySQL客户端工具的选择
- Dos攻击的方式及解决方案
- Riot Game前高管:游戏玩家将成为Web3真正粉丝的15大原因
- 前端面试之浏览器/HTML/CSS问题
- 新教育杂志新教育杂志社新教育编辑部2023年第6期目录
- 1017:浮点型数据类型存储空间大小
- linux系统提升硬盘写速度的方法
- STM32之如何在ST官方网站下载资料
- 医院服务器虚拟化平台,医院虚拟化平台项目建设
- TYVJ P1172 自然数拆分Lunatic版
热门文章
- Database2Sharp重要更新之生成Winform框架界面代码
- js面向对象的程序设计 --- 中篇(创建对象) 之 工厂模式和 构造函数模式
- 【剑指Offer】俯视50题之1-10题
- wav格式的音频文件 16位转化成8位的
- spring中的context:include-filter和context:exclude-filter的区别
- 使用Angularjs的ng-cloak指令避免页面乱码
- MongoDB Sharding 机制分析
- Win32 SDK - 打开文件对话框
- Linux 之 利用Google Authenticator实现用户双因素认证
- Shell脚本监控LVS后台服务器存活状态