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