一文学习NIO和Netty

文章详细介绍了网络编程的基础知识,包括同步阻塞与非阻塞IO,BIO、NIO模型,以及Reactor模式的单Reactor单线程、单Reactor多线程和主从Reactor多线程。重点讨论了缓冲区、通道、选择器在NIO中的作用,以及Java中的Socket和ServerSocketChannel。文章还提到了零拷贝的概念和Linux的sendFile函数,以及Windows和Linux系统的IO模型差异。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

网络编程基础

网络编程

基础

操作系统基础

主要涉及:内核态和用户态、内核空间和用户空间、内核空间和用户空间的IO

  • 系统态(内核态)和用户态:在处理器的存储保护中,主要有两种权限状态,一种是核心态,也被称为特权态;一种是用户态。核心态是操作系统内核所运行的模式,运行在该模式的代码,可以无限制地对系统存储、外部设备进行访问,在网络编程中进行IO操作需要内核态才能进行相关处理
    • 硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。
  • 用户空间和内核空间:
    • 用户空间只有应用进程占有的内存空间,以及操作系统提供的函数库,可以通过系统接口调用内核空间的相关功能
    • 内核空间:可以进行进程管理(进程线程运行状态切换和PCB\JCB申请等)、虚拟文件系统管理(磁盘操作)、内存管理和硬件管理以及网络支持;
    • image-20220316162117929
  • Linux系统IO
    • 基础
      • 脏页:所谓的脏页就是数据已经修改但是还未回刷到磁盘的数据页;
        • 脏页刷回:主要是以下几种情况:
          • 用户调用syncfsyncfdatasync,内核缓存会刷到磁盘上
          • 系统线程周期性的扫描脏页刷回
          • 当发现脏页太多的时候,内核会把一定数量的脏页数据写到磁盘上;
      • 虚拟文件系统IO:我们知道磁盘IO和外设IO一般是通过DMA方式,只在开始和完成时占用CPU;
      • 内存IO简单来说就是CPU控制的的IO操作,例如系统态和用户态间的IO,系统态内存区域将的IO
    • 直接IO和非直接IO
      • 直接IO:不会发生内核缓存和用户程序之间数据复制跳过操作系统的页缓存,直接经过文件系统访问磁盘。
      • 非直接IO:读操作时,数据从内核缓存中拷贝给用户程序,**写操作时,数据从用户程序拷贝给内核缓存,再由内核决定什么时候写入数据到磁盘;**显然,非直接IO可以进行脏页缓存,减少磁盘IO操作;
    • 缓存IO和非缓存IO
      • 缓冲 I/O,利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件。
        • 全缓冲:缓冲区满后才进行系统调用(read、write等)
        • 行缓冲:当在输入和输出中遇到换行符时,标准 I/O 库执行 I/O 操作。
      • 非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。
    • 网络IO的过程
      • 网络和协议栈
        • img
        • 在计算机网络中,我们知道主机通过逐级解析协议栈的的协议然后向上级传输数据
          • 协议栈中,所有数据均在一个内核空间的DMA缓冲区(环形缓冲区)中保存,避免了逐级解析时数据的复制;DMA将网卡驱动接收到的数据传到缓冲区后,协议栈就可以进行解析读取同理封装发送也是通过协议栈的内核空间,然后提交到发送缓存(流量控制,滑动窗口)
          • 数据最后会提交到该应用进程网络端口(UDP/TCP),即内核的socket Buffer中,所以就有所谓的两次内核复制
        • socket状态的监控
          • 就像进程有PCB、线程有JCB一样,文件有文件作业符,socket有自己的fd,以实现管理socket(记录了socket的状态);
          • 操作系统内核进程在对socket操作(数据提交到socket端口后,修改fd状态标志);
          • select:将用户线程其注册监控的所有文件fb放入数组,然后复制到内核进程,内核进行不断的轮询(检查各个fb标志是否进入可读、可写、可连接)状态;如果是,将fb数组复制到用户进程,用户进程再遍历数组检查出就绪的fb;内核进程需要将内核fb数组进行复位;
            • 这里俩个问题:最多只能监控1024个socket、用户进程需要遍历数组查找真正发生感兴趣事件
          • poll:类似,就是通过链表实现,不再仅仅是1024个
          • epoll:通过回调实现,比较复杂
      • select、poll、epoll监控端口fd标志
      • 传统过程(需要四次切换状态,2次CUP IO和2次DMA拷贝
        • image-20220316211246653
      • mmap内存映射(同样需要4次切换状态,但是通过内存映射可以减少一次CPU 拷贝
        • image-20220316211608420
      • Linux2.1版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer
        • image-20220316211737788
      • Linux在2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socketbuffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
        • image-20220316211811761
  • 用户线程调用系统调用接口
    • 当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)
    • 线程通过中断的方式进入系统态:
      • 软中断是指进程发生了异常事件;硬中断就有很多种,例如时钟周期、IO等
  • Linux和Win系统
    • WIndows系统支持异步IO:IOCP是asynchronous I/O,支持Proactor模式
    • Linux(Unix)系统:不支持异步IO但是支持IO多路复用的同步非阻塞IO,支持Reacor模式
    • epoll, kqueue、select是Reacor模式,IOCP是Proactor模式。
    • java nio包是select模型,java支持AIO(如果操作系统也支持的话)

IO基础

BIO(同步阻塞)NIO(同步非阻塞IO)再到IO多路复用模型(事件驱动的同步非阻塞IO)再到现在的AIO模型(异步非阻塞);需要说明的是异步必然是非阻塞的,同步非阻塞并不是不阻塞,而是通过分阶段:即没有任何IO数据阶段,阻塞,而且进行IO时仍然会阻塞,换句话说会有俩个阻塞阶段(;

  • 同步和异步:描述的是用户线程与内核的交互方式
    • 同步:同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;
    • 异步:异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数
  • 阻塞和非阻塞:描述的是用户线程调用内核IO操作的方式
    • 阻塞:阻塞是指等待IO操作可以进行才返回到用户空间
    • 非阻塞:非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作可以进行。
  • 事件驱动:事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。
  • Socket状态:连接具有生命周期(即TCP的三次握手和四次挥手在客户端和服务器产生的状态
    • LISTEN - 侦听来自远方TCP端口的连接请求;
    • SYN-SENT -在发送连接请求后等待匹配的连接请求;
    • SYN-RECEIVED- 在收到和发送一个连接请求后等待对连接请求的确认;
    • ESTABLISHED- 代表一个打开的连接,数据可以传送给用户;
    • FIN-WAIT-1 - 等待远程TCP的连接中断请求,或先前的连接中断请求的确认;
    • FIN-WAIT-2 - 从远程TCP等待连接中断请求;
    • CLOSE-WAIT - 等待从本地用户发来的连接中断请求;
    • CLOSING -等待远程TCP对连接中断的确认;
    • LAST-ACK - 等待原来发向远程TCP的连接中断请求的确认;
    • TIME-WAIT -等待足够的时间以确保远程TCP接收到连接中断请求的确认;
    • CLOSED - 没有任何连接状态
同步阻塞和同步非阻塞
  • 同步阻塞

    • 用户线程调用系统调用接口,操作系统进行中断处理;
    • 线程进入内核态运行,等待内核态完成IO操作,执行中断返回(恢复);
    • 返回后线程继续在用户态进行后面操作;
    • image-20220316161658177
    • 特点
      • image-20220316203734941
  • 同步非阻塞

    • 最初的同步非阻塞:需要不断的轮询是否可以进行IO操作(即在没有IO操作时不阻塞等待,等到有IO请求的时候进入阻塞IO
    • image-20220316203910651
    • image-20220316203848156
  • IO多路复用模型

    • IO多路复用模型体现了分层的思想,一次网络IO需要俩个步骤:
      • 第一阶段:(NIO的同步非阻塞就是通过利用这个阶段(所谓的非阻塞),优化了BIO实现的)
        • 发送端可以发送状态:数据均到达内核空间(socket内存),DMA将其复制到网卡外设(网卡外设可写涉及到TCP滑动窗口、UDP的流发送完毕),内核空间的该内存(进程复制内核数据的内存块)进入可写状态;
        • 接受端可以接受状态:数据到达网卡外设,DMA将其复制到内核空间(该socket内存,然后CUP将数据复制到进程复制内核数据的内存块【Linux2.4前】),该进程数据复制的内核空间的数据进入可读状态;
      • 第二阶段:NIO这个阶段真正调用channel进行read、write,很明显read、write需要(同步)阻塞
        • 发送端IO操作:服务端知道数据到达内核空间(进程复制内核数据的内存块),可读;将数据由内核空间复制到用户空间;
        • 接收端IO操作:客户端知道当前内核空间(进程复制内核数据的内存块)已经复制到socket内核空间,可写;将数据由用户空间复制到内核空间
    • IO多路复用模型本质还是需要阻塞的,只不过这阻塞是阻塞读写不是阻塞等待可读可写,同时还多了一个select监控,导致多了一次用户态和系统态的转换,如果从这个角度而言IO多路复用并没有什么优势(如果没有多个socket被监控的话);
    • IO多路复用的优势体现在可以实现Reactor模式,Reactor模式通过一个专门的线程进行事件循环监听
    • image-20220316195611729
    • IO多路复用实现Reactor模式
      • 下图并不完全正确:
        • 首先单Reactor单线程模式下,Reactor自身就是用户线程;
        • 在NIO中selector是通过epoll(linux内核版本>=2.6)进行非阻塞等待的,否则进行非阻塞的轮询poll;
        • 在NIO中,channel本身就是一个socket的通道,selector需要其为非阻塞,然后才能调用他进行(epoll或者poll)
      • image-20220316204707145

网络编程模型

Reactor模型
  • reactor模型主要有三种:单reactor单线程、单reactor多线程、主从reactor多线程
  • 角色:Reactor模型有三个标准角色
    • Acceptor(专门处理连接的handler):负责接收Accept事件,然后将连接注册成读事件,传递给dispatch;
    • Dispatch(nio成为selector、reactor):负责分发事件(即前面的进行阻塞等待的线程),如果是接收到Accept事件,则分发给Acceptor。如果是Read事件,将请求交给ReadHandler处理;
    • Handler(由于NIO称为channel,所以下文称channel):负责处理具体事件;
单Reactor单线程

单Reactor单线程实现就如前面那样仅仅实现了IO多路复用

  • Reactor负责阻塞等待连接及读写IO就绪;
  • 上述就绪后,线程根据事件类型选择处理accept或者对应的channel
    • accept处理完后注册到reactor中,其自身是一个channel,等待监听到读写事件
    • channel通道负责读写IO;(由于是单线程reactor,所以channel读写和业务处理均在该线程进行)
  • 很明显这里的问题是如果业务处理时间比较长,将会导致其他连接的IO请求没有及时响应
image-20220316212925725
单reactor多线程
  • 当Reactor多线程核心在于,业务处理和IO本质是可以分开处理的,所以通过线程池进行业务处理,通过Reactor进行IO处理
    • 单Reactor意味着这个Reactor既要负责阻塞等待连接事件和IO事件,也要负责channel的IO和连接建立;(这点和单Reactor单线程没有区别,最大不同是对于数据读取完成后,交由线程池进行业务处理
image-20220316214908536
主从reactor多线程(多Reactor多线程)
  • 这里最大的不同在于主Reactor只负责监听处理连接事件;
  • 各个子Reactor负责处理主Reactor分发给他的channel的读写事件;
  • 线程池负责业务逻辑处理
image-20220316215722479

BIO

基础

通讯模式

标准BIO

image-20220316180333369
ServerSocket serverSocket = new ServerSocket(xxxx);
//不断监听端口号xxxx的连接请求,当存在连接请求是,建立连接然后创建线程进行相关处理
while(true) {
       final Socket accept = serverSocket.accept();
       new MyBioThread(accept).start();
}
  • 由于每个socket都有一个线程处理,最严重问题是可能会使得服务器线程数量激增,这会导致线程频繁切换

伪异步阻塞

image-20220316181107960
  • 这里的不同在于将所有IO请求交由线程池进行处理;
  • 这里的弊端很明显:由于IO仍然使用InputStream\OutputStream流方式(是单向的);

使用

  • java的Net包;
    • 该包支持java.net是java最早的网络编程工具包。
    • 主要是C/S结构的BIO模型,效率比较低
    • 包括三部分:地址、socket、Url支持
      • URL
        • URLConnection
地址

地址:地址显然就是封装IP端口的网络地址部分

  • 主要类型
    • **InetAddress:**此类表示互联网协议 (IP) 地址。
    • **Inet4Address:**此类表示 Internet Protocol version 4 (IPv4) 地址。
    • **Inet6Address:**此类表示互联网协议第 6 版 (IPv6) 地址。
    • **SocketAddress:**此类表示不带任何协议附件的 Socket Address。作为一个抽象类,应通过特定的、协议相关的实现为其创建子类。
    • **InetSocketAddress:**此类实现 IP 套接字地址(IP 地址 + 端口号)。它还可以是一个对(主机名 + 端口号),在此情况下,将尝试解析主机名。如果解析失败,则该地址将被视为未解析 地址,但是其在某些情形下仍然可以使用,比如通过代理连接。
  • InetSocketAddress:主要包括 IP地址,端口(常用)
img
Socket

Socket套接字socket支持TCP和UDP,尽管UDP不需要建立连接;socket可以分为socket连接和socket状态管理两部分;

  • 主要类
    • SocketImplFactory:(接口)此接口定义用于套接字实现的工厂。SocketServerSocket 类使用它来创建实际的套接字实现。
    • DatagramSocketImplFactory:(接口)此接口定义用于数据报套接字实现的工厂。DatagramSocket 类使用它来创建实际的套接字实现。****
    • **SocketOptions:(接口)**获取/设置套接字选项的方法的接口。此接口由 SocketImplDatagramSocketImpl 实现。它们的子类应该重写此接口的方法来支持它们自己的选项
    • SocketImpl:(抽象)抽象类 SocketImpl 是实际实现套接字的所有类的通用超类。创建客户端和服务器套接字都可以使用它。
    • DatagramSocketImpl:数据报和多播套接字实现的抽象基类。
    • Socket:此类实现客户端套接字(也可以就叫“套接字”)。套接字是两台机器间通信的端点。
    • ServerSocket:此类实现服务器套接字。服务器套接字等待请求通过网络传入。它基于该请求执行某些操作,然后可能向请求者返回结果。
    • DatagramSocket:此类表示用来发送和接收数据报包的套接字。
    • DatagramPacket:此类表示数据报包。
    • MulticastSocket:多播数据报套接字类用于发送和接收 IP 多播包。MulticastSocket 是一种 (UDP) DatagramSocket,它具有加入 Internet 上其他多播主机的“组”的附加功能
  • SocketOptions:套接字状态,说明一个套接字的名称和类型。
  • TCP:TCP的socket主要分为Socket(客户端)和ServerSocket(服务器socket)
    • Socket主要方法
      • bind:绑定当前客户端的IP+端口以建立连接(如果不设置,将会由系统随机选择端口)
      • close:关闭连接
      • connect() :发起连接
      • getInputStream()/getOutputStream():获取输入输出流
      • getChannel():获取channel,一个双向通道(比输入/输出流优一点)
    • ServerSocket主要方法
      • bind(绑定监听的ip+端口)、close
      • accept():阻塞等待连接
      • setSoTimeout:设置阻塞等待的超时时间
  • UDP
    • DatagramPacket
img

NIO

基础

NIO就是java支持IO多路复用模型而出现的新的IO编程技术,从而避免了每个连接一个线程导致巨大的线程开销和阻塞导致的线程资源浪费;NIO的更多知识随便都能查到,下面是基本的介绍:

  • NIO是以块的方式处理数据;
  • 采用了通道和缓冲区的形式来进行处理数据的;
  • 通道是可以双向的,可以进行全双工通信;
  • 缓存区可读可写,但是在某一时刻只能是一种状态;
  • 通过轮询的方式进行通道状态检查

我们知道为了支持NIO,有三个核心组件:缓冲区(buffer)、通道(channel)、选择器(selector),下面是基本介绍

buffer

1.缓冲区的实现思想: 1.在内存中申请一块空间、利用指针三个(5个部分)指针(可写和读开始位置、最后位置、读和写限制位置,【容量、数据保存数组】)在内存中操作该空间;
				2.写模式下,读写限制位置在最后位置,切换到读模式时,读写限制位到达可写和读位置,可写和读到达头部执行读;【充分体现了分块的思想】
				3.读模式下,从头至尾读,直到到达限制位,切换为写模式有: 1.覆盖写入和压缩写入;覆盖写入则,可写的位置直接移动到头部,否则先将未读的移动到头部再将读写位置移动到相应位置

#虚拟机内存(非直接内存)和直接内存
	1.直接内存读写速度快,但是要自主释放、创建速度相对慢一点(调用系统内存)
    2.非直接内存不需要释放,GC会回收,但是读取会有2次读取(拷贝)
2.byteBuffer:俩种状态,读和写;
	ByteBuffer一开始默认是写模式,即可向数据缓冲区写数据;使用flip()方法切换到读模式;同样在读模式下可用:clear()【覆盖写入】\compact()方法【压缩写入】切换到写模式;
	具体方法参考api
3.其他常用bytebuffer:1.MappedByteBuffer:直接在内存中修改文件而无须拷贝
				2.ReadOnlyByteBuffer:只读
4.buffer的聚合和分散:

channel

通道实现的基本思想:为了配合缓冲区可读可写,通道也必须要双向;
1.channel分类:
	1.文件通道:FileChannel(FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式)
	2.TCP网络通道:ServerSocketChannel:(相当于DispatchServlet,所有希望创建连接先经过其,其创建该连接对应的SocketChannel)、SocketChannel(相当于socket或servlet)	     
	3.DatagramChannel:UDP协议的channel;
2.方法:具体查看jdk8的api;read【从当前通道写入缓冲区】、write【从缓冲区写入当前通道】、transferFrom【从目标通道复制数据到当前通道】和transferTo:【从当前通道复制给目标通道】

selector

1.功能:注册channel、监控channel是否有事件发生、获取事件并处理;(NIO实现多路复用的关键)
2.方法:1.用静态方法创建:Select.open();
		2.select方法:(select和selectNow;select可用wakeup唤醒)
			1.无参:阻塞方法,直到有channel发生事件才返回;
			2.带超时时间参数:
			3.selectNow:非阻塞
		3.selectionKey

了解了基础知识后,下面系统介绍三大组件:

DMA和NIO

  • 四个阶段,我们不妨将CPU看做selector、DMA看做channel,总线看做线程,需要处理内存区域看作buffer
    • 首先DMA控制器希望获取总线的控制权,向CPU发出请求;
      • selector在某一时刻检查到该channel发出获取线程请求
    • CPU在某一时刻接受请求,中断和授权;
      • selector将自身线程给通道单selector单线程模型,相当于只有一条中央总线的CPU模型,由于只有一条总线,这个过程显然CPU也是不能使用总线的(表现在selector上就是selector不能工作轮询channel)),就是后面的单reactor单线程模型
      • 或者进行完读写IO后,将channel提交工作线程单selector多线程模型,相当于CPU有自己的总线,计算机有一条中央总线的模型(双总线模型)【显然线程和总线最大的区别就是理论上线程可以有无效多个(只要内存足够大)】,这个显然selector还能继续工作但是selector还要像CPU一样进行DMA前后进行中断处理(表现在selector上就是开始和最后需要handler(即提交任务是在selector上)进行read、send结束后的处理)),后面的单reactor多线程(主要是怎么分离业务实现多线程)
      • 再或者selector自己不处理交给子selector(多核)、子selector再交给相应的线程处理(主从selector多线程模型,类似多核处理中断(就让core 0)接受外部中断请求,内部中断其他核处理,所以主selector仅仅就是接受外部建立连接请求,具体连接后需要处理由子selector进行工作分配安排【这个终于摆脱了对于主selector的严重依赖】),主从reactor多线程模型,很明显主从reactor可以利用selector分别注册serverSocketChannel、socketChannel实现;
    • DMA使用总线完成传输;
      • 显然就是工作线程处理各个channel的业务工作
    • DMA结束使用将总线控制权返还,CPU接收到请求后中断然后回收总线控制权;
      • 线程的管理由线程池完成,但是完成后还是要将处理结果交给selector,selector进行后续收尾工作
  • DMA请求、DMA响应、DMA传输、DMA结束;

核心组件

channel

channel获取

我们知道我们使用通道就是为了让线程做尽可能多的事(而不是被阻塞的线程占用浪费),就是提升线程的利用率这个思想和计算机硬件的DMA尽可能提升CPU利用率差不多)不妨稍微回忆一下DMA的过程:

有了这个概念,我们参考DMA方式来具体学习

前面已经说过了,channel是一个双向通道,channel有核心四种基本类型(当然还有其他):

本地IO channel:FileChannel;

网络IO channel:ServerSocketChannelSocketChannelDatagramChannel

channel接口

/**
	1.通道要么打开要么关闭。 通道在创建时是打开的,一旦关闭,它就会保持关闭状态。 
	2.一旦通道关闭,任何对其调用 I/O 操作的尝试都将导致抛出ClosedChannelException 。 
	3.通道是否打开可以通过调用它的isOpen方法来测试。
*/
public interface Channel extends Closeable {
	//检查是否打开
    public boolean isOpen();
	//关闭
    public void close() throws IOException;
}
  • 第一点我们知道java希望我们同打开的方式获取通道(即调用open静态方法,事实上我们也不能自己实例化channel,因为channel的实现类本身并不是public的除非我们自己定义channel)
  • 为了程序的健壮性应该使用前检查通道是否关闭;

由于没有定义open方法,我们直接到ServerSocketChannel,然后就会进入SelectorProvider获取到已经打开的通道基于这个角度,我们可以自己直接使用SelectorProvider获取各种channel

    public static ServerSocketChannel open() throws IOException {
        //open使用的是:provider模式,我觉得可以认为是对象级别的分发模式,需要什么就调用provider的相应的方法
        return SelectorProvider.provider().openServerSocketChannel();
    }

通道俩种基本状态、通道的获取已经介绍了,就到通道怎么工作

channel工作

前文已经说过了channel具有承上启下的作用,既要被selector管理,又要管理buffer,下面围绕网络的channel学习

ServerSocketChannel

  • 服务器连接channel,就是Reactor模式的acceptor
  • 通过ServerSocketChannel获取SocketChannel,ServerSocketChannel没有实现任何read、write方法(当然作为server也没必要实现),所以既不能读也不能写
  • 专门负责实现端口监听和连接建立以及获取SocketChannel
image-20211219111244608
  • 很明显ServerSocketChannel实现了ServerSocket的接口;
    • 具有监听端口建立连接的能力;bind–绑定监听端口
    • 然后建立连接获取SocketChannel;accept–接受连接,返回socketChannel
  • 其次就是注册到selector,一般channel显然需要被selector监控运行以实现一个线程对应多个channel(按照nio模型而言),不过对于ServerSocketChannel可能单线程效果更好:register
    • 当然单selector单线程并不是我们想要的结果,后面我们还要再对slelctor进行业务分离实现一个selector对应多线程(线程池)

SocketChannel

  • 就是前面Reactor模式的专门进行处理的Handler;
  • 所以即具有基本的读写能力;channel还可以附带业务操作
  • 另外就是状态检查能力
image-20211221102400703

主要看SocketChannel的方法,很明显就是读写的方法、然后就是关闭读、写、打开关闭socket和获取socket;另外就是setOptions:设置socket连接的相关参数;

  • socket连接管理
    • 获取连接的socket:socket
    • 关闭读、写流通道,但是保留连接(和另一个通道):shutdownInput、shutdownOutput
    • 建立连接(显然一般客户端会使用这个,服务端是通过serversocketChannel建立的):connect
    • 绑定IP端口:bind
  • socket连接检查
    • finishConnect
  • socket数据管理
    • read:在(文件或端口)缓冲区读取数据到buffer缓存区(很明显buffer是一块内存,对于buffer是一块怎么样的内存这就涉及到OS的内容了);
    • write:将buffer数据写入到缓冲区(文件或者端口);
    • setOptions:设置socket连接的相关参数,例如KEEPLIVE(保活)等;

buffer

基础

​ 前面已经说过buffer是一块内存空间(用户态下的),**专门用来保持数据,以进行块处理;**buffer有:

  • java堆内缓存:HeapXXXBuffer:很明显就是在java堆内构造缓存,这也符合一般JAVA程序的思维(毕竟buffer就是一个数组嘛,显然是一个对象),可以通过JVM的GC进行垃圾回收;
  • java堆外缓存(0拷贝):DirectByteBuffer如果在java堆内构造缓冲区,最明显的问题就是需要对数据进行多余的拷贝(占用CPU的拷贝,就是内存到内存的拷贝),没有很好的利用DMA方式减少IO占用CPU时间;零拷贝就是:跳过数据由系统态到用户态再由用户态到系统态,通过mmap内存映射实现; 当然由于外设和CPU速度严重不匹配,CPU还是会将数据进行一次内存拷贝,即将数据拷贝到进行DMA的内存空间,例如socket就将数据由mmap映射空间拷贝到socketbuffer的DMA拷贝空间,换句话说就是一次完整的IO可以少一次CPU拷贝;
    • image-20211221113558509
    • 直接内存映射缓存:MappedByteBuffer,具体实现类就是DirectByteBuffer
java的buffer

前面已经说过java的buffer覆盖了基本数据类型的buffer:例如ByteBuffer、IntBuffer等等、每个buffer又有自己的堆内和堆外内存;

前面说过所有buffer通过三个指针记录当前数组的储存情况和容量大小俩种状态:读和写;buffer类:定义了基本的操作,以方便完成读写(get、put)操作:

image-20211221193911357
  • flip:倒置,即将position记为0,limit指向原来position位置(如上图),使其进入读状态
  • limit:返回此缓冲区的限制。
  • clear:清空缓存区;
  • put和get就省略了(下下图以charBuffer为例子)
image-20211221193553754 image-20211221194524069

Selector

SelectionKey
  • SelectionKey

    • 主要就是定义了关注事件的类型的int值
      • 四种事件用SelectionKey的四个常量(即register时指定可以通过或运算表示监听多个事件):
        • SelectionKey.OP_CONNECT:可连接
        • SelectionKey.OP_ACCEPT:连接
        • SelectionKey.OP_READ:读就绪
        • SelectionKey.OP_WRITE:写就绪
      • image-20220317200840150
  • SelectionKeyImpl(jdk1.8)中其包含了

    • //该SelectionKey对应的通道
      final SelChImpl channel;
      //该SelectionKey注册的选择器
      public final SelectorImpl selector;
      //该SelectionKey在注册选择器中储存SelectionKey集合中的下标索引,当该SelectionKey被撤销时,index为-1
      private int index;
      //SelectionKey的关注操作符
      private volatile int interestOps;
      //SelectionKey的预备操作符
      private int readyOps;
      

上面说了直接对selector主从selector多线程的编程将会极大的增加编程的难度,在tianmaossm的test里某一个精简版的实现,在该版本中没有进行任何多余操作(例如解析协议,数据封装、安全校验等)也非常的复杂;所以使用netty,这一封装好了主从Reactor模型的NIO编程;

Selector
  • reactor的选择器Reactor在NIO中就是selector

  • 在Reactor模式中,我们知道selector需要完成:

    • 管理各个channel和其注册的感兴趣的事件;
    • 能够阻塞等待感兴趣的事件出现;
  • 属性

    • //SelectionKey集合,保存所有就绪key集合,即已经操作事件准备就绪的选择key  
         	protected Set<SelectionKey> selectedKeys = new HashSet();
         	//SelectionKey集合,保存所有其关注的channel及其状态
       protected HashSet<SelectionKey> keys = new HashSet();
         	//外部访问key集合的代理
       private Set<SelectionKey> publicKeys;
         	//外部访问就绪key集合代理     
       private Set<SelectionKey> publicSelectedKeys;
      
  • Selector本身线程安全但是他的四个属性,Set并不是线程安全的,所以Selector将key分为两类(外部访问的快照、内部使用的快照)

  • 这样就能使得外部操作只在下一次影响内部可以,所以外部的key需要自己同步;换句话说Set的修改将有延迟,如果直接操作keys需要手动同步)

  • 同时对于select需要加锁操作,这导致可能会出现死锁(例如例如主从selector,由于主selector和从selector不在一个线程,从selector如果使用阻塞select将导致主selector的register注册的channel没有被注册进从selector,从selector由阻塞等到(彻底死锁,而不是假死)),所以需要使用select(TIME)或者非阻塞,这又导致没有很好利用LINUX的事件回调而变得像不断轮询;(所以直接用NIO编程将比较麻烦)

  • Selector和channel一样使用provider模式创建、通过open静态方法创建、同样是只有俩种状态开启or关闭

  • selector主要就是轮询(LINUX使用epoll即事件回调)是否有感兴趣的时间在channel中发生,这意味着:1.通道要向selector注册本通道要选择器监听的类型(即读、写、连接就绪和连接),一个channel需要保持多种信息(例如感兴趣的状态、已就绪的状态、channel信息所以需要一个类保持:SelectionKey),一个就是selector要记录注册在其中的所有通道(即SelectionKey的set集合);

  • 监听方法很明显selector就是轮询实现,三种实现分别表示
    • selector:阻塞知道有事件、selectorNow:非阻塞轮询、selector(long)一定时间内;
    • selectorKeys:获取有感兴趣事件的通道的SelectorKey,显然需要通过SelectorKey获取感兴趣的通道进行相关操作
    • wakeup:显然是唤醒阻塞的selector
    • image-20211221200457462

Netty

核心架构

Netty组织模型:

image-20211221105222458

前面提到直接使用NIO实现主从Reactor多线程(就是带分发的Selector)十分复杂,所以Netty帮助我们对于快速实现Reactor模型实现了该封装;除此之外,还封装了Buffer支持了mmap的零拷贝以及提供了大量的services和support工具帮助我们解析常用的协议和对TCP数据包进行处理,我们还可以自定义协议(;

线程模型:我们在Netty的线程中每一个EventLoopGroup对应一个Reactor多线程模型,一般我们通过ServerBootstrap组合变成主从多线程Reactor模型:即如下图

image-20211222121751537

组件

  • 前面我们已经介绍了Netty帮助我们封装了对于Selector、buffer和channel,使得我们可以快速搭建自己的Reactor模型类型;
  • 引导类
    • ServerBootstrapBootstrap
  • NioEventLoopGroup事件循环组,可以包含多个NioEventLoop;
    • 方便我们组主从模式的Reactor(处理连接的NioEventLoopGroup只有一个NioEventLoop,但是可以有多从,即处理channel读写的NioEventLoop可以有多个,实现一主多从的Reactor模型)
  • NioEventLoop封装了Selector, 实现多路复用, 由唯一绑定的一个线程去进行三大步骤循环操作: 监听事件,处理事件,执行任务
  • ChannelNetty完全重新实现了Channel,并进行了增强,没有使用Nio的channel;
    • NioServerSocketChannel\NioSocketChannel就是对于Nio的ServerSocketChannel和SocketChannel的功能的支持类
    • 增强
      • 包含Unsafe,专门进行底层数据读写
      • 包含专门的处理链ChannelPipeline,使得每一个Channel有一套自己的逻辑业务处理机制
      • 保存管理的NioEventLoop
      • 有唯一ID
  • ChannelPipeline每个Channel都与唯一的一个Pipeline关联. 当Channel读取到数据以后,后续的具体操作都交给管道Pipeline去进行
  • ChannelHandler:用于具体的业务逻辑处理,Netty实现了常用的的handler
    • ChannelInboundHandlerChannelOutputboundHandler:分别对应inout和output的处理
  • ByteBuf对于Nio的Buffer的封装
  • 单线程池SingleThreadEventExector:单线程的线程池,通过LinkedBlockingQueue作为阻塞队列;实现了RejectedExecutionHandlers使用直接抛异常的拒绝策略
  • 线程池:对于长时间的IO请求,可以通过放入线程池处理实现异步效果;
这里写图片描述
IO线程池
NioEventLoop和NioEventLoopGroup
  • NioEventLoop:事件循环几乎可以说是Netty的核心之一了,本身又是一个线程池(单核心线程的线程池)
业务线程池
EventExecutorGroup和EventExecutor
  • EventExecutor:是一个连接对应一个EventExecutor,一个EventExecutor就是一个单线程的线程池;
Channel和channelHandler
channel
增强

对Nio的channel增强在哪?

  • 通过内部的unsafe进行IO操作
  • 实现了对于异步的支持
    • netty的NioServerSocketChannel/NioSocketChannel的IO操作是异步的
    • 封装了一系列读写快捷方法(write、writeAndFlush等等)
    • bind、write、read都是异步的
  • 提供了感知能力(就是知道其注册在哪个EventLoop上
  • 通过id唯一标记,同理可以通过id查找channel
  • 可以有父channel
  • 包含专门的处理链ChannelPipeline
Channel类型
  • NioSocketChannel, 代表异步的客户端 TCP Socket 连接.
  • NioServerSocketChannel, 异步的服务器端 TCP Socket 连接.
  • NioDatagramChannel, 异步的 UDP 连接
  • NioSctpChannel, 异步的客户端 Sctp 连接.
  • NioSctpServerChannel, 异步的 Sctp 服务器端连接.
  • OioSocketChannel, 同步的客户端 TCP Socket 连接.
  • OioServerSocketChannel, 同步的服务器端 TCP Socket 连接.
  • OioDatagramChannel, 同步的 UDP 连接
  • OioSctpChannel, 同步的 Sctp 服务器端连接.
  • OioSctpServerChannel, 同步的客户端 TCP Socket 连接.
channel状态
channelHandler
类型
  • ChannelInboundHandler
    • 处理入站数据以及各种状态的变化
    • image-20220318232839246
  • ChannelOutputboundHandler
    • 处理出站数据并且允许拦截所有的操作
    • image-20220318234228029
引导类
ServerBootstrap
Bootstrap

下面就开始针对上面逐一学习:

核心

EventLoopGroup的设置

类图

Netty使用了java的并发包JUC实现自己的线程池定义了一系列的EventLoop接口以实现高效事件处理

image-20211222160654819
类的介绍

我们很明显看到使用了Executor框架,而且是具有定时任何周期任务的ScheduledExecutorService,显然多线程主要是为了实现异步非阻塞处理;然后就是网络层类型负责Selector和Channel和事件处理器的配合

事件处理和多线程

  • ScheduleExecutorService:在JUC的时候我们就学过,这个专门为定时任务和周期任务定义API;换句话说EventExecutorGroup可以处理定时任务和周期任务;

  • EventExecutorGroup:EventExecutor的组,说明是管理着一组EventExecutor,负责通过其next()方法提供EventExecutor的使用(在EventExecutorGroup中就是将channel和EventExecutor绑定)Netty的高效性体现在这里),并负责管理这些EventExecutor的生命周期。

  • AbstractEventExecutorGroup:基本实现,最重要的是:实现next()选择一个EventExecutor对象,然后将执行任务的工作都是委托给这个对象

  • MultithreadEventExecutorGroupEventExecutorGroup的抽象实现,内部组合了多个EventExecutor用于对外提供服务,并负责管理一组EventExecutor实例。


  • EventExecutor事件执行器,它是一个只使用一个线程来执行任务的特殊线程池,其扩展了EventExecutorGroup,主要是为了方便代理EventExecutorGroup中的方法;此外,EventExecutor中也定义一些自己的API,如:用于识别线程身份的方法inEventLoop,创建各种通知器Promise的方法。

  • AbstractScheduledEventExecutorScheduleExecutorService实现,(参考JUC,其持有一个PriorityQueue用于存放调度任务,并实现了ScheduledExecutorService中定义的提交调度任务的方法)。

  • OrderedEventExecutor:它是一个标识接口,没有任何方法和属性,仅仅表明实现该接口的类拥有按顺序串行执行任务的能力。

  • SingleThreadEventExecutor一个可执行普通任务和调度任务的单线程执行器,也就是说,提交到该线程池的所有任务都有同一个线程来完成。内部具有一个阻塞队列用于保存所有提交到该执行器的任务。同时,其扩展了OrderedEventExecutor,表明需要按顺序串行执行所有提交的任务,这里体现了Netty无锁化设计

总结一下事件处理和多线程这一部分主要完成:

  • 我们可以大致把执行器接口分为两类:执行器和执行器组
    • 执行器组将多个执行器统一打包成一个整体以对外提供服务
    • 执行器使用单线程的异步非阻塞定时任务线程模型真正执行channel的IO处理
  • 执行器组
    • 提供执行器沟通的通道,即对于执行器检测到有channel事件,需要先检测这个channel是否注册在其他执行器上,没有这注册在本执行器;
    • 提供记录执行器和注册channel的记录以保证可以;

网络层

img
  • EventLoopGroup:简单来说就是将多个EventLoop打包统一提供对外服务同时定义了注册Channel的方法,用于将一个EventLoop与Channel绑定

  • MultithreadEventLoopGroup:EventLoopGroup的抽象实现,初始化时会确认用于IO操作的EventLoop线程数量,默认值是处理器个数的2倍。从EventLoopGroup继承的register方法主要委托给next返回的EventLoop来完成。同时还扩展了MultithreadEventExecutorGroup,这样从EventExecutorGroup继承的方法都得到默认实现,但从MultithreadEventExecutorGroup中继承的newChild没有默认实现,它需要由由最终子类来实现。

  • DefaultEventExecutorGroup:EventExecutorGroup的默认实现,当需要使用DefaultEventExecutor来执行任务时,可使用该实现,比较少用。

  • NioEventLoopGroup:扩展自MultithreadEventLoopGroup,定义NIO的独特实现,主要实现了newChild方法。


  • EventLoop一旦与Channel绑定,将处理该Channel上的所有I/O操作EventLoop可同时处理多个Channel中I/O操作,也可以只处理一个Channel上的I/O操作,具体由不同网络I/O确定。如Oio只能处理单个Channel的I/O操作,NIO则可以处理多个Channel的I/O操作。 EventLoop所有子类都将顺序串行执行任务,因为其扩展了OrderedEventExecutor。

  • SingleThreadEventLoopEventLoop的抽象实现,同时扩展了SingleThreadEventExecutor,负责用单个线程来执行所有提交到当前EventLoop的任务。SingleThreadEventLoop内部持有一个tailTasks队列,不知道干嘛用,目前内部也没有任何地方调用。SingleThreadEventLoop中主要实现了register相关方法。不同网络I/O类型通过扩展该类来完成底层实现。

  • NioEventLoopNIO实现,内部聚合了Java Selector,使得EventLoop成为一个真正意义的Reactor线程。内部除了实现Selector相关的一些操作,同时实现了执行任务的核心逻辑run方法

  • ThreadPerChannelEventLoop:主要用于Oio的EventLoop实现,一个EventLoop只处理一个Channel的I/O操作。

总结

  • 首先
image-20211222173814006

很明显NioEventLoopGroup管理的是NioEventLoop,换句话说就是NioEventLoop管理一个周期性的定时任务实现;所以他的核心方法就是run;(在我们那个简陋的多线程主从selector模型中,其实selector还是单线程的,仅仅是将任务提交到工作线程池,返回的时候还要从selector再处理);Netty的多线程Reactor模型不仅仅是工作线程是多线程的,就连selector也是多线程的;

image-20211222155449979

image-20211222153206731
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

舔猫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值