Socket IO与NIO(四)

阻塞IO和非阻塞IO

若是要到达百万级别,那么消耗的硬件资源时很是高的,咱们分析消耗性能的一个点是IO模块,也就是阻塞IO的问题,由于阻塞IO的存在,致使 咱们只能使用一个线程去进行等待,而咱们使用线程的时候,会额外的消耗一部分线程资源,这部分线程资源也会引发CPU的调度问题,若是说 咱们的数量爆发,到达必定的数量以后的话,咱们当前的链接数量若是已经到达到了上万级别,那么这个时候其实消息发送是很是频繁的,而若是 这个时候咱们有大量的时间处于一个线程的切换上面,那么这一部分时间实际上是彻底被浪费掉了,咱们要作的是把线程的切换尽量的减小,让CPU 去作真正的一个数据处理的一个消耗。java

咱们每一个客户端到的,都给他建立了一个线程去作read write close操做,那这部分操做呢,其实大部分状况下都是出于一个阻塞状态,也就是 阻塞到了咋们的一个read或者是write,这个时候咱们线程其实什么事情都没干,他仅仅作的一件事情就是去等待CPU调度,而CPU每次调度过来的 时候,他会扫描到咋们的线程上,发现咋们的线程没有去取消他等待任何的一个触发机制的存在,也就是说数据并无到达,那么这个时候并不会 取消他的等待,这个线程还会继续等待。此时CPU看似没有消耗,但CPU要消耗一个线程与线程之间的一个切换,一个扫描的时间,这个时候CPU其实 有一些额外的时候花费在了线程扫描和线程切换上面,以及若是有信息达到了,A线程有数据到达,那么此时会从阻塞的read到执行状态,可是一旦 咋们的线程从阻塞到执行状态,而咱们又只有一个CPU调度状况下,那么必然存在CPU正在执行的任务和如今待执行的任务的一个切换,那这个时候 两个任务都执行,但又只有一个CPU存在,那么此时CPU能干的事情是切换运行这两个任务线程,那么切换运行是属于内核当中的切换,而此时的切换 消耗是比较高的,由于他要从用户执行状态切换到系统级别的一个内核态,而从用户状态切换内核态之间的一个状态切换会消耗大量的时间,而 这些时候都是能够避免的。减小一个线程的数量也就减小了一个状态转换的时间消耗,还能够减小CPU扫描线程状态的时间。 从内存状态来讲,每个线程建立的时候,必然存在维护这个线程的一系列状态的一些参数,好比说维护状态是否运行,维护这个线程是否处于运行 以及他的一系列IO的调度,还有咋们和用户态 内核态之间的一些链接关系上的一些参数的维持,那么这些东西其实都是输入咋们线程的。你建立 线程达到必定数量以后,那么这部分的内存累计其实很是可怕的,一个线程内存累计很是上,在1.4之前咱们的线程大概会占用1M左右,那么甚至 在老版本上回暂用2M左右的内存,虽然咱们如今测试下来到咋们java8甚至java9上面一个线程建立的消耗是很是低的 也就几百k,可是这个几百k 到达上万级别的时候,其实累加起来也是比较大的消耗,那么这部分消耗彻底能够用来作数据处理。数组

非阻塞IO线程优点

全部客户端到达以后,服务器都会收到一个到底的事件,例如说A客户端到达了,那这个时候服务端会收到一个客户端到达事件,此时会和客户端 进行一个链接创建,创建好了以后,我此时仅仅只是说我要注册一下和A客户端这个链接通道上面的观察,观察什么呢?观察咋们的事件,也就是 读事件,就是说当A客户端有数据到达的时候,你再来回调我,没有的时候就不要回调我,也不要阻塞我。服务器端线程其实只有一个,也就是 主线程,主线程在运行的状况下,首先会注册一个说有哪些客户端到达,而后每一个客户端到达以后,仅仅只是给每一个客户端都注册说你有数据到达 的时候在通知我,以后主线程继续干他的监听,监听有没有事情到达。这个时候假设A计算机给服务器发送消息了,咱们会多建立一个线程吗? 不会!咱们在和A计算机创建好链接以后,我紧跟着干了一件当你有数据到达的时候,你再来通知我,注册好了以后,我此时处于一个等待事件的 过程。你创建链接,这是一个事件,你有数据来也是一个事件,我能够把这些事件都放到主线程当中去等待。主线程说A计算机有数据来了,这个 时候我将A计算机的数据读取完了,读取完了以后,我又回到等待事件的流程。这个时候B计算机来了,咱们把B计算机数据处理完了以后,咱们又 无论了,又继续等待。在这样的一个状况下,咱们的主线程实际上是一个串行的工做模式,串行的去处理全部计算机的消息以及他的回送和读取的操做。 这种状况下主线程是很是频繁的,他作的事情很是多,他作了链接客户端,创建客户端之间的一个关系,而后读取客户端的数据,而且在必定的 状况下咱们可能会把数据回送到对应的客户端,这是主线程要作的事情,咱们使用一个线程就完成了1000个客户端的链接,这是很是高效的。这个 性能不是说信息处理速度的性能,而是说线程处于一个繁忙状态,充分利用了计算机的当前线程的资源。可是咱们不能说充分利用了整个计算机的 资源,由于整个计算机不仅一个CPU,只用一个线程确定是弱化了计算机的能力,此时咱们会把事情进行必定的分组,而后使用不一样的线程池去 作对应的事情,从而知足计算机性能的调度。 服务器能够用一个线程就完成上千上万个客户端的链接,固然这样的状况下,好比线程在读取A计算机数据的时候,就算B C计算机到达了数据,那 这个时候仅仅只是你到达了数据,线程这个时候并不能去处理大家到达的数据,这就是他的劣势。服务器

非阻塞IO

NIO全称:Non-blocking I/O。网络

JDK1.4引入全新的输入输出标准库NIO,也叫New I/O。多线程

在标准Java代码中提供了高速的、可伸缩性的、面向块的、非阻塞的IO操做。数据是面向块的不是一个字节一个字节的处理,少了不少校验。并发

NIO也并非一个很是好的设计,由于他有必定的缺陷。异步

阻塞IO的线程消耗:

当一个客户端链接过来时,server.accept()会从阻塞态变成一个执行态,执行的时候会获得一个Socket,这个时候作的一件事情是把这个Socket 当前的inputStream和outputstream转换成了2个线程,或者说最少也有一个线程inputstream读取数据,由于outputstream不是每时每刻都须要 写入,那么咱们的写入能够放在真正须要写入的时候,使用一个线程池来作到这样一个效果,可是读取操做是必定会消耗的。高并发

NIO family一览

  • Buffer 缓冲区:用于数据处理的基本单元,客户端发送与接收数据都须要经过Buffer转发进行。不能一个字节一个字节的处理数据,须要一个 东西来打包咱们的数据,这个就是buffer。
  • Channel通道:相似于流;但不一样于IN/OUT。
  • Stream;流具备独占性与单向性;通道则偏向于数据的流通多样性。
  • Selectors选择器:处理客户端全部事情的分类器。非阻塞IO的事件注册与产生是由selectors来管理的。

Charset扩展部分

  • Charset 字符编码:加密 解密。
  • 原生支持的、数据通道级别的数据处理方式,能够用于数据传输级别的数据加密 解密操做。

NIO-buffer

  • Buffer包括:Buffer是一个父类 抽象类 ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer
  • 与传统的不一样,数据写的时候先写到Buffer->Channel;读取反之。
  • 为NIO块状操做提供基础,数据都按“块”进行传输。
  • 一个Buffer表明一“块”数据。

NIO-Channel

  • 能够从通道中获取数据也能够输出数据到通道;按“块”Buffer进行。
  • 能够并发也能够异步读写数据。能够并发往里面写数据,也能够并发的从通道读取数据,这个时候通常会存在一个问题,一个Channel表明一个 链接,表明我服务器端的一个channel就表明和客户端的一个链接,他分别有两个东西一个是读,一个是写,当咱们使用多线程去进行写的 时候,必而后会存在每一个线程在丢数据给他的时候,你确定是先丢一个Buffer给他,你A线程丢了一个buffer给channel,B线程丢了一个 buffer给channel,这个时候发送的顺序是不定的,有可能B先被调度到了就先被发送出去了,而后再发A的buffer,那么客户端接受的数据 也会是乱的,当你的数据是依赖于buffer顺序的时候,不要并发读写操做。
  • 读数据时读取到Buffer,写数据则必须经过Buffer写数据。
  • 包括:FileChannel、SocketChannel、DatagramChannel等。

Selector注册事件

  • SelectorKey.OP_CONNECT链接就绪。客户端想要链接到服务器的时候,链接是要通过3次握手,4次挥手。不管是NIO仍是普通IO,Socket的基本 原理是不变的。3次握手多是一个比较耗时的操做,由于在网络比较差的状况下,其实这个链接是必定耗时的操做,假如如今有个链接操做, 客户端想要的是我想要链接服务器,可是何时链接好了再进行后面的数据处理,没有链接好的话,我在界面上给显示一个loading..., 此时咱们就能够注册一个OP_CONNECT事件。我先调用一次链接而且我调用链接以前,我先把本身设置为非阻塞IO,而后进行一个链接,链接 的时候,我再注册一个说,当我链接就绪的时候请你告诉我,我去干其余事情。而后链接好了,再回调回来讲,这个事件链接就绪了,链接 就绪了以后,我就能够作后面的数据发送或者是接受。当你要发送数据的时候,这时网卡也不必定在线,也有可能你要给他发送数据的时候, 网卡刚好是一个繁忙的状态,你就是发不出去,就是要等,你发10个字节也有可能要等几十毫秒以上,这个时间是不定的取决于网卡的繁忙 状态,这个时候就须要注册一个写的事件。
  • SelectorKey.OP_ACCEPT接受就绪。当客户端发送链接到服务器端的时候,服务器端这个时候会收到客户端的链接请求到来,这个时候服务器端 能够选择拒绝客户端链接或者是正常的创建好客户端的链接。当客户端链接创建好了以后,服务器端就会收到一个ACCEPT(链接就绪), 服务器端天然也是同样,服务器端本身先注册一个当有客户端链接就绪的时候,请告诉个人事情,当有客户端链接上了以后,这个事件就会 触发,服务器端就能够获得客户端链接的Socket,而后进行后面的一些操做读写。后面的两个操做就取决于网卡状态了。
  • SelectorKey.OP_READ 读就绪 就是说有数据来了。
  • SelectorKey.OP_WRITE 写就绪 就是说当前网卡是能够输出数据的。

Selector使用流程

  • open()开启一个选择器,能够给选择器注册须要关注的事件。为何不new一个Selector,由于Selector也是一个抽象类,有不少子类,内部是有 一个缓冲机制的,open()多是从缓冲当中取出来一个当前空闲的Selector给你使用。
  • register() 将一个Channel注册到选择器,当选择器触发对应关注事件时回调到Channel中,处理相关数据。你注册那4个事件的时候关注的是 channel的状态。Selector不是一个观察者模式,他是一个半观察者模式,你仅仅只是能够注册这个事件,也能够取消一个关注事件。可是 事件到达的时候,并不会直接回送给你,你须要本身去遍历这个池子。你去遍历的时候也就须要一个最基本的线程,因此说你最少须要一个 线程。
  • select()/selectNow()一个通道Channel,处理一个当前的可用、待处理的通道数据。select()是一个阻塞操做,阻塞到真正有事件到达的时候。 有什么事情到达呢?有一个channel的事件到达。咱们能够在一个select上注册不少个channel去关注不一样的事件,好比第一个客户端达到的 读事件和第二个客户端到达的写的事件,注册分别是不同的。调用select拿到的是一个集合。当第一个客户端读是可用的,第二个客户端 的写是可用的,select拿回来的就是2个元素的数组,而后分别把数组里面的数据取出来讲第一个Channel的读是就绪的,这个时候我就去处理 第一个Channel的读操做,第二个Channel的写也就绪的,这个时候也处理它的写操做。

Selector使用流程

  • SelectorKeys()拿到当前就绪的通道,咱们select()的时候是一个阻塞状态,阻塞事件到达,若是此时想要退出整个程序怎么作?你是阻塞状态 退不了程序。
  • wakeUp()唤醒一个处于select状态的选择器。唤醒他,就算这个时候没有一个可用的事件到达,他也能够直接唤醒select状态,这个时候select 返回来的数量是0。
  • close()关闭一个选择器,注销全部关注的事件。

Selector注意事项

  • 注册到选择器的通道必须为非阻塞状态。oop

  • FileChannel不能用于Selector,由于FileChannel不能切换为非阻塞模式;套接字通道能够。文件通道,他可使用通道的方式去操做文件,也 就是说可使用快状的方式去操做文件,能够把一整块文件放到Buffer当中,而后一整块写入到File,或者说从File当中读取一整块数据到 Buffer,而后再把Buffer数据拿出来用。你不能把FileChannel注册到Selector上,你不能跟他说当前文件可读的时候请你告诉我,当前文 件可写的时候请你告诉我,文件何时可读,他永远的可读,惟一区别是磁盘IO可能会受限于必定的IO速度,咱们磁盘的速度确定是低于 内存速度的,因此这个地方他必定是个阻塞状态的。性能

  • Selector SelectionKey Inetrest集合(当前全部的集合,你注册一个Channel进去的时候,你不必定注册一个事件,能够注册多个事件)、Ready集合(当前已经就绪的集合)。

  • Channel集合。

  • Selector选择器。

  • obj附加值。你能够在注册这个事件的时候,能够传一个事件的附加值进去,当触发的时候,你能够把这个附加值拿出来直接使用,好比你想要发送 一个数据,但此时IO写并非可用的,你能够去注册一个说当你可用的时候告诉我,同时你把想要发送的数据放到obj里面,当他可用的时候 obj就携带回来了刚刚你想要发送的数据,能够直接把obj数据发送出去。

Channel输出数据到Buffer,Channel他不必定对应到一个Buffer,他能够输出到多个不一样的Buffer。 Channel从Buffer读取数据也是同样的,能够从多个不一样的Buffer都把数据写给一个Channel。

现有线程模型

Selector进过accept以后出现A Channel和B Channel,这两个链接创建好以后,看下线程消耗。首先第一个线程用来轮训selector状态,直到 有哪些客户端进行链接,而后把链接为SocketChannel,SocketChannel里面有两个东西,一个是用来读的Thread和一个写的Thread,因此一个 channel对应2个Thread,同理B Channel也同样。在创建两个链接的状况下,咱们一共创建了5个线程。咱们在进行发送消息的时候,还有一个轮训 和一个转发。因此在线程消耗上能够看出是很是高的。

单Thread模型

一个单线程就完成了全部的客户端消息收发,可是他的麻烦点在于你只有一个线程,一旦这个线程正在处理某一个客户端的读取消息的时候,这个 时候咱们是没法接受新的客户端的链接,同时也没法承担对其余客户端的消息输出,他必定是一个串行的。这个单线程是很是繁忙的,他几乎占用 了全部CPU的资源在进行一个轮训,可是他的效率并不高效,由于CPU并非只有一个核心,并无发挥CPU多核的优点。同理,咱们由于轮训是一 个串行的模式,就会致使咋们的后续的一些操做是否要阻塞,好比咱们在进行读或者写的时候,若是这时候耗费了大量时间,这会致使咋们新来的 链接没法创建,甚至说咱们某些客户端的一个等待处于一个长时的等待,这种状况其实会致使不少问题,因此不建议采用单Thread模型方式。

监听与数据处理线程分离

AccepterThread作的事情是监听ServerSocketChannel新客户端的链接,而且完成链接的过程。链接创建好以后就是SocketChannel,把Socket Channel的一系列IO输出或者是输入操做,咱们放到一个线程池当中,ProcessorThread是一个Processing loop操做,你可使用一个线程也 可使用线程池来作。使用一个线程就意味着大家全部客户端的消息收发是串行的。那么使用一个线程池呢,会尽量保证部分客户端的处理是 一个分离的过程,但也并不能保证他必定是一个并行的过程。 此时若是有一个线程池有4个线程,有20个客户端链接,而且同时具备20个客户端 都在给你发消息,那么这个时候,你的线程池仅仅只能处理4个线程,其余的必需要等待前面的完成以后才能进入到线程池里面处理,这个时候 也是一个并行与串行结合的一种东西。咱们不须要给全部的客户端都分配一个线程,由于在某一个时刻,计算机网络带宽是有限制的,并非全部的 客户端都须要在这一时刻处理数据,因此咱们仅仅只须要一个线程池尽量的处理高并发这样一个客户端之间的数据。
相关文章
相关标签/搜索