Bootstrap
- Bootstrap是引导的意思,它的作用是配置整个Netty程序,将各个组件都串起来,最后绑定端口、启动 Netty服务
- Netty中提供了2种类型的引导类,一种用于客户端(Bootstrap),而另一种(ServerBootstrap)用于服务器 ,区别在于: 1、ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而 Bootstrap 则是由想要连接 到远程节点的客户端应用程序所使用的 2、引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个
Channel
Ø Netty中的Channel是与网络套接字相关的,可以理解为是socket连接,在客户端与服务端连接的时候就会 建立一个Channel,它负责基本的IO操作,比如:bind()、connect(),read(),write() 等 Ø 主要作用:
1. 通过Channel可获得当前网络连接的通道状态。
2. 通过Channel可获得网络连接的配置参数(缓冲区大小等)。
3. Channel提供异步的网络I/O操作,比如连接的建立、数据的读写、端口的绑定等。
Ø 不同协议、不同的I/O类型的连接都有不同的 Channel 类型与之对
EventLoopGroup&EventLoop
Ø Netty是基于事件驱动的,比如:连接注册,连接激活;数据读取;异常事件等等,有了事件,就需要一 个组件去监控事件的产生和事件的协调处理,这个组件就是EventLoop(事件循环/EventExecutor),在 Netty 中每个Channel 都会被分配到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。每个 EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件。
Ø EventLoopGroup 是用来生成 EventLoop 的,包含了一组EventLoop(可以初步理解成Netty线程池)
eventLoopThreads 是多少?
EventLoopGroup 是接口,我们采用的实现是NioEventLoopGroup
// 主线程,不处理任何业务逻辑,只是接收客户的连接请求
EventLoopGroup boss = new NioEventLoopGroup();
// 工作线程,处理注册其上Channel的I/O事件及其他
Task EventLoopGroup worker = new NioEventLoopGroup();
Ø核心线程数默认:cpu核数*2
Ø核心线程数在创建时可通过构造函数指定
Ø对于boss group,我们其实也 只用到了其中的一个线程因为服务端一般只会绑定一个端口启动
ChannelHandler&ChannelHandlerContext&ChannelPipeline
(这些组件比较重要,单独在netty从入门到精通(中)详细做了介绍)
ChannelHandler复用
每个客户端Channel创建后初始化时, 均会向与该Channel绑定的Pipeline中 添加handler,此种模式下,每个 Channel享有的是各自独立的Handler
@Sharable注解可以解决这个问题
备注:handler被复用可能导致线程安全问题,比如在handler中操作成员变量。线程安全的问题需要开发者自行保证。
ChannelInboundHandlerAdapter&SimpleChannelInboundHandler
对于编写Netty数据入站处理器,可以选择继承 ChannelInboundHandlerAdapter,也可以选择继承 SimpleChannelInboundHandler,区别是什么?
ByteBuf
概念和作用:
Ø Java NIO 提供了ByteBuffer 作为它的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。Netty 使用ByteBuf来替代ByteBuffer,它是一个强大的实现,既解决了JDK API 的局限性, 又为网络应用程序的 开发者提供了更好的API
Ø 从结构上来说,ByteBuf 由一串字节数组构成。数组中每个字节用来存放信息,ByteBuf提供了两个索引, 一个用于读取数据(readerIndex ),一个用于写入数据(writerIndex)。这两个索引通过在字节数组中 移动,来定位需要读或者写信息的位置。而JDK的ByteBuffer只有一个索引,因此需要使用flip方法进行读 写切换
ByteBuf的三个指针
Ø readerIndex:指示读取的起始位置, 每读取一个字节, readerIndex自增累加1。 如果readerIndex 与 writerIndex 相等,ByteBuf 不可读。
Ø writerIndex:指示写入的起始位置, 每写入一个字节, writeIndex自增累加1。如果增加到 writerIndex 与 capacity() 容量相等,表示 ByteBuf 已经不可写,但是这个时候,并不代表不能往 ByteBuf 中写数据了, 如果 发现往ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩容到底层的内存大小为 maxCapacity
Ø maxCapacity:指示ByteBuf 可以扩容的最大容量, 如果向ByteBuf写入数据时, 容量不足, 可以进行扩容的最 大容量
常用API
容量API:
- Ø capacity():表示 ByteBuf 底层占用了多少字节的内存(包括丢弃的字节、可读字节、可写字节),不同的底层实 现机制有不同的计算方式。
- Ø maxCapacity(): ByteBuf 底层最大能够占用多少字节的内存,当向 ByteBuf 中写数据的时候,如果发现容量不 足,则进行扩容,直到扩容到 maxCapacity,超过这个数,就抛异常。
- Ø readableBytes() 与 isReadable():readableBytes() 表示 ByteBuf 当前可读的字节数,它的值等于 writerIndex-readerIndex,如果两者相等,则不可读,isReadable() 方法返回 false
- Ø writableBytes()、 isWritable() 、maxWritableBytes():writableBytes() 表示 ByteBuf 当前可写的字节数,它的 值等于 capacity()-writerIndex,如果两者相等,则表示不可写,isWritable() 返回 false,但是这个时候,并不代 表不能往 ByteBuf 中写数据了, 如果发现往ByteBuf 中写数据写不进去的话,Netty 会自动扩容 ByteBuf,直到扩 容到底层的内存大小为 maxCapacity,而 maxWritableBytes() 就表示可写的最大字节数,它的值等于 maxCapacity-writerIndex。
读写指针相关的API:
- Ø readerIndex() 与 readerIndex(int readerIndex):前者表示返回当前的读指针 readerIndex, 后者表示设置读指针
- Ø writeIndex() 与 writeIndex(int writerIndex):前者表示返回当前的写指针 writerIndex, 后者表示设置写指针
- Ø markReaderIndex() 与markWriterIndex():表示把当前的读指针/写指针保存起来,操作形式为: markedReaderIndex = readerIndex / markedWriterIndex = writerIndex;
读写操作API:
- Ø writeBytes(byte[] src): 表示把字节数组 src 里面的数据全部写到 ByteBuf,src字节数组大小的长度通常小于等于 writableBytes()
- Ø readBytes(byte[] dst):把 ByteBuf 里面的数据全部读取到 dst,dst 字节数组的大小通常等于 readableBytes()
- Ø writeByte(int value)、readByte():writeByte() 表示往 ByteBuf 中写一个字节,而 readByte() 表示从 ByteBuf 中读 取一个字节,类似的 API 还有 writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、 writeFloat()、writeDouble() 与 readBoolean()、readChar()、readShort()、readInt()、readLong()、 readFloat()、readDouble() 等等
丢弃、清理,释放:
- Ø discardReadBytes(): 丢弃已读取的字节空间,可写空间变多
- Ø clear():重置readerIndex 、 writerIndex 为0,需要注意的是,重置并没有删除真正的内容
- Ø release():真正去释放bytebuf中的数据,
- Ø ReferenceCountUtil.release(buf):工具方法,内部还是调用release()
wrap:
通过Wrap操作可以快速转换或得到一个ByteBuf对象,Unpooled 工具类中提供了很多重载的wrappedBuffer方法
理解:wrappedBuffer相当于剪切粘贴,copiedBuffer相当于复制粘贴
ByteBuf分类
1.按照内存位置划分:分为Heap 和 Direct
Ø 堆缓冲区(HeapByteBuf):内存分配在jvm堆,分配和回收速度比较快,可以被JVM自动回收,缺点是,如果进行 socket的IO读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下 降。由于在堆上被 JVM 管理,在不被使用时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。
Ø 直接缓冲区(DirectByteBuf):内存分配的是堆外内存(系统内存),相比堆内存,它的分配和回收速度会慢一些, 但是将它写入或从Socket Channel中读取时,由于减少了一次内存拷贝,速度比堆内存块。
Ø 复合缓冲区(CompositeByteBuf):顾名思义就是将两个不同的缓冲区从逻辑上合并,让使用更加方便。
Netty默认使用的是DirectByteBuf,如果需要使用HeapByteBuf模式,则需要进行系统参数的设置
堆外内存
2.根据内存结构:Pooled 池化内存和 Unpooled 非池化内存
Ø 对于Pooled类型的ByteBuf,不管是PooledDirectByteBuf还是PooledHeapByteBuf都只能由Netty内部自己使用(构造是私有和受保护的)。
Ø Netty提供Unpooled工具类创建的ByteBuf都是unpooled类型,默认采用的Allocator是direct类型;当然用户可以自己选择创建UnpooledDirectByteBuf和UnpooledHeapByteBuf
3.Unsafe 和非 Unsafe
Unsafe:是 JDK 底层的一个负责 IO 操作的对象,可以直接拿到对象的内存地址,基于内存地址进行读写操作。
ByteBuf 的释放
ByteBuf如果采用的是堆缓冲区模式的话,可以由GC回收,但是如果采用的是直接缓冲区,就不受GC的管理,就得手 动释放,否则会发生内存泄露,Netty自身引入了引用计数,提供了ReferenceCounted接口,当对象的引用计数>0 时要保证对象不被释放,当为0时需要被释放
关于ByteBuf的释放,分为手动释放与自动释放:
Ø 手动释放 ,就是在使用完成后,调用ReferenceCountUtil.release(byteBuf); 进行释放,这种方式的弊端就是一旦忘 记释放就可能会造成内存泄露
Ø 自动释放有三种方式 ,分别是:
Ø TailContext:Inbound流水线的末端,如果前面的handler都把消息向后传递最终由TailContext释放该消息,需 要注意的是,如果没有进行向下传递,是不会进行释放操作的
Ø SimpleChannelInboundHandler:自定义的InboundHandler继承自SimpleChannelInboundHandler,在 SimpleChannelInboundHandler中自动释放
Ø HeadContext:outbound流水线的末端,出站消息一般是由应用所申请,到达最后一站时,经过一轮复杂的调 用,在flush完成后终将被release掉