Netty随记

本文深入探讨了Netty框架的使用,包括为何不直接使用NIO、高并发场景下的优势、编码器与解码器的执行顺序、ChannelHandler的使用以及EventLoop在Netty中的关键作用。此外,还讲解了Netty的零拷贝技术、 ByteBuf的管理和数据传输方式。文章详细阐述了各种关键组件和方法,为理解Netty提供了全面的指导。
摘要由CSDN通过智能技术生成

Netty随记

为何不直接用NIO

  • 需要自己构建协议
  • 解决TCP的传输问题,如粘包半包
  • nio本身还有bug,如epoll空轮询导致cpu 100%

高并发

Netty由于NIO非阻塞的特性,以为这传统的阻塞IO(BIO)不同,不再需要一个线程对应一个socket连接了,单个线程即可支持很多连接,因此理论是可以实现单机百万级的长连接的,但是linux环境下系统会限制全局最大打开的文件数(linux每个进程,连接都是文件),单个进程最大打开的句柄数也会限制,需要修改linux的系统配置

但是单机支持的连接数多,只是解决了服务器线程的瓶颈,高并发意味着高QPS,通过实验得知,一个线程下的多个子channel同时并发产生IO事件后,该线程还是一个一个去处理这些IO事件,当处理一个请求即执行入站handler的代码过程中,是不会去中间暂停去处理其他子channel产生的IO事件,简单来说,一个线程处理一个请求的过程中不会去暂停然后处理其他请求。这个特性是极其重要的,如果中途处理其他请求,会导致ThreadLocal数据污染的问题,因为很多情况下需要用ThreadLocal来存储数据。

至于如何提高高QPS下的并发能力,那就不是netty的问题了

NOTE

  • 继承ChannelInboundHandlerAdapter,重写了channelRead方法后,如果在重写代码里没有调用super.channelRead…,此事件不会流转到下一个handler里面。这个同样适用于inboundHandler和outboundHandler的其他事件响应回调方法。
  • 引导类(bootstrap)的handler与childHandler 之间是有区别的,childHandler需要连接客户端之后,这个handler的所有事件才会生效,官方说法就是前者所添加的ChannelHandler 由接受子 Channel 的 ServerChannel 处理,而childHandler()方法所添加的 ChannelHandler 将由已被接受的子 Channel处理,其代表一个绑定到远程节点的套接字
  • 服务端接收到请求会创建一个子channel,而绑定本地端端口也会创建一个channel,所以服务端会有两种channel,直观上,每当有客户端连接(connect)服务端,服务端引导程序的childHandler方法都会被重新执行一遍,创建新的子channel对象。所以可以再这里记录连接数
  • 虽然netty4是支持链式调用的方法来引导初始化channel的,如bootstrap.group().channel().handler().childHandler().bind()…但是handler().handler()或childHandler().childHandler()这种写法,生效的只有末端被调用的方法。如果想注入多个handler到pipeline可以用channelInitializer。
  • eventLoopGroup从某种程度来说可以看做一个线程池,设计程序时尽量复用
  • exceptionCaught这个方法在程序发生异常后被调用, 需要注意的是本质上这个也是个入站事件, 如果pipeline上用于处理异常的handler在抛出异常的handler前面,那么这个异常不会被处理,另外这个方法执行后,如果在主程序写了channel.closeFuture().sync(),这串代码会被执行完成,即连接会被立马关闭。在服务端程序里,应避免关闭一个channel,因为一个channel可能有多个客户端连接。

引导配置

引导对象bootstrap用于构建连接,在构建调用链中有个option方法用于设置连接的一些配置,一下是各种配置的含义:

ChannelOption 类型 说明
ALLOCATOR ByteBufAllocator 设置
ALLOW_HALF_CLOSURE Boolean 设置是否允许半关闭的连接
AUTO_CLOSE Boolean 设置是否在写失败时自动关闭
AUTO_READ Boolean 设置是否自动读取数据
CONNECT_TIMEOUT_MILLIS Integer 设置连接超时的毫秒数
DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION Boolean 设置是否在注册时激活
IP_MULTICAST_ADDR InetAddress 设置
IP_MULTICAST_IF NetworkInterface 设置
IP_MULTICAST_LOOP_DISABLED Boolean 设置是否禁用
IP_MULTICAST_TTL Integer 设置
IP_TOS Integer 设置
MAX_MESSAGES_PER_READ Integer 设置每次读取的最大消息数
MAX_MESSAGES_PER_WRITE Integer 设置每次写入的最大消息数
MESSAGE_SIZE_ESTIMATOR MessageSizeEstimator 设置消息大小估计器
RCVBUF_ALLOCATOR RecvByteBufAllocator 设置接收缓冲区分配器
SINGLE_EVENTEXECUTOR_PER_GROUP Boolean 设置是否为每个
SO_BACKLOG Integer 设置服务端套接字的请求队列长度
SO_BROADCAST Boolean 设置是否启用广播模式
SO_KEEPALIVE Boolean 设置是否启用
SO_LINGER Integer 设置延迟关闭的时间
SO_RCVBUF Integer 设置接收缓冲区大小
SO_REUSEADDR Boolean 设置是否允许重用地址
SO_SNDBUF Integer 设置发送缓冲区大小
SO_TIMEOUT Integer 设置套接字超时时间

编码器和解码器

这一部分在netty实战中讲的比较浅,尤其是关于他们的执行顺序,基本没提,下面补充几个:

执行顺序

出站

对于出站事件(ChannelOutboundHandler),执行顺序和pipeline的处理器添加顺序是相反的。

pipeline.addLast(new MyChannelOutboundHandler(1))
.addLast(new MyChannelOutboundHandler(2))
.addLast(new MyChannelOutboundHandler(3));

那么调用channel.writeAndFlush(xxx)时,执行顺序是:
new MyChannelOutboundHandler(3)—>new MyChannelOutboundHandler(2)—>new MyChannelOutboundHandler(1)

入站

入站就是按添加的顺序执行。

编码器
  • 编码器分MessageToMessageEncoder和MessageToByteEncoder这两个抽象类,我们在实现他们应注意,pipeline出站链第一个编码器应该是MessageToByteEncoder的实现,否则服务端的channelRead不会被调用。
pipeline.addLast(a class extends MessageToByteEncoder<...>).addLast(...).addLast(...)...
  • MessageToMessageEncoder的实现,在转换数据后,需要将转换结果添加到一个list里面。接着list的每个元素都会沿着MessageToMessageEncoder后面的调用链走一遍。
pipeline.add(new a class extends MessageToByteEncoder<...>) // 0
.addLast(new MyChannelOutboundHandler(1)) // 1
.addLast(new MyChannelOutboundHandler(2)) // 2
.addLast(new MyChannelOutboundHandler(3)) // 3
.addLast(new a class extends MessageToMessageEncoder<...>); // 4

前文说过出站是倒着来的,所以执行顺序是4,3,2,1。
当我们在MessageToMessageEncoder的实现方法内往list插入2个元素,那么执行链就应该是4,3,2,1,3,2,1,每个3,2,1的执行链开端的数据就是list的一个元素的数据。
注意事无绝对。MessageToByteEncoder接收一个泛型来决定这个编码器处理的数据类型,如果调用链走到编码器时,数据变成了类型A,而编码器的泛型类型是类型B,那么这个编码器不会执行。服务端的channelRead也不会调用,此时的调用链就是4,3,2,3,2

  • MessageToByteEncoder的encode方法,调用ByteBuf参数的write…方法会改变调用链的数据,MessageToMessageEncoder的encode方法,调用list.add方法会改变调用链的数据,channelOutboundHandler的write方法里调用super.write(…)会调用调用链的数据

SimpleChannelInboundHandler 与 ChannelInboundHandler

你可能会想:为什么我们在客户端使用的是 SimpleChannelInboundHandler,而不是在 EchoServerHandler 中所使用的 ChannelInboundHandlerAdapter 呢?
这和两个因素的相互作用有关:业务逻辑如何处理消息以及 Netty 如何管理资源。
在客户端,当 channelRead0()方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler 负责释放指向保存该消息的 ByteBuf 的内存引用。
在 EchoServerHandler 中,你仍然需要将传入消息回送给发送者,而 write()操作是异步的,直到 channelRead()方法返回后可能仍然没有完成。
为此,EchoServerHandler扩展了 ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息。
消息在 EchoServerHandler 的 cha

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值