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());}
}
  1. 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>
  1. 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/

  1. MUI前端开发

    1. 创建项目/页面/添加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

  1. 获取页面元素/添加点击事件

获取页面元素

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. 发起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");
  1. 构建项目

    1. 项目功能需求技术架构介绍

功能需求

登录/注册

个人信息

搜索添加好友

好友聊天

技术架构

前端

开发工具:HBuilder 

框架:MUI、H5+

后端

开发工具IDEA

框架:Spring BootMyBatis、Spring MVC、FastDFS、Netty

数据库:mysql

  1. 使用模拟器进行测试

安装附件中的夜神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开始调试

  1. 前端 - HBuilder前端项目导入

将资料中的heima-chat.zip解压并导入到HBuilder中

  1. 后端 - 导入数据库/SpringBoot项目/MyBatis逆向工程

导入数据库

将资料中的hchat.sql脚本在开发工具中执行

数据库表结构介绍

tb_user用户表

tb_friend朋友表

tb_friend_req申请好友表

tb_chat_record聊天记录表

使用MyBatis逆向工程生成代码

将资料中的generatorSqlmapCustom项目导入到IDEA中,并配置项目所使用的JDK

创建Spring Boot项目

拷贝资料pom.xml依赖

拷贝资料中的application.properties配置文件

  1. 后端 - Spring Boot整合Netty搭建后台

spring boot整合Netty

导入资料中配置文件中的spring-netty文件夹中的java文件

启动Spring Boot,导入HTML页面,使用浏览器打开测试Netty是否整合成功

  1. 业务开发 - 用户注册/登录/个人信息
  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);}@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("用户不存在");}
}
  1. 用户登录功能 - 前端&测试
  2. 注册功能 - 后端

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);}
}
  1. 注册功能 - 前端&测试
  2. 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

  1. FASTDFS - 整合Spring Boot

导入ComponetImport.java工具类

导入FastDFSClient.javaFileUtils.java工具类

  1. 个人信息 - 后端照片上传功能开发

注入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;
}
  1. 个人信息 - 前端&测试头像上传
  1. 个人信息 - 修改昵称后端实现

编写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);
}
  1. 个人信息 -重新加载用户信息后端实现

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;
}
  1. 个人信息 - 修改昵称前端测试
  1. 个人信息 - 二维码生成后端编写

二维码是在用户注册的时候,就根据用户的用户名来自动生成一个二维码图片,并且保存到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);}
}
    1. 个人信息 - 二维码生成前端测试
  1. 业务开发 - 发现页面与通信录
    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;
}
  1. 搜索朋友 - 前端测试联调
  2. 添加好友 - 发送好友请求后端开发

添加好友需要发送一个好友请求。

编写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("您已经请求过了");}
}
  1. 添加好友 -前端测试
  2. 展示好友请求 -后端开发

前端页面展示

编写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;
}
  1. 展示好友请求 - 前端测试
  2. 添加好友 - 接受好友请求后端开发

添加好友需要双方互相添加。

例如: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)));}
}
  1. 添加好友 -拒绝添加好友后端开发

在用户选择忽略好友请求时,我们只需要将之前的好友请求状态(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);
}
  1. 通信录功能 - 后端

通信录功能就是要根据当前登录用户的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;
}
  1. 业务开发 - 聊天业务

    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<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());}
}
  1. 聊天业务 - 用户断开连接、连接异常取消关联通道

服务器端应该根据通道的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();
}
  1. 聊天业务 - 发送聊天消息后端开发

将消息发送到好友对应的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();
}
  1. 聊天业务 - 加载聊天记录功能

根据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;
}
  1. 聊天业务 - 已读/未读消息状态标记

已读消息

当用户接收到聊天消息,且聊天窗口被打开,就会发送一条用来签收的消息到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;
  1. 聊天业务 - 未读消息读取

在用户第一次打开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);
}
  1. 业务开发 - 心跳机制

    1. 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网络编程聊天项目相关推荐

  1. 基于 Netty 网络编程项目实战课程

    一 基于 Netty 网络编程项目实战课程 1项目介绍 2Netty 介绍与相关基础知识 2.1Netty 介绍 简介 Netty 是由 JBOSS 提供的一个 java 开源框架.Netty 提供异 ...

  2. Netty网络编程实战2,使用Netty开发聊天室功能

    目录 一.服务端 1.主程序类 2.自定义初始化器 3.自定义处理器 二.客户端 1.主程序类 2.自定义初始化器 3.自定义处理器 三.启动服务端.客户端 1.服务端:你好,我是服务端,哪吒编程 2 ...

  3. Netty网络编程第三卷

    Netty网络编程第三卷 三. Netty 进阶 1. 粘包与半包 1.1 粘包现象 1.2 半包现象 1.3 现象分析 MSS 限制 Nagle 算法 1.4 解决方案 方法1,短链接 方法2,固定 ...

  4. Netty网络编程第八卷

    Netty网络编程第八卷 整体架构 ByteBuf Channel EventLoop和EventLoopGroup ChannelFuture ChannelHandler和ChannelPipel ...

  5. java网络编程-聊天室

    目录 V01 # 聊天室客户端(V1) # 聊天室服务端(V1) V02 # 聊天室客户端(V2) # 聊天室服务端(V2) V03 V04 # 聊天室客户端(V4) # 聊天室服务端(V4) V05 ...

  6. Socket网络编程--聊天程序(8)

    上一节已经完成了对用户的身份验证了,既然有了验证,那么接下来就能对不同的客户端进行区分了,所以这一节讲实现私聊功能.就是通过服务器对客户端的数据进行转发到特定的用户上, 实现私聊功能的聊天程序 实现的 ...

  7. java 网络编程 聊天_Java——网络编程(实现基于命令行的多人聊天室)

    目录: 1.ISO和TCP/IP分层模型 2.IP协议 3.TCP/UDP协议 4.基于TCP的网络编程 5.基于UDP的网络编程 6.基于TCP的多线程的聊天室的实现 1.ISO和TCP/IP分层模 ...

  8. Python编程:从入门到实践+爬虫开发与项目实战+网络编程基础+项目开发实战

    给还在苦苦自学Python的小伙伴们分享一波学习教程~有了它们,至少能节省50%的时间,少走一半的弯路. 书不在多,而在于精~ <Python编程:从入门到实践>豆瓣评分9.2 本书是针对 ...

  9. 【netty篇】- 第0章netty网络编程必备知识[持续更新中]~

    一.三大组件简介 Channel与Buffer Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer).通道表示打开到 IO 设备(例如:文件.套接字)的连接.若需要使用 NI ...

最新文章

  1. 数据库单表数据过亿_最受欢迎的三大数据库,你用过吗?
  2. WPF游戏,使用move游戏开发
  3. Flask 中的数据库迁移
  4. MySQL innodb_page_size
  5. clock函数的时间单位_PAT B1026:程序运行时间
  6. 利用数组求前n个质数
  7. 错误解决 “No module named ‘pytest‘“
  8. 深入理解Java的反射与动态代理
  9. 随机森林评估特征重要性
  10. 这三款提升工作效率的小工具,你都用过吗?
  11. MySQL客户端工具的选择
  12. Dos攻击的方式及解决方案
  13. Riot Game前高管:游戏玩家将成为Web3真正粉丝的15大原因
  14. 前端面试之浏览器/HTML/CSS问题
  15. 新教育杂志新教育杂志社新教育编辑部2023年第6期目录
  16. 1017:浮点型数据类型存储空间大小
  17. linux系统提升硬盘写速度的方法
  18. STM32之如何在ST官方网站下载资料
  19. 医院服务器虚拟化平台,医院虚拟化平台项目建设
  20. TYVJ P1172 自然数拆分Lunatic版

热门文章

  1. Database2Sharp重要更新之生成Winform框架界面代码
  2. js面向对象的程序设计 --- 中篇(创建对象) 之 工厂模式和 构造函数模式
  3. 【剑指Offer】俯视50题之1-10题
  4. wav格式的音频文件 16位转化成8位的
  5. spring中的context:include-filter和context:exclude-filter的区别
  6. 使用Angularjs的ng-cloak指令避免页面乱码
  7. MongoDB Sharding 机制分析
  8. Win32 SDK - 打开文件对话框
  9. Linux 之 利用Google Authenticator实现用户双因素认证
  10. Shell脚本监控LVS后台服务器存活状态