netty记录
Netty介绍
最流行的NIO框架之一,其他还有Mina等
Netty基于事件驱动,当Channel进行I/O操作会产生对应的I/O事件,然后驱动事件在ChannelPipeline中传播,
由对应的ChannelHandler对事件进行拦截与处理。
Netty提供异步的、事件驱动的网络应用程序框架和工具
NIO的特点:事件驱动模型、单线程处理多任务、非阻塞I/O,I/O读写不再阻塞,而是返回0、
基于block的传输比基于流的传输更高效、更高级的IO函数zero-copy、IO多路复用大大提高了Java网络应用的可伸缩性和实用性。
基于Reactor线程模型
在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生,事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。如在Reactor中实现读:注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
(1)select==>时间复杂度O(n)
它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
(2)poll==>时间复杂度O(n)
poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
(3)epoll==>时间复杂度O(1)
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epoll是Linux所特有,而select则应该是POSIX所规定,一般操作系统均有实现
select:内核需要将消息传递到用户空间,都需要内核拷贝动作
poll:同上
epoll:epoll通过内核和用户空间共享一块内存来实现的。
首先要通过epoll理解buffer与channal!这两者是不同的,channal从物理上将是位于机器的内核空间,处于一个接收网络数据、本地文件数据等一系列前置底层的数据操作!
而buffer则不然,对于java来说,就是为了将这些数据进行结构化,同时在物理上讲这是一种位于用户空间,也就是java进程中为了与对应java基本数据类型等的映射!
Selector BUG出现的原因
若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%,
Netty的解决办法
- 对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,
- 若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。
- 重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭。
总结
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。
1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。
Netty原理
Server端
一、根据netty权威指南,ServerBootstrap是socket服务端的启动辅助类,用途是封装启动参数;
二、使用绑定Reactor线程池,处理多路复用的管道channal (netty书上说使用EventLoopGroup,但是我没有找到对应类的源码,可能读的不够深吧),也就是连接;
三、使用ServerSocketChannel嫁接数据 =》处理活跃连接的管道上,处理活跃连接;
四、ChannalPipeline这一步应该并不归于建立连接!用于处理请求数据的,比如验证信息、编码解码、心跳检测、流量控制等;
五、绑定监听端口,ServerSocketChannal注册到Selector(多路复用器,也就是为了处理epoll中的epoll_wait活跃事件)
六、轮询Selector,找出对应的Channal;
最终建立连接会有一个listen方法传入文件描述符、backlog,具体的实现类方法可以看DualStackPlainSocketImpl
ServerBootstrap(EventLoopGroup group,EventLoopGroup childGroup)
NIO线程组
EventLoopGroup bossGroup 创建mainReactor 接收selectors
EventLoopGroup workerGroup 创建工作线程组 处理selectors
Client端
终于到了client端了!关于这块暂时不具体考虑netty的实现,而且不再按照书上说的代码执行顺序讲,具体的后续补发!
一、建立双工连接,可以使用NioSocketChannal、SocketChannal;
二、根据tcp的三次握手报文段,确认SelectionKey的枚举类型;
如果进入ChannalActive阶段,也就是epoll中epoll_waite活跃的连接,则设置网络操作位为SelectionKey.OP_READ阶段,否则连接到selector(多路复用器)阶段;
Netty的Channel
Channel的IO类型主要有两种:非阻塞IO(NIO)以及阻塞IO(OIO);数据传输类型有两种:按事件消息传递(Message)以及按字节传递(Byte);适用方类型也有两种:服务器(ServerSocket)以及客户端(Socket)。还有一些根据传输协议而制定的的Channel,如:UDT、SCTP等。
Netty的Channel Pipeline
Pipeline则是Channel里面非常重要的概念。从数据结构的角度,它是一个双向链表,每个节点均是DefaultChannelHandlerContext对象;从逻辑的角度,它则是netty的逻辑处理链,每个节点均包含一个逻辑处理器(ChannelHandler),用以实现网络通信的编/解码、处理等功能。
Pipeline的链表上有两种handler,Inbound Handler和Outbound handler。从Netty内部IO线程接读到IO数据,依次经过N个Handler到达最内部的逻辑处理单元,这种称之为Inbound Handler;从Channel发出IO请求,依次经过M个Handler到达Netty内部IO线程,这种称之为Outbound Handler。内部代码实现流程则是:Head -> Tail (Inbound),Tail -> Head (Outbound)。下图截取自ChannelPipeline的注释中,简单明了:
Netty使用
<dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>5.0.0.Alpha1</version> </dependency>
public class TimeServer { public void bind(int port) throws Exception { // NIO线程组 // 创建mainReactor EventLoopGroup bossGroup = new NioEventLoopGroup(); // 创建工作线程组 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); // 组装NioEventLoopGroup b.group(bossGroup, workerGroup) // 设置channel类型为NIO类型 .channel(NioServerSocketChannel.class) // 设置连接配置参数 .option(ChannelOption.SO_BACKLOG, 1024) .childOption(ChannelOption.SO_KEEPALIVE, true) .childOption(ChannelOption.TCP_NODELAY, true) // 配置入站、出站事件handler .childHandler(newChannelInitializer<NioSocketChannel>() { @Override protected void initChannel(NioSocketChannel ch) { // 配置入站、出站事件channel ch.pipeline().addLast(...); ch.pipeline().addLast(...); } } .childHandler(new ChildChannelHandler()); // 绑定端口,同步等待成功 ChannelFuture f = b.bind(port).sync(); // 等待服务端监听端口关闭 f.channel().closeFuture().sync(); } finally { // 优雅退出,释放线程池资源 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } private class ChildChannelHandler extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel arg0) throws Exception { arg0.pipeline().addLast(new TimeServerHandler()); } } }
public class TimeServerHandler extends ChannelHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf)msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("The time server receive order:" + body); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER"; ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); ctx.write(resp); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } }
Client
public class TimeClient { public void connect(int port, String host) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeClientHandler()); }; }); // 发起异步连接操作 ChannelFuture f = b.connect(host, port).sync(); // 等待客户端连接关闭 f.channel().closeFuture().sync(); } finally { // 优雅退出,释放NIO线程组 group.shutdownGracefully(); } } } public class TimeClientHandler extends ChannelHandlerAdapter { private static final Logger LOGGER = LoggerFactory.getLogger(TimeClientHandler.class); private final ByteBuf firstMessage; public TimeClientHandler() { byte[] req = "QUERY TIME ORDER".getBytes(); firstMessage = Unpooled.buffer(req.length); firstMessage.writeBytes(req); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(firstMessage); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf)msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("Now is:" + body); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { LOGGER.warn("Unexcepted exception from downstream:" + cause.getMessage()); ctx.close(); } }
Netty VS NIO
NIO的类库和API繁杂,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer。
必须对多线程和网络编程非常熟悉才能写出高质量的NIO程序
JDK NIO的BUG,例如著名的epoll bug,该问题会导致Selector空轮训,最终导致CPU 100%。
BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理
NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
BIO是面向流的,NIO是面向缓冲区的;
BIO的各种流是阻塞的。而NIO是非阻塞的;
BIO的Stream是单向的,而NIO的channel是双向的。
Netty优点:
API使用简单、开发门槛低、功能强大、性能高、成熟、稳定,Netty修复了已经发现的所有JDK NIO的BUG
Netty运用了reactor模式,采用了监听线程池和IO线程池分离的思想,数据的流转在Netty中采取了类似职责链的设计模式,因此数据看起来就像在管道中流动一样了。
NIO的服务端建立过程
Selector.open():打开一个Selector;ServerSocketChannel.open():创建服务端的Channel;bind():绑定到某个端口上。并配置非阻塞模式;register():注册Channel和关注的事件到Selector上;select()轮询拿到已经就绪的事件
第一步,绑定一个服务的端口
第二步,打开通道管理器Selector并在Selector上注册一个事件
第三步,轮循访问Selector,当注册的事件到达时,方法返回
public void listen() throws IOException {
// 轮询访问selector
while (true) {
// 当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<?> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey) ite.next();
//删除已选的key,以防重复处理
ite.remove();
//这里可以写我们自己的处理逻辑
handle(key);
}
}
}
Netty通信
有自己的消息协议,会encode和decode
使用netty原因:
Netty的编程API使用简单,开发门槛低,无需编程者去关注和了解太多的NIO编程模型和概念;
对于编程者来说,可根据业务的要求进行定制化地开发,通过Netty的ChannelHandler对通信框架进行灵活的定制化扩展;
Netty框架本身支持拆包/解包,异常检测等机制,让编程者可以从JAVA NIO的繁琐细节中解脱,而只需要关注业务处理逻辑;
Netty解决了(准确地说应该是采用了另一种方式完美规避了)JDK NIO的Bug(Epoll bug,会导致Selector空轮询,最终导致CPU 100%);
Netty框架内部对线程,selector做了一些细节的优化,精心设计的reactor多线程模型,可以实现非常高效地并发处理;
Netty已经在多个开源项目(Hadoop的RPC框架avro使用Netty作为通信框架)中都得到了充分验证,健壮性/可靠性比较好。
序列化
protobuf 位运算 占位少 所以比原生序列号小
Tcp/Ip的头部结构
16位端口号:标示该段报文来自哪里(源端口)以及要传给哪个上层协议或应用程序(目的端口)。进行tcp通信时,一般client是通过系统自动选择的临时端口号,而服务器一般是使用知名服务端口号或者自己指定的端口号。
32位序号:表示一次tcp通信过程(从建立连接到断开)过程中某一次传输方向上的字节流的每个字节的编号。假定主机A和B进行tcp通信,A传送给B一个tcp报文段中,序号值被系统初始化为某一个随机值ISN,那么在该传输方向上(从A到B),后续的所有tcp报文断中的序号值都会被设定为ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是ISN+1025。
32位确认号:用作对另一方发送的tcp报文段的响应。其值是收到对方的tcp报文段的序号值+1。假定主机A和B进行tcp通信,那么A发出的tcp报文段不但带有自己的序号,也包含了对B发送来的tcp报文段的确认号。反之也一样。
4位头部长度:表示tcp头部有多少个32bit字(4字节),因为4位最大值是15,所以最多有15个32bit,也就是60个字节是最大的tcp头部长度。
6位标志位:
URG:紧急指针是否有效
ACK:表示确认好是否有效,携带ack标志的报文段也称确认报文段
PSH:提示接收端应用程序应该立即从tcp接受缓冲区中读走数据,为后续接收的数据让出空间
RST:表示要求对方重建连接。带RST标志的tcp报文段也叫复位报文段
SYN:表示建立一个连接,携带SYN的tcp报文段为同步报文段
FIN标志:表示告知对方本端要关闭连接了。
16为窗口大小:是TCP流量控制的一个手段,这里说的窗口是指接收通告窗口,它告诉对方本端的tcp接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。
16为校验和:由发送端填充,接收端对tcp报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意这个校验不仅包括tcp头部,也包括数据部分。这也是tcp可靠传输的一个重要保障。
16位紧急指针:是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此这个字段是紧急指针相对当前序号的偏移量。不妨称之为紧急便宜,发送紧急数据时会用到这个。
TCP头部选项:最后一个选项字段是可变长的可选信息,最多包含40字节的数据。典型的tcp头部选项结构:
我觉得挺容易理解的,作为通信双方必须知道相互的地址,所以要有源地址与目标地址的标记;序列号与确认序列号,这个涉及到数据包分片,比如mtu导致了数据分片,如何分片与如何重组分片;偏移数据表示tcp头部结构能够占用的最大数据长度;保留位大概就是保留用的吧;标记位,6个就是我们常说的同步报文段、确认报文段的标记;窗口大小表示一次传输数据的大小,由于是16位,按照二进制计算也就是65535字节;校验和据说是为了教研数据有效性的,我通过抓tcp的包看到类似这样的
TCP首部的主要选项:
最大报文段长度MSS(Maximum Segment Size)是TCP报文段中的数据字段的最大长度。
MSS告诉对方TCP:“我的缓存所能接收的报文段的数据字段的最大长度是MSS个字节。”
窗口扩大因子,用于长肥管道。
TCP的流量控制
TCP采用大小可变的滑动窗口进行流量控制。窗口大小的单位是字节。
TCP报文段首部的窗口字段写入的数值就是当前给对方设置的发送窗口数值的上限。
发送窗口在连接建立时由双方商定。但在通信的过程中,接收端可根据自己的资源情况,随时动态地调整对方的发送窗口上限值(可增大或减小)。
发送端要发送900字节长的数据,划分为9个100字节长的报文段,而发送窗口确定为500字节。
发送端只要收到了对方的确认,发送窗口就可前移。
发送TCP要维护一个指针。每发送一个报文段,指针就向前移动一个报文段的距离。
全双工连接
对于java来说,原生的java nio,其存在固有的复杂性与bug,难以令人满意!而netty则将用户边界做了封装,降低用户的开发难度,实际上我对于netty这本书的讲解流程并不满意,因为它是按照代码的执行顺序讲解的,实际上并不符合人的思维逻辑;接下来我将会先从书中将的内容顺序梳理,然后再通过思维流程进行梳理一遍。
Netty5
ServerBootstrap serverbootStrap = new ServerBootstrap(); EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup();
面试问题
1.BIO、NIO和AIO的区别?
BIO:一个连接一个线程,客户端有连接请求时服务器端就需要启动一个线程进行处理。线程开销大。
伪异步IO:将请求连接放入线程池,一对多,但线程还是很宝贵的资源。
NIO:一个请求一个线程,但客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO:一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
BIO是面向流的,NIO是面向缓冲区的;BIO的各种流是阻塞的。而NIO是非阻塞的;BIO的Stream是单向的,而NIO的channel是双向的。
NIO的特点:事件驱动模型、单线程处理多任务、非阻塞I/O,I/O读写不再阻塞,而是返回0、基于block的传输比基于流的传输更高效、更高级的IO函数zero-copy、IO多路复用大大提高了Java网络应用的可伸缩性和实用性。基于Reactor线程模型。
在Reactor模式中,事件分发器等待某个事件或者可应用或个操作的状态发生,事件分发器就把这个事件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。如在Reactor中实现读:注册读就绪事件和相应的事件处理器、事件分发器等待事件、事件到来,激活分发器,分发器调用事件对应的处理器、事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返还控制权。
2.NIO的组成?
Buffer:与Channel进行交互,数据是从Channel读入缓冲区,从缓冲区写入Channel中的
DirectByteBuffer可减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高,不可控,通常会用内存池来提高性能。直接缓冲区主要分配给那些易受基础系统的本机I/O 操作影响的大型、持久的缓冲区。如果数据量比较小的中小应用情况下,可以考虑使用heapBuffer,由JVM进行管理。
Channel:表示 IO 源与目标打开的连接,是双向的,但不能直接访问数据,只能与Buffer 进行交互。
Selector可使一个单独的线程管理多个Channel,open方法可创建Selector,register方法向多路复用器器注册通道,可以监听的事件类型:读、写、连接、accept。
Selector在Linux的实现类是EPollSelectorImpl,委托给EPollArrayWrapper实现,其中三个native方法是对epoll的封装,而EPollSelectorImpl. implRegister方法,通过调用epoll_ctl向epoll实例中注册事件,还将注册的文件描述符(fd)与SelectionKey的对应关系添加到fdToKey中,这个map维护了文件描述符与SelectionKey的映射。
fdToKey有时会变得非常大,因为注册到Selector上的Channel非常多(百万连接);过期或失效的Channel没有及时关闭。fdToKey总是串行读取的,而读取是在select方法中进行的,该方法是非线程安全的。
Pipe:两个线程之间的单向数据连接,数据会被写到sink通道,从source通道读取
NIO的服务端建立过程:Selector.open():打开一个Selector;ServerSocketChannel.open():创建服务端的Channel;bind():绑定到某个端口上。并配置非阻塞模式;register():注册Channel和关注的事件到Selector上;select()轮询拿到已经就绪的事件
3.Netty的特点?
一个高性能、异步事件驱动的NIO框架,它提供了对TCP、UDP和文件传输的支持
使用更高效的socket底层,对epoll空轮询引起的cpu占用飙升在内部进行了处理,避免了直接使用NIO的陷阱,简化了NIO的处理方式。
采用多种decoder/encoder 支持,对TCP粘包/分包进行自动化处理
可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持
可配置IO线程数、TCP参数, TCP接收和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf
通过引用计数器及时申请释放不再引用的对象,降低了GC频率
使用单线程串行化的方式,高效的Reactor线程模型
大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用
4.Netty的线程模型?
Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理。