springboot整合netty 实现聊天

引入Netty的Maven依赖

<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.1.50.Final</version>
</dependency>

配置Netty的host和port

netty:
  host: 0.0.0.0  #0.0.0.0表示绑定任意ip
  port: 9998

springboot 异步启动netty
配置线程池

#线程池配置
async:
  executor:
    thread:
      # 核心线程数
      core_pool_size: 5
      # 最大线程数
      max_pool_size: 20
      # 任务队列大小
      queue_capacity: 100
      # 线程池中线程的名称前缀
      name_prefix: async-service-
      # 缓冲队列中线程的空闲时间
      keep_alive_seconds: 100

线程池 ThreadPoolConfig

package com.hongyu.thread;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * 线程池配置
 *
 * @author JHY
 * @date 2023/03/09
 */
@Configuration
@EnableAsync
@Slf4j
public class ThreadPoolConfig {

    @Value("${async.executor.thread.core_pool_size}")
    private int corePoolSize;
    @Value("${async.executor.thread.max_pool_size}")
    private int maxPoolSize;
    @Value("${async.executor.thread.queue_capacity}")
    private int queueCapacity;
    @Value("${async.executor.thread.name_prefix}")
    private String namePrefix;
    @Value("${async.executor.thread.keep_alive_seconds}")
    private int keepAliveSeconds;

    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        /**
         *第一种:有具体的计算公式算出来的
         * 比如:线程数 = N * U * ( 1 + W/C )
         * N = cpu数量(可以理解为cpu核数,即同一时刻能并行处理线程的数量)
         * U = 目标cpu使用率(0-1区间范围内)
         * W = 等待时间
         * C = 计算时间
         * W/C = 等待时间和计算时间的比例
         *第二种:
         *  如果是计算密集型的应用则设置N+1线程数
         *  如果是I0密集性的应用则设置2N的线程数
         *  - N是cpu数量
         */
        // 设置核心线程数
        executor.setCorePoolSize(corePoolSize);
        // 设置最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        // 设置队列容量
        executor.setQueueCapacity(queueCapacity);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(keepAliveSeconds);
        // 设置默认线程名称
        executor.setThreadNamePrefix(namePrefix);
        // 设置拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        log.info("创建一个线程池 corePoolSize is [" + corePoolSize + "] maxPoolSize is [" + maxPoolSize + "] queueCapacity is [" + queueCapacity +
                "] keepAliveSeconds is [" + keepAliveSeconds + "] namePrefix is [" + namePrefix + "].");
        return executor;
    }
}

启动类

package com.hongyu;

import com.hongyu.netty.NettyAsyncServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;


/**
 * 应用程序
 *
 * @author JHY
 * @date 2023/3/9
 */
@Slf4j
@EnableScheduling
@EnableAsync
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(App.class, args);
        NettyAsyncServer nettyAsyncServer = ctx.getBean("NettyAsyncServer", NettyAsyncServer.class);
        nettyAsyncServer.start();
    }
}

编写NettyServer

package com.hongyu.netty;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.net.InetSocketAddress;

/**
 * Netty服务
 *
 * @author JHY
 * @date 2023/03/08
 */
@Slf4j
@Component("NettyAsyncServer")
public class NettyAsyncServer {
    @Value("${netty.host}")
    private String host = "0.0.0.0";
    @Value("${netty.port}")
    private Integer port = 9998;

    @Async
    public void start() {
        InetSocketAddress socketAddress = new InetSocketAddress(host, port);
        // 主线程池: 用于接收客户端请求连接,不做任何处理
        EventLoopGroup masterGroup = new NioEventLoopGroup();
        // 从线程池: 主线程池会把任务交给他,让其做任务
        EventLoopGroup subGroup = new NioEventLoopGroup();

        try {
            // 创建服务器启动类
            ServerBootstrap server = new ServerBootstrap();
            // 设置主从线程组
            server.group(masterGroup, subGroup)
                    // 设置双向通道
                    .channel(NioServerSocketChannel.class)
                    // 添加子处理器,用于处理从线程池的任务
                    .childHandler(new NettyServerInitializer())
                    .localAddress(socketAddress);
            // 启动服务,并且设置端口号,设置成同步方式
            ChannelFuture future = server.bind(socketAddress).sync();
            log.info("NettyServer启动成功: ws:/{}/ws", socketAddress);
            // 监听关闭的channel,设置成同步方式
            future.channel().closeFuture().sync();
        } catch (Exception e) {
            log.error("NettyServer异常:" + e.getMessage());
        } finally {
            masterGroup.shutdownGracefully();
            subGroup.shutdownGracefully();
        }
    }

}

NettyServerInitializer

package com.hongyu.netty;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;

/**
 * 网状服务器初始化
 *
 * @author JHY
 * @date 2023/03/08
 */
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        // 通过SocketChannel获取对应的管道
        ChannelPipeline pipeline = channel.pipeline();
        // 通过管道添加Handler
        // HttpServerCodec由Netty提供的助手类,可以理解为拦截器
        // 当请求到服务器需要解码,响应到客户端需要编码
        pipeline.addLast("HttpServerCodec", new HttpServerCodec());
        // 写大数据流支持
        pipeline.addLast("ChunkedWriteHandler", new ChunkedWriteHandler());
        // 对httpMessage 聚合处理
        pipeline.addLast("HttpObjectAggregator", new HttpObjectAggregator(1024 * 64));
        // 增加心跳支持,已秒为单位
        pipeline.addLast("IdleStateHandler", new IdleStateHandler(8, 10, 12));
        // 自定义空闲状态检测
        pipeline.addLast("HeartHandler", new HeartHandler());
        // 支持ws,处理一些繁重复杂的事情,处理握手动作{close,ping.pong}
        pipeline.addLast("WebSocketServerProtocolHandler", new WebSocketServerProtocolHandler("/ws"));
        // 添加自定义助手类
        pipeline.addLast("CustomHandler", new CustomChatHandler());


    }
}

心跳HeartHandler

package com.hongyu.netty;

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;

/**
 * 心处理程序
 * 检测心跳
 *
 * @author JHY
 * @date 2023/03/08
 */
@Slf4j
public class HeartHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            // 强制类型转换
            IdleStateEvent event = (IdleStateEvent) evt;
            // 读空闲
            if (event.state() == IdleState.READER_IDLE) {
                log.info("客户端[{}]进入读空闲", ctx.channel().id().asShortText());
            } else if (event.state() == IdleState.WRITER_IDLE) {
                log.info("客户端[{}]进入写空闲", ctx.channel().id().asShortText());
            } else if (event.state() == IdleState.ALL_IDLE) {
                log.info("客户端[{}]进入全部空闲", ctx.channel().id().asShortText());
                log.info("关闭之前,users数量为:" + CustomChatHandler.users.size());
                Channel channel = ctx.channel();
                // 资源释放
                channel.close();
                log.info("关闭之后,users数量为:" + CustomChatHandler.users.size());
            }
        }
    }
}

自定义助手类

package com.hongyu.netty;

import com.alibaba.fastjson.JSON;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 自定义聊天处理程序
 *
 * @author JHY
 * @date 2023/03/08
 */
@Slf4j
public class CustomChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    /**
     * 记录和管理客户端的通道
     */
    public static ChannelGroup users = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 1.获取客户端发来的消息
        String text = msg.text();
        Channel channel = ctx.channel();
        String now = LocalDateTime.now().format(formatter);
        log.info("接收到[{}]的数据:{},接收时间:{}", channel.id().asShortText(), text, now);
        int textIndexOf = text.indexOf("{");
        if (textIndexOf >= 0) {
            DataContent dataContent = JSON.parseObject(text, DataContent.class);
            /**
             * 2.1 当websocket第一次open时 初始化 channel,把当前channel和userid关联起来
             * 2.2 判断消息类型,把聊天内容记录到数据库,标签消息的读取状态,未读
             * 2.3 读取消息类型 , 针对具体的消息进行读取,修改读取状态,已读
             * 2.5 心跳类型的消息
             */
            String types = dataContent.getTypes();
            if (StringUtils.isNotBlank(types)) {


                Integer meId = dataContent.getMeId();
                Integer chatId = dataContent.getChatId();
                String content = dataContent.getContent();
                Integer msgId = dataContent.getMsgId();
                String extended = dataContent.getExtended();
                if ("第一次连接".equals(types)) {
                    UserChannelRelation.put(meId, channel);
                } else if ("单聊".equals(types)) {
                    // 参考接口 一对一聊天 逻辑
                    // 保存消息进数据库 设为未读
                    // 需要使用SpringUtil来注入service
                    Channel receiverChannel = UserChannelRelation.get(chatId);
                    if (receiverChannel == null) {
                        // 离线用户
                    } else {
                        Channel findChannel = users.find(receiverChannel.id());
                        if (findChannel != null) {
                            // 用户在线
                            receiverChannel.writeAndFlush(new TextWebSocketFrame(JSON.toJSONString(dataContent)));
                        } else {
                            // 离线用户
                        }
                    }

                } else if ("群聊".equals(types)) {

                } else if ("读取".equals(types)) {
                    // 如果多条消息批处理

                } else if ("心跳".equals(types)) {
                    log.info("收到来自[{}]的心跳包,当前时间:{}", channel.id().asLongText(), now);
                    channel.writeAndFlush(new TextWebSocketFrame("{\"types\":\"心跳回复\",\"timestamp\":\"" + now + "\"}"));
                }
            }
        } else if ("PING".equals(text)) {
            channel.writeAndFlush(new TextWebSocketFrame("PONG"));
        }

//        // 将数据刷新到客户端上
//        String writeText = String.format("服务器在:%s,接收到的消息内容为:%s", LocalDateTime.now(), msg.text());
//        users.writeAndFlush(new TextWebSocketFrame(writeText));
    }

    /**
     * 处理程序添加
     *
     * @param ctx ctx
     * @throws Exception 异常
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        users.add(ctx.channel());
        log.info("客户端[{}]连接成功,当前{}人在线", ctx.channel().id().asShortText(), users.size());
    }

    /**
     * 处理程序删除
     *
     * @param ctx ctx
     * @throws Exception 异常
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        users.remove(ctx.channel());
        log.info("客户端[{}]断开连接,当前{}人在线", ctx.channel().id().asShortText(), users.size());
    }

    /**
     * 异常
     *
     * @param ctx ctx
     * @param e   异常
     * @throws Exception 异常
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception {
        log.error("连接发生异常:{}", e.getMessage());
        // 发生异常,关闭连接,同时ChannelGroup移除
        ctx.channel().close();
        users.remove(ctx.channel());
    }
}

DataContent

package com.hongyu.netty;

import lombok.Data;
import lombok.ToString;

import java.io.Serializable;

/**
 * 数据内容
 *
 * @author JHY
 * @date 2023/03/08
 */
@Data
@ToString
public class DataContent implements Serializable {
    /**
     * 类型,单聊 群聊 心跳
     */
    private String types;
    /**
     * 个人ID
     */
    private Integer meId;
    /**
     * 聊天id
     */
    private Integer chatId;
    /**
     * 聊天内容
     */
    private String content;
    /**
     * 消息id
     */
    private Integer msgId;
    /**
     * 扩展字段
     */
    private String extended;
}

UserChannelRelation

package com.hongyu.netty;

import io.netty.channel.Channel;
import lombok.Data;
import lombok.ToString;

import java.io.Serializable;
import java.util.HashMap;

/**
 * 用户渠道关系
 *
 * @author JHY
 * @date 2023/03/08
 */
@Data
@ToString
public class UserChannelRelation implements Serializable {
    private static HashMap<Integer, Channel> manage = new HashMap<>();

    public static void put(Integer meId, Channel channel) {
        manage.put(meId, channel);
    }

    public static Channel get(Integer meId) {
        return manage.get(meId);
    }

}

```

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
### 回答1: Spring Boot可以很方便地整合Netty实现聊天功能。Netty是一个高性能的网络编程框架,可以用来实现各种网络应用,包括聊天室。 在Spring Boot中,可以使用Netty的ChannelHandler来处理客户端连接、消息接收和发送等操作。可以使用Spring Boot的WebSocket支持来实现浏览器与服务器之间的实时通信。 具体实现步骤如下: 1. 引入Netty和WebSocket的依赖 在pom.xml文件中添加以下依赖: ``` <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.25.Final</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> ``` 2. 创建Netty服务器 创建一个Netty服务器,监听指定的端口,处理客户端连接和消息接收等操作。可以使用Spring Boot的@Configuration注解和@Bean注解来创建Netty服务器。 ``` @Configuration public class NettyConfig { @Value("${netty.port}") private int port; @Autowired private ChannelInitializer<SocketChannel> channelInitializer; @Bean public EventLoopGroup bossGroup() { return new NioEventLoopGroup(); } @Bean public EventLoopGroup workerGroup() { return new NioEventLoopGroup(); } @Bean public ServerBootstrap serverBootstrap() { ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup(), workerGroup()) .channel(NioServerSocketChannel.class) .childHandler(channelInitializer); return serverBootstrap; } @Bean public ChannelFuture channelFuture() throws InterruptedException { return serverBootstrap().bind(port).sync(); } } ``` 其中,@Value("${netty.port}")注解用来读取配置文件中的端口号,@Autowired注解用来注入ChannelInitializer,用于处理客户端连接和消息接收等操作。 3. 创建ChannelInitializer 创建一个ChannelInitializer,用于初始化Netty的ChannelPipeline,添加ChannelHandler来处理客户端连接和消息接收等操作。 ``` @Component public class ChatServerInitializer extends ChannelInitializer<SocketChannel> { @Autowired private ChatServerHandler chatServerHandler; @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new HttpServerCodec()); pipeline.addLast(new HttpObjectAggregator(65536)); pipeline.addLast(new WebSocketServerProtocolHandler("/chat")); pipeline.addLast(chatServerHandler); } } ``` 其中,HttpServerCodec用于处理HTTP请求和响应,HttpObjectAggregator用于将HTTP请求和响应合并为一个完整的消息,WebSocketServerProtocolHandler用于处理WebSocket握手和消息传输,chatServerHandler用于处理客户端连接和消息接收等操作。 4. 创建ChannelHandler 创建一个ChannelHandler,用于处理客户端连接和消息接收等操作。 ``` @Component @ChannelHandler.Sharable public class ChatServerHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> { private static final ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE); @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { channels.add(ctx.channel()); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { channels.remove(ctx.channel()); } @Override protected void channelRead(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception { String message = msg.text(); channels.writeAndFlush(new TextWebSocketFrame(message)); } } ``` 其中,@ChannelHandler.Sharable注解用于标记该ChannelHandler可以被多个Channel共享,channels用于保存所有连接的Channel,channelActive方法用于添加新的Channel,channelInactive方法用于移除已关闭的Channel,channelRead方法用于处理接收到的消息,将消息广播给所有连接的客户端。 5. 创建WebSocket客户端 在前端页面中创建WebSocket客户端,连接到Netty服务器,发送和接收消息。 ``` var socket = new WebSocket("ws://localhost:808/chat"); socket.onopen = function(event) { console.log("WebSocket connected."); }; socket.onmessage = function(event) { console.log("Received message: " + event.data); }; socket.onclose = function(event) { console.log("WebSocket closed."); }; function sendMessage() { var message = document.getElementById("message").value; socket.send(message); } ``` 其中,WebSocket连接的URL为ws://localhost:808/chat,onopen方法用于在连接建立时输出日志,onmessage方法用于在接收到消息时输出日志,onclose方法用于在连接关闭时输出日志,sendMessage方法用于发送消息。 6. 运行程序 运行Spring Boot程序,访问前端页面,即可实现聊天功能。 以上就是使用Spring Boot整合Netty实现聊天功能的步骤。 ### 回答2: Spring Boot是一个用来简化创建Spring应用程序的框架,而Netty是一个高性能的网络编程框架。将它们整合起来可以实现一个简单的聊天应用程序。 首先,我们需要创建一个基于Spring Boot的项目。可以使用Spring Initializer工具来快速生成一个基本的Spring Boot项目骨架。在pom.xml文件中添加Netty的依赖,这样项目就可以使用Netty库。 接下来,创建一个Netty的服务器类,用于处理客户端的连接和消息。在这个类中,我们需要实现Netty的ChannelInboundHandlerAdapter接口来处理连接和消息的事件。在连接建立时,可以将连接信息保存到一个Map中,以便后续消息的转发。当接收到消息时,可以根据消息内容将消息转发给指定的客户端。 在Spring Boot的配置文件中,我们需要配置Netty服务器的监听端口和服务器线程池的大小等参数。可以使用Spring Boot的注解来标记需要进行自动配置的类和方法。 在Spring Boot的控制器中,我们可以添加一个用于接收用户发送的消息的API接口。当这个接口被调用时,它会将用户发送的消息转发给Netty服务器,然后由服务器进行处理和转发。 此外,可以根据实际需求添加一些其他的功能,比如用户身份验证、消息加解密等。可以在Netty服务器的代码中添加相应的逻辑来实现这些功能。 最后,我们可以使用Spring Boot的打包工具将项目打包成一个可执行的Jar文件。然后可以将这个Jar文件部署到服务器上,并启动它来运行这个聊天应用程序。 总之,通过将Spring Boot和Netty整合起来,我们可以快速构建一个简单的聊天应用程序。Spring Boot提供了便捷的开发和配置方式,而Netty能够提供高性能的网络通信能力,这使得我们可以很容易地实现一个高效的聊天系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

软件开发北泽

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值