Netty学习笔记

Linux网络I/O模型

Linux内核将所有外部设备看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)

UNIX网络编程5中I/O模型

阻塞:应用进程I/O操作占用CPU资源,即为阻塞

  • 阻塞I/O:进程空间中调用recvfrom,其系统调用直到数据包到达切被复制到应用进程的缓冲区或发生错误时才返回,期间一直占用CPU
  • 非阻塞I/O:recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般会对非阻塞I/O轮训检查这个状态,看内核是不是有数据到来,此进程只操作一个socketfd,数据从内核缓冲到应用缓冲区时,应用进程仍然阻塞
  • I/O复用模型:Linux提供一种select/poll,进程将一个或多个fd传递给select或poll调用,阻塞在select操作上,帮我们侦测fd是否处于就绪状态。select/poll是顺序扫描fd,并且有fd数量限制(准确的说是select有数量限制,poll没有)。因此使用存在一些制约。Linux还提供了一种epoll系统调用,epoll将准备就绪的流操作通知应用进程,由应用进程处理读写操作,此时读写仍是阻塞的。
  • 信号驱动I/O模型:首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数,此时系统调用立即返回(调用非阻塞),应用进程继续工作,当数据准备就绪时就为该进程生成一个SIGIO信号,通过信号回调应用进程recvfrom来读取数据,此时读写仍为阻塞的
  • 异步I/O:告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。

总结:各个模型对于应用进程而言会存在不同的阻塞点,有时候我们很容易被混淆,但对于网络编程中,阻塞即为进程占用CPU资源,无论CPU是阻塞响应还是阻塞COPY数据

Java中的NIO和AIO

Java的NIO有两种定义,New IO 和 Non-Block IO,对于JDK而言,是在1.4推出的新的IO模型,对于大多数的应用开发人员,更愿意基于原理来定义,即非阻塞IO。Java的nio包下定义了三类角色来支撑nio的体系结构,Channel:一个已打开的链接,并且具有IO能力的实体对象。ByteBuffer:NIO中的数据存储容器。Selector:多路选择器,用于SelectableChannel的多路数据处理。NIO的使用也有两种,一种是基于循环服务端Channel(例如SocketServerChannel)的accept()方法,进行IO操作,一种是基于Selector选择器,循环select()方法,返回多个准备就绪的SelectionKeys,通过SelectionKey获取对应的Channel,来进行数据的读写。

Java的AIO只有一种定义,就是异步非阻塞IO,异步的一定是非阻塞的,因为只有在非阻塞的前提下才能实现异步。Jdk1.8的API中对于异步IO的AsynchronousServerSocketChannel 类有这样的一段描述 An asynchronous channel for stream-oriented listening sockets.
在程序的使用上立竿见影的结果就是accept() 方法返回的Future中的AsynchronousChannel异步对数据进行读写操作。可以理解为当前应用线程的业务与IO的操作逻辑上独立,多核CPU上可能绝对独立。

总结:关于Java的BIO和AIO的具体使用可以参考其他blog中更优秀的Demo,这里不给出具体代码实现主要是两个原因:1、旨在简化理解其主要使用流程,可以方便映射到UNIX网络模型中。2、不建议将JDK原生的API直接使用在业务场景中,其开发的代价是很高的,下面会给出具体原因。

选择Netty的原因

Netty权威指南中给出如下总结:

  1. NIO的类库和API繁杂,使用麻烦
  2. NIO编程涉及到Reactor模式,需要对多线程网络编程非常熟悉,才能编写出高质量的NIO程序
  3. 可靠性的能力补齐,工作量和难度非常大。例如客户端的断链、重链、网络闪断
  4. JDK NIO的bug。例如臭名昭著epoll 的bug 导致 selector空轮训,进而导致CPU使用率达到100%。bug仍存在与jdk1.7的版本中

Netty的核心组件

Channel、EventLoop、ChannelFuture

Channel

Channel 是 Java NIO 的一个基本构造。

	它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,
如读操作和写操作。

目前,可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以
被打开或者被关闭,连接或者断开连接。

Netty基于Channel提供了与其绑定的ChannelPipelineHandler,ChannelPipeline可以理解为对于Channel的入站和出站数据进行的一系列操作。ChannelPipelineHandler中存在多个ChannelHandler,每个ChannelHandler持有一个ChannelHandlerContext,并且可以通过ChannelHandlerContext调用下一个ChannelHandler。至于每个组建更多的细节可以参考Netty API,这里主要目的是最简单直接的理解Netty组件的关系。

一个Channel只能绑定一个Pipeline
一个ChannelHandler可以绑定一个或多个Pipeline
一个ChannelHandler绑定一个ChannelHandlerContext,而且是唯一不变的
在这里插入图片描述

  • Netty中被@Shareable注解的ChannelHandler可以绑定多个ChannelPipeline,所以在使用时,应确保@Shareable注解的ChannelHandler无状态
  • ChannelHandlerContext 和 ChannelHandler 之间的关联(绑定)是永远不会改变的,所以缓存对它的引用是安全的;
  • 相对于其他类的同名方法,ChannelHandlerContext的方法将产生更短的事件流,应该尽可能地利用这个特性来获得最大的性能。

EventLoop

EventLoop 定义了 Netty 的核心抽象,用于处理连接的生命周期中所发生的事件。下图在高层次上说明了 Channel、EventLoop、Thread 以及 EventLoopGroup 之间的关系。

在这里插入图片描述
Netty4中的IO和事件处理不再像Netty3那样,交给调用线程来协同处理IO操作,这样避免了线程的上下文切换。EventLoop至始至终只有一个Thread,并且EventLoop中的所有Channel的IO操作都在这个线程中,没有线程池的线程切换的开销。在多线程处理IO操作时,EventLoop会判断当前线程是否为其绑定的线程(inEventLoop(Thread)),如果不是,将其加入调度队列中,如果是,则执行其IO及事件处理。

一个 EventLoopGroup 包含一个或者多个 EventLoop
一个 EventLoop 在它的生命周期内只和一个 Thread 绑定
所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理
一个 Channel 在它的生命周期内只注册于一个 EventLoop
一个 EventLoop 可能会被分配给一个或多个 Channel。

ChannelFuture

Netty 中所有的 I/O 操作都是异步的。因为一个操作可能不会立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。为此,Netty 提供了ChannelFuture 接口,其 addListener()方法注册了一个 ChannelFutureListener,以便在某个操作完成时(无论是否成功)得到通知。

Netty中的ChannelFuture有两种状态:uncompleted和completed,当开始一个I/O操作时,一个新的ChannelFuture被创建,此时处于uncompleted——非失败、非成功、非取消,一旦I/O操作完成,ChannelFuture将被置于completed。状态迁移如下图
在这里插入图片描述
注意: 不要在ChannelHandler中调用ChannelFuture的wait()方法,因为在I/O线程和调用线程是同一个线程时,会导致,永远无法收到ChannelFuture的回调。所以,使用Listener回调的方式是最好的方式,除非有调用超时限制。调用超时还需要大于I/O超时。

关于Future、Callback、Promise:首先JDK中的Future的实现FutureTask是在独立于调用线程的另外的线程中,调用线程需要阻塞获取Future的返回信息。而JDK8提供的CompletableFuture可以异步回调。Netty的ChannelFuture的实现类似于CompletableFuture,同时Netty通过调用 ChannelPromise 上的 setSuccess()和 setFailure()方法,可以使一个操作的状态在 ChannelHandler 的方法返回给其调用者时便即刻被感知到

ChannelHandler 和 ChannelPipeline

正如上面对于Channel的描述中,已经描述了ChannelPipeline和ChannelHandler的整体关系,接下来主要了解一下ChannelHandler主要有哪些子类型。

ChannelInboundHandler和ChannelOutboundHandler

  • ChannelInboundHandler是入站的ChannelHandler,这种类型的 ChannelHandler 接收入站事件和数据,这些数据随后将会被你的应用程序的业务逻辑所处理。当你要给连接的客户端发送响应时,也可以从 ChannelInboundHandler 冲刷数据。你的应用程序的业务逻辑通常驻留在一个或者多个 ChannelInboundHandler 中。
  • ChannelOutboundHandler 的一个强大的功能是可以按需推迟操作或者事件,这使得可以通过一些复杂的方法来处理请求。例如,如果到远程节点的写入被暂停了,那么你可以推迟冲刷操作并在稍后继续。

编码器和解码器

当你通过 Netty 发送或者接收一个消息的时候,就将会发生一次数据转换。入站消息会被解码;也就是说,从字节转换为另一种格式,通常是一个 Java 对象。如果是出站消息,则会发生相反方向的转换:它将从它的当前格式被编码为字节。这两种方向的转换的原因很简单:网络数据总是一系列的字节。
对应于特定的需要,Netty为编码器和解码器提供了不同类型的抽象类。例如,你的应用程 序可能使用了一种中间格式,而不需要立即将消息转换成字节。你将仍然需要一个编码器,但是 它将派生自一个不同的超类。为了确定合适的编码器类型,你可以应用一个简单的命名约定。
通常来说,这些基类的名称将类似于 ByteToMessageDecoder 或 MessageToByte- Encoder。对于特殊的类型,你可能会发现类似于 ProtobufEncoder 和 ProtobufDecoder 这样的名称——预置的用来支持 Google 的 Protocol Buffers。
严格地说,其他的处理器也可以完成编码器和解码器的功能。但是,正如有用来简化 ChannelHandler 的创建的适配器类一样,所有由 Netty 提供的编码器/解码器适配器类都实现 了 ChannelOutboundHandler 或者 ChannelInboundHandler 接口。

总结

  1. 对于阻塞、异步、回调、网络I/O模型需要更多的理解
  2. 对于Netty的实战及Demo还是参考Netty in action及Netty权威指南,其中会给出更好的实现
  3. 对于Netty的使用经验总结,将在后续的整理中,慢慢积累

欢迎各位指正错误的理解~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值