Skip to content

netty介绍

netty是一个异步事件驱动NIO框架,由jboss提供的一个java开源框架,是业界最流行的NIO架构,整合了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的经验实现, 精心设计的框架,在多个大型商业项目中得到充分验证。

  1. API使用简单

  2. 成熟、稳定

  3. 社区活跃 有很多种NIO框架,如mina

  4. 经过大规模行业(互联网、大数据、网络游戏、电信行业)

一、netty线程模型和reactor模式

设计模式--reactor模式(反应器设计模式),是一种基于事件驱动的设计模式,在事件驱动的应用中,将一个或多个客户的服务请求分离(demultiplex)和调度(dispatch)给应用程序。 在事件驱动的应用当中,同步的、有序的处理同时接收的多个服务请求。一般出现在高并发系统中,比如netty、redis等

优点:

  1. 响应快,不会因为单个同步而阻塞,虽然reactor本身依然是同步的。

  2. 编程相对简单,最大程度的避免了复杂的多线程以及同步问题,并且避免了多线程/进程之间的切换开销

  3. 可拓展性,可以方便的通过reactor实例个数充分利用CPU资源

缺点:

  1. 相比传统的简单模型,reactor增加了一定的复杂性,因而有一定的门槛,不利于调试

  2. reactor模式需要系统底层的支持,比如java中的selector支持,操作系统中的select系统调用支持

reactor模式基于事件驱动,适合处理海量的IO事件,属于同步非阻塞IO(NIO)

二、reactor单线程模型(少用)

  1. 作为NIO服务端,接收客户端的TCP连接;作为NIO的客户端,像服务端发起TCP连接;

  2. 服务端读请求数据并响应;客户端读写并读取响应。

使用场景:

对于小业务合适,编码简单;对于高负载、大并发的应用场景不合适,一个NIO线程处理太多请求,则负载过高,并且可能响应变慢,导致大量的请求超时,万一线程挂了,则不可用。

三、reactor多线程模型

一个acceptor线程,一组NIO线程,一般是自带的线程池,包含一个任务队列和多个可用线程

使用场景:

可满足大多数场景,但是当acceptor需要负责操作的时候,比如认证等耗时操作,在高并发情况下也会有性能问题。

四、reactor主从线程模型

acceptor不是在一个线程,而是一组NIO线程;IO线程也是一组NIO线程,这样就是两个线程池去处理接入连接和处理IO

使用场景:

满足目前大部分场景的需求,也是netty使用的线程模型。

bossgroup和workgroup

五、EventLoop和EventLoopGroup线程模型

  • 高性能rpc框架的三个要素:IO模型、数据协议(http/protobuf/Thrift)、线程模型

  • EventLoop好比一个线程,一个EventLoop可以服务多个Channel,一个Channel只有一个EventLoop。

通过创建多个EventLoop来优化资源,也就是EventLoopGroup。(类似线程池)

  • EventLoopGroup 负责分配EventLoop到新创建的Channel,里面包含多个EventLoop

EventLoopGroup ->拥有多个EventLoop

​ EventLoop ->维护一个Selector(可以用单个线程来处理多个Channels)

selector学习资料:http://ifeve.com/selectors/

  • EventLoopGroup 默认线程池数量:CPU核心数*2

六、netty启动引导类Bootstrap模块

服务器启动引导类ServerBootstrap

  • group:设置线程组模型,Reactor线程模型对比EventLoopGroup

    1. 单线程:只有一个线程在处理任务

        //创建主线程--接收连接的池子
              EventLoopGroup bossGroup = new NioEventLoopGroup(1);
              try {
                  //创建服务器端的启动助手配置参数
                  ServerBootstrap bootstrap = new ServerBootstrap();
                  bootstrap
                          //指定线程池--主线程和工作线程
                          .group(bossGroup)
    2. 多线程:接收线程为一个主线程,工作线程为多个线程

        //创建主线程--接收连接的池子
              EventLoopGroup bossGroup = new NioEventLoopGroup(1);
              //创建工作线程--真正的工作线程,主线程调用工作线程里的
              EventLoopGroup workerGroup = new NioEventLoopGroup();
              try {
      
                  //创建服务器端的启动助手配置参数
                  ServerBootstrap bootstrap = new ServerBootstrap();
      
                  bootstrap
                          //指定线程池--主线程和工作线程
                          .group(bossGroup,workerGroup)
    3. 主从线程:接收线程和工作线程都是一个组

        //创建主线程--接收连接的池子
              EventLoopGroup bossGroup = new NioEventLoopGroup();
              //创建工作线程--真正的工作线程,主线程调用工作线程里的
              EventLoopGroup workerGroup = new NioEventLoopGroup();
              try {
      
                  //创建服务器端的启动助手配置参数
                  ServerBootstrap bootstrap = new ServerBootstrap();
      
                  bootstrap
                          //指定线程池--主线程和工作线程
                          .group(bossGroup,workerGroup)
  • channel:设置channel管道类型NioServerSocketChannel、OioServerSocketChannel(已废弃)

  • option:作用于每个新连接的channel,设置tcp连接中的一些参数。

    1. ChannelOption.SO_BACKLOG:存放已完成三次握手请求的等待队列最大长度;

linux服务器tcp连接底层知识:

syn queue:半连接队列,防洪水攻击,tcp_max_syn_backlog

accept queue:全连接队列,net.core.somaxconn

系统默认的somaxconn参数要足够大,如果backlog比somaxconn大,则会优先选择后者。

  1. ChannelOption.TCP_NODELAY:为了解决Nagle的算法问题,默认是false,要求高实时性,有数据时马上发送,就将该选项设置为true关闭Nagle算法;如果减少发送次数,就设置为false,会积累一定的大小后再发送。
  • childOption:作用于被accept以后的连接,设置连接的状态等

  • childHandler:指定通道工作内容,当该事件通道收到消息以后,如何处理。需要传入实现 ChannelInitializer<SocketChannel> 接口的类。

七、channel模块

  • channel :客户端和服务端建立的一个连接通道

  • channelHandler:负责channel的逻辑处理

  • channelPipeLine:负责管理channelHandler的有序容器

一个channel包含一个channelPipeLine,所有的channelHandler都会顺序加入channelPipeLine当中,创建channel时会自动创建一个channelPipeLine, 每个channel都有一个管理它的pipeline,这个关联是永久性的

当channel状态出现变化,就会触发对应的事件。

状态(顺序也是channel的生命周期):

  1. channelRegistered:channel注册到一个EventLoop

  2. channelActive:变为活跃状态,即连接到了远程主机,可以接收和发送数据。

  3. channelInactive:channel处于非活跃状态,没有连接到远程主机。

  4. channelUnregistered:创建了一个channel,但是未注册到EventLoop里,也就是没有和Selector绑定

八、channelHandler和channelPipeline

handler里的方法:

  • handlerAdded:当channelHandler添加到channelPipeline调用。

  • handlerRemoved:当channelHandler从channelPipeline移除时调用。

  • exceptionCaught:执行抛出异常时调用。

channelHandler下主要接口:

  • channelInboundHandler:处理数据和channel状态类型的改变。

适配器:channelInboundHandlerAdapter

常用:SimpleChannelInboundHandler

  • channelOutboundHandler:处理输出数据

适配器 ChannelOutboundHandlerAdapter

ChannelPipeline:

类似工厂流水线一样,上面可以添加多个channelHandler,也可看成是一串ChannelHandler实例,拦截穿过Channel的输入输出event,ChannelPipeline实现了一种拦截器的高级形式, 使得用户可以对事件的处理以及ChannelHanler之间交互获得完全的控制权。

九、ChannelHandlerContext

ChannelHandlerContext是连接ChannelHandler和ChannelPopeline之间的桥梁。ChannelHandlerContext部分方法和channel和ChannelPopeline重合, 好比调用write方法。

channel、channelPipeline、channelHandlerContext都可以调用此方法,前两者都会在整个管道流里传播,而ChannelHandlerContext只会在后续的Handler里面传播

channelInboundHandler之间的传递,主要是通过调用ctx里的fireXXX()方法来实现下一个handler的调用,比如handler1里调用read方法,需要调用下一个handler的read, 需要添加代码:ctx.fireRead();

十、InboundHandler和OutboundHandler

一般在pipeline里添加的InboundHandler和OutboundHandler,执行的顺序是按照下面的规则执行。

InboundHandler是按顺序执行,OutboundHandler是倒序执行。如客户端如下:

ch.pipeline().addLast(new OutboundHandler1());
ch.pipeline().addLast(new OutboundHandler2());
ch.pipeline().addLast(new InboundHandler1());
ch.pipeline().addLast(new InboundHandler2());
ch.pipeline().addLast(new InboundHandler3());
  • InboundHandler之间的的数据传递是通过ctx.fireChannelRead(msg)。

  • InboundHandler通过ctx.write(msg)传递到OutboundHandler。

  • ctx.write(msg)传递消息,Inbound需要放在结尾,在Outbound之后,不然OutboundHandler不会执行

但是如果通过ctx.channel.write(msg)或ctx.pipeline.write(msg),情况会不一样,都会执行。

  • Outbound和Inbound谁先执行,针对客户端和服务端而言,客户端是发起请求再接收数据,先Outbound再Inbound,服务端则相反

十一、异步操作ChannelFuture

Netty中所有的I/O操作都是异步的,这就意味着任何I/O调用都会立即返回,而ChannelFuture会提供有关的I/O操作的结果或状态

ChannelFuture状态:

  • 未完成:当I/O操作开始时,将创建一个新的对象,新的最初是未完成的,它即没有成功,也没有被取消,因为I/O操作尚未完成。

  • 已完成:当I/O操作完成,不管是成功、失败还是取消,future的标记都是已完成的。

失败的时候也有具体的信息,例如失败原因,但是状态一定是已完成的状态。

注意:不要在I/O线程(Handler)内调用future对象的sync或者await方法,不能在channelHandler中调用sync和await。因为netty的底层就是一个异步的操作, 如果调用了会造成堵塞,甚至会死锁。

channelPromise:继承channelFuture,进一步拓展用于设置IO操作的结果。

十二、编码和解码

编解码:java序列化/反序列化

netty自己开发 编解码的原因(java自带序列化的缺点):

  1. 无法跨语言

  2. 序列化以后码流太大,也就是数据包太大

  3. 序列化和反序列化性能比较差

业界里面也有其它的编码框架:google的protobuf(PB)、facebook的Trift、Jboss的Marshaling、kyro等

netty里面的编解码:

  • 解码器:负责处理入站(InboundHandler)的数据

  • 解码器:负责处理出站(OutboundHandler)的数据

netty提供默认的编码器,也可以自定义。

Encoder:编码器

Decoder:解码器

Codec:编解码器,组合编码器和解码器,以此提供于字节和消息都相同的操作。优点:成对出现,编解码都是在一个类完成。 缺点:耦合在一起,拓展性不佳。

十三、Encoder编码器

Encoder对应的就是ChannelOutboundHandler,消息对象转换为字节数组

Netty本身未提供和解码一样的编码器,是因为场景不同,两者非对等的。

  • MessageToByteEncoder:消息转为字节数组,调用write方法,会先判断当前编码器是否支持需要发送的消息类型,如果不支持,则透传。

  • MessageToMessageEncoder用于从一种消息编码到另一种消息(从一个POJO到另外的POJO)

十四、Decoder解码器

Decoder对应就是ChannelInboundHandler,主要就是字节数组转换为消息对象。主要两个方法:

  1. decode:一般用这个

  2. decodeLast:用于最后几个字节的处理也就是channel关闭的时候,产生的最后一个消息

自定义解码器,需要继承以下几个抽象解码器:

  • ByteToMessageDecoder:用于将字节转为消息,需要检查缓冲区是否有足够的字节。

    public class EchoDecode extends ByteToMessageDecoder {
    
        @Override protected void decode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out) throws Exception{
            //字节的大小为4,需要判断字节是否大于等于一个字节数
             if(in.readableBytes() >=4){
                 //添加到解码信息里
                 out.add(in.readInt());
             }
        }
    }
  • ReplayingDecoder:继承ByteToMessageDecoder。不需要检查缓冲区是有足够多的数据,速度略慢于ByteToMessageDecoder。

ByteToMessageDecoder和ReplayingDecoder是字节码解码,它们之间的选择:项目复杂性比较高则使用ReplayingDecoder,否则使用ByteToMessageDecoder

  • MessageToMessageDecoder:MessageToMessageDecoder是字符解码,用于从一种消息解码到另一种消息(从一个POJO到另外的POJO)

netty自带的解码器,用的比较多的解码器(主要是为了解决TCP底层的粘包和拆包问题):

  • DelemiterBasedFrameDecoder:指定消息分隔符的解码器,如指定:xxx、&&&。会对消息进行分割,类似split

  • LineBasedFrameDecoder:以换行符为结束标志的解码器

  • FixedLengthFrameDecoder:固定长度的解码器

  • LengthFieldBasedFrameDecoder:message = header + body,基于长度解码的通用解码器

  • StringDecoder:文本解码器,将接收到的文本和消息转为字符串,一般会与上面的几种进行组合,然后再后面加业务的handler

十五、字节容器ByteBuf

netty的字节容器ByteBuf对比jdk原生的ByteBuffer:

  • JDK原生ByteBuffer:共用读写索引,每次读写操作都需要flip();扩容麻烦,而且扩容后容易造成浪费

  • Netty的ByteBuf:读写使用不同的索引,所以操作便捷;自动扩容,使用便捷等

ByteBuf(传递字节数据的容器)创建方法:

  1. ByteBufAllocator:

netty4.x以后默认使用池化:PooledByteBufAllocator,提升性能并且最大程度减少内存碎片

非池化:UnpooledByteBufAllocator,每次返回新的实例

  1. Unpooled:提供静态方法创建未池化的ByteBuf,可以直接创建堆内存和直接内存缓存区

ByteBuf使用模式:

  1. 堆缓存区HEAP BUFFER:

优点:存储在JVM的堆空间里,可以快速的分配和释放

缺点:每次使用前都会拷贝到直接缓存区(堆外内存)

  1. 直接缓存区DIRECR BUFFER:

优点:存储在堆外内存,堆外内存直接分配,不会占用堆空间

缺点:内存的分配和释放,比在堆缓冲区更复杂

  1. 复合缓冲区COMPOSITE BUFFER:

可以创建多个不同的ByteBuf,然后放在一起,但是只是一个视图

//创建复合缓冲区
CompositeByteBuf compositeByteBuf = Unpooled.compositeBuffer();
//堆缓冲区
ByteBuf heapBuffer = Unpooled.buffer(16);
//堆外缓冲区
ByteBuf directBuffer = Unpooled.directBuffer(16);
//添加缓冲区
compositeByteBuf.addComponents(heapBuffer,directBuffer);

16、linux设置网络最大连接数

linux默认局部文件句柄限制(fd:单个进程最大文件打开数)为:65535

修改局部的fd最大数:ulimit -n查看一个进程的最大打开文件数

修改:vi /etc/security/limits.conf

把 root soft nofile、root hard nofile、soft nofile、hard nofile都设成想要的最大连接数,修改完需要重启

修改全局的最大数(所有进程最大打开文件数):cat /proc/sys/fs/file-max查看命令

修改:vi /etc/sysctl.conf 增加一行:fs.file-max=100000 设置最大数

执行sysctl -p 立即生效