自定义标题

  • 1.阻塞IO(BIO)
    • 1.普通的单线程的通信方式
    • 2.普通的多线程的通信方式
  • 3.非阻塞IO(NIO)
  • 4.IO复用

能看到这篇博客的人估计已经对BIO和NIO有了一定的了解
采用不同的IO对系统的性能来说影响也是不容小觑的。
下面我们来看普通的BIO的通信方式。

1.阻塞IO(BIO)

1.普通的单线程的通信方式

/*
*采用单个线程进行侦听并且进行事件的处理
*/
public class ServerTest {public static void main(String[] args) {ServerSocket server;boolean goon = true;byte[] bytes = new byte[1024];try {server = new ServerSocket(54188);while(goon) {       Socket socket = server.accept();DataInputStream dis = new DataInputStream(socket.getInputStream());dis.read(bytes);dealMessage(bytes);DataOutputStream dos = new DataOutputStream(socket.getOutputStream());dos.write(bytes);}} catch (IOException e) {e.printStackTrace();}}public static void dealMessage(byte[] value) {//TODO//处理数据}}

弱点:在不考虑多线程的情况下,BIO无法处理多个客户端的请求,并且会有两处阻塞,accept和read。

2.普通的多线程的通信方式

服务端代码:

public class ServerTest {    public static void main(String[] args) {ServerSocket server;boolean goon = true;byte[] bytes = new byte[1024];try {server = new ServerSocket(54188);while(goon) {        Socket socket = server.accept();//侦听到一个链接,利用线程池开启一个线程,进行后续的read 和 write操作new ThreadPool().Execute(new Commucation(socket) {@Overridepublic void dealMessage(String message) {// TODO 进行数据的处理                    }}); }} catch (IOException e) {e.printStackTrace();}}
}

进行数据的读写类,实现Runnable接口

public abstract class Commucation implements Runnable{private Socket socket;private DataInputStream dis;private DataOutputStream dos;private volatile boolean goon;Commucation(Socket socket) {this.socket = socket;goon = true;try {dis = new DataInputStream(socket.getInputStream());dos = new DataOutputStream(socket.getOutputStream());} catch (IOException e) {e.printStackTrace();}}public boolean isGoon() {return goon;}public void sendMessage(String message) {try {dos.writeUTF(message);} catch (IOException e) {//TODO//进行socket的关闭,和资源的回收}}public abstract void dealMessage(String message);@Overridepublic void run() {while (goon) {           String message = null;try {message = dis.readUTF();//不管是对端异常掉线还是自己关闭自己都会触发异常dealMessage(message);} catch (IOException e) {//TODO//处理异常掉线的问题}      }}
}

一般的多线程的方法也是我们采用BIO最常用的一种线程模式,虽然解决了单线程BIO无法处理并发的弱点但问题时,但是每个链接需要独立的线程/进程单独处理,我们知道在创建新线程的时候,每个新线程会消耗系统资源,并且每个线程拥有自己的数据结构栈,消耗系统内存。
并且一个线程阻塞的时候,jvm会保存其状态,并且在上下文切换时恢复阻塞线程的状态,随着线程数的增加,会导致系统花费更多的时间来处理上下文的切换和线程的管理,更少的时间来处理链接服务。
上例虽然采用了线程池的方式,其优点是可以将线程重复利用,缺点是如果创建的线程数量较少,客户端可能等待很长的时间才能获取服务,线程池大小经过设置之后,比较固定,不能够进行动态的调整。

对于上边的BIO的阻塞情况,我们可能会采用进一步的方法进行解决就是采用非阻塞的IO,将普通的ServerSocket替换为ServerSocketChannel 将Socket替换为ScoketChannel,并且设置通道为非阻塞模式,采用轮询的方式进行设计。
采用单个线程对轮询表进行轮询。

3.非阻塞IO(NIO)

服务端主线程负责侦听

//采用单个线程记性轮询,侦听到便产生一个socketChannel,加入到轮询表,
public class ServerTest {public static void main(String[] args) {ServerSocketChannel serverSocketChannel;boolean goon = true;try {serverSocketChannel= ServerSocketChannel.open();serverSocketChannel.bind(new InetSocketAddress(54188));serverSocketChannel.configureBlocking(false);//设置为非阻塞Handler handler = new Handler();new Thread(handler).start();while(goon) {      //serverSocketChannel设置了非阻塞模式,当侦听不到链接的时候,也不会进行阻塞//而是进行循环侦听SocketChannel socket = serverSocketChannel.accept();handler.addChannels(socket);            }} catch (IOException e1) {e1.printStackTrace();}}
}

Handler 进行轮询并且进行数据的处理

public class Handler implements Runnable{private List<SocketChannel> socketList;private volatile boolean goon;private ByteBuffer buffer;public Handler() {socketList = new CopyOnWriteArrayList<SocketChannel>();goon = true;buffer = ByteBuffer.allocate(1024);}public void addChannels(SocketChannel socketChannel) {try {socketChannel.configureBlocking(false);} catch (IOException e) {e.printStackTrace();}socketList.add(socketChannel);       }@Overridepublic void run() {while(goon) {for(SocketChannel one : socketList) {try {//因为设置了非阻塞模式,通过read进行读取的时候不会阻塞//当没有东西可读时返回值为0int length = one.read(buffer);if(length != 0) {dealMessage(buffer);}else {//表明没有消息传输}} catch (IOException e) {e.printStackTrace();}              }}}public void dealMessage(ByteBuffer message) {//TODO//进行数据的处理}}

其实我们在使用socket对象的时候,其实socket(套接字)只是一个引用,这个套接字的对象实际上是放在操作系统内核中,当我们采用write进行写入字节数组的时候,是将字节数组拷贝到内核区套接字对象的writeBuffer中,内核网络模块会有单独的线程负责将writeBuffer的数据拷贝到网卡及硬件。

同样,服务器内核的网络模块也会有单独线程不停的将收到的数据拷贝到套接字的readBuffer中,等待用户层来取,最终服务器的用户程序通过socket引用的read方法将readBuffer中的数据拷贝到用户程序内存中。
所以其实对于每次的write和read都会进行一次系统调用。这个系统调用就是判断是否有数据到达,如果到达就将到达的数据拷贝到套接字的buffer中。

优点:每次发起的IO请求能够立即返回,不用阻塞等待,相比之前的两种线程的模式来说,实时性较好,
缺点:虽说比前面的线程设计方案看起来更好,只采用了两个线程完成,但本身依旧存在缺点,轮询的期间,不管是read还是write,虽然不需要急进行阻塞,都要不断的访问内核,比较耗费CPU的资源,系统资源利用率较低,而且当连接数量很大时,轮询的效率比较低。

其实真正的NIO解决方法不会采用代码层的轮询方式,就是采用java代码写一个轮询的方案,而是将轮询部分的代码交给操作系统级别,主动感知socket。那就是采用一个系统的调用方法(select)。

4.IO复用

IO复用指的是应用程序只会阻塞在系统提供的方法select上,当多个通道有可读的情况的时候,select才会返回,应用程序再通过recvfrom进行系统调用进行从内核空间的数据拷贝到用户空间。
在使用的过程中,我们需要将多个SocketChannel注册到selector选择器上,并且为这些通道标记根据什么事件可以触发通道的响应,然后采用select方法进行轮询监测,一旦哪个通道的事件被触发,select即直接返回,我们可以根据selector得到注册在其上并且有数据可读的通道,然后我们通过得到的通道进行数据读取,并且处理。

我们写的Java程序其本质在轮询每个Socket的时候也需要去调用系统函数,那么轮询一次调用一次,会造成不必要的上下文切换开销。
Select会将请求从用户态空间全量复制一份到内核态空间,在内核态空间来判断每个请求是否准备好数据,完全避免频繁的上下文切换。所以效率是比我们直接在应用层写轮询要高的。

简单的Reactor模型举例IO复用

服务器端测试类:

public class serverTest {public static void main(String[] args) {try {new Thread(new Reactor(54188)).start();} catch (IOException e) {e.printStackTrace();}  }}

Reactor类:

public class Reactor implements Runnable{final Selector selector;//相当于管理了很多个通道,也管理的侦听的通道。final ServerSocketChannel serverSocket;//侦听通道。public Reactor(int port) throws IOException {selector = Selector.open();serverSocket = ServerSocketChannel.open();serverSocket.socket().bind(new InetSocketAddress(port));serverSocket.configureBlocking(false);//注册感兴趣的事情。SelectionKey sk = serverSocket.register(selector, SelectionKey.OP_ACCEPT);   sk.attach(new Acceptor());//当serverScoketChannel触发侦听响应的时候,调用Acceptor类中的方法进行侦听链接,并处理} class Acceptor implements Runnable{//此时的这个Acceptor是特殊的handler@Overridepublic void run() {SocketChannel channel;try {channel = serverSocket.accept();if(channel != null) {//表明有客户端的链接//创建一个与客户端对应的通道,并将此通道注册到selector上new Handler(selector, channel);//对于侦听的客户端产生channel,但只是产生一个对象//并没有开启线程,并将这个channel注册到选择器上。}} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}//侦听链接的客户端,并且生成一个通道}}public void dispatch(SelectionKey key) {//进行事件的分发的处理Runnable handle = (Runnable)key.attachment();if(handle != null) {handle.run();//进行执行方法;}}@Overridepublic void run() {//将侦听的channel,和普通的channel都注册到selector上,针对不同的key来进行处理while(!Thread.interrupted()) {try {selector.select();//会进行阻塞等待,知道系统侦听到通道有响应的事件发生。Set<SelectionKey> selected = selector.selectedKeys();Iterator<SelectionKey> it = selected.iterator();System.out.println(selected.size());while(it.hasNext()) {//通过注册的通道进行,分发,//对于侦听来说,就是侦听到一个用户,然后创建通道,//对于用户的发送信息来说,就是普通的通道的处理过程dispatch(it.next());//需要进行遍历过的键进行删除,//如果对于侦听的serverSocketChannel来说,selector检测其触发了链接的响应,则会建立对应通道的key,之后通过可以会真正执行accept,//如果没有进行删除的话,这个通道的键会保留在key的集合中//那么当有别的通道有对应响应事件的时候,就会开始进行key集合的循环处理,那么这时就算serverScoketChannel没有链接的响应事件//同样也会去执行accept,多此一举it.remove();}} catch (IOException e) {e.printStackTrace();}     }}
}

Handler类:

public class Handler implements Runnable{private static final int LENGTH = 64;final SocketChannel channel;final SelectionKey sk;ByteBuffer input = ByteBuffer.allocate(LENGTH);ByteBuffer output = ByteBuffer.allocate(LENGTH);public Handler(Selector selector, SocketChannel c) throws IOException {channel = c;c.configureBlocking(false);sk = channel.register(selector, SelectionKey.OP_READ);sk.attach(this);//相当于给通道绑定一个事件处理的类也就是一个handler的对象,当通道有数据,触发读响应的时候//会通过主线程进行dispatch分发,处理事件响应通道中的信息,那么就是执行this中的run方法,进行数据的处理,同样就是handler的功能。}public void dealMessage() {//TODO//对缓冲区的的数据进行处理//这里对消息一个简单的输出System.out.println(new String(input.array()));   byte[] bytes = "欢迎你客户端".getBytes();output = ByteBuffer.wrap(bytes);sk.interestOps(SelectionKey.OP_WRITE);return;}@Overridepublic void run() {try {if(sk.isReadable()) {read();}if(sk.isValid() && sk.isWritable()){send();}} catch (IOException e) {e.printStackTrace();}}private void read() throws IOException{//对于读写来说是先读后写,try {int length = channel.read(input);if(length < 0) {channel.close();}else {dealMessage();}} catch (IOException e) {channel.close();}       }private void send() throws IOException {channel.write(output);if(!output.hasRemaining()) {sk.interestOps(SelectionKey.OP_READ);}}
}

客户端测试类:

public class ClientDemo {private static final String SERVER = "127.0.0.1";private static final int PORT = 54188;private static final int LENGTH = 64;public static void main(String[] args) throws IOException {byte[] argument = "你好,服务端".getBytes();    SocketChannel scoketChannels = SocketChannel.open();scoketChannels.configureBlocking(false);if(!scoketChannels.connect(new InetSocketAddress(SERVER, PORT))) {while(!scoketChannels.finishConnect()) {System.out.println("等待中链接中....");} }ByteBuffer writeBuffer = ByteBuffer.wrap(argument);//将数组包装在缓冲区中ByteBuffer readBuffer = ByteBuffer.allocate(LENGTH);//分配一个新的字节缓冲区int readed = -1;while(readed <= 0) {if(writeBuffer.hasRemaining()) {//判断缓冲区中是否拥有可发送的字节              scoketChannels.write(writeBuffer);              }//当没有接受到数据的时候返回值为0readed = scoketChannels.read(readBuffer);}System.out.println("Received : " + new String(readBuffer.array()));scoketChannels.close();}
}

和阻塞IO相比,都需要阻塞等待,但是IO复用,同时关注了多个socketChannel,节省了线程的开销,和非阻塞IO来对比的话,因为非阻塞IO采用的是代码层面的的轮询,所有对于每个通道来说都需要进行read判断,每一轮的轮询中的每一个通道都会都会进行一次系统调用,可能存在很多通道没有数据可读,同样也需要进行系统调用。
对于IO复用来说,通过select内部轮询的方式,我们只需要对注册在selector上的有数据可读的通道进行read操作真正的读取数据,而不用每个通道都进行读取。

其实上例是简单的Reactor模型,应用了IO复用,可以基于一个阻塞对象,同时在多个事件描述上等待就绪,而不是使用多个线程,这样可以大大节省系统资源。

此博文只是一个简单的开头,之后会继续进行学习NIO和AIO,并进行总结写出。

BIO输给NIO了吗 —————— 开开开山怪相关推荐

  1. IO(BIO),NIO,AIO的深度解析和区别

    IO(BIO),NIO,AIO的深度解析和区别    IO    概念: Socket又称"套接字",应用程序通常通过"套接字"向网络发出请求或者应答网络请求. ...

  2. OSI网络模型,IO模型,BIO模型,NIO模型,AIO模型,TCP/IP协议

    文章目录 一.OSI网络模型 1.1.网络的7层架构 1.1.1.七层架构的网络图 1.1.2 七层架构的功能和作用 1.物理层 2.数据链路层 3.网络层 4.传输层 5.会话层 6.表示层 7.应 ...

  3. 面试必会系列 - 5.1 网络BIO、NIO、epoll,同步/异步模型、阻塞/非阻塞模型,你能分清吗?

    本文已收录至 Github(MD-Notes),若博客中图片模糊或打不开,可以来我的 Github 仓库,包含了完整图文:https://github.com/HanquanHq/MD-Notes,涵 ...

  4. java中io.nio.aio_Java中网络IO的实现方式-BIO、NIO、AIO

    在网络编程中,接触到最多的就是利用Socket进行网络通信开发.在Java中主要是以下三种实现方式BIO.NIO.AIO. 关于这三个概念的辨析以前一直都是好像懂,但是表达的不是很清楚,下面做个总结完 ...

  5. BIO,NIO,AIO区别

    BIO,NIO,AIO 总结 Java 中的 BIO.NIO和 AIO 理解为是 Java 语言对操作系统的各种 IO 模型的封装.程序员在使用这些 API 的时候,不需要关心操作系统层面的知识,也不 ...

  6. java io bio nio aio 详解

    BIO.NIO.AIO的区别: BIO就是基于Thread per Request的传统server/client实现模式, NIO通常采用Reactor模式, AIO通常采用Proactor模式, ...

  7. 【Java IO模式】Java BIO NIO AIO总结

    一同步与异步阻塞与非阻塞 1同步与异步 2阻塞与非阻塞 3IO模式 二BIO 概念描述 特点 代码实现 三NIO 概念描述 特点 代码描述 四AIO 一.同步与异步.阻塞与非阻塞 1.同步与异步 同步 ...

  8. 【挑战学习一百天冲刺实习面试】第二十一天:全面理解BIO、NIO、AIO

    偶尔搞搞实习吧,然后学年鉴定等等杂事(还有刷抖音.打王者-) 一些基本概念 阻塞与非阻塞 指的是等待调用结果返回之前,调用方的状态 阻塞:发出请求等待请求结果时不能进行其他操作 非阻塞:发出请求等待请 ...

  9. 【挑战学习一百天冲刺实习面试】第二十二天:全面理解BIO、NIO、AIO(完结)

    一些基本概念 阻塞与非阻塞 指的是等待调用结果返回之前,调用方的状态 阻塞:发出请求等待请求结果时不能进行其他操作 非阻塞:发出请求等待请求结果时可以进行其他操作 同步与异步 指的是通信机制的区别,等 ...

  10. BIO和NIO消耗的cpu和内存比较

    这个其实是遇到的一道面试题,其题目描述也很简单,BIO和NIO消耗的cpu和内存哪个比较大.因为确实从来没遇到过去从这个角度去比较NIO和BIO的,所以我尝试变解释原理边分析,但是整个过程对方三次认为 ...

最新文章

  1. MongoDB学习笔记——Master/Slave主从复制
  2. HTTPS 证书配置
  3. linux打开vivado_ubuntu启动vivado UBUNTU 16.04安装VIVADO成功启动SDK - Linux - 服务器之家...
  4. 第六届蓝桥杯JavaC组_垒骰子_详解
  5. linux输入命令对话框,linux里命令的对话框whiptail
  6. sklearn自学指南(part44)--生成数据集
  7. [转载]Python量化交易平台开发教程系列0-引言
  8. 解密Oracle备份工具-exp/imp
  9. [转]中国网游为何出不了魔兽世界:研发周期才1-2年
  10. 单片机课程设计——交通灯
  11. 【Unity】替换场景、Prefab字体 工具类
  12. Excel比较两列的值
  13. 计算机考试office难还是c语言难,计算机二级考试c语言难不难
  14. C# 实时监控线程类
  15. Vue中的@blur/@focus事件
  16. 命令模式实例与解析--实例一:电视机遥控器
  17. 特性开关框架选型之FF4J vs Togglz
  18. 【springboot】- 导入第三方maven库时出现Cannot Resolve的解决办法
  19. Xmind 8 pro 软件破解版(经济条件允许的情况下,请支持正版)
  20. 计算机软件应用职业规划,计算机软件专业的职业生涯规划

热门文章

  1. 阿里云DataV结合LayUI的一次实战
  2. AudioToolbox之AudioQueue.h(二)Creating and Disposing of Audio Queues
  3. Vue项目打包文件过大(优化)
  4. html 表格单元格点击事件,bootstrap table onClickCell点击单元格事件
  5. word论文排版和写作05:从word中导出pdf
  6. 差异表达基因变化倍数_差异表达基因
  7. excel合并工作簿怎么做?
  8. matlab 三维图形改变线宽,用PANDAS改变线宽绘制三维线图
  9. Echarts图表移动端手机横屏展示
  10. MyBatis事务管理