Springboot项目Netty-WebSocket前后端消息交互

WebSocket实现前后端消息交互-CSDN博客的基础上采用Netty提供更稳健的交互。

Netty 官网 Netty: Home

1 什么是Netty?

Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序,是目前最流行的 NIO 框架。

2 为什么要用Netty?

首先,Netty天然地支持websocket。

  1. 高性能:Netty是一个专为高并发、高性能设计的网络应用程序框架。它利用非阻塞I/O和事件驱动模型,能够高效地处理大量并发连接,这对于需要维持长连接的WebSocket应用尤为重要。这通常意味着在处理大量实时通信时,Netty可以提供更低的延迟和更高的吞吐量。

  2. 异步编程模型:Netty的异步特性允许在不阻塞线程的情况下处理网络事件,这意味着服务器可以同时处理更多的请求,而无需为每个连接分配独立的线程,从而减少了资源消耗并提高了效率。

  3. 稳定性与成熟度:Netty作为一个成熟的开源项目,经过了广泛的实战检验,拥有活跃的社区支持和持续的更新维护。它解决了许多底层网络编程中的常见问题,如内存管理、线程模型优化等,使得开发者可以更专注于业务逻辑而非基础架构问题。

  4. 简化开发复杂度:相比原生WebSocket实现,Netty提供了更高级别的抽象,使得开发者能够以更简洁的代码实现复杂的网络通信功能。API的设计倾向于简洁易用,降低了开发门槛。

3 落地实现

后端:(Java)

(1)引入依赖

<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-all</artifactId>
    <version>4.1.100.Final</version>
</dependency>

(2)创建Netty服务端类NettyServer

让Spring来管理

@Configuration
@Slf4j
@Data
public class NettyServer {
}

属性:

@Value("${netty-server.port}")
private int port;// netty服务器的端口可以自己指定
private ChannelFuture channelFuture;// 用于异步管理Netty服务器的channel
//负责处理接受进来的链接
private EventLoopGroup bossGroup;
//负责处理已经被接收的连接上的I/O操作
private EventLoopGroup workerGroup;
@Resource
private ThreadPoolExecutor threadPoolExecutor;// 线程池,后面说明

方法:

@Async("threadPoolExecutor")// 实现线程池,让这个方法异步执行
public void start() throws Exception {
    bossGroup = new NioEventLoopGroup();
    workerGroup = new NioEventLoopGroup();
    try {
        ServerBootstrap sb = new ServerBootstrap();
        sb.option(ChannelOption.SO_BACKLOG, 1024);
        sb.group(bossGroup, workerGroup) // 绑定线程池
            .channel(NioServerSocketChannel.class) // 指定使用的channel
            .localAddress(this.port)// 绑定监听端口
            .childHandler(new ChannelInitializer<SocketChannel>() { // 绑定客户端连接时候触发操作

                @Override
                protected void initChannel(SocketChannel ch) {
                    log.info("收到新连接");
                    //websocket协议本身是基于http协议的,所以这边也要使用http解编码器
                    ch.pipeline().addLast(new HttpServerCodec());
                    //以块的方式来写的处理器
                    ch.pipeline().addLast(new ChunkedWriteHandler());
                    ch.pipeline().addLast(new HttpObjectAggregator(8192));
                    ch.pipeline().addLast(new MyWebSocketHandler());
                    ch.pipeline().addLast(new WebSocketServerProtocolHandler("/websocket", null, true, 65536 * 10));
					// 客户端连接到服务端的地址:ip:port/websocket
                }
            });
        channelFuture = sb.bind().sync(); // 服务器异步创建绑定
        log.info(NettyServer.class + " 启动正在监听: " + channelFuture.channel().localAddress());
        channelFuture.channel().closeFuture().sync(); // 关闭服务器通道
    } finally {
        workerGroup.shutdownGracefully().sync(); // 释放线程池资源
        bossGroup.shutdownGracefully().sync();
    }
}
@PreDestroy // Server实例被销毁时执行的方法,主要是关闭资源
public void stopServer(){
    if (channelFuture != null && !channelFuture.isDone()) {
        channelFuture.cancel(true);
    }
    workerGroup.shutdownGracefully();
    bossGroup.shutdownGracefully();
}

(3)定义channelGroup

用来管理目前所有已连接的channel

public class ChannelHandlerPool {
    public ChannelHandlerPool(){}
    public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
}

(4)定义MyWebSocketHandler类

这个类用于处理连接开启、关闭、发送消息等操作,类似于controller。

@Slf4j
public class MyWebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
​
    // 用于绑定channel和用户的ID
    public static final Map<String, ChannelHandlerContext> webSocketMap = new ConcurrentHashMap<>();
​
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("与客户端建立连接,通道开启!");
​
        //添加到channelGroup通道组
        ChannelHandlerPool.channelGroup.add(ctx.channel());
    }
​
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        log.info("与客户端断开连接,通道关闭!");
        //添加到channelGroup 通道组
        ChannelHandlerPool.channelGroup.remove(ctx.channel());
    }
​
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //首次连接是FullHttpRequest,处理参数
        if (msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            String uri = request.uri();
​
            Map paramMap=getUrlParams(uri);
            webSocketMap.put(paramMap.get("id").toString(),ctx);
            log.info("接收到的参数是:" + JSON.toJSONString(paramMap));
​
            //如果url包含参数,需要处理
            if(uri.contains("?")){
                String newUri=uri.substring(0,uri.indexOf("?"));
                log.info(newUri);
                request.setUri(newUri);
            }
​
        }else if(msg instanceof TextWebSocketFrame){
            //正常的TEXT消息类型
            TextWebSocketFrame frame=(TextWebSocketFrame)msg;
            log.info("客户端收到服务器数据:" +frame.text());
//            sendAllMessage(frame.text());
            ctx.writeAndFlush(new TextWebSocketFrame("Hello,与服务器的连接已建立!"));
        }
        super.channelRead(ctx, msg);
    }
​
    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, TextWebSocketFrame textWebSocketFrame) throws Exception {
​
    }
​
    private void sendAllMessage(String message){
        //收到信息后,群发给所有channel
        ChannelHandlerPool.channelGroup.writeAndFlush( new TextWebSocketFrame(message));
    }
​
    public static void sendMessage(String id,String message){
        ChannelHandlerContext ctx = webSocketMap.get(id);
        if (ctx != null) {
            log.info("发送给客户端消息:" + message);
            ctx.channel().writeAndFlush(new TextWebSocketFrame(message));
        }
    }
​
    private static Map getUrlParams(String url){
        Map<String,String> map = new HashMap<>();
        url = url.replace("?",";");
        if (!url.contains(";")){
            return map;
        }
        if (url.split(";").length > 0){
            String[] arr = url.split(";")[1].split("&");
            for (String s : arr){
                String key = s.split("=")[0];
                String value = s.split("=")[1];
                map.put(key,value);
            }
            return  map;
​
        }else{
            return map;
        }
    }
}

前端:

const WebSocketComponent = (userId: any) => {
​
  useEffect(() => {
    const id = userId.userId;
    console.log(userId)
    // 创建WebSocket连接
    const url = 'ws://127.0.0.1:6848/websocket?id='+id
    console.log(url)
    const socket = new WebSocket(url);
    if(!socket){
        alert("您的浏览器不支持WebSocket协议!");
    }
    // 处理连接成功事件
    socket.onopen = () => {
      console.log('WebSocket连接已打开');
      socket.send('Hello, WebSocket!'); // 发送一条消息
      console.log('已发送消息');
    };
​
    // 处理接收到消息事件
    socket.onmessage = (event) => {
      const messageContent = event.data;
      console.log('收到消息:', messageContent);
      // 使用Ant Design的message组件显示消息
      message.success(messageContent,5)
      // 您的[]业务已处理完毕,请前往[]查看
    };
​
    // 处理连接关闭事件
    socket.onclose = () => {
      socket.send('Hello, WebSocket!'); // 发送一条消息
      console.log('已发送消息');
      console.log('WebSocket连接已关闭');
    };
​
    // 处理错误事件
    socket.onerror = (error) => {
      console.error('WebSocket发生错误:', error);
    };
​
    // 在组件卸载时关闭WebSocket连接
    return () => {
      socket.close();
    };
  }, [userId]);
​
  return;
}
​
export default WebSocketComponent;

4 一些细节

(1)客户端连接会记录客户端ip、连接的channel信息,但这些与业务无关,我们真正需要的是知道用户是谁,即用户ID。

所以前端在发起请求时,需要携带请求参数id,后端接收时MyWebSocketHandler会把用户ID和channel用Map绑定,后面服务器推送消息可以用ID直接找到对应的channel。

(2)NettyServer启动监听客户端连接会阻塞Springboot项目的其他业务,所以应该用线程池让Netty异步启动。

在springboot的主启动类

@SpringBootApplication(exclude = {RedisAutoConfiguration.class})
@MapperScan("com.polaris.project.mapper")
@EnableScheduling
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@EnableAsync
public class MainApplication implements ApplicationRunner {
    @Resource
    private NettyServer nettyServer;
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class, args);
    }
​
    @Override
    public void run (ApplicationArguments args) throws Exception{
        Log.info("Netty Server started on port: {}", String.valueOf(nettyServer.getPort()));
        nettyServer.start();
    }
}

前面已经在start()加了@Async。

(3)关于Netty心跳保活

在server添加空闲状态处理器

// 这里设置5秒内没有从 Channel 读写数据时会触发一个 READER_IDLE 事件。
ch.pipeline().addLast(new IdleStateHandler(5, 0, 0, TimeUnit.SECONDS));

可以参考Netty 心跳机制详解_netty心跳机制-CSDN博客

参考:

SpringBoot2+Netty+WebSocket(netty实现websocket,支持URL参数)_netty websocket 加url参数-CSDN博客

使用springBoot初始化启动netty创建的两个服务,解决只能启动一个,另一个不能执行问题_springboot 多模块项目 一个能启动,一个不能启动-CSDN博客

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Netty-WebSocket-Spring-Boot-Starter是一个用于将Websocket集成到Spring Boot应用程序中的库。它使用Netty作为底层框架,提供了一种快速和可靠的方式来处理异步通信。 这个库提供了一种简单的方法来创建Websocket端点,只需要使用注释和POJO类即可。在这些端点上可以添动态的事件处理程序,以处理连接、断开连接和消息事件等。 此外,Netty-WebSocket-Spring-Boot-Starter还包括了一些安全性的特性,如基于令牌的授权和XSS保护,可以帮助您保持您的Websocket应用程序安全。 总的来说,Netty-WebSocket-Spring-Boot-Starter提供了一种快速和易于使用的方式来构建Websocket应用程序,使得它成为应用程序开发人员的有用工具。 ### 回答2: netty-websocket-spring-boot-starter 是一个开源的 Java Web 开发工具包,主要基于 Netty 框架实现WebSocket 协议的支持,同时集成了 Spring Boot 框架,使得开发者可以更方便地搭建 WebSocket 服务器。 该工具包提供了 WebSocketServer 配置类,通过在 Spring Boot 的启动配置类中调用 WebSocketServer 配置类,即可启动 WebSocket 服务器。同时,该工具包还提供了多种配置参数,如端口号、URI 路径、SSL 配置、认证配置等等,可以根据业务需求进行自定义配置。 此外,该工具包还提供了一些可扩展的接口和抽象类,如 WebSocketHandler、ChannelHandlerAdapter 等,可以通过继承和实现这些接口和抽象类来实现业务逻辑的处理和拓展。 总的来说,netty-websocket-spring-boot-starter 提供了一个高效、简单、易用的 WebSocket 服务器开发框架,可以减少开发者的开发成本和工作量。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值