【Netty学习】2.Java原生网络编程


1 常见术语

1.1 Socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

主机 A的应用程序要能和主机 B的应用程序通信,必须通过Socket建立连接,而建立Socket连接必须需要底层TCP/IP协议来建立TCP连接。建立TCP连接需要底层IP协议寻址网络中的主机。我们知道网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过 TCP 或 UPD 的地址也就是端口号来指定。这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。

1.2 短连接

短连接过程:连接->传输数据->关闭连接。

传统 HTTP无状态的,浏览器和服务器每进行一次 HTTP 操作,就建立一次连接,但任务结束就中断连接。

也可以这样说:短连接是指 SOCKET 连接后,发送后,接收完数据后马上断开连接。

1.3 长连接

过程:连接->传输数据->保持连接->传输数据-> 。。。 ->关闭连接`。

长连接指建立SOCKET连接后不管是否使用都保持连接

什么时候用长连接,短连接?

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多的情况。每个TCP连接都需要三次握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,下次处理时直接发送数据包就 OK 了,不用建立 TCP 连接。

例如:数据库的连接长连接,如果用短连接频繁的通信会造成socket错误,而且频繁的socket创建也是对资源的浪费。

而像WEB网站的HTTP服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源。

总之,长连接和短连接的选择要视情况而定。

2 通用常识

2.1 服务端

在通信编程里提供服务的叫服务端

2.2 客户端

连接服务端使用服务的叫客户端

2.3 三件事

在通信编程里,我们关注的其实也就是三个事情:

  • 连接(客户端连接服务器,服务器等待和接收连接)
  • 读网络数据
  • 写网络数据

服务端提供 IP监听端口,客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手连接,如果连接成功建立,双方就可以通过套接字进行通信。

3 IO模型

3.0 什么是IO?

IO (Input/Output,输入/输出),即数据的读取(接收)写入(发送)操作。
通常用户进程中的一个完整IO分为两阶段

  • 等待数据准备 (Waiting for the data to be ready)
    IO请求一般需要请求特殊的资源(如磁盘、RAM、文件),当资源被上一个使用者使用没有被释放时,IO请求就会被阻塞,直到能够使用这个资源。 在等待数据阶段,IO分为:

    • 阻塞IO:资源不可用时,IO请求一直阻塞,直到反馈结果(有数据或超时)。
    • 非阻塞IO:资源不可用时,IO请求离开返回,返回数据标识资源不可用
  • 将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
    真正进行数据接收和发生。将数据从内核拷贝到用户进程。在使用资源阶段,IO分为:

    • 同步IO:应用阻塞在发送或接收数据的状态,直到数据成功传输或返回失败。
    • 异步IO:应用发送或接收数据后立刻返回,数据写入OS缓存,由OS完成数据发送或接收,并返回成功或失败的信息给应用。
IO过程

IO有内存IO网络IO磁盘IO三种,通常我们说的IO指的是后两者。

《UNIX网络编程》[^1]作者指出,5种IO模型分别是阻塞式IO模型非阻塞式IO模型IO复用模型信号驱动式IO异步IO;前4种为同步IO操作,只有异步IO模型异步IO操作。

3.1 BIO

BIO就是:Blocking IO。

BIO

应用程序请求IO操作,并一直等待处理结果;操作系统同时也进行IO操作,并等待设备的处理结果;可以看出,应用程序的请求线程和操作系统的内核线程都是等待状态。

  • 优点

    应用的程序开发非常简单,在阻塞等待数据期间,用户线程挂起;在阻塞期间,用户线程基本不会占用CPU资源。
    实现难度低、开发应用较容易;
    适用并发量小的网络应用开发;

  • 缺点

    一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大

  • 适用场景

    适用于连接数目比较小固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解.

3.2 NIO

NIO: Non-Blocking IO.

NIO

应用程序请求IO,并且不用一直等待返回结果就去做其他事情。隔一定的周期,再去询问操作系统上次IO操作有没有结果,直到某一次询问从操作系统拿到IO结果;操作系统内核线程在进行IO操作时,还是处理一直等待设备返回操作结果的状态。

  • 特点

    应用程序的线程需要不断地进行IO系统调用轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。

  • 优点

    每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好,适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。 

  • 缺点

    不断地轮询内核,这将占用大量的CPU时间,效率低下。

3.3 IO多路复用

指的是一个进程/线程可以同时监视多个文件描述符(一个网络连接,操作系统底层使用一个文件描述符来表示),一旦其中的一个或者多个文件描述符可读或者可写,系统内核就通知该进程/线程。

IO Multiplexing
  • 特点

    IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用,比如select/epoll。

  • 优点

    与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接,系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销

  • 缺点

    本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的

目前流行的IO多路复用实现主要包括四种:selectpollepollkqueue

IO多路复用技术最适用的是“高并发”场景,所谓高并发是指1毫秒内至少同时有上千个连接请求准备好。其他情况下IO多路复用技术发挥不出来它的优势。另一方面,使用JAVA NIO进行功能实现,相对于传统的Socket套接字实现要复杂一些,所以实际应用中,需要根据自己的业务需求进行技术选择。

3.4 信号驱动IO

与非阻塞IO类似,其在数据等待阶段并不阻塞,但是原理不同。信号驱动IO是在套接字上注册了一个信号调用方法。这个注册动作会将内核发出一个请求,在套接字的收到数据时内核会给进程发出一个sigio信号。该注册调用很快返回,因此应用程序可以转去处理别的任务。当内核准备好数据后,就给进程发出了信号。进程就可以通过recvfrom调用来读取数据。其模型如下:

信号驱动IO模型
  • 优点:在数据包到达之前,进程不会被阻塞。而且采用通知的方式也避免了轮训带来的损耗。

这种模型在Java中并没有对应的实现。

3.5 AIO

异步IO则是采用“订阅-通知”模式:即应用程序向操作系统注册IO监听,然后继续做自己的事情。当操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数。

AIO
  • 特点

    适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持

  • 优点

    在内核等待数据复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数

  • 缺点

    应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。Windows系统下通过IOCP实现了真正的异步IO。而在Linux系统下,异步IO模型在2.6版本才引入,目前并不完善,其底层实现仍使用epoll,与IO多路复用相同,因此在性能上没有明显的优势。

与信号驱动 IO 相比,最大的不同在于信号驱动 IO是内核通知应用程序可以读取数据了;而异步IO是内核通知应用程序数据已经读取完毕了

参见大神的讲解架构设计:系统间通信(5)——IO通信模型和JAVA实践 下篇

3.6 对比

来看下五种 IO 模型的对比,如下:

五种 IO 模型对比

4 原生网络编程

4.1 BIO

BIO
  • 应用-RPC

    RPC(Remote Procedure Call 远程过程调用)框架,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络的技术。

    RPC

4.2 NIO

4.2.1 重要概念

  • Selector 选择器

    • 一个IO事件的查询器。
    • 作用:一个线程可以查询多个通道的IO事件的就绪状态。
    • 最大优势:系统开销小,系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减小了系统的开销。
  • Channel 管道

    • 相当于OIO(Old IO的 socket);OIO有两个流:输入流输出流
    • NIO中网络连接使用channel,且只有一个,双工的
  • Buffer 缓冲区

    • 应用程序与通道(Channel)主要的交互操作,就是进行数据的read读取write写入
    • 通道的读取,就是将数据从通道读取缓冲区中。
    • 通道的写入,就是将数据从缓冲区写入通道中。
IO Multiplexing Flow

4.2.2 NIO 之Reactor 模式

“好莱坞法则”(不要调用我,让我来调用你):具体事件处理程序不调用反应器,而向反应器注册一个事件处理器,表示自己对某些事件感兴趣,有时间来了,具体事件处理程序通过事件处理器对某个指定的事件发生做出反应。

  • Acceptor

    • 处理客户端新连接,并分派请求到处理器链中。
  • Reactor

    • 负责监听和分配事件,将I/O事件分派给对应的Handler。
    • 新的事件包含连接建立就绪、读就绪、写就绪等。
  • Handler

    • 将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。
    • 可用资源池来管理。

4.2.2.1 单Reactor-单线程

前台接待员和服务员是同一个人,全程为顾客服务.

  • 过程
    • 服务器端的Reactor一个线程对象,该线程会启动事件循环,并使用Selector(选择器)来实现IO多路复用注册一个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
    • 客户端向服务器端发起一个连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,这样一来Reactor就会监听该连接的READ事件了。
    • 当Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。比如,读处理器会通过SocketChannelread()方法读取数据,此时read()操作可以直接读取到数据,而不会堵塞与等待可读的数据到来。
    • 每当处理完所有就绪的、感兴趣的 I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理。

注意,Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所有的I/O的accept()read()write()以及connect()操作都在一个线程上完成的。
但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。

单Reactor-单线程

4.2.2.2 单Reactor-多线程

1个前台接待员,多个服务员,接待员只负责接待.

  • 非I/O操作从Reactor线程中移出转交给工作线程池来执行.

  • 对于高负载大并发大数据量的应用场景却不合适.

  • 使用线程池的优势

    • 通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建销毁过程产生的巨大开销
    • 另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性
    • 通过适当调整线程池的大小,可以创建足够多的线程以便处理器保持忙碌状态。同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。

改进的版本中,所有的I/O操作依旧由一个Reactor来完成,包括I/O的accept()read()write()以及connect()操作。

对于一些小容量应用场景,可以使用单线程模型。但是对于高负载大并发大数据量的应用场景却不合适,主要原因如下:

  • 一个 NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送;
  • NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压处理超时,成为系统的性能瓶颈。
单Reactor-多线程

4.2.2.3 主从多线程Reactor模式

1个大堂经理,多个前台接待员,多个服务员.

  • 过程

    • 注册一个Acceptor事件处理器mainReactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样mainReactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。启动mainReactor的事件循环。
    • 客户端向服务器端发起一个连接请求mainReactor监听到了该ACCEPT事件并将该ACCEPT事件派发给Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将这个SocketChannel传递给subReactor线程池
    • subReactor线程池分配一个subReactor线程给这个SocketChannel,即,将SocketChannel关注的READ事件以及对应的READ事件处理器注册subReactor线程中。当然也注册WRITE事件以及WRITE事件处理器subReactor线程中以完成I/O写操作Reactor线程池中的每一个Reactor线程都会有自己的Selector线程分发的循环逻辑。
    • 当有I/O事件就绪时,相关的subReactor就将事件派发给相应的处理器处理。注意,这里subReactor线程只负责完成I/O的read()操作,在读取到数据后将业务逻辑的处理放入到线程池中完成,若完成业务逻辑后需要返回数据给客户端,则相关的I/O的write操作还是会被提交回subReactor线程来完成。
  • mainReactor可以只有一个,但subReactor一般会有多个.

  • mainReactor线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给subReactor,由subReactor来完成和客户端的通信.

  • 所有的I/O操作(包括I/O的accept()read()write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程或subReactor线程)中完成的.

  • ThreadPool(线程池)仅用来处理非I/O操作的逻辑.

主从多线程Reactor模式

Netty中的Reactor架构:

Netty中的Reactor

除了Reactor模型,还有Proactor模型[^3],前者是非阻塞同步网络模式,感知的是就绪可读写事件;那后者就是异步网络模式,感知的是已完成的读写事件
因此,Reactor可以理解为来了事件操作系统通知应用进程,让应用进程来处理,而Proactor可以理解为来了事件操作系统来处理,处理完再通知应用进程。这里的「事件」就是有新连接、有数据可读、有数据可写的这些I/O事件;这里的「处理」包含从驱动读取到内核以及从内核读取到用户空间。

4.2.3 JAVA对IO多路复用的支持

JAVA IO多路复用实现
JAVA NIO Selector实现接口

4.2.4 IO多路复用的优缺点[^2]

  • 不用再使用多线程来进行IO处理了(包括操作系统内核IO管理模块和应用程序进程而言)。当然实际业务的处理中,应用程序进程还是可以引入线程池技术的

  • 同一个端口可以处理多种协议,例如,使用ServerSocketChannel测测的服务器端口监听,既可以处理TCP协议又可以处理UDP协议

  • 操作系统级别的优化:IO多路复用技术可以是操作系统级别在一个端口上能够同时接受多个客户端的IO事件。同时具有之前我们讲到的阻塞式同步IO非阻塞式同步IO的所有特点。Selector的一部分作用更相当于轮询代理器

  • 都是同步IO:目前我们介绍的阻塞式IO非阻塞式IO甚至包括IO多路复用,这些都是基于操作系统级别同步IO的实现。我们一直在说“同步IO”,一直都没有详细说,什么叫做“同步IO”。实际上一句话就可以说清楚:只有上层(包括上层的某种代理机制)系统询问我是否有某个事件发生了,否则我不会主动告诉上层系统事件发生了。

[^1]:Richard Stevens的《UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking》
[^2]:IO通信模型和JAVA实践 中篇
[^3] 高性能网络模式:Reactor 和 Proactor

参考文献:
1.5种IO模型、阻塞IO和非阻塞IO、同步IO和异步IO
2.IO同步,异步,阻塞,非阻塞
3.肝了一月的Netty知识点
4.系统间通信(3)——IO通信模型和JAVA实践 上篇


文章作者: Kezade
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kezade !
评论
  目录