pom文件
<dependencies>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--阿里巴巴druid 数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!--mybatis 逆向生成器-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.2</version>
</dependency>
<!--mapper -->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>1.2.4</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.50.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.tobato/fastdfs-client -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.27.2</version>
</dependency>
<!-- apache 工具类 -->
<!--用于摘要运算、编码解码的包。常见的编码解码工具Base64、MD5、Hex、SHA1、DES等-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 二维码 -->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.3.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
netty服务器
@Component
public class WebSocketServer {
private static class SingletionWSServer{
static final WebSocketServer instant=new WebSocketServer();
}
public static WebSocketServer getInstance()
{
return SingletionWSServer.instant;
}
//主线程池
private EventLoopGroup mainGroup;
//从线程池
private EventLoopGroup subGroup;
private ServerBootstrap server;
private ChannelFuture future;
public WebSocketServer(){
mainGroup=new NioEventLoopGroup();
subGroup=new NioEventLoopGroup();
server=new ServerBootstrap();
server.group(mainGroup,subGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new WebSocketInitialize());
}
public void start()
{
this.future=server.bind("192.168.5.137",8888);
if(future.isSuccess())
{
System.out.println("启动netty成功");
}
}
}
初始化管道
public class WebSocketInitialize extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new HttpServerCodec());
//在http上有一些数据流产生,有大有小,我们对其进行处理
pipeline.addLast(new ChunkedWriteHandler());
//对httpMessage进行聚合处理,聚合成request或者response
pipeline.addLast(new HttpObjectAggregator(1024*64));
/**
* 本handler会帮你处理一些繁重复杂的事情,帮你处理握手动作
* handshaking (close,ping,pong) ping+pong=心跳
* 对于webSocket来说,都是以frames 进行传输的,不同数据类型对应的frames也不同
*/
pipeline.addLast(new WebSocketServerProtocolHandler("/ws"));
//自定义handler
pipeline.addLast(new ChatHandler());
}
}
子处理器
public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
//用于记录和管理所以客户端的channel
public static ChannelGroup users=new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)
throws Exception {
//获取客户端所传递的消息
String content = msg.text();
//1:获取客户端所传递的消息
DataContent dataContent = JsonUtils.jsonToPojo(content, DataContent.class);
Integer action = dataContent.getAction();
Channel channel=ctx.channel();
//2:判断消息的类型,根据不同的类型处理不同的业务
if(action== MsgActionEnum.CONNECT.type){
//2.1 当webSocket 第一次启动的时候,初始化channel,把channel和userId关联起来
String senderId = dataContent.getChatMsg().getSenderId();
UserChannelRel.put(senderId,channel);
/**
* 测试
*/
for(Channel c:users)
{
System.out.println(c.id().asLongText());
}
UserChannelRel.outPut();
}else if(action== MsgActionEnum.CHAT.type){
//2.2 聊天类型的消息,把聊天记录保存到数据库中,同时标记消息的签收状态(未签收)
ChatMsg chatMsg=dataContent.getChatMsg();
String msgContent = chatMsg.getMsg();
String senderId = chatMsg.getSenderId();
String receiverId = chatMsg.getReceiverId();
//保存消息到数据库,并且标记为未签收
UserService userService= (UserService) SpringUtil.getBean("userServiceImpl");
String msgId = userService.saveMsg(chatMsg);
chatMsg.setMsgId(msgId);
DataContent dataContentMsg = new DataContent();
dataContentMsg.setChatMsg(chatMsg);
//发送消息
Channel receiveChannel = UserChannelRel.get(receiverId);
if(receiveChannel==null){
//离线用户
} else {
//当 receiveChannel 不为空的时候,从ChannelGroup 去查找对应的channel是否存在
Channel findChannel = users.find(receiveChannel.id());
if(findChannel!=null){
receiveChannel.writeAndFlush(
new TextWebSocketFrame(
JsonUtils.objectToJson(dataContentMsg)
)
);
} else {
//离线用户
}
}
}else if(action== MsgActionEnum.SIGNED.type){
//2.3 签收消息类型,针对具体消息进行签收,修改数据库中对应消息的签收状态(已签收)
UserService userService= (UserService) SpringUtil.getBean("userServiceImpl");
//扩展字段signed类型消息中,代表要求签收消息的id,逗号间隔
String msgIdStr = dataContent.getExtand();
String[] msgsId = msgIdStr.split(",");
List<String> msgIdList = new ArrayList<>();
for(String mid:msgsId)
{
if(StringUtils.isNotBlank(mid))
{
msgIdList.add(mid);
}
}
System.out.println(msgIdList.toString());
if(msgIdList!=null&&!msgIdList.isEmpty()&&msgIdList.size()>0)
{
//对消息批量签收
userService.updateMsgSigned(msgIdList);
}
}else if(action== MsgActionEnum.KEEPALIVE.type){
//2.4 心跳类型的消息
System.out.println("收到来自channel 为【"+channel+"】的心跳包");
}
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
users.add(ctx.channel());
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
users.remove(ctx.channel());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
//发生了异常需要关闭连接,同时从ChannelGroup 中移除
ctx.channel().close();
users.remove(ctx.channel());
}
}
用户id和channel之间的关联关系处理
/**
* ProjectName im_bird_sys
* 用户id和channel之间的关联关系处理
* @author xieyucan
* <br>CreateDate 2022/9/28 18:12
*/
public class UserChannelRel {
private static HashMap<String, Channel> manager=new HashMap<>();
public static void put(String sendId,Channel channel)
{
manager.put(sendId, channel);
}
public static Channel get(String sendId)
{
return manager.get(sendId);
}
public static void outPut()
{
for(Map.Entry<String, Channel> entry:manager.entrySet())
{
System.out.println("UserId:"+entry.getKey()+",channelId:"
+entry.getValue().id().asLongText());
}
}
}
聊天内容类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DataContent implements Serializable {
/**
* 动作类型
*/
private Integer action;
/**
* 聊天内容
*/
private ChatMsg chatMsg;
/**
* 扩展字段
*/
private String extand;
}
心跳机制
/**
* 用于检测channel 的心跳handler
* 继承ChannelInboundHandlerAdapter,目的是不需要实现ChannelRead0 这个方法
*/
public class HeartBeatHandler extends ChannelInboundHandlerAdapter {
public void userEventTriggered(ChannelHandlerContext ctx, Object evt)
throws Exception {
if(evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent)evt;//强制类型转化
if(event.state()== IdleState.READER_IDLE){
System.out.println("进入读空闲......");
}else if(event.state() == IdleState.WRITER_IDLE) {
System.out.println("进入写空闲......");
}else if(event.state()== IdleState.ALL_IDLE){
System.out.println("channel 关闭之前:users 的数量为:"+
ChatHandler.users.size());
Channel channel = ctx.channel();
//资源释放
channel.close();
System.out.println("channel 关闭之后:users 的数量为:"+
ChatHandler.users.size());
}
}
}
}
监听netty状态,时刻开启netty
@Component
public class NettyBooter implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
if(event.getApplicationContext().getParent()==null)
{
try {
WebSocketServer.getInstance().start();
}catch (Exception e){
e.printStackTrace();
}
}
}
}