建立Maven项目导入坐标
使用RabbitMq和Redis对分布式的消息进行存储使用
核心坐标,基于脚手架,项目代码最终会上传至gitee,链接在最后一篇文章贴出:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>League-management</artifactId>
<groupId>com.hncj</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>chat</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>test</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.hncj</groupId>
<artifactId>mybatis-plus</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.hncj</groupId>
<artifactId>rabbitmq</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.hncj</groupId>
<artifactId>redis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.hncj</groupId>
<artifactId>common</artifactId>
<version>1.0-SNAPSHOT</version>
<exclusions>
<exclusion>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.90.Final</version>
</dependency>
<dependency>
<groupId>com.hncj</groupId>
<artifactId>jwt</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
熟悉的黑马Netty的聊天室案例Netty服务器
对黑马的本地NettyServer进行了一些改写,将原本解决ByteBuf粘包半包的handler改为Netty支持WebScoket的那套handler:HttpServerCodec、ChunkedWriteHandler、HttpObjectAggregator、WebSocketServerProtocolHandler
对于指令处理延用原本的方式,不得不说很巧妙
本文只给出Netty和SpringBoot的集成、Handler的具体实现放在后面的文章细说
@Component
@Slf4j
public class ChatServer {
/**
* 数据解析工作和token鉴权
*/
@Resource
private WebSocketHandler WEB_SOCKET_HANDLER;
@Resource
private LoginRequestMessageHandler LOGIN_HANDLER;
@Resource
private ChatRequestMessageHandler CHAT_HANDLER;
@Resource
private GroupCreateRequestMessageHandler GROUP_CREATE_HANDLER;
@Resource
private GroupJoinRequestMessageHandler GROUP_JOIN_HANDLER;
@Resource
private GroupMembersRequestMessageHandler GROUP_MEMBERS_HANDLER;
@Resource
private GroupQuitRequestMessageHandler GROUP_QUIT_HANDLER;
@Resource
private GroupChatRequestMessageHandler GROUP_CHAT_HANDLER;
@Resource
private QuitHandler QUIT_HANDLER;
public void run(){
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup workers = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(boss,workers)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel nsc) throws Exception {
//空闲检测,检查连接是否正常,读写或空闲时间异常检查
//60s内如果没有数据接收,会触发一个事件
//配合客户端进行心跳检测,方式失效连接占用资源
nsc.pipeline().addLast(new IdleStateHandler(600, 0, 0));
//可同时作为入站和出站处理器
nsc.pipeline().addLast(new ChannelDuplexHandler() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
IdleStateEvent e = (IdleStateEvent) evt;
//当读空闲超时
if (e.state() == IdleState.READER_IDLE) {
log.warn("已经600S没有读到数据了!关闭连接!");
ChannelUtils.close(ctx.channel());
}
super.userEventTriggered(ctx, evt);
}
});
nsc.pipeline().addLast(new HttpServerCodec())
//大数据流支持
.addLast(new ChunkedWriteHandler())
//对http消息做聚合操作,会产生两个对象:FullHttpRequest和FullHttpResponse
.addLast(new HttpObjectAggregator(1024 * 64))
//ws支持
.addLast(new WebSocketServerProtocolHandler("/"));
//指令解析
nsc.pipeline().addLast(WEB_SOCKET_HANDLER)
//从本地模式改为在线模式
//需要添加新指令时,创建一个新的Message子类,然后在此处添加对应的Handler
.addLast(LOGIN_HANDLER)
.addLast(CHAT_HANDLER)
.addLast(GROUP_CREATE_HANDLER)
.addLast(GROUP_JOIN_HANDLER)
.addLast(GROUP_MEMBERS_HANDLER)
.addLast(GROUP_QUIT_HANDLER)
.addLast(GROUP_CHAT_HANDLER)
.addLast(QUIT_HANDLER)
//服务器不支持的指令处理
.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
log.warn("无效的指令数据");
}
});
}
});
Channel channel = bootstrap.bind(8000).sync().channel();
channel.closeFuture().sync();
} catch (InterruptedException e) {
log.error("Server Error ",e);
} finally {
boss.shutdownGracefully();
workers.shutdownGracefully();
}
}
}
SpringBoot启动类
当运行启动类时,我们要异步加载ChatServer.run(),这里使用之前文章提过的初始化接口的方式:CommandLineRunner,重写它的run方法,然后运行ChatServer.run即可,这里将运行方式写为一个接口,方便我们调用接口检查是否执行
allMsg是用来判断本服务器有多少Channel连接,调试可以用到
/**
* @author 14501
*/
@SpringBootApplication
@EnableWebMvc
@Slf4j
@RestController
public class Application implements CommandLineRunner{
@Resource
private ChatServer chatServer;
@Resource
private Session session;
static volatile boolean chatServerRunning = false;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
DefaultEventLoopGroup group = new DefaultEventLoopGroup(1);
@RequestMapping("/init")
public void run() {
if(!chatServerRunning){
chatServerRunning = true;
log.info("启动Netty服务器");
group.execute(()->chatServer.run());
}else{
log.info("Netty服务器正在运行");
}
}
@RequestMapping("/allMsg")
public String allMsg(){
return "ok:" + session.listMsg(new ResponseResult<>(Code.SUCCESS.code,"你好").toString());
}
@Override
public void run(String... args) {
run();
}
}
到这里已经完成了初步集成
其余铺垫改动
为了实现我们的在线聊天功能,GroupSession相关类可以直接移除,Session要做一些改动才能使用
基础sql表结构
/*
Navicat Premium Data Transfer
Source Server : localhost
Source Server Type : MySQL
Source Server Version : 80028
Source Host : localhost:3306
Source Schema : chat
Target Server Type : MySQL
Target Server Version : 80028
File Encoding : 65001
Date: 03/12/2023 16:26:46
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for chat_group
-- ----------------------------
DROP TABLE IF EXISTS `chat_group`;
CREATE TABLE `chat_group` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`group_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for chat_group_msg
-- ----------------------------
DROP TABLE IF EXISTS `chat_group_msg`;
CREATE TABLE `chat_group_msg` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`chat_user` bigint NOT NULL,
`chat_group` bigint NOT NULL,
`text` tinytext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for chat_msg
-- ----------------------------
DROP TABLE IF EXISTS `chat_msg`;
CREATE TABLE `chat_msg` (
`id` bigint NOT NULL,
`send_user` bigint NOT NULL,
`receive_user` bigint NOT NULL,
`text` tinytext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for chat_user
-- ----------------------------
DROP TABLE IF EXISTS `chat_user`;
CREATE TABLE `chat_user` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `chat_user_pk`(`username` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_group
-- ----------------------------
DROP TABLE IF EXISTS `user_group`;
CREATE TABLE `user_group` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`user` bigint NOT NULL,
`group_id` bigint NOT NULL,
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_handler
-- ----------------------------
DROP TABLE IF EXISTS `user_handler`;
CREATE TABLE `user_handler` (
`id` bigint UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`type` varchar(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '0表示登录操作,1表示退出登录操作',
`create_time` datetime NOT NULL,
`update_time` datetime NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 145 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
Dao层生成
使用idea 的 MybatisX插件可以直接生成Dao层
右键选择MybatisX-Generator
然后点击‘下一步(Next)’,在下个页面这么选,然后点击Finish即可,项目目录下会生成一个generator包和resources下面的mapper包:
对mapper和serviceImpl添加@Mapper和@Service
对每个生成的实体类,删除Equals和HashCode方法,进行添加注解,如下:
/**
*
* @author 14501
* @TableName chat_msg
*/
@EqualsAndHashCode(callSuper = false)
@TableName(value ="chat_msg")
@NoArgsConstructor
@Data
public class ChatMsg implements Serializable {
/**
*
*/
@TableId
private Long id;
/**
*
*/
private Long sendUser;
/**
*
*/
private Long receiveUser;
/**
*
*/
private String text;
/**
*
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
*
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
public ChatMsg(ChatRequestMessage chatMsg) {
this.receiveUser = chatMsg.getTo();
this.sendUser = chatMsg.getFrom();
this.text = chatMsg.getContent();
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", sendUser=").append(sendUser);
sb.append(", receiveUser=").append(receiveUser);
sb.append(", text=").append(text);
sb.append(", createTime=").append(createTime);
sb.append(", updateTime=").append(updateTime);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
Session改造
将Session以Service方式进行改造,交由Spring进行管理,并且将原本的username对应channel,改为userId对应channel
Session接口:
/**
* 会话管理接口
* @author 14501
*/
public interface Session {
/**
* 通过Channel获取UserId
* @param channel 通道
* @return UserId
*/
Long getUserId(Channel channel);
/**
* 绑定会话
* @param channel 哪个 channel 要绑定会话
* @param userId 会话绑定用户
* @return token
*/
String bind(Channel channel, Long userId);
/**
* 解绑会话
* @param channel 哪个 channel 要解绑会话
*/
void unbind(Channel channel);
/**
* 获取属性
* @param channel 哪个 channel
* @param name 属性名
* @return 属性值
*/
Object getAttribute(Channel channel, String name);
/**
* 设置属性
* @param channel 哪个 channel
* @param name 属性名
* @param value 属性值
*/
void setAttribute(Channel channel, String name, Object value);
/**
* 根据用户名获取 channel
* @param userId 用户名
* @return channel
*/
Channel getChannel(Long userId);
/**
* 遍历Channel 群发消息
* @return 群发成功个数
*/
Integer listMsg(Object msg);
}
SessionImpl:
/**
* @author 14501
*/
@Slf4j
@Service
public class SessionMemoryImpl implements Session {
private final Map<Long, Channel> usernameChannelMap = new ConcurrentHashMap<>();
private final Map<Channel, Long> channelUserIdMap = new ConcurrentHashMap<>();
private final Map<Channel,Map<String,Object>> channelAttributesMap = new ConcurrentHashMap<>();
@Resource
private ChatUserService chatUserService;
@Override
public Long getUserId(Channel channel) {
return channelUserIdMap.get(channel);
}
@Override
public String bind(Channel channel, Long userId) {
usernameChannelMap.put(userId, channel);
channelUserIdMap.put(channel, userId);
channelAttributesMap.put(channel, new ConcurrentHashMap<>());
return chatUserService.login(userId);
}
@Override
public void unbind(Channel channel) {
try {
Long userId = channelUserIdMap.remove(channel);
usernameChannelMap.remove(userId);
channelAttributesMap.remove(channel);
chatUserService.logout(userId);
ChannelUtils.close(channel);
}catch (Exception e) {
log.error("不存在的channel");
}
}
@Override
public Object getAttribute(Channel channel, String name) {
return channelAttributesMap.get(channel).get(name);
}
@Override
public void setAttribute(Channel channel, String name, Object value) {
channelAttributesMap.get(channel).put(name, value);
}
@Override
public Channel getChannel(Long userId) {
return usernameChannelMap.get(userId);
}
@Override
public Integer listMsg(Object msg) {
Integer count = 0;
for(Map.Entry< Long,Channel> entry: usernameChannelMap.entrySet()){
try {
entry.getValue().writeAndFlush(new TextWebSocketFrame((String) msg));
count ++;
}catch (Exception e) {
e.printStackTrace();
}
}
return count;
}
@Override
public String toString() {
return usernameChannelMap.toString();
}
}
以上就是对项目的初步改造,目前已经写完了私聊,剩下的就是业务完善,把思路构思地差不多才来开始做记录,难免会有些细节没有提到,需要了解的同学可以留言或者私信我
本专栏会完整记录我的全过程,做这个项目之前,在网上没搜到太多有用的相关文章,所以才有将这个项目记录下来的念头。