当前位置: 首页 > news >正文

Socket IO与NIO(四)

阻塞IO和非阻塞IO

如果要到达百万级别,那么消耗的硬件资源时非常高的,我们分析消耗性能的一个点是IO模块,也就是阻塞IO的问题,因为阻塞IO的存在,导致 我们只能使用一个线程去进行等待,而我们使用线程的时候,会额外的消耗一部分线程资源,这部分线程资源也会引起CPU的调度问题,如果说 我们的数量爆发,到达一定的数量之后的话,我们当前的连接数量如果已经到达到了上万级别,那么这个时候其实消息发送是非常频繁的,而如果 这个时候我们有大量的时间处于一个线程的切换上面,那么这一部分时间其实是完全被浪费掉了,我们要做的是把线程的切换尽可能的减少,让CPU 去做真正的一个数据处理的一个消耗。

我们每个客户端到的,都给他创建了一个线程去做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注意事项

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

  • 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个线程,其他的必须要等待前面的完成之后才能进入到线程池里面处理,这个时候 也是一个并行与串行结合的一种东西。我们不需要给所有的客户端都分配一个线程,因为在某一个时刻,计算机网络带宽是有限制的,并不是所有的 客户端都需要在这一时刻处理数据,所以我们仅仅只需要一个线程池尽可能的处理高并发这样一个客户端之间的数据。

相关文章:

  • 【算法导论】学习笔记——第6章 堆排序
  • 转:网络协议概览
  • 5分钟了解 Python 中的super函数是如何实现继承的
  • LINQ To SQL在N层应用程序中的CUD操作、批量删除、批量更新
  • 问题:什么情况UDP的非阻塞写会失败?
  • 一次服务器CPU占用率高的定位分析
  • [HNOI2015]实验比较
  • Springboot简介01
  • 我的作业,来看看把
  • ReentrantLock
  • OSChina 周日乱弹 —— 去应聘男友吧
  • 在网站开发中很有用的8个 jQuery 效果【附源码】
  • 装上这几个 VSCode 插件后,上班划水摸鱼不是梦
  • 三谈属性动画——Keyframe以及ViewPropertyAnimator
  • 湖北分布式智能数据采集方法有哪些?
  • [译]前端离线指南(上)
  • android百种动画侧滑库、步骤视图、TextView效果、社交、搜房、K线图等源码
  • angular2 简述
  • Centos6.8 使用rpm安装mysql5.7
  • CSS居中完全指南——构建CSS居中决策树
  • ES10 特性的完整指南
  • exif信息对照
  • LeetCode刷题——29. Divide Two Integers(Part 1靠自己)
  • mongodb--安装和初步使用教程
  • MYSQL如何对数据进行自动化升级--以如果某数据表存在并且某字段不存在时则执行更新操作为例...
  • ReactNative开发常用的三方模块
  • 高度不固定时垂直居中
  • 区块链共识机制优缺点对比都是什么
  • 手写双向链表LinkedList的几个常用功能
  • 微信小程序实战练习(仿五洲到家微信版)
  • 一起来学SpringBoot | 第十篇:使用Spring Cache集成Redis
  • 做一名精致的JavaScripter 01:JavaScript简介
  • #etcd#安装时出错
  • #if #elif #endif
  • (02)vite环境变量配置
  • (PHP)设置修改 Apache 文件根目录 (Document Root)(转帖)
  • (附源码)流浪动物保护平台的设计与实现 毕业设计 161154
  • (力扣)1314.矩阵区域和
  • (转)setTimeout 和 setInterval 的区别
  • (转)大型网站的系统架构
  • **PHP分步表单提交思路(分页表单提交)
  • .net 8 发布了,试下微软最近强推的MAUI
  • .net MVC中使用angularJs刷新页面数据列表
  • .NET/C# 判断某个类是否是泛型类型或泛型接口的子类型
  • .NET高级面试指南专题十一【 设计模式介绍,为什么要用设计模式】
  • /var/log/cvslog 太大
  • ??myeclipse+tomcat
  • [BIZ] - 1.金融交易系统特点
  • [BJDCTF2020]The mystery of ip
  • [CareerCup] 2.1 Remove Duplicates from Unsorted List 移除无序链表中的重复项
  • [go 反射] 进阶
  • [Google Guava] 1.1-使用和避免null
  • [HDU 3555] Bomb [数位DP]
  • [IE技巧] 使IE8以单进程的模式运行
  • [Json.net]快速入门