Netty - 快速开始

一、为什么使用Netty

1. NIO的缺点

  • NIO的类库和API繁杂,学习成本高,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  • 需要熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能写出高质量的NIO程序。
  • 臭名昭著的epoll bug。它会导致Selector空轮询,最终导致CPU 100%。直到JDK1.7版本依然没得到根本性的解决。

2. Netty的优点

  • API使用简单,学习成本低。
  • 功能强大,内置了多种解码编码器,支持多种协议。
  • 性能高,对比其他主流的NIO框架,Netty的性能最优。
  • 社区活跃,发现BUG会及时修复,迭代版本周期短,不断加入新的功能。
  • Dubbo、Elasticsearch都采用了Netty,质量得到验证。

二、Netty架构图

这张图片来自 Netty官网(https://netty.io

我们从上到下分析一下:

  • 绿色的部分Core核心模块,包括零拷贝、API库、可扩展的事件模型。
  • 橙色部分Protocol Support协议支持,包括Http协议、webSocket、SSL(安全套接字协议)、谷歌Protobuf协议、zlib/gzip压缩与解压缩、Large File Transfer大文件传输等等。
  • 红色的部分Transport Services传输服务,包括Socket、Datagram、Http Tunnel等等。

以上可看出Netty的功能、协议、传输方式都比较全,比较强大。

三、搭建 Hello World

首先搭建一个HelloWord工程,先熟悉一下API,还有为后面的学习做铺垫。

以下面这张模型图为依据:
Netty Reactor 工作架构图

模型说明:

  1. Netty抽象出2组线程池:
    • BossGroup:专门接受客户端的连接
    • WorkerGroup:专门负责网络的读写
  2. BossGroup和WorkerGroup类型都是NioEventLoopGroup
  3. NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是NioEventLoop
  4. NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯
  5. NioEventLoop可以有多个线程,即可以含有多个NioEventLoop
  6. 每个Boss NioEventLoop循环执行的步骤有3步:
    • 轮询accept事件
    • 处理accept事件,与client建立连接,生成NioSocketChannel,并将其注册到某个worker
    • 处理任务队列的任务,即runAllTasks
  7. 每个Worker NIOEventLoop循环执行的步骤:
    • 轮询read,write事件
    • 处理I/O事件,即read,write事件,在对应NioSocketChannel处理
    • 处理任务队列的任务,即runAllTasks
  8. 每个Worker NIOEventLoop处理业务时,会使用pipeline(管道),pipeline中包含了channel,即通过pipeline可以获取到对应channel,管道中维护了很多的处理器

0. 项目结构概览

1. 引入Maven依赖

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

2. 创建服务端启动类

package org.example.server;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class NettyServer {

    public static void main(String[] args) {
        NettyServer server = new NettyServer();
        server.start();
    }

    public  void start() {
        //创建两个线程组 boosGroup、workerGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //创建服务端的启动对象,设置参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            //设置两个线程组boosGroup和workerGroup
            bootstrap.group(bossGroup, workerGroup)
                //设置服务端通道实现类型
                .channel(NioServerSocketChannel.class)
                //设置线程队列未被 Accept 的等待队列的最大长度为 128
                //注意,此设置对已 Accept 的 ServerSocket 总数量没影响
                .option(ChannelOption.SO_BACKLOG, 128)
                //设置保持活动连接状态
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                //给workerGroup的EventLoop对应的管道设置处理器
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    //使用匿名内部类的形式初始化通道对象
                    @Override
                    protected void initChannel(SocketChannel socketChannel)
                        throws Exception {
                        //给pipeline管道设置处理器
                        socketChannel.pipeline().addLast(new MyNettyServerHandler());
                    }
                });
            System.out.println("java技术爱好者的服务端已经准备就绪...");
            //绑定端口号,启动服务端
            ChannelFuture channelFuture = bootstrap.bind(6666).sync();
            //对关闭通道进行监听
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

}

3. 创建服务端处理类

package org.example.server;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

import java.util.concurrent.TimeUnit;

/**
 * 自定义的Handler需要继承Netty规定好的HandlerAdapter
 * 才能被Netty框架所关联,有点类似SpringMVC的适配器模式
 **/
public class MyNettyServerHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //获取客户端发送过来的消息
        ByteBuf byteBuf = (ByteBuf) msg;
        String strMsg = byteBuf.toString(CharsetUtil.UTF_8);
        System.out.println("收到客户端" + ctx.channel().remoteAddress() + "发送的消息:" + strMsg);

        /*
         * 如果Handler处理器有一些长时间的业务处理,可以交给taskQueue异步处理。
         */
        // 长时间操作,不至于长时间的业务操作导致Handler阻塞。
        ctx.channel().eventLoop().execute(new LongTimeMethod(strMsg));

        // 延时任务队列和上面介绍的任务队列非常相似,只是多了一个可延迟一定时间再执行的设置
        ctx.channel().eventLoop().schedule(new LongTimeMethod(strMsg), 5, TimeUnit.SECONDS);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        //发送消息给客户端
        ctx.writeAndFlush(Unpooled.copiedBuffer("服务端已收到消息,并给你发送一个信号。", CharsetUtil.UTF_8));
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //发生异常,关闭通道
        ctx.close();
    }

}

class LongTimeMethod implements Runnable{
    private  String message;

    public  LongTimeMethod (String message) {
        this.message = message;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("长时间的业务处理: " + message);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

4. 创建客户端启动类

package org.example.client;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClient {

    public static void main(String[] args) throws Exception {
        NettyClient client = new NettyClient();
        client.start();

    }

    public void start() {
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        try {
            //创建bootstrap对象,配置参数
            Bootstrap bootstrap = new Bootstrap();
            //设置线程组
            bootstrap.group(eventExecutors)
                    //设置客户端的通道实现类型
                    .channel(NioSocketChannel.class)
                    //使用匿名内部类初始化通道
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel channel) throws Exception {
                            //添加客户端通道的处理器
                            channel.pipeline().addLast(new MyNettyClientHandler());
                        }
                    });
            System.out.println("客户端准备就绪,随时可以起飞~");
            //连接服务端
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6666).sync();
            this.addFutureListener(channelFuture);
            //对通道关闭进行监听
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            //关闭线程组
            eventExecutors.shutdownGracefully();
        }
    }

    private void  addFutureListener (ChannelFuture future) {
        //添加监听器
        future.addListener(new ChannelFutureListener() {
            //使用匿名内部类,ChannelFutureListener接口
            //重写operationComplete方法
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                //判断是否操作成功
                if (future.isSuccess()) {
                    System.out.println("连接成功");
                } else {
                    System.out.println("连接失败");
                }
            }
        });
    }


}




5. 创建客户端处理类

package org.example.client;


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

public class MyNettyClientHandler extends ChannelInboundHandlerAdapter {

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //发送消息到服务端
        String msg = "你好服务端,见到你真高兴!!";
        System.out.println("给服务端发送信息:" + msg);

        ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        //接收服务端发送过来的消息
        ByteBuf byteBuf = (ByteBuf) msg;
        String strMsg = byteBuf.toString(CharsetUtil.UTF_8);

        System.out.println("收到服务端" + ctx.channel().remoteAddress() + "的消息:" + strMsg);
    }
}


6. 测试

先启动服务端,再启动客户端,就可以看到结果:

服务端结果:

# java技术爱好者的服务端已经准备就绪...
# 收到客户端/127.0.0.1:11972发送的消息:你好服务端,见到你真高兴!!
# 长时间的业务处理: 你好服务端,见到你真高兴!!
# 长时间的业务处理: 你好服务端,见到你真高兴!!

客户端结果:

# 客户端准备就绪,随时可以起飞~
# 给服务端发送信息:你好服务端,见到你真高兴!!
# 连接成功
# 收到服务端/127.0.0.1:6666的消息:服务端已收到消息,并给你发送一个信号。

此 Netty 笔记为学习尚硅谷韩老师讲的 Netty 整理完成,原视频讲解十分详细,建议对 Netty 框架感兴趣的同学们可以看一遍原视频:
https://www.bilibili.com/video/BV1DJ411m7NR






Ref

posted @ 2023-11-16 18:30  炎黄子孙,龙的传人  阅读(9)  评论(0编辑  收藏  举报