Netty入门系列之一:操作系统IO模型
Netty想必我们作为新时代的猿人都有所耳闻的,其中更有不少人有深度使用或者即将列入重点攻关的技能。为啥Netty会如此重要呢?
Netty本质上就是一个函数库,核心功能就是将Java对系统层面网络访问操作,进行了一系列的封装,特别是操作系统非阻塞IO,多路复用等能力进行了非常友好的API包装,对连接管理、数据包组装、编解码等网络编程中最复杂的部分提供了默认的处理机制,极大降低了网络编程的复杂度。此外,Netty强大的IO多路复用模型,有效提升了服务端的高并发和海量连接处理能力,是眼下网游服务端、及时消息、分布式服务框架等新兴业务场景的最佳技术人选。
说到网络编程,相信大家脑海中一定会浮现出bio,nio,socket,selector、epoll等经典概念或者代码片段。这些都没错,个别已经涉及用户编程接口的上层交互,远离网络编程的核心原理,经常会引发大家对其中部分API的质疑和不解,API为什么要设计成这样?所谓万变不离其宗,拨云见日,了解那些深奥不解的api之前,我们先来打一些基础,做好准备铺垫工作。
本文将从操作系统IO、几种经典IO模型、同步异步、阻塞非阻塞等几个非常重要的概念说起。JDK相关的IO知识在下一篇文章来说。
什么是IO?
IO是Input/Output(输入输出)的简写,是操作系统主存和外部设备(磁盘、外部设备、网络)之间复制数据的过程。再细一点,我们来看下图:
运行在电脑上的程序按照系统层级,分为:用户级、系统内核、物理硬件,对应的逻辑上可以分为用户空间、内核空间。
用户空间: 用户的应用程序运行的部分成为用户空间,用户空间无法直接操作硬件,需要编写代码调用内核命令来完成。
系统内核: 直接与硬件交互的程序,通常指代我们的操作系统,它通过提供一些组件来协助用户进程与硬件完成通信交互。
硬件: 在网络编程场景,硬件就是我们的网卡和驱动,它可以把发送信息到互联网,也可以接受其他机器的通信数据。
小结:我们程序代码不能直接操作硬件,必须通过调用内核命令,借助操作系统内核来实现。
跨机器间进程数据传输,应用程序需要将数据发送到操作系统内核,再由系统内核将数据送给网卡驱动器,借助TCP/IP协议最后路由到目标机器网卡,然后硬件通知操作系统数据达到,由操作系统将数据收集并返给用户进程,如上图所示。
用户进程读取网络数据步骤:
系统调用:用户进程向操作系统发送数据读取请求,等待内核从硬件设备获取到数据直到数据报准备完成。
数据拷贝:内核将设备准备好的数据从内核空间复制到用户空间,这个时间用户进程即可读取数据。
我们常说的IO阻塞,通常就是发生的系统调用阶段,用户程序在读取网络数据的时候,会给内核发送recvfrom函数调用。调用函数的时候需要从用户进程的上下文空间切换到内核空间中运行,用户进程阻塞在内核调用处,只有等到数据准备好或者超时时间后才能切换回来。
数据读取根据不同的IO模型,系统调用函数不同,阻塞的方式也有差异。
有哪些IO模型?
首先,明白一点:我们常说的bio、nio都是操作系统层面IO能力的体现。如果说内核不支持nio,那咱们JDK的NIO也无从谈起。OK,这部分得翻翻压箱底的葵花宝典,这里找了几张经典的图片,每种类型简单总结一下。
1、阻塞IO模型:即Blocking I/O。用户发起一个recvfrom系统调用,内核会等待数据从网络中到达。一旦数据准备就绪,系统内核将把自己的缓冲区中的数据拷贝到用户进程的缓冲区。在系统内核等待数据、复制数据的过程中,用户进程是不能做其他任何事情的,只能等待内核完成上述一系列的操作。
2、非阻塞式IO模型:与阻塞式I/O相比,最大的不同在于recvfrom系统调用会立刻返回,数据未准备就绪时,内核调用返回EWOULDBLOCK错误码,防止用户进程阻塞,当设备数据准备就绪时,recvfrom调用会阻塞进程直到数据从内核复制到用户空间,供用户程序读取。使用非阻塞式IO可以防止设备空闲时的进程阻塞,但是应用程序需要调整,当收到数据未准备就绪的信号时,需要开启不停的轮训操作,不断询问数据是否准备就绪,该操作会消耗大量的CPU资源。
3、IO多路复用模型:IO多路复用,需要借助linux系统提供的select,poll,epoll等多路复用器,用户进程阻塞于select函数调用,当注册在selector上的一个或者多个channel有可读事件时,函数调用返回可操作的通道清单。用户进程进而对准备就绪的channel发起recv函数调用,逐个读取通道的数据。
IO多路复用可以让我们同时获取多个就绪的通道,在Java的NIO编程中主要就是使用多路复用器,可以做到一个线程监听和管理多个连接的通信状态。
4、信号驱动IO模型:核心要点就是用户给内核发送指令,内核在数据就绪时发送SIGIO通知信号,用户程序收到信号后开始数据读取操作,该模型使用较少,时序图如下。
5、异步IO模型:用户程序通过系统调用,向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作,整个过程中用户进程都不阻塞。该方案其实是完美的IO解决方案,但linux目前这块还不完善,netty也未基于aio封装,使用的也是多路复用模型。
小结:上述几种IO模型本质上都是阻塞的,即使是信号驱动模型,在数据从内核缓冲区拷贝到进程缓冲区也是阻塞的,真正是异步的只有最后一种,但目前机制还不够成熟,也无可靠的落地案例。
在说IO模型过程中,有几个词一直撇不开,同步异步、阻塞非阻塞。非常容易混淆,不好理解。这里借助网上一个经典的例子:
老张烧水,水壶放到炉子上,然后专心等待水烧开~~~同步阻塞,(老张太傻了)
老张烧水,水壶放到炉子上,然后去客厅看电视,时不时去看看水有没有烧开~~~同步非阻塞(老张觉得自己变聪明了)
老张烧水,使用响水壶,水放到炉子上后等待水壶响~~~异步阻塞(老张还是有点傻)
老张烧水,使用响水壶,水放到炉子上后就去客厅看电视,等待水壶响后提壶~~异步非阻塞(老张觉得自己很聪明)
所谓同步异步,只是对于水壶而言。 普通水壶,同步;响水壶,异步。 虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。 同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。
所谓阻塞非阻塞,仅仅对于老张而言。 立等的老张,阻塞;看电视的老张,非阻塞。 情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。
同步异步:主要是指两个线程间通信的关系模型。A访问B后,什么都不做一直等待B返回结果,那么就是同步的。A访问B后,A直接做别的事情了,等到B处理结束后通知或者主动调用A将结果返回,这就是异步。
阻塞非阻塞:主要描述的单个线程的状态。A访问B后,A线程在等待B结果的过程中被挂起、休眠,什么也做不了,阻塞调用;反过来,A访问B后,直接做别的事情了,就是非阻塞状态。