初识Netty的奇经八脉

一、Netty解决了哪些问题

  1. 降低了原生NIO的开发门槛: Selector、Channel、ByteBuffer
  2. 适配线程模型并解决性能问题: 主从Reactor多线程模型(解决C10K、C10M问题)
  3. 屏蔽原生NIO可能出现的问题: 6000+的问题数量

解决JDK NIO 的BUG数量:6744个

  1. 预置多种协议粘包\粘包 (多种编解码)、扩展性强
  2. 强大的内存管理能力:零拷贝技术、内存池机制
  3. 事件驱动与异步处理:Future-Listener

二、Netty是什么

是什么?

官网

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

Netty 是一个异步事件驱动 的网络应用程序框架用于快速开发可维护的高性能协议服务器和客户端。

性能:

  • Better throughput, lower latency:更高的吞吐量、更低的延迟
  • Less resource consumption:减少资源消耗
  • Minimized unnecessary memory copy:最大限度地减少不必要的内存复制

Netty为什么高性能?

白话总结:通过灵活且可扩展的事件模型,屏蔽了难点、解决了原生NIO的问题、把控了细节,分离出关注点。

三个细节的例子:

  1. 通过设定的线程数决定如何注册Channel到Selector
typescript 复制代码
public EventExecutorChooser newChooser(EventExecutor[] executors) {
    if (isPowerOfTwo(executors.length)) {
        return new PowerOfTwoEventExecutorChooser(executors);
    } else {
        return new GenericEventExecutorChooser(executors);
    }
}
  1. 接收数据时,动态计算要使用多大的内存去接收
java 复制代码
private final class HandleImpl extends MaxMessageHandle {
        HandleImpl(int minIndex, int maxIndex, int initialIndex, int minCapacity, int maxCapacity) {
          // ...
        }

        @Override
        public void lastBytesRead(int bytes) {
            // 最后一次读
           record(bytes);
        }

        @Override
        public int guess() {
            return nextReceiveBufferSize;
        }

        private void record(int actualReadBytes) {
            // 动态计算内存大小
        }

        @Override
        public void readComplete() {
            record(totalBytesRead());
        }
    }
  1. 从Reactor线程不只承担IO事件的处理,还有定时任务和普通队列中的任务
ini 复制代码
@Override
protected void run() {
    int selectCnt = 0;
    for (;;) {
        int strategy;
        strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
        switch (strategy) {
        case SelectStrategy.CONTINUE:
        case SelectStrategy.BUSY_WAIT:
        case SelectStrategy.SELECT:
            if (!hasTasks()) {
                strategy = select(curDeadlineNanos);
            }
                break;
        default:
        // 运行所有任务
        ranTasks = runAllTasks(0);
        }
    }
}

三、从内核IO看Netty

1、网络包接收流程

内核接收数据: 当网络包到达网卡后,通过DMA方式拷贝到环形缓冲区RingBuffer(网卡启动时分配和初始化的环形队列),DMA操作完成网卡给CPU发送硬中断告诉CPU有数据到达,硬中断程序会为网络数据帧创建内核数据结构sk_buffer,并将网络数据拷贝到sk_buffer,之后发起软中断,告诉内核有数据帧到达。内核线程ksoftirqd发现软中断请求之后,调用网卡驱动的poll函数,将sk_buffer中的网络数据包送到内核协议栈中注册的ip_rcv函数中,从函数中取出IP头,发送数据给传输层,如果是TCP协议,在内核协议栈中的tcp_rcv函数处理,根据四元组查找对应的socket,将socket的数据放到接收缓冲区中。

应用程序读取数据: 程序调用系统的read函数读取socket接收缓冲区,没有数据将会阻塞,有数据CPU将内核空间的数据拷贝到用户空间,最后读取数据。

2、网络包发送流程

应用程序调用send系统函数发送数据时,触发用户态到内核态转换,根据fd(文件描述符)找出socket之后构造struct msghdr对象(存放着要发送的数据),调用内核协议栈函数inet_sendmsg(找对应协议的函数),如TCP协议,使用tcp_sendmsg函数并创建内核数据结构sk_buffer,将struct msghdr中的数据拷贝到sk_buffer中,调用tcp_write_queue_tail函数获取Socket发送队列中的队尾元素,将新创建的sk_buffer添加到Socket发送队列的尾部。之后通过tcp_write_xmit内核函数,循环获取socket的队列获取数据,将队列中的sk_buffer拷贝一份,设置sk_buffer副本中的TCP HEADER。之后通过调用ip_queue_xmit 内核函数,正式来到内核协议栈网络层的处理。之后来到邻居子系统(邻居子系统位于内核协议栈中的网络层和网络接口层之间,用于发送ARP请求获取MAC地址),再通过网络设备子系统进行填充数据到sk_buffer,在网卡驱动程序函数dev_hard_start_xmit中会将sk_buffer映射到网卡可访问的内存 DMA 区域,最终网卡驱动程序通过DMA的方式将数据帧通过物理网卡发送出去。

3、IO模型

通过网络包的接收和发送流程,可以看出都有性能开销:

  1. 用户态和内核态的转换
  2. 硬中断和软中断的执行
  3. sk_buffer的拷贝
  4. DMA拷贝

从网络接收和发送的过程历经了五种IO模型:

  • 阻塞IO
  • 非阻塞IO
  • IO多路复用
  • 信号驱动IO(不适用TCP)
  • 异步IO

BIO:阻塞IO

图解:

示例:

java 复制代码
public class BIOServerDemo {
	public static void main(String[] args) throws IOException {
		ServerSocket serverSocket = new ServerSocket(8080);
		while (true) {
			final Socket socket = serverSocket.accept();
			new Thread(new Runnable() {
				@Override
				public void run() {
					try {
						InputStream is = socket.getInputStream();
						byte[] b = new byte[1024];
						while (true) {
							// read() 阻塞点
							int data = is.read(b);
							if (data != -1) {
								String info = new String(b, 0, data, "GBK");
							} else {
								break;
							}
						}
					} catch (Exception e) {
						e.printStackTrace();
					}
				}
			}).start();
		}
	}
}

NIO:IO多路复用

服务端示例:

  1. 初始化Selector并注册ServerSocketChannel: Selector selector = Selector.open()
  2. 看是否有事件(阻塞点):int events = selector.select() ;
  • 读事件:public static final int OP_READ = 1 << 0;
  • 写事件:public static final int OP_WRITE = 1 << 2;
  • 连接事件:public static final int OP_CONNECT = 1 << 3;
  • socket接收事件:public static final int OP_ACCEPT = 1 << 4;
  1. 有事件获取事件:Set selectionKeys = selector.selectedKeys()
  2. 处理事件
java 复制代码
public class NioServer {

    private int port;
    private Selector selector;
    private ExecutorService service = Executors.newFixedThreadPool(5);

    public NioServer(int port) {
        this.port = port;
    }
    
    public static void main(String[] args){
        new NioServer(8080).start();
    }
    
    private void accept(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel sc = ssc.accept();
            sc.configureBlocking(false);
            sc.register(selector, SelectionKey.OP_READ);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        // 初始化
        ServerSocketChannel ssc = null;
        try {
            ssc = ServerSocketChannel.open();
            ssc.configureBlocking(false);
            ssc.bind(new InetSocketAddress(port));
            selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
        }
        
        
        // 接收数据
        while (true) {
            try {
                int events = selector.select();
                if (events > 0) {
                    Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
                    while (selectionKeys.hasNext()) {
                        SelectionKey key = selectionKeys.next();
                        selectionKeys.remove();
                        if (key.isAcceptable()) {
                            accept(key);
                        } else {
                            // 处理数据
                            service.submit(new NioServerHandler(key));
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static class NioServerHandler implements Runnable{

        private SelectionKey selectionKey;

        public NioServerHandler(SelectionKey selectionKey) {
            this.selectionKey = selectionKey;
        }

        @Override
        public void run() {
            try {
                if (selectionKey.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    socketChannel.read(buffer);
                    buffer.flip();
                    //将数据添加到key中
                    ByteBuffer outBuffer = ByteBuffer.wrap(buffer.array());
                    socketChannel.write(outBuffer);// 将消息回送给客户端
                    selectionKey.cancel();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端示例:

  1. 初始化Selector并注册ServerSocketChannel: Selector selector = Selector.open()
  2. 看是否有事件(阻塞点):int events = selector.select();
  3. 有事件获取事件:Set selectionKeys = selector.selectedKeys();
  4. 分为连接事件和读事件
ini 复制代码
public class NioClient {
    private static final String host = "127.0.0.1";
    private static final int port = 8080;
    private Selector selector;

    public static void main(String[] args){
        NioClient client = new NioClient();
        client.connect(host, port);
        client.listen();
    }

    public void connect(String host, int port) {
        try {
            SocketChannel sc = SocketChannel.open();
            sc.configureBlocking(false);
            this.selector = Selector.open();
            sc.register(selector, SelectionKey.OP_CONNECT);
            sc.connect(new InetSocketAddress(host, port));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void listen() {
        while (true) {
            try {
                int events = selector.select();
                if (events > 0) {
                    Iterator<SelectionKey> selectionKeys = selector.selectedKeys().iterator();
                    while (selectionKeys.hasNext()) {
                        SelectionKey selectionKey = selectionKeys.next();
                        selectionKeys.remove();
                        //连接事件
                        if (selectionKey.isConnectable()) {
                            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                            if (socketChannel.isConnectionPending()) {
                                socketChannel.finishConnect();
                            }

                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector, SelectionKey.OP_READ);
                            socketChannel.write(ByteBuffer.wrap(("Hello this is " + Thread.currentThread().getName()).getBytes()));
                        } else if (selectionKey.isReadable()) {
                            SocketChannel sc = (SocketChannel) selectionKey.channel();
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            sc.read(buffer);
                            buffer.flip();
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

AIO:异步IO

非阻塞异步IO:在数据存不存在时,由内核主动告知应用程序。

异步IO,系统支持:

  • Window中的IOCP实现了非常成熟的异步IO机制
  • Linux不够成熟,与NIO相比效果也不明显(Linux kernel 在5.1版本,引入io_uring比Epoll性能高,后续可关注)

Netty对三种I/O模型的支持

BIO -> OIO**(Deprecated)** NIO AIO**(Removed)**
COMMON Linux macOS/BSD
ThreadPerChannelEventLoopGroup NioEventLoopGroup EpollEventLoopGroup KQueueEventLoopGroup AioEventLoopGroup
ThreadPerChannelEventLoop NioEventLoop EpollEventLoop KQueueEventLoop AioEventLoop
OioServerSocketChannel NioServerSocketChannel EpollServerSocketChannel KQueueServerSocketChannel AioServerSocketChannel
OioSocketChannel NioSocketChannel EpollSocketChannel KQueueSocketChannel AioSocketChannel

4、Reactor与Proactor模型

Proactor模型是对应的AIO,主要关心IO完成事件,需要用户程序向内核传递用户空间的读缓冲区地址,每个请求都要有单独的缓冲区,内存开销大,编码逻辑复杂,对于高IO的性能高,低IO效果差。

Reactor模型(重点)

  • 使用IO多路复用模型,比如select,poll,epoll,kqueue,进行IO事件的注册和监听。
  • 将监听到就绪的IO事件分发dispatch 到各个具体的处理Handler中进行相应的IO事件处理。

Reactor 是一种开发模式,核心流程:注册感兴趣的事件 -> 扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理。

分为三种模式:

  1. 单Reactor单线程
ini 复制代码
EventLoopGroup eventGroup = new NioEventLoopGroup( 1 );

ServerBootstrap serverBootstrap = new ServerBootstrap();

serverBootstrap.group(eventGroup);
  1. 单Reactor多线程
ini 复制代码
EventLoopGroup eventGroup = new NioEventLoopGroup();

ServerBootstrap serverBootstrap = new ServerBootstrap();

serverBootstrap.group(eventGroup);
  1. 主从Reactor多线程
ini 复制代码
EventLoopGroup bossGroup = new NioEventLoopGroup();

EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap serverBootstrap = new ServerBootstrap();

serverBootstrap.group(bossGroup, workerGroup);

EventLoopGroup不指定线程数的默认值:

DEFAULT_EVENT_LOOP_THREADS = Math.max(

1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2)

);

四、Netty的奇经八脉

1、Netty有哪些组件?

  1. ServerBootstrap:服务端核心的启动配置类,负责初始化并引导Netty服务端应用程序
  2. Bootstrap:客户端核心的启动配置类,负责初始化并引导Netty客户端应用程序
  3. EventLoopGroup:网络事件处理和线程调度的核心组件,管理一组EventLoop
  4. EventLoop:定义了Netty的核心抽象,负责处理I/O操作、事件分发和任务执行,用来处理连接的生命周期中所发生的事件。在内部,将会为每个Channel分配一个EventLoop,用于处理用户连接请求、对用户请求的处理等所有事件
  5. Channel:重写了JDK NIO的Channel,负责网络通信、注册以及相关的数据操作
  6. ChannelPipeline:基于责任链设计模式设计的一个内部双向链表结构的Handler业务处理器,用来处理SocketChannel的数据。
  7. ChannelHandlerContext:它是ChannelHandler上下文,可看做是一个管理它所关联的ChannelHandler的组件。每一个ChannelHandler被添加到ChannelPipeline中时,都会创建一个与其对应的ChannelHandlerContext
  8. ChannelHandler:用于处理输入和输出数据的强大的业务处理工具
  9. ByteBuf:实现的一套区别于原生JDK NIO的ByteBuffer新的数据容器,做为server和client之间通信的数据传输载体

2、那组件之间是什么关系?

3、官方启动器示例

服务端:ServerBootStrap

java 复制代码
public final class EchoServer {

    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
        
        final SslContext sslCtx = ServerUtil.buildSslContext();

        // 主从reactor线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        final EchoServerHandler serverHandler = new EchoServerHandler();
        try {
            // 服务端启动器
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
            // 定义IO模型
             .channel(NioServerSocketChannel.class)
            // 声明网络参数
             .option(ChannelOption.SO_BACKLOG, 100)
            // 打印日志
             .handler(new LoggingHandler(LogLevel.INFO))
            // 声明客户端网络参数
             .childOption(null)
            // 客户端请求处理
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc()));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(serverHandler);
                 }
             });

            // 绑定端口
            ChannelFuture f = b.bind(PORT).sync();

        
            f.channel().closeFuture().sync();
        } finally {
            // 关闭连接
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

带child前缀的都是给客户端SocketChannel设置的。

客户端:

java 复制代码
public final class EchoClient {

    static final String HOST = System.getProperty("host", "127.0.0.1");
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
    static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));

    public static void main(String[] args) throws Exception {
        // Configure SSL.git
        final SslContext sslCtx = ServerUtil.buildSslContext();

        // 读写线程组
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap b = new Bootstrap();
            b.group(group)
             .channel(NioSocketChannel.class)
             .option(ChannelOption.TCP_NODELAY, true)
             .handler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(new EchoClientHandler());
                 }
             });

            // 连接服务端
            ChannelFuture f = b.connect(HOST, PORT).sync();

            f.channel().closeFuture().sync();
        } finally {
            // 关闭连接
            group.shutdownGracefully();
        }
    }
}

4、线的角度看Netty

右侧是找到每个节点的关键位置的源码

5、如何使用Netty开发?

五、性能调优

  1. 调整参数
  • 设置系统文件句柄:ulimit -n [xxx]
  • 设置Netty参数
diff 复制代码
-   网络参数:ChannelOption.[XXX]
-   客户端参数:SO_SNDBUF、SO_RCVBUF、SO_KEEPALIVE、SO_REUSEADDR、SO_LINGER、IP_TOS、TCP_NODELAY
-   服务端参数:SO_RCVBUF、SO_REUSEADDR 、SO_BACKLOG
-   Netty自定义参数:System property (-Dio.netty.xxx,50+ )

如:

io.netty.availableProcessors:指定 availableProcessors

io.netty.leakDetection.level:内存泄漏检测级别,默认 SIMPLE

  1. 让程序好排查
  • 线程指定名称:new NioEventLoopGroup(new DefaultThreadFactory("线程名-"));
  • 指定handler的名称:pipline.addLast("echoClientHandler",new EchoClientHandler());
  • 添加Netty的日志: serverBootStrap.handler(new LoggingHandler(LogLevel.INFO))
  1. 观看指标
  • 统计当前系统连接数
  • 连接信息统计
  • 收数据统计
  • 发数据统计
  • 异常统计
  • 待处理任务
  • 积累的数据
  • 触发事件统计
  1. 内存检测: 引用计数(buffer.refCnt())+ 弱引用(Weak reference)
  • 堆外内存未释放:(PlatformDependent.freeDirectBuffer(buffer))
  • 池化内存为归还:(recyclerHandle.recycle(this))
  • 设置检测级别:io.netty.util.ResourceLeakDetector.Level
  1. 使用好注解
  • @Sharable:标识 handler 提醒可共享,不标记共享的不能重复加入 pipeline
  • @Skip: 跳过 handler 的执行
  1. 提升吞吐量
  • 使用handler的channelRead进行write,channelReadComplete进行flush
  • 内置的handler:FlushConsolidationHandler
  1. 流量整形(限流)
  • GlobalTrafficShapingHandler
  • ChannelTrafficShapingHandler
  • GlobalChannelTrafficShapingHandler

六、源码环境搭建

版本是4.1.XXX,用的是window环境操作。

1、不同操作系统的前置设置

mac

安装以下:

brew install autoconf automake libtool openssl

window

git设置空格问题,拉取代码前设置LF自动转CRLF:

git config --global core.autocrlf true

2、本地编译

第一步:install模块netty-dev-tools

第二步:install模块netty-common (必须要执行,这里有groovy脚本生成的类)

第三步:先注释掉集成测试的模块

javascript 复制代码
<module>testsuite-autobahn</module>
<module>testsuite-http2</module>
<module>testsuite-osgi</module>
<module>testsuite-shading</module>
<module>testsuite-native</module>
<module>testsuite-native-image</module>
<module>testsuite-native-image-client</module>
<module>testsuite-native-image-client-runtime-init</module>
<module>transport-blockhound-tests</module>

第四步:在netty项目根目录下执行命令进行install

mvn install -DskipTests -T1C

Issues1、git拉取源码无法拉取原因:项目太大无法clone

解决:

先拉取最后一次提交

bash 复制代码
git clone --depth=1 <https://github.com/netty/netty.git>

依次加深层级拉取

ini 复制代码
git fetch --depth=100
git fetch --depth=500
git fetch --depth=1000

拉取全量

sql 复制代码
git fetch --unshallow

如果出错,调大第三步的depth

拉取所有分支

sql 复制代码
git fetch -pv

3、Netty自带示例

应该好好利用Netty自带的案例,有多种不同协议的使用方法,我们实际使用过程中一定要去参考官方的正确使用方法。

七、总结与思考

  1. 总结:

从问题看Netty的优势,简单了解了Netty是什么,从网络层面理解三种IO模型以及Netty是如何支持的,最终俯视Netty的组件,从关联关系到线角度进行串联进行理解。介绍了常规怎样使用Netty进行开发,更好的去使用,有一个模版的思维。

  1. 思考:
  • Netty的核心类的层级关系是什么样的?
  • ByteBuf读写在使用过程中要理解使用并要注意内存使用,优先使用堆外内存
  • Netty中的NioEventLoop做了三件事,IO事件监听、队列任务、定时任务
  • pipline中的handler是如何处理的?
  • ......

本次内容仅是对Netty的本质和一个整体视角的理解,不涉及如何使用和源码中的细节。

注意:部分资料内容整合来源于网络

相关推荐
异常君8 小时前
Netty Reactor 线程模型详解:构建高性能网络应用的关键
java·后端·netty
南客先生13 小时前
马架构的Netty、MQTT、CoAP面试之旅
java·mqtt·面试·netty·coap
异常君3 天前
一文吃透 Netty 处理粘包拆包的核心原理与实践
java·后端·netty
猫吻鱼4 天前
【Netty4核心原理】【全系列文章目录】
netty
用户90555842148055 天前
AdaptiveRecvByteBuAllocator 源码分析
netty
菜菜的后端私房菜6 天前
深入剖析 Netty 中的 NioEventLoopGroup:架构与实现
java·后端·netty
码熔burning9 天前
【Netty篇】Channel 详解
netty·nio·channel
Pitayafruit16 天前
📌 Java 工程师进阶必备:Spring Boot 3 + Netty 构建高并发即时通讯服务
spring boot·后端·netty
猫吻鱼18 天前
【Netty4核心原理④】【简单实现 Tomcat 和 RPC框架功能】
netty