Netty总结(一)

目录

第一章、初识Netty:背景、现状与趋势

学习后做到以下四点:

解开Netty面纱

三部分

Netty本质上是一个jar包

为什么不使用JDK NIO?

Netty做的更多

Netty做的更好

为什么独选Netty

Netty的前尘往事

废弃5.0原因

与Apache Mina关系

Github地址:https://github.com/netty/netty

第二章: Netty 源码:从“点”(领域知识)的角度剖析

Netty 怎么切换三种 I/O 模式

经典的三种I/O模式

阻塞与非阻塞

同步与异步

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

为什么 Netty 仅支持 NIO 了?

为什么 Netty 有多种 NIO 实现?

NIO 一定优于 BIO 么?

Netty 怎么切换 I/O 模式的?

Netty 如何支持三种 Reactor

什么是 Reactor 及三种版本

TCP 粘包、半包 Netty 全搞定

什么是粘包和半包

解决问题的根本手段:找出消息的边界

为什么需要“二次”编解码

常见的“二次”编解码方式

选择编码方式的要点

Google Protobuf 简介

Netty 对二次编解码的支持

keepalive 与 Idle 监测

为什么需要keepalive

为什么还需要应用层keepalive?

什么是idle检测

开启keepalive

Netty 的那些“锁”事

分析同步问题的核心三要素

锁的分类

Netty 玩转锁的五个关键点

Netty 如何玩转内存使用

内存使用技巧的目标

Netty内存使用技巧

源码解读Netty内存使用


参考课程:Netty源码剖析与实战  https://time.geekbang.org/course/intro/237

第一章、初识Netty:背景、现状与趋势

学习后做到以下四点:

  • 掌握Java网络编程基础知识和原理

  • 使用Netty构建一个能推向产品线的Java网络服务器

  • 熟悉Netty核心源码以及其深层原理

  • 能够熟练诊断、分析并排除Netty使用中的各种故障

解开Netty面纱

Netty由 Trustin Lee(韩国,Line公司)2004年开发

  • 本质:网络应用程序框架

  • 实现:异步、时间驱动

  • 特性:高性能、可维护、快速开发

  • 用途:开发服务器和客户端

 

三部分

  • 最底层的核心层

    • 零复制的功能丰富的Byte Buffer

    • 通用的通信层API

    • 可扩展的事件模型

  • 左上:支持的传输层,包括TCP的socket、UDP的Datagtam、Http的Tunnel和In-VM的Pipe

  • 右上:支持的各种各样协议

Netty本质上是一个jar包

为什么不使用JDK NIO?

  • Netty做的更多

  • 支持常用应用层协议;

  • 解决传输问题:粘包、半包现象;

  • 支持流量整形

  • 完整的断连、Idle等异常处理等。

  • Netty做的更好

    • 规避JDK NIO bug

      • 经典的epoll bug:异常唤醒空转导致CPU 100%

      • IP_TOS参数(IP包的优先级和QoS选项)使用时的抛出异常 --> java.lang.Assertion:Option not found

    • API更友好强大

      • JDK的NIO一些API不够友好,功能薄弱,例如:ByteBuffer --> Netty's ByteBuf

      • 其他一些增强:Threadlocal --> Netty's FastThreadLocal

    • 隔离变化、屏蔽细节

      • 隔离JDK NIO的实现变化:nio -> nio2(aio) -> ...

      • 屏蔽JDK NIO的实现细节

为什么独选Netty

Apache Mina

同一作者,推荐Netty

Sun Grizzly

三少:用的少、文档少、更新少

Apple SwiftNIO、ACE等

其他语言,不考虑

Cindy

生命周期不长

Tomcat、Jetty

还没独立出来

Netty的前尘往事

  • 废弃5.0原因

复杂、没有证明明显性能优势、维护不过来

  • 与Apache Mina关系

同一作者开发,都处于维护阶段(2004.06 Netty2发布 2005.05 Mina发布)

Github地址:https://github.com/netty/netty

第二章: Netty 源码:从“点”(领域知识)的角度剖析

Netty 怎么切换三种 I/O 模式

  • 经典的三种I/O模式

排队打饭模式

BIO (阻塞 I/O)

JDK1.4 之前

点单、等待被叫模式

NIO (非阻塞 I/O)

JDK1.4(2002 年,java.nio 包)

包厢模式

AIO(异步 I/O)

JDK1.7 (2011 年)

  • 阻塞与非阻塞

    • 菜没好,要不要死等 -> 数据就绪前要不要等待?

    • 阻塞:没有数据传过来时,读会阻塞直到有数据;缓冲区满时,写操作也会阻塞。

      非阻塞遇到这些情况,都是直接返回。

  • 同步与异步

  • 菜好了,谁端 -> 数据就绪后,数据操作谁完成?

  • 数据就绪后需要自己去读是同步,数据就绪直接读好再回调给程序是异步。

  • Netty 对三种 I/O 模式的支持

  • 为什么 Netty 仅支持 NIO 了?

  • 为什么不建议(deprecate)阻塞 I/O(BIO/OIO)?

连接数高的情况下:阻塞 -> 耗资源、效率低

  • 为什么删掉已经做好的 AIO 支持?

Windows 实现成熟,但是很少用来做服务器。

Linux 常用来做服务器,但是 AIO 实现不够成熟。

Linux 下 AIO 相比较 NIO 的性能提升不明显 。

  • 为什么 Netty 有多种 NIO 实现?

通用的 NIO 实现(Common)在 Linux 下也是使用 epoll,为什么自己单独实现?

实现得更好!

• Netty 暴露了更多的可控参数,例如:

• JDK 的 NIO 默认实现是水平触发

• Netty 是边缘触发(默认)和水平触发可切换

• Netty 实现的垃圾回收更少、性能更好

  • NIO 一定优于 BIO 么?

BIO 代码简单;特定场景:连接数少,并发度低,BIO 性能不输 NIO。

  • Netty 怎么切换 I/O 模式的?

  • 怎么切换? 例如对于服务器开发:从 NIO 切换到 OIO

NIO

OIO 

NioEventLoopGroup

OioEventLoopGroup

NioServerSocketChannel

OioServerSocketChannel

 

  • 原理是什么? 例如对于 ServerSocketChannel:工厂模式+泛型+反射实现

  • 为什么服务器开发并不需要切换客户端对应NioSocketChannel ?

ServerSocketChannel 负责创建对应的 SocketChannel 。

  • Netty 如何支持三种 Reactor

  • 什么是 Reactor 及三种版本

    1. 一个人包揽所有:迎宾、点菜、做饭、上菜、送客等 --> Reactor 单线程

    2. 多招几个伙计:大家一起做上面的事情 --> Reactor 多线程模式

    3. 进一步分工:搞一个或者多个人专门做迎宾 --> 主从 Reactor 多线程模式

  • Reactor 是一种开发模式,模式的核心流程:

    注册感兴趣的事件 -> 扫描是否有感兴趣的事件发生 -> 事件发生后做出相应的处理。

BIO

NIO

AIO

Thread-Per-Connection

Reactor

Proactor

 

模式

模型

Netty中如何使用

Thread-Per-Connection

 

 

Reactor模式v1:单线程

 

EventLoopGroup eventGroup = new NioEventLoopGroup(1);

ServerBootstrap serverBootstrap = new ServerBootstrap();

serverBootstrap.group(eventGroup);

Reactor模式v2:多线程

 

EventLoopGroup eventGroup = new NioEventLoopGroup();

ServerBootstrap serverBootstrap = new ServerBootstrap();

serverBootstrap.group(eventGroup);

Reactor模式v3:主从多线程

 

EventLoopGroup bossGroup = new NioEventLoopGroup();

EventLoopGroup workerGroup = new NioEventLoopGroup();

ServerBootstrap serverBootstrap = new ServerBootstrap();

serverBootstrap.group(bossGroup, workerGroup);

TCP 粘包、半包 Netty 全搞定

  • 什么是粘包和半包

ABC DEF --> ABCDEF? AB CD EF?

粘包的主要原因:

  • 发送方每次写入数据 < 套接字缓冲区大小

  • 接收方读取套接字缓冲区数据不够及时

半包的主要原因:

  • 发送方写入数据 > 套接字缓冲区大小

  • 发送的数据大于协议的 MTU(Maximum Transmission Unit,最大传输单元),必须拆包

换个角度看:

  • 收发 一个发送可能被多次接收,多个发送可能被一次接收

  • 传输 一个发送可能占用多个传输包,多个发送可能公用一个传输包

根本原因:

TCP是流式协议,消息无边界。

提醒:UDP 像邮寄的包裹,虽然一次运输多个,但每个包裹都有“界限”,一个一个签收,所以无粘包、半包问题

解决问题的根本手段:找出消息的边界

方式\比较

寻找消息边界方式

优点

缺点

Netty封帧方式

推荐度

解码

编码

TCP连接改为短连接,一个请求一个短连接

建立连接到释放连接之间的信息即为传输信息

简单

效率低下

 

 

不推荐

封装成帧

(Framing)

固定长度

 

满足固定长度即可

简单

空间浪费

FixedLengthFrameDecoder

简单

不推荐

分割符

 

分割符之间

空间不浪费,也比较简单

内容本身出现分隔符时需转义,所以需要扫描内容

DelimiterBasedFrameDecoder

推荐

固定长度字段存个内容的长度信息

 

先解析固定长度的字段获取长度,然后读取后续内容

精确定位用户数据,内容也不用转义

长度理论上有限制,需提前预知可能的最大长度从而定义长度占用字节数

 

LengthFieldBasedFrameDecoder

LengthFieldPrepender

推荐+

其他方式

每种都不同,例如JSON 可以看{}是否应已经成对

衡量实际场景,很多是对现有协议的支持

为什么需要“二次”编解码

假设我们把解决半包粘包问题的常用三种解码器叫一次解码器,那么我们在项目中,除了可选的的压缩解压缩之外,还需要一层解码,因为一次解码的结果是字节,需要和项目中所使用的对象做转化,方便使用,这层解码器可以称为“二次解码器”,相应的,对应的编码器是为了将 Java 对象转化成字节流方便存储或传输。

  • 一次解码器:ByteToMessageDecoder

io.netty.buffer.ByteBuf (原始数据流)--> io.netty.buffer.ByteBuf (用户数据)

  • 二次解码器:MessageToMessageDecoder<I>

    io.netty.buffer.ByteBuf (用户数据)--> Java Object

  • 是否一步到位,合并一次和二次解码?

可以,但不建议;没有分层,不够清晰;耦合性高,不容易置换方案。

常见的“二次”编解码方式

Java序列化、Marshaling、XML、JSON、MessagePack、Protobuf、其他

选择编码方式的要点

  • 空间 

          编码后的占用空间,需要比较不同的数据大小情况

  • 时间

          编解码速度,需要比较不同的数据大小情况

  • 是否追求可读性
  • 多语言(Java、C、Python 等)的支持:例如 msgpack 的多语言支持

Google Protobuf 简介

  • Protobuf 是一个灵活的、高效的用于序列化数据的协议。

  • 相比较 XML 和 JSON 格式,Protobuf 更小、更快、更便捷。

  • Protobuf 是跨语言的,并且自带了一个编译器(protoc),只需要用它进行编译,可以自动生成 Java、python、C++ 等代码,不需要再写其他代码。

Netty 对二次编解码的支持

ch.pipeline().addLast(new ProtobufVarint32FrameDecoder());

ch.pipeline().addLast(new ProtobufDecoder(PersonOuterClass.Person.getDefaultInstance()));

ch.pipeline().addLast(new ProtobufVarint32LengthFieldPrepender());

ch.pipeline().addLast(new ProtobufEncoder());

keepalive 与 Idle 监测

为什么需要keepalive

为什么还需要应用层keepalive?

  • 协议分层,各层关注点不同:

    传输层关注是否“通”,应用层关注是否可服务? 类比前面的电话订餐例子,电话能通,不代表有人接;服务器连接在,但是不定可以服务(例如服务不过来等)。

  • TCP 层的 keepalive 默认关闭,且经过路由等中转设备 keepalive 包可能会被丢弃。

  • TCP 层的 keepalive 时间太长:

默认 > 2 小时,虽然可改,但属于系统参数,改动影响所有应用。

 

  • 提示:

HTTP 属于应用层协议,但是常常听到名词“ HTTP Keep-Alive ”指的是对长连接和短连接的选择:

• Connection : Keep-Alive 长连接(HTTP/1.1 默认长连接,不需要带这个 header)

• Connection : Close 短连接

什么是idle检测

Idle 监测,只是负责诊断,诊断后,做出不同的行为,决定 Idle 监测的最终用途:

  • 发送 keepalive :一般用来配合 keepalive ,减少 keepalive 消息。

Keepalive 设计演进:V1 定时 keepalive 消息 -> V2 空闲监测 + 判定为 Idle 时才发keepalive。

  • V1:keepalive 消息与服务器正常消息交换完全不关联,定时就发送;

  • V2:有其他数据传输的时候,不发送 keepalive ,无数据传输超过一定时间,判定为 Idle,再发 keepalive 。

  • 直接关闭连接:

  • 快速释放损坏的、恶意的、很久不用的连接,让系统时刻保持最好的状态。

  • 简单粗暴,客户端可能需要重连。

实际应用中:结合起来使用。按需 keepalive ,保证不会空闲,如果空闲,关闭连接。

开启keepalive

  • Server 端开启 TCP keepalive

bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true)

bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)

提示:.option(ChannelOption.SO_KEEPALIVE,true) 存在但是无效

  • 开启不同的 Idle Check:

ch.pipeline().addLast(“idleCheckHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));

/**
  * @see #IdleStateHandler(boolean, long, long, long, TimeUnit)
  */
public IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime,TimeUnit unit) {
  this(false, readerIdleTime, writerIdleTime, allIdleTime, unit);
}

Netty 的那些“锁”事

分析同步问题的核心三要素

  • 原子性:“并无一气呵成,岂能无懈可击”

  • 可见性:“你做的改变,别人看不见”

  • 有序性:“不按套路出牌”

锁的分类

  • 对竞争的态度:乐观锁(java.util.concurrent 包中的原子类)与悲观锁(Synchronized)

  • 等待锁的人是否公平而言:公平锁 new ReentrantLock (true)与非公平锁 new ReentrantLock ()

  • 是否可以共享:共享锁与独享锁:ReadWriteLock ,其读锁是共享锁,其写锁是独享锁

Netty 玩转锁的五个关键点

  • 在意锁的对象和范围 -> 减少粒度

          初始化 channel (io.netty.bootstrap.ServerBootstrap#init)

          Synchronized method -> Synchronized block

  • 注意锁的对象本身大小 -> 减少空间占用

         统计待发送的字节数(io.netty.channel.ChannelOutboundBuffer

         AtomicLong -> Volatile long + AtomicLongFieldUpdater

         Atomic long VS long:

        前者是一个对象,包含对象头(object header)以用来保存 hashcode、lock 等信息,32 位系统占

        用8字节;64 位系统占 16 字节,所以在 64 位系统情况下:

         • volatile long = 8 bytes

        • AtomicLong = 8 bytes (volatile long)+ 16bytes (对象头)+ 8 bytes (引用) = 32 bytes

        至少节约 24 字节!

        结论:Atomic* objects -> Volatile primary type + Static Atomic*FieldUpdater

  • 注意锁的速度 -> 提高并发性

1)记录内存分配字节数等功能用到的LongCounter io.netty.util.internal.PlatformDependent#newLongCounter()

高并发时:java.util.concurrent.atomic.AtomicLong ->java.util.concurrent.atomic.LongAdder (JDK1.8)

结论: 及时衡量、使用 JDK 最新的功能

2)曾经根据不同情况,选择不同的并发包实现:JDK < 1.8 考虑ConcurrentHashMapV8(ConcurrentHashMap 在 JDK8 中的版本)

  • 不同场景选择不同的并发类 -> 因需而变

1)关闭和等待关闭事件执行器(Event Executor):

Object.wait/notify --> CountDownLatch

io.netty.util.concurrent.SingleThreadEventExecutor#threadLock

2)Nio Event loop中负责存储task的Queue

Jdk’s LinkedBlockingQueue (MPMC) -> jctools’ MPSC

io.netty.util.internal.PlatformDependent.Mpsc#newMpscQueue(int)

  • 衡量好锁的价值 -> 能不用则不用

1)局部串行:Channel 的 I/O 请求处理 Pipeline 是串行的

2)整体并行:多个串行化的线程(NioEventLoop)

    Netty 应用场景下:局部串行 + 整体并行 > 一个队列 + 多个线程模式:

  • 降低用户开发难度、逻辑简单、提升处理性能

  • 避免锁带来的上下文切换和并发保护等额外开销

3)避免用锁:用 ThreadLocal 来避免资源争用,例如 Netty 轻量级的线程池实现

      io.netty.util.Recycler#threadLocal

Netty 如何玩转内存使用

内存使用技巧的目标

目标:

• 内存占用少(空间)

• 应用速度快(时间)

对 Java 而言:减少 Full GC 的 STW(Stop the world)时间

Netty内存使用技巧

减少对象本身大小

1)用基本类型就不要用包装类型

2)应该定义成类变量的不要定义为实例变量:

  • 一个类 -> 一个类变量

  • 一个实例 -> 一个实例变量

  • 一个类 -> 多个实例

  • 实例越多,浪费越多。

3)Netty 中结合前两者:

io.netty.channel.ChannelOutboundBuffer#incrementPendingOutboundBytes(long, boolean)

统计待写的请求的字节数

对分配内存进行预估

1)对于已经可以预知固定 size 的 HashMap避免扩容,可以提前计算好初始size或者直接使用

com.google.common.collect.Maps#newHashMapWithExpectedSize

2)Netty 根据接受到的数据动态调整(guess)下个要分配的 Buffer 的大小。可参考

io.netty.channel.AdaptiveRecvByteBufAllocator

Zero-Copy

1)使用逻辑组合,代替实际复制。

例如 CompositeByteBuf:io.netty.handler.codec.ByteToMessageDecoder#COMPOSITE_CUMULATOR

2)使用包装,代替实际复制。

byte[] bytes = data.getBytes();

ByteBuf byteBuf = Unpooled.wrappedBuffer(bytes);

3)调用 JDK 的 Zero-Copy 接口。

Netty 中也通过在 DefaultFileRegion 中包装了 NIO 的 FileChannel.transferTo() 方法实现了零拷贝:io.netty.channel.DefaultFileRegion#transferTo

堆外内存
  • 优点:

    • 更广阔的“空间 ”,缓解店铺内压力 -> 破除堆空间限制,减轻 GC 压力

    • 减少“冗余”细节(假设烧烤过程为了气氛在室外进行:烤好直接上桌:vs 烤好还要进店内)-> 避免复制

  • 缺点:

    • 需要搬桌子 -> 创建速度稍慢

    • 受城管管、风险大 -> 堆外内存受操作系统管理

内存池

为什么引入对象池:

  • 创建对象开销大

  • 对象高频率创建且可复用

  • 支持并发又能保护系统

  • 维护、共享有限的资源

如何实现对象池?

  • 开源实现:Apache Commons Pool

  • Netty 轻量级对象池实现 io.netty.util.Recycler

源码解读Netty内存使用

怎么从堆外内存切换堆内使用?

  • 方法 1:参数设置

io.netty.noPreferDirect = true;

  • 方法 2:传入构造参数false

ServerBootstrap serverBootStrap = new ServerBootstrap();

UnpooledByteBufAllocator unpooledByteBufAllocator = new UnpooledByteBufAllocator(false);

serverBootStrap.childOption(ChannelOption.ALLOCATOR, unpooledByteBufAllocator)

堆外内存的分配?

  • ByteBuffer.allocateDirect(initialCapacity)

内存池/非内存池的默认选择及切换方式?

默认选择:安卓平台 -> 非 pooled 实现,其他 -> pooled 实现。

  • 参数设置:io.netty.allocator.type = unpooled;

  • 显示指定:serverBootStrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT)

内存池实现?

核心要点:有借有还,避免遗忘

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值