netty

netty

异步的,基于事件驱动的网络通信框架,本质是一个NIO框架,简化了NIO的使用。

netty
NIO
JDK原生
tcp/ip

javaIO 模型

bio 连接数较少且比较固定
nio 连接数多,且时间短 聊天服务器
aio 连接数多 且时间长。

io

IO分两阶段(一旦拿到数据后就变成了数据操作,不再是IO):
1.数据准备阶段
2.内核空间复制数据到用户进程缓冲区(用户空间)阶段

在操作系统中,程序运行的空间分为内核空间和用户空间。
应用程序都是运行在用户空间的,所以它们能操作的数据也都在用户空间。

阻塞IO和非阻塞IO的区别在于第一步发起IO请求是否会被阻塞:
如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。

一般来讲:
阻塞IO模型、非阻塞IO模型、IO复用模型(select/poll/epoll)、信号驱动IO模型都属于同步IO,因为阶段2是阻塞的(尽管时间很短)。

同步IO和异步IO的区别就在于第二个步骤是否阻塞:
如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO
五种IO模型

JAVA NIO 不是同步非阻塞I/O吗,为什么说JAVA NIO提供了基于Selector的异步网络I/O:

java nio的io模型是同步非阻塞,这里的同步异步指的是真正io操作(数据内核态用户态的拷贝)是否需要进程参与。而说java nio提供了异步处理,这个异步应该是指编程模型上的异步。基于reactor模式的事件驱动,事件处理器的注册和处理器的执行是异步的

select,poll,epoll之间的区别

(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所规定,一般操作系统均有实现

nio

nio三大组件:

  • selector
  • channel
  • buffer
  • 本质上是一个可以读写数据的内存块。
    除了boolean都有
  • 在这里插入图片描述

四个属性:
mark
position
limit
capacity
mark<position<limit<capacity
在这里插入图片描述
常用方法:
在这里插入图片描述

零拷贝

以linux 为例,有两种 零拷贝实现方式mmap和sendfile

  • 传统IO
    传统IO
    普通的拷贝过程经历了四次内核态和用户态的切换(上下文切换),四次拷贝。两次CPU从内存中进行数据的读写过程,这种拷贝过程相对来说比较消耗系统资源。

  • mmap
    内存映射方式
    可以看到这种内存映射的方式减少了CPU的读写次数,但是用户态到内核态的切换(上下文切换)依旧有四次,三次拷贝。这种方式可以让应用程序对数据进行相应的读写操作。

  • sendfile
    sendfile

依旧有一次CPU进行数据拷贝,两次用户态和内核态的切换操作,相比较于内存映射的方式有了很大的进步,但问题是程序不能对数据进行修改,而只是单纯地进行了一次数据的传输过程。

  • 升级版sendfile
    升级版sendfile
    这是真正意义上的零拷贝,因为其间CPU已经不参与数据的拷贝过程,当然这样的过程需要硬件的支持才能实现。
  • java中的零拷贝

Java中的零拷贝也是通过操作系统的系统调用来实现的。

MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());

NIO中的FileChannel.map()方法其实就是采用了操作系统中的内存映射方式

fileChannel.transferTo(0, fileChannel.size(), socketChannel);

transferTo()的实现方式就是通过系统调用sendfile()(当然这是Linux中的系统调用,Windows中系统调用有所不同),同理transferFrom()也是这种实现方式。

NIO中的Buffer都在用户空间中,包括DirectBuffer,也是C语言malloc出来的一块内存。

MappedByteBuffer 调用的是操作系统的 mmap, 那 DirectByteBuffer呢?
也是map()系统调用,DirectByteBuffer是MappedByteBuffer的子类,实现了DirectBuffer。优化了MappedByteBuffer只能依靠full gc才能回收内存的短板,自身提供了clean()方法,可主动回收内存。

netty任务队列

1.普通任务
普通任务课已通过绑定的channel,向所绑定的NioEventLoop中的taskqueue中添加任务,自己实现Runnable接口。
这里注意所有的任务都是在一个队列中,处于一个线程,所以是顺序执行,是阻塞的。
2.定时任务
定时任务也可已通过channel向ScheduleQueue里添加,是阻塞的
3.用户自定义的非IO任务
可以再绑定Handler是拿到channel向taskQueue和ScheduleQueue中添加

netty异步模型

1.Netty中的 I/O 操作是异步的, 包括 Bind、Write、Connect 等操作会简单的返回一个ChannelFuture。
调用者不能立刻获得结果, 而是通过Future-Listener 机制, 用户可以方便的主动获取或者通过通知机制获得IO操作结果。
2.Netty的异步模型是建立在future和callback之上的。callback就是回调。
3.Future的核心思想是: 假设一个方法func(), 其计算过程可能很耗时, 等待func()返回不合适。那么就可以在调用func()的时候, 立马返回一个Future, 后续可以通过Future去监控方法func()的处理过程(即: Future-Listener机制)
在这里插入图片描述

netty核心组件

Bootstrap和ServerBootStrap

在这里插入图片描述

Future,ChannelFuture

在这里插入图片描述

Channel

在这里插入图片描述

Selector

在这里插入图片描述

ChannelHandler及其实现类

在这里插入图片描述

Pipeline和ChannelPipeline

pipeline为责任链设计模式
在这里插入图片描述
在这里插入图片描述

EeventLoopGroup和其实现类NioEventLoopGroup

在这里插入图片描述

Bytebuf

和Nio中的ByteBuffer类似
Bytebuf提供接口方法要比ByteBuffer简洁并强大
Bytebuf分为pool、unpool、heap、Direct
在这里插入图片描述

  • 堆缓冲区(HeapByte)
    是将数据存储在JVM堆空间中,特点是在内存的分配和回收速度快,可以被JVM自动回收,缺点是如果进行socket的I/O读写,需要额外一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度下降。

    • 直接内存(DirectByte)
      非堆内存,在堆外进行内存分配,相比于堆内存,分配和回收速度会慢一些,但将它写入或从socket channel中读取时,由于少一次内存复制,速度比堆内存快。

      通常在I/OI/O通信线程的读写缓冲区使用DirectByteBuf,后端业务消息的编解码模块使用HeapByteBuf,可以达到很好的性能效果。

      从内存回收角度看,ByteBuf分为两类:基于pooled的和普通的ByteBuf。两者的区别是基于pooled的ByteBuf可以重复利用,自己维护一个内存池,可以循环利用创建的ByteBuf,提升内存使用效率,降低由于高负载导致的频繁GC。

在实践中如果业务应用是I/O密集型,使用DirectByteBuf实现。而针对于运算密集型,比如tomcat等推荐使用heapByteBuf。

心跳机制

粘包拆包

  • 情况1:服务器分二次读取到了二个独立的数据包,分别是D1和D2,并没有粘包和拆包。

  • 情况2:服务器一次接收到二个数据包,D1和D2粘合在一起,被成为TCP粘包。

  • 情况3:服务器分二次读取到了二个数据包,第一次读取到了完整的D1包和D2包的一部分,第二次读取到了D2包的剩余部分,这被成为TCP拆包。

  • 情况4:服务器分二次读取到了二个数据包,第一次读取到了D1包的部分内容 ,第二次读取到了D1包剩余部分和完整的D2包,这被成为TCP拆包。

  • 情况5:如果服务器的TCP接收滑窗非常小,而数据包D1和D2比较大,很可能发生服务器多次才能将D1和D2接收完成,期间发生多次拆包。

  • 解决思路:
    由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决。业界的主流协议的解决方案,可以归纳如下:

  1. 消息定长,报文大小固定长度,例如每个报文的长度固定为200字节,如果不够空位补空格;
  2. 包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分;
  3. 将消息分为消息头和消息体,消息头中包含表示信息的总长度(或者消息体长度)的字段;
  4. 更复杂的自定义应用层协议
  • netty中的解决方案:
    LineBasedFrameDecoder (换行)
    DelimiterBasedFrameDecoder(添加特殊分隔符报文来分包)
    FixedLengthFrameDecoder(使用定长的报文来分包)
    自定义协议(长度+消息体)

websocket

inbound,outbound顺序

netty的Pipeline模型用的是责任链设计模式,当boss线程监控到绑定端口上有accept事件,此时会为该socket连接实例化Pipeline,并将InboundHandler和OutboundHandler按序加载到Pipeline中,然后将该socket连接(也就是Channel对象)挂载到selector上。一个selector对应一个线程,该线程会轮询所有挂载在他身上的socket连接有没有read或write事件,然后通过线程池去执行Pipeline的业务流。selector如何查询哪些socket连接有read或write事件,主要取决于调用操作系统的哪种IO多路复用内核,如果是select(注意,此处的select是指操作系统内核的select IO多路复用,不是netty的seletor对象),那么将会遍历所有socket连接,依次询问是否有read或write事件,最终操作系统内核将所有IO事件的socket连接返回给netty进程,当有很多socket连接时,这种方式将会大大降低性能,因为存在大量socket连接的遍历和内核内存的拷贝。如果是epoll,性能将会大幅提升,因为他基于完成端口事件,已经维护好有IO事件的socket连接列表,selector直接取走,无需遍历,也少掉内核内存拷贝带来的性能损耗。

Pipeline的责任链是通过ChannelHandlerContext对象串联的,ChannelHandlerContext对象里封装了ChannelHandler对象,通过prev和next节点实现双向链表。Pipeline的首尾节点分别是head和tail,当selector轮询到socket有read事件时,将会触发Pipeline责任链,从head开始调起第一个InboundHandler的ChannelRead事件,接着通过fire方法依次触发Pipeline上的下一个ChannelHandler,如下图:
在这里插入图片描述

inbound 的顺序是跟 add 顺序一致的, 而 outbound 的顺序是跟 add 顺序相反的

以及, read 的 IO 触发顺序是 “socketChannel.read() -> 顺序 handler -> TailContext.channelRead().releaseMsg”

而 write 的 IO 触发顺序是 “逆序 handler -> HeadContext.socketChannel.write()”

也就是说 read 是先触发 socket 的 read IO 事件, 再进入 handler, 而如果我们最后一个 handler 未能完全处理消息, 调用了 super.channelRead, 则会进入 TailContext. 此时TailContext 会打出 debug 消息告诉你消息进入了最后一个 Handler 而未被处理. 因为一般来讲都应该在自己的 handler 里把消息处理掉. 而不是让他进入到默认 handler 里.

而对于 write 来说, 则是先进入自定义 handler, 最后在进入 HeadContext 触发 IO 事件。

在这里插入图片描述

  • read
    HeadContext中的channelRead方法只做了一个事情,那就是传递给下一个handler
ctx.fireChannelRead(msg);

1.HttpServerCodec是一个特殊的handler,既实现了ChannleInboundHandler,又实现了ChannelOutboundHandler,其里面包含了两个handler变量,分别是HttpServerRequestDecoder类型和HttpServerResponseEncoder类型,负责Http解码和Http编码的工作。所以当有消息到了HttpServerCodec时,实际是HttpServerRequestDecoder在处理。

2.HttpServerRequestDecoder继承ByteToMessageDecoder。ByteToMessageDecoder是用来将Byte转化为message对象,其channelRead方法主要做了三个事情:
(1)收集数据
(2)调用重写的decode方法
(3)将数据传递给下一个handler
回到HttpServerRequestDecoder的encode方法,其实际是调用了HttpObjectDecoder的encode,在这里,会逐个部分的读取Http消息,包括请求行/请求头/body,并且对chunked类型的消息做了处理,最后会输出HttpMessage和HttpContent类型的对象到out里。

3.HttpObjectAggregator继承MessageToMessageDecoder。MessageToMessageDecoder是用来将message对象转化为另一个message对象,其主要做这几个事情:
(1)acceptInboundMessage,判断是否为可处理的类型,像此时,只接收HttpObject对象
(2)将传进来的对象强转型为HttpObject对象
(3)调用重写的decode方法
(4)释放掉旧message
(5)将新message传递给下一个handler
于是HttpObjectAggregator把HttpMessage和HttpContent组装成FullHttpMessage。
这里要注意的是,如果自己继承MessageToMessageDecoder,旧message在后面还会使用到,需要调用retain方法,否则会被释放掉。

4.ChunkedWriteHandler不为inboundhandler,直接跳过

5.HttpFileServerHandler用于实现业务逻辑,继承SimpleChannelInboundHandler。一般的业务逻辑处理器可以直接继承SimpleChannelInboundHandler,其handlerRead方法会做这些操作:
(1)acceptInboundMessage,判断是否为可处理的类型
(2)强转型
(3)调用重写的channelRead0
(4)释放消息
所以,HttpFileServerHandler类中只要重写channelRead0方法即可,需要注意的是,如果仍需要传递给下一个handler,需要手动fireChannelRead,如果msg在后面仍要用到,也需要调用msg.retain方法。
这里没有写fireChannelRead,所以读的流程到此为止,消息经历过了以下历程。
在这里插入图片描述

  • write
    在HttpFileServerHandler中处理完业务逻辑之后,就需要把数据返回到客户端中,此时调用的是ctx.write方法。
    write与read类似,当调用write方法时,会在当前ctx,往前找下一个outboundhandler,然后调用下一个的write方法。

ChunkedWriteHandler为写chunked类型的消息提供了支持。

HttpServerCodec内含的HttpServerResponseEncoder,继承MessageToMessageEncoder,负责把返回的消息根据Http协议转换成ByteBuf,如为请求头和请求行之间增加换行等。

编解码器

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值