Netty源码(三):I/O模型和Java NIO底层原理

简介: 上一篇文章我们主要讲解了Netty的Channel和Pipeline,了解到不同的Channel可以提供基于不同网络协议的通信处理.既然涉及到网络通信,就不得不说一下多线程,同步异步相关的知识了.Netty的网络模型是多线程的Reactor模式,所有I.

上一篇文章我们主要讲解了Netty的 ChannelPipeline,了解到不同的 Channel可以提供基于不同网络协议的通信处理.既然涉及到网络通信,就不得不说一下多线程,同步异步相关的知识了.Netty的网络模型是多线程的 Reactor模式,所有I/O请求都是异步调用,我们今天就来探讨一下一些基础概念和Java NIO的底层机制.

 为了节约你的时间,本文主要内容如下:

  • 异步,阻塞的概念

  • 操作系统I/O的类型

  • Java NIO的Linux底层实现

异步,同步,阻塞,非阻塞

同步和异步关注的是消息通信机制,所谓同步就是调用者进行调用后,在没有得到结果之前,该调用一直不会返回,但是一旦调用返回,就得到了返回值,同步就是指调用者主动等待调用结果;而异步则相反,执行调用之后直接返回,所以可能没有返回值,等到有返回值时,由被调用者通过状态,通知来通知调用者.异步就是指被调用者来通知调用者调用结果就绪所以,二者在消息通信机制上有所不同,一个是调用者检查调用结果是否就绪,一个是被调用者通知调用者结果就绪*

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.阻塞调用是指在调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会继续执行.非阻塞调用是指在不能立刻得到结构之前,调用线程不会被挂起,还是可以执行其他事情.

 两组概念相互组合就有四种情况,分别是同步阻塞,同步非阻塞,异步阻塞,异步非阻塞.我们来举个例子来分别类比上诉四种情况.

 比如你要从网上下载一个1G的文件,按下下载按钮之后,如果你一直在电脑旁边,等待下载结束,这种情况就是同步阻塞;如果你不需要一直呆在电脑旁边,你可以去看一会书,但是你还是隔一段时间来查看一下下载进度,这种情况就是同步非阻塞;如果你一直在电脑旁边,但是下载器在下载结束之后会响起音乐来提醒你,这就是异步阻塞;但是如果你不呆在电脑旁边,去看书,下载器下载结束后响起音乐来提醒你,那么这种情况就是异步非阻塞.

Unix的I/O类型

 知道上述两组概念之后,我们来看一下Unix下可用的5种I/O模型:

  • 阻塞I/O

  • 非阻塞I/O

  • 多路复用I/O

  • 信号驱动I/O

  • 异步I/O

     前4种都是同步,只有最后一种是异步I/O.需要注意的是Java NIO依赖于Unix系统的多路复用I/O,对于I/O操作来说,它是同步I/O,但是对于编程模型来说,它是异步网络调用.下面我们就以系统 read的调用来介绍不同的I/O类型.

     当一个 read发生时,它会经历两个阶段:

  • 1 等待数据准备

  • 2 将数据从内核内存空间拷贝到进程内存空间中

     不同的I/O类型,在这两个阶段中有不同的行为.但是由于这块内容比较多,而且多为表述性的知识,所以这里我们只给出几张图片来解释,具体解释大家可以参看这篇博文

Blocking I/O

NonBlocking I/O

Multiplexing I/O

Asynchronous I/O

对比

Java NIO的Linux底层实现

 我们都知道Netty通过JNI的方式提供了Native Socket Transport,为什么 Netty要提供自己的Native版本的NIO呢?明明Java NIO底层也是基于 epoll调用(最新的版本)的.这里,我们先不明说,大家想一想可能的情况.下列的源码都来自于OpenJDK-8u40-b25版本.

open方法

 如果我们顺着 Selector.open()方法一个类一个类的找下去,很容易就发现 Selector的初始化是由 DefaultSelectorProvider根据不同操作系统平台生成的不同的 SelectorProvider,对于Linux系统,它会生成 EPollSelectorProvider实例,而这个实例会生成 EPollSelectorImpl作为最终的 Selector实现.


  
  
  1. class EPollSelectorImpl extends SelectorImpl
  2. {
  3. .....
  4. // The poll object
  5. EPollArrayWrapper pollWrapper;
  6. .....
  7. EPollSelectorImpl(SelectorProvider sp) throws IOException {
  8. .....
  9. pollWrapper = new EPollArrayWrapper();
  10. pollWrapper.initInterrupt(fd0, fd1);
  11. .....
  12. }
  13. .....
  14. }

EpollArrayWapper将Linux的epoll相关系统调用封装成了native方法供 EpollSelectorImpl使用.


  
  
  1. private native int epollCreate();
  2. private native void epollCtl(int epfd, int opcode, int fd, int events);
  3. private native int epollWait(long pollAddress, int numfds, long timeout,
  4. int epfd) throws IOException;

 上述三个native方法就对应Linux下epoll相关的三个系统调用


  
  
  1. //创建一个epoll句柄,size是这个监听的数目的最大值.
  2. int epoll_create(int size);
  3. //事件注册函数,告诉内核epoll监听什么类型的事件,参数是感兴趣的事件类型,回调和监听的fd
  4. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  5. //等待事件的产生,类似于select调用,events参数用来从内核得到事件的集合
  6. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

 所以,我们会发现在 EpollArrayWapper的构造函数中调用了 epollCreate方法,创建了一个epoll的句柄.这样, Selector对象就算创造完毕了.

register方法

 与 open类似, ServerSocketChannelregister函数底层是调用了 SelectorImpl类的 register方法,这个 SelectorImpl就是 EPollSelectorImpl的父类.


  
  
  1. protected final SelectionKey register(AbstractSelectableChannel ch,
  2. int ops,
  3. Object attachment)
  4. {
  5. if (!(ch instanceof SelChImpl))
  6. throw new IllegalSelectorException();
  7. //生成SelectorKey来存储到hashmap中,一共之后获取
  8. SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
  9. //attach用户想要存储的对象
  10. k.attach(attachment);
  11. //调用子类的implRegister方法
  12. synchronized (publicKeys) {
  13. implRegister(k);
  14. }
  15. //设置关注的option
  16. k.interestOps(ops);
  17. return k;
  18. }

EpollSelectorImpl的相应的方法实现如下,它调用了 EPollArrayWrapperadd方法,记录下Channel所对应的fd值,然后将ski添加到 keys变量中.在 EPollArrayWrapper中有一个byte数组 eventLow记录所有的channel的fd值.


  
  
  1. protected void implRegister(SelectionKeyImpl ski) {
  2. if (closed)
  3. throw new ClosedSelectorException();
  4. SelChImpl ch = ski.channel;
  5. //获取Channel所对应的fd,因为在linux下socket会被当作一个文件,也会有fd
  6. int fd = Integer.valueOf(ch.getFDVal());
  7. fdToKey.put(fd, ski);
  8. //调用pollWrapper的add方法,将channel的fd添加到监控列表中
  9. pollWrapper.add(fd);
  10. //保存到HashSet中,keys是SelectorImpl的成员变量
  11. keys.add(ski);
  12. }

 我们会发现,调用 register方法并没有涉及到 EpollArrayWrapper中的native方法 epollCtl的调用,这是因为他们将这个方法的调用推迟到 Select方法中去了.

Select方法

 和 register方法类似, SelectorImpl中的 select方法最终调用了其子类 EpollSelectorImpldoSelect方法


  
  
  1. protected int doSelect(long timeout) throws IOException {
  2. .....
  3. try {
  4. ....
  5. //调用了poll方法,底层调用了native的epollCtl和epollWait方法
  6. pollWrapper.poll(timeout);
  7. } finally {
  8. ....
  9. }
  10. ....
  11. //更新selectedKeys,为之后的selectedKeys函数做准备
  12. int numKeysUpdated = updateSelectedKeys();
  13. ....
  14. return numKeysUpdated;
  15. }

 由上述的代码,可以看到, EPollSelectorImpl先调用 EPollArrayWapperpoll方法,然后在更新 SelectedKeys.其中 poll方法会先调用 epollCtl来注册先前在 register方法中保存的Channel的fd和感兴趣的事件类型,然后 epollWait方法等待感兴趣事件的生成,导致线程阻塞.


  
  
  1. int poll(long timeout) throws IOException {
  2. updateRegistrations(); ////先调用epollCtl,更新关注的事件类型
  3. ////导致阻塞,等待事件产生
  4. updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
  5. .....
  6. return updated;
  7. }

 等待关注的事件产生之后(或在等待时间超过预先设置的最大时间), epollWait函数就会返回. select函数从阻塞状态恢复.

selectedKeys方法

 我们先来看 SelectorImpl中的 selectedKeys方法.


  
  
  1. //是通过Util.ungrowableSet生成的,不能添加,只能减少
  2. private Set<SelectionKey> publicSelectedKeys;
  3. public Set<SelectionKey> selectedKeys() {
  4. ....
  5. return publicSelectedKeys;
  6. }

 很奇怪啊,怎麽直接就返回 publicSelectedKeys了,难道在 select函数的执行过程中有修改过这个变量吗?

publicSelectedKeys这个对象其实是 selectedKeys变量的一份副本,你可以在 SelectorImpl的构造函数中找到它们俩的关系,我们再回头看一下 selectupdateSelectedKeys方法.


  
  
  1. private int updateSelectedKeys() {
  2. //更新了的keys的个数,或在说是产生的事件的个数
  3. int entries = pollWrapper.updated;
  4. int numKeysUpdated = 0;
  5. for (int i=0; i<entries; i++) {
  6. //对应的channel的fd
  7. int nextFD = pollWrapper.getDescriptor(i);
  8. //通过fd找到对应的SelectionKey
  9. SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
  10. if (ski != null) {
  11. int rOps = pollWrapper.getEventOps(i);
  12. //更新selectedKey变量,并通知响应的channel来做响应的处理
  13. if (selectedKeys.contains(ski)) {
  14. if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
  15. numKeysUpdated++;
  16. }
  17. } else {
  18. ski.channel.translateAndSetReadyOps(rOps, ski);
  19. if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
  20. selectedKeys.add(ski);
  21. numKeysUpdated++;
  22. }
  23. }
  24. }
  25. }
  26. return numKeysUpdated;
  27. }

后记

 看到这里,详细大家都已经了解到了NIO的底层实现了吧.这里我想在说两个问题.

 一是为什么Netty自己又从新实现了一边native相关的NIO底层方法? 听听Netty的创始人是怎麽说的吧链接

 二是看这么多源码,花费这么多时间有什么作用呢?我感觉如果从非功利的角度来看,那么就是纯粹的希望了解的更多,有时候看完源码或在理解了底层原理之后,都会用一种恍然大悟的感觉,比如说 AQS的原理.如果从目的性的角度来看,那么就是你知道底层原理之后,你的把握性就更强了,如果出了问题,你可以更快的找出来,并且解决.除此之外,你还可以按照具体的现实情况,以源码为模板在自己造轮子,实现一个更加符合你当前需求的版本.

 后续如果有时间,我希望好好了解一下epoll的操作系统级别的实现原理.

相关文章
|
7天前
|
监控 Java API
探索Java NIO:究竟在哪些领域能大显身手?揭秘原理、应用场景与官方示例代码
Java NIO(New IO)自Java SE 1.4引入,提供比传统IO更高效、灵活的操作,支持非阻塞IO和选择器特性,适用于高并发、高吞吐量场景。NIO的核心概念包括通道(Channel)、缓冲区(Buffer)和选择器(Selector),能实现多路复用和异步操作。其应用场景涵盖网络通信、文件操作、进程间通信及数据库操作等。NIO的优势在于提高并发性和性能,简化编程;但学习成本较高,且与传统IO存在不兼容性。尽管如此,NIO在构建高性能框架如Netty、Mina和Jetty中仍广泛应用。
23 3
|
4月前
|
Java 调度
Netty运行原理问题之ChannelHandler在Netty中扮演什么角色
Netty运行原理问题之ChannelHandler在Netty中扮演什么角色
|
14天前
|
存储 监控 Java
Java的NIO体系
通过本文的介绍,希望您能够深入理解Java NIO体系的核心组件、工作原理及其在高性能应用中的实际应用,并能够在实际开发中灵活运用这些知识,构建高效的Java应用程序。
30 5
|
1月前
|
消息中间件 缓存 Java
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
零拷贝技术 Zero-Copy 是指计算机执行操作时,可以直接从源(如文件或网络套接字)将数据传输到目标缓冲区, 而不需要 CPU 先将数据从某处内存复制到另一个特定区域,从而减少上下文切换以及 CPU 的拷贝时间。
java nio,netty,kafka 中经常提到“零拷贝”到底是什么?
|
2月前
|
Java
让星星⭐月亮告诉你,Java NIO之Buffer详解 属性capacity/position/limit/mark 方法put(X)/get()/flip()/compact()/clear()
这段代码演示了Java NIO中`ByteBuffer`的基本操作,包括分配、写入、翻转、读取、压缩和清空缓冲区。通过示例展示了`position`、`limit`和`mark`属性的变化过程,帮助理解缓冲区的工作原理。
38 2
|
3月前
|
存储 网络协议 Java
Java NIO 开发
本文介绍了Java NIO(New IO)及其主要组件,包括Channel、Buffer和Selector,并对比了NIO与传统IO的优势。文章详细讲解了FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel及Pipe.SinkChannel和Pipe.SourceChannel等Channel实现类,并提供了示例代码。通过这些示例,读者可以了解如何使用不同类型的通道进行数据读写操作。
Java NIO 开发
|
4月前
|
Java
"揭秘Java IO三大模式:BIO、NIO、AIO背后的秘密!为何AIO成为高并发时代的宠儿,你的选择对了吗?"
【8月更文挑战第19天】在Java的IO编程中,BIO、NIO与AIO代表了三种不同的IO处理机制。BIO采用同步阻塞模型,每个连接需单独线程处理,适用于连接少且稳定的场景。NIO引入了非阻塞性质,利用Channel、Buffer与Selector实现多路复用,提升了效率与吞吐量。AIO则是真正的异步IO,在JDK 7中引入,通过回调或Future机制在IO操作完成后通知应用,适合高并发场景。选择合适的模型对构建高效网络应用至关重要。
101 2
|
4月前
|
微服务
成功解决:java.lang.NoSuchMethodError: reactor.netty.http.client.HttpClient.chunkedTransfer(Z)Lreactor/ne
这篇文章讲述了在微服务架构中整合gateway网关时遇到的`java.lang.NoSuchMethodError`错误的解决方法。问题主要是由于`spring-boot-starter-parent`的版本和`spring-cloud-starter-gateway`的版本不匹配所导致。文章提供了具体的版本不一致的错误配置,并给出了匹配的版本配置方案,以及成功测试的截图。
成功解决:java.lang.NoSuchMethodError: reactor.netty.http.client.HttpClient.chunkedTransfer(Z)Lreactor/ne
|
4月前
|
网络协议 C# 开发者
WPF与Socket编程的完美邂逅:打造流畅网络通信体验——从客户端到服务器端,手把手教你实现基于Socket的实时数据交换
【8月更文挑战第31天】网络通信在现代应用中至关重要,Socket编程作为其实现基础,即便在主要用于桌面应用的Windows Presentation Foundation(WPF)中也发挥着重要作用。本文通过最佳实践,详细介绍如何在WPF应用中利用Socket实现网络通信,包括创建WPF项目、设计用户界面、实现Socket通信逻辑及搭建简单服务器端的全过程。具体步骤涵盖从UI设计到前后端交互的各个环节,并附有详尽示例代码,助力WPF开发者掌握这一关键技术,拓展应用程序的功能与实用性。
157 0
|
4月前
|
存储 网络协议 Java
【Netty 神奇之旅】Java NIO 基础全解析:从零开始玩转高效网络编程!
【8月更文挑战第24天】本文介绍了Java NIO,一种非阻塞I/O模型,极大提升了Java应用程序在网络通信中的性能。核心组件包括Buffer、Channel、Selector和SocketChannel。通过示例代码展示了如何使用Java NIO进行服务器与客户端通信。此外,还介绍了基于Java NIO的高性能网络框架Netty,以及如何用Netty构建TCP服务器和客户端。熟悉这些技术和概念对于开发高并发网络应用至关重要。
94 0