Netty学习之旅

133 阅读5分钟

Netty与websocket

最近在写毕设,在我的毕设项目中需要有聊天的功能,在网上做一些的搜索后最终选择了后端使用Netty来实现websocket通信。

Netty是什么?

对于Java的网络通信进行封装(Java的NIO)和加强了的同步非阻塞的网络通信框架

Websocket是什么?

WebSocket 是基于 HTTP 协议的一种新的通信协议,他的特点就是服务器同样可以主动的向客户端发送消息,并且连接成功一次之后就一直连接着(持久化协议)。(而HTTP是无状态的非持久化协议,需要客户端携带一些信息才能记住你是哪个客户,服务器很被动)

后端

Netty重要组件

1.老板线程组 EventLoopGroup (线程池 cpu核心乘2)

  • 作用:主要负责连接,还有下发任务
  • 主要负责连接建立、监听IO事件、IO事件读写以及将事件分发到Handlers 处理器。

2.员工线程组 EventLoopGroup (线程池 cpu核心乘2)

  • 作用:主要工作 非阻塞的执行业务处理逻辑

3.服务端的启动类 ServerBootstarp

  • 作用:负责构造

4.通道 channel 是nio的ServerSocketChannel

  • 主要用来接收信息的通道

5.业务责任链 channelHandler (主要编写代码逻辑区域

  • 每一个handler责可以处理一些特定的事情

所有配置完毕后,使用channel绑定端口,由于这些是多线程都是异步的所以需要使用sync方法阻塞当前线程,才能收到想要的信息

基本绑定监听端口

 public static void main(String[] args) throws InterruptedException {
         //主线程 老板负责分发命令
         NioEventLoopGroup bossGroup = new NioEventLoopGroup();
         //子线程 负责处理工作
         NioEventLoopGroup workGroup = new NioEventLoopGroup();
 ​
         try {
             //服务器类
             ServerBootstrap serverBootstrap = new ServerBootstrap();
             //将主线程 和 子线程放入其中
             serverBootstrap.group(bossGroup,workGroup)
                     //放入一个服务器管道
                     .channel(NioServerSocketChannel.class)
                    //自定义管道初始化类需要继承 ChannelInitializer<SocketChannel>
                     .childHandler(new ServerInitialzer());
             //绑定端口号,启动服务端
             ChannelFuture future = serverBootstrap.bind(7530).sync();
             //对关闭通道进行监听
             future.channel().closeFuture().sync();
         }finally {
             //结束之后无论如何都要关闭流
             bossGroup.shutdownGracefully();
             workGroup.shutdownGracefully();
         }
     }

设置管道

 public class ServerInitialzer extends ChannelInitializer<SocketChannel> {
     @Override
     protected void initChannel(SocketChannel socketChannel) throws Exception {
         //获取管道
         ChannelPipeline pipeline = socketChannel.pipeline();
         //http编解码器
         pipeline.addLast(new HttpServerCodec());
         //写操作流添加
         pipeline.addLast(new ChunkedWriteHandler());
         //最大的聚合字节流大小
         pipeline.addLast(new HttpObjectAggregator(1024*64));
         //类似拦截器 只要是路径有/chat就会进行关于netty的操作
         pipeline.addLast(new WebSocketServerProtocolHandler("/chat"));
         //自定义处理器,正真处理的业务类
         pipeline.addLast(new ChatHandler());
     }
 }

1.HttpServerCodec :HttpRequestDecoder 和 HttpResponseEncoder 的组合,因为在处理 Http 请求时这两个类是经常使用的,所以 Netty 直接将他们合并在一起更加方便使用。

2.ChunkedWriteHandler:写处理器

3.HttpObjectAggregator:设置管道最大流限制

4.WebSocketServerProtocolHandler:设置那路径进入这个管道

5.ChatHandler:自定一的一个管道操作处理类 需要继承SimpleChannelInboundHandler

管道处理器

 public class ChatHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
     //用于记录通道
     private static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
     @Override
     protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
         //客户端获得的消息
         String content = msg.text();
         System.out.println("接收到的消息" + content);
         //将数据刷新到客户端
         clients.writeAndFlush(new TextWebSocketFrame("服务器在:" + LocalDateTime.now()) + "接收到的信息为" + content);
 ​
     }
 ​
     /**
      * 用户进入客户端
      * @param ctx
      * @throws Exception
      */
     @Override
     public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
         //添加如客户端通道
         clients.add(ctx.channel());
     }
 ​
     /**
      * 用户离开客户端
      * @param ctx
      * @throws Exception
      */
     @Override
     public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
         clients.remove(ctx.channel());
     }
 }

channelRead0:默认需要实现接口,只要是这个管道的数据都需要在次操作

handlerAdded:进入这个管道的第一步就会走这个接口

handlerRemoved:断开连接就会走这个接口

前端

如何使用websocket协议与后端的Neety进行通信

答:websocket协议与http协议的连接方式不同http协议开头是http://localhost:8080。对于websocket是ws://localhost:8081来进行连接,其实用起来和很简单如下

这边我后端使用的是7530(这边不要和springboot的web端口号重了),并且我们上面设置了netty监听的是/chat路径的所有消息

 const socket = new WebSocket('ws://localhost:7530/chat');

接下里就介绍一下我现在这个阶段用的比较多的几个方法

1.检验是否连接成功

 //连接成功就会出发这个方法
 socket.onopen = function() {
   console.log('WebSocket连接已建立');
 };

2.向服务器直接发送json消息

 socket.send(JSON.stringify(sendChat))

3.服务器返回的消息

 // 监听接收到服务器消息的事件
 socket.onmessage = function(event) {
   const message = event.data;
    console.log('接收到服务器的消息',mssage); 
 }

连接后的效果

跟http还是有很大区别的这个Message里面有你上传到服务器的消息就是蓝色的向上箭头,服务器给你返回的消息是红色的向下的箭头,都是可以在前端的onmessage的方法中取到

image-20240102230136571.png

遇到的问题

在Netty的ChatHandler处理器中获取不到springboot的bean

使用set方法在能获取Bean的地方给他注入进去

如下

     @Resource
     private  ChatMsgService chatMsgService;
     @Resource
     private  UsersService usersService;
 ​
 ​
 ​
     @PostConstruct
     public void start() throws InterruptedException {
             //服务器类
             ServerBootstrap serverBootstrap = new ServerBootstrap();
             //将主线程 和 子线程放入其中
             serverBootstrap.group(bossGroup,workGroup)
                     //放入一个服务器管道
                     .channel(NioServerSocketChannel.class)
                     //初始化管道
                     .childHandler(new ServerInitialzer());
             //绑定端口号,启动服务端
             ChannelFuture future = serverBootstrap.bind(7530).sync();
             //对关闭通道进行监听
 //            future.channel().closeFuture().sync();
              ChatHandler.setChatHandler(chatMsgService,usersService);
             if (future.isSuccess()) {
                 log.info("启动 Netty Server");
             }
     }

ChatHandler内部的方法

 public static void setChatHandler(ChatMsgService chatService,UsersService userService){
         chatMsgService = chatService;
         usersService = userService;
     }