每日一句
花半秒钟就看透事物本质的人,和花一辈子都看不清事物本质的人,注定是截然不同的命运。 ——《教父1》
写在开篇
在这百年未有之大变局的情势下,Netty学习的系列文章也终于面世。这是我第一次对外输出技术类型的文章,本篇文章作为整个系列的第一篇,相较而言可能会显得唠叨居多,当然你也不用担心,因为干货也不少。
我是谁
一名平平无奇的JAVA后端开发工程师,喜欢技术,喜欢解决问题,喜欢分享交流,喜欢了解事物背后的本质,喜欢罗技和ikbc,还喜欢蓝瓶红星和细支炫赫门。
我要干什么
把学到的知识通过系列文章的方式分享出来。
- 实现利己
巩固自己的所学,践行“解释效应”并且加深思考; - 实现利他
把文章送给需要的同学,如果对你们能起到帮助作用那是最好,无论是技术层面还是感悟认知层面,哪怕是文中只有一句话能帮到你,那咱哥们就也算是没白忙活。下面咱们书归正传。
Netty是什么
Netty是JBOSS公司研发并开源的一款JAVA开源框架,应用方向是简化网络应用程序的开发过程,包括服务端和客户端,基于JAVA原生的NIO类库,扩展并优化出的一个异步、事件驱动,高性能高可靠、灵活稳定的工具集合。
Netty的出现为了解决什么问题
- 作为一款优秀框架,他首先简化了网络应用程序的开发过程,并且统一了编程范式;
统一编程范式这个事真的是谁用谁知道。比如现在所有后端都离不开的SpringBoot框架,MVC分层模型,RESTful风格,这都是统一范式的体现,当我们第一次看Netty的程序可能会感觉有些陌生,但当我们再看过一二次之后,就很快能够摸清它的“编程套路”,这无疑也是一种优秀架构设计的表现,注重开发的一致性且扩展点明确;
- 通过优良的设计,将原生NIO中繁杂的API进行高度抽象,减轻编码人员的上手压力;
减轻压力翻译过来也就是促进“关注点分离”:使业务逻辑从网络基础设施应用程序中分离,咱们程序员把更多精力还是集中到自己要实现的业务需求上来,其实这也是任何一个框架的设计基础,就是让底层技术和业务逻辑解耦,通过开放简单的接口和方法,更好的为业务服务;
- 在框架内部,友好的提供了对各种常用网络场景的支持,比如半包粘包的通用解决方案,http协议栈的包装,websocket协议栈的包装,让我们无需重复造轮子,提供了开箱即用的能力。
当然Netty还有很多优势,就不一一列举了,总结起来就是十个字吧:网络通讯领域,非常牛B。:)
哪些项目在使用Netty
ElasticSearch
Redisson
Apache Spark
Apache Pulsar
我这就只是列举了几个自认为知名度比较高的项目,其他的项目你们可以自己去官网查阅。
行了,对Netty的夸赞就先到此为止,本文毕竟也不是对Netty技术做的针对性调研报告,咱们还是回归主题。
为什么事情要从IO开始说起
前边提了,Netty要解决的是网络通信的问题,说白了也就是客户端向服务端发消息,服务端接收消息并给客户端响应,技术层面的本质就是一次网络IO交互或者叫开销,而且它的出现也确实是基于JAVA原生NIO类库做的扩展,那咱们就有必要先了解一下目前的IO模型,为后边学好Netty打下理论基础,这就是理由。
什么是网络IO?
先说你在内存中计算并准备好了一份数据,可以是给好友回复的一条聊天消息,可以是一份用户信息数据UserInfo,可以是订单信息数据等等,也可以是一份复杂的JSON文档,但这些都不是我们的重点,重点是现在需要把数据传输给另一台服务器,或者读取到一块U盘中,或者发送给某个http协议的接口,甚至基于蓝牙协议写入到移动设备里,只有存在这样的需求时,才涉及到网络IO过程。我上边举的例子都叫O(output),也就是“我把数据输出出去,给到别人”,反之,如果你想从外部设备读取数据到自己的内存里,这就叫做I(input),也就是别人把数据传输给我,输入到我的内存里。
对于一次网络IO的通信过程,会涉及两个对象,一个是发起这个IO操作的用户程序(所谓用户程序也就是我们自己写的代码,也叫用户线程),另外一个就是内核程序。在服务器的进程中分为用户空间和内核空间,用户线程不能直接访问内核空间。
当用户线程发起I/O操作,网络数据读取操作会经历两个步骤:
- 用户线程等待内核将数据从网卡拷贝到内核空间。
- 内核将数据从内核空间拷贝到用户空间。
各种I/O模型的区别就是:它们实现这两个步骤的方式是不一样的。
以上这一小节你要是感觉不好理解可以再读一遍,对里边的某些名词如果感觉陌生可以结合搜索引擎一起食用,这没什么大不了的。
好了,现在我们已经知道各种IO模型的本质区别了,根据在《UNIX网络编程》这本书中的介绍,网络IO模型一共分为5种,阻塞式I/O、非阻塞式I/O、I/O多路复用、异步I/O、信号驱动式I/O。本文打算只介绍前4种,为什么?因为第5种我不会。
同步阻塞 I/O
当用户线程发起读取调用后就会进入阻塞(read),让出CPU。内核等待网卡数据到来,然后把数据从网卡拷贝到内核空间,接着内核程序再把数据拷贝到用户空间,最后把用户线程叫醒。
同步非阻塞I/O
用户线程不断的发起read调用,数据没到内核空间时,每次都返回一个特有标识,直到数据达到了内核空间,这一次read调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程叫醒。
多路复用I/O
用户线程的读取操作分成两步了,线程先发起select调用,目的是问内核数据准备好了吗?等内核把数据准备好了,用户线程再发起 read 调用。在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的。那为什么叫I/O多路复用呢?因为一次 select 调用可以向内核查多个数据通道(Channel)的状态,所以叫多路复用。也就是说一次select调用返回后,可能得到的是read准备好,也可能得到accept准备好,或者write没准备好等多种状态,多路指的就是通过复用一个线程来管理多个通信线路上的动作。这么个多路复用。
异步 I/O
用户线程发起read调用的同时注册一个回调函数,read方法会立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。
以上就是4种常见的IO模型,第五种虽然没提到,那我作为补偿,给大家扩展一个零拷贝的内容。
零拷贝
在多路复用I/O的模型中,在最终执行读写I/O操作时依然是阻塞的,而且还存在着多次内存拷贝和上下文切换,给系统增加了性能开销。准确的说,一次IO会涉及到数据的4次内存拷贝:
数据到达I/O设备-->拷贝到内核空间-->拷贝到用户空间-->处理完之后再拷贝到内核空间-->最后拷贝到其它IO设备
而零拷贝是一种避免多次内存复制的技术,用来优化IO读写的操作。
Linux内核中的mmap函数可以代替read和write的 IO读写操作,能够实现内核空间与用户空间共享一个缓存数据地址。mmap技术将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块内存地址上,不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射去真正映射到物理内存地址上。这种方式避免了内核空间与用户空间的数据交换。
在Java的NIO编程中,则是使用到了DirectBuffer来实现内存的零拷贝。Java直接在JVM内存空间之外开辟了一个物理内存空间,这样内核和用户进程都能共享一份缓存数据。
后记
本来这系列的第一篇文章是打算直接从Netty的HelloWorld说起,但是我思来想去还是决定从IO说起,因为如果不说清楚IO模型直接聊Netty总是显得太生硬,我们可能只知道它有这样那样的各种优点,但却完全体会不到它之所以能够诞生的必然性,还是那句话,学习和使用技术,要形成自己思考的思维体系,一项技术为什么会出现?是为了解决某个问题或者某一类问题?是原有解决方案不够出色?是竞品公司要分一块蛋糕?还是大厂工程师的KPI产物?还是某个大神无聊时的一个灵光乍现?先搞清楚了这件事情,可能你以后的思考就会更加深刻,百利而无一害,不亏不亏。
好了,本篇文章到这里就结束了,希望您能有所收获,祝您生活愉快。