IO模型

IO模型就是说用什么样的通道进行数据的发送和接收,Java共支持3种网络编程IO模式:BIO,NIO,AIO

BIO(Blocking IO )

同步阻塞模型,一个客户端连接对应一个处理线程

BIO代码示例:

import java.net.ServerSocket;
import java.net.Socket;
import java.util.logging.Handler;public class SocketServer {public static void main(String[] args) throws  Exception {ServerSocket serverSocket = new ServerSocket(9000);while (true){System.out.println("等待连接");//阻塞连接Socket clientSocket = serverSocket.accept();System.out.println("有客户端连接。。。");
//            handle(clientSocket);new Thread(new Runnable() {@Overridepublic void run() {try {handle(clientSocket);} catch (Exception e) {e.printStackTrace();}}}).start();}}public  static void handle(Socket clientSocket) throws  Exception{byte[] bytes = new byte[1024];System.out.println("准备read。。");//接收客户端的数据,阻塞方法,没有数据可读时就阻塞int read = clientSocket.getInputStream().read(bytes);System.out.println("read 完毕。");if (read !=-1){System.out.println("接收到客户端数据:" + new String(bytes,0,read));}clientSocket.getOutputStream().write("helloClient".getBytes());clientSocket.getOutputStream().flush();}
}
//客户端代码import java.io.IOException;
import java.net.Socket;public class SocketClient {public static void main(String[] args) throws IOException {Socket socket = new Socket("localhost", 9000);//向服务端发送数据socket.getOutputStream().write("HelloServer".getBytes());socket.getOutputStream().flush();System.out.println("向服务端发送数据结束");byte[] bytes = new byte[1024];//接收服务端回传的数据socket.getInputStream().read(bytes);System.out.println("接收到服务端的数据:" + new String(bytes));socket.close();}
}

缺点:

从上面的代码我们可以看出来,BIO代码中连接事件和读写数据事件都是阻塞的,所以这种模式的缺点非常的明显

1、如果我们连接完成以后,不做读写数据操作会导致线程阻塞,浪费资源

2、如果没来一个连接我们都需要启动一个线程处理,那么会导致服务器线程太多,压力太大,比如C10K;

应用场景:

BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,但是程序比较简单。

NIO(Non Blocking IO)

同步非阻塞模型,服务器实现模式为一个线程可以处理多个请求连接,客户端发送的连接请求都会注册到多路复用器(selector)上,多路复用器轮询到连接有IO请求就进行处理,JDK1.4开始引入。

//没有引入多路复用器的代码import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;public class NioServer {static List<SocketChannel> channelList = new ArrayList<>();public static void main(String[] args) throws Exception {//创建NIOServerSocketChannel serverSocket = ServerSocketChannel.open();serverSocket.socket().bind(new InetSocketAddress(9000));//设置非阻塞serverSocket.configureBlocking(false);System.out.println("服务启动。。");while (true) {//非阻塞模式accept方法不会阻塞,否则会阻塞//NIO的非阻塞模式是由操作系统内部实现,底层调用了Linux内核的accept函数SocketChannel socketChannel = serverSocket.accept();if (socketChannel != null) {System.out.println("连接成功");//设置socketchannel为非阻塞socketChannel.configureBlocking(false);//保存客户端连接到listchannelList.add(socketChannel);}//遍历连接读数据Iterator<SocketChannel> iterator = channelList.iterator();while (iterator.hasNext()) {SocketChannel sc = iterator.next();ByteBuffer byteBuffer = ByteBuffer.allocate(128);//非阻塞模式read 方式不会阻塞 否则会阻塞int len = sc.read(byteBuffer);if (len > 0) {System.out.println("接收到消息:" + new String(byteBuffer.array()));} else if (len == -1) { // 如果客户端断开,把socket从集合中去掉iterator.remove();System.out.println("客户端断开连接");}}}}
}

缺点:

如果连接数太多的话,会有大量的无效遍历,假如有10000个连接,其中只有1000个 连接有写数据,但是 由于其他9000个连接并没有断开看我们还是每次轮询遍历一万次,其中有 十分之一的遍历都是无效的,这显然是一个非常浪费资源的做法。

NIO引入多路复用器的代码示例:

package com.jack.nio;import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.security.Key;
import java.util.Iterator;
import java.util.Set;public class NioSelectorServer {public static void main(String[] args) throws Exception {//创建NIO ServerSocketChannleServerSocketChannel serverSocket = ServerSocketChannel.open();serverSocket.bind(new InetSocketAddress(9000));//设置ServerSocketChannel为非阻塞serverSocket.configureBlocking(false);//打开Selector处理channel,即创建epollSelector selector = Selector.open();//把ServerSocketChannel注册selector上,并且select对客户端accept连接操作感兴趣serverSocket.register(selector, SelectionKey.OP_ACCEPT);System.out.println("服务启动");//while (true) {//阻塞等待需要处理的事件发生selector.select();//获取selector中注册的全部事件的SelectionKey实例Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();//遍历selectionKeys对事件进行处理while (iterator.hasNext()) {SelectionKey key = iterator.next();//如果是accept事件,则进行连接获取和事件注册if (key.isAcceptable()) {ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel socketChannel = server.accept();socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);System.out.println("客户端连接成功");} else if (key.isReadable()) {//进行数据读取SocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(128);int len = socketChannel.read(byteBuffer);// 如果有数据,把数据打印出来if (len > 0) {System.out.println("接收到消息:" + new String(byteBuffer.array()));} else if (len == -1) { // 如果客户端断开连接,关闭SocketSystem.out.println("客户端断开连接");socketChannel.close();}}//从事件集合里删除本次处理的key,防止下次select重复处理iterator.remove();}}}
}

上面代码是利用NIO一个线程处理所有请求,这种单个线程处理的方式肯定是存在问题的,例如现在有10w个请求中,有1w个连接进行读写数据,那么SelectionKey就会有1w个请求,所以我们需要循环这1w个事件进行处理,比较费时间,如果这个时候再有连接进来,只能阻塞。。

NIO有三大核心组件:Channel(通道),Buffer(缓冲区)Selector(多路复用器)

1、channel 类似流,每个channel对应一个buffer缓冲区,buffer底层是个数组。

2、channel会注册到selector上,由selector根据channel的读写事件发生将其交由某个空闲的线程处理

3、NIO的Buffer和channel都是既可以读又可以写的

NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,跟上面的NioServer代码类似,selector每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO。

举个例子:例如我们去酒吧喝酒,在吧台坐下了20个人,中间一个服务员,select()或者poll()模式就是,服务员每次都是询问这个20个人是否需要喝酒,而epoll模型则是,20个人谁需要喝酒谁就举手,服务员每次只处理举手的那几个人即可

NioSelectorServer 代码里如下几个方法非常重要,我们从Hotspot与Linux内核函数级别来理解下

Selector.open()  //创建多路复用器socketChannel.register(selector, SelectionKey.OP_READ)  //将channel注册到多路复用器上
selector.select()  //阻塞等待需要处理的事件发生

总结:

NIO整个调用流程就是Java调用了操作系统的内核函数来创建Socket,获取Socket文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的文件描述符上,进行事件的异步通知,这样就实现了使用一条线程,并且不需要太多的无效遍历,将事件处理交给了操作系统内核(操作系统的终端程序),大大提高了效率。

Epoll函数详解

int epoll_create(int size);

创建一个epoll实例,并返回一个非负数作为文件描述符,用于对epoll接口的所有后续调用。参数size代表可能会容纳size个描述符,但size不是一个最大值,只是提示操作系统它的数量级,现在这个参数基本上已经弃用了。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

使用文件描述符epfd引用epoll实例,对目标文件描述符fs执行op操作。

参数epfd表示epoll对应的文件描述符,参数fd表示socket对应的文件描述符。

参数op有以下几个值:

EPOLL_CTL_ADD:注册新的fd到epfd中,并关联事件event;

EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

EPOLL_CTL_DEL:从epfd中移除fd,并且忽略掉绑定的event,这时event可以为null;

参数event是一个结构体

    struct epoll_event {__uint32_t   events;      /* Epoll events */epoll_data_t data;        /* User data variable */};typedef union epoll_data {void        *ptr;int          fd;__uint32_t   u32;__uint64_t   u64;} epoll_data_t;

events有很多可选值,这里只举例最常见的几个:

EPOLLIN :表示对应的文件描述符是可读的;

EPOLLOUT:表示对应的文件描述符是可写的;

EPOLLERR:表示对应的文件描述符发生了错误;

成功则返回0,失败返回-1

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

等待文件描述符epfd上的事件。

epfd就是Epoll对应的文件描述符,events表示调用者所有可用事件的集合,maxevents表示最大等到多少个事件就返回,timeout是超时时间。

I/O多路复用底层主要用Linux内核函数(select 、poll、epoll)来实现。

  select poll epoll(jdk1.5及以上)
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率

每次调用都进行线性遍历,时间复杂度O(n)

每次调用都进行线性遍历,时间复杂度O(n) 事件通知方式,每当有IO事件放生,系统注册的回调函数就会被调用,事件复杂度O(1)
最大连接 有上限 无上限 无上限

AIO模型

异步非阻塞模型,由操作系统完成后回调通知服务端程序启动线程去处理,一般适用于连接数比较多且连接时间比较长的应用

应用场景

AIO方式适用于连接数 多且连接比较长(重操作)的架构,JDK1.7开始支持

package com.jack.aio;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousChannel;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;public class AIOServer {public static void main(String[] args) throws  Exception {final AsynchronousServerSocketChannel serverChannel =AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {@Overridepublic void completed(AsynchronousSocketChannel socketChannel, Object attachment) {try {System.out.println("2--" + Thread.currentThread().getName());//再此接收客户端连接,如果不写这行代码后面的客户端连接不上服务端serverChannel.accept(attachment,this);System.out.print(socketChannel.getRemoteAddress());ByteBuffer buffer = ByteBuffer.allocate(1024);socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {@Overridepublic void completed(Integer result, ByteBuffer buffer) {System.out.println("3--"+Thread.currentThread().getName());buffer.flip();System.out.println(new String(buffer.array(), 0, result));socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));}@Overridepublic void failed(Throwable exc, ByteBuffer buffer) {exc.printStackTrace();}});} catch (IOException e) {e.printStackTrace();}}@Overridepublic void failed(Throwable exc, Object attachment) {}});System.out.println("1--"+Thread.currentThread().getName());Thread.sleep(Integer.MAX_VALUE);}
}
//客户端
package com.jack.aio;import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;public class AIOClient {public static void main(String... args) throws Exception {AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get();socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes()));ByteBuffer buffer = ByteBuffer.allocate(512);Integer len = socketChannel.read(buffer).get();if (len != -1) {System.out.println("客户端收到信息:" + new String(buffer.array(), 0, len));}}
}

为什么Netty使用NIO而不是AIO?

因为在Linux系统上,AIO的底层实现扔使用Epoll模型,没有很好的使用AIO,因此在性能上没有明显的优势,而且被JDK封装了一层不容易再次进行深度优化,Linux上AIO还不够成熟。Netty是异步非阻塞框架,Netty在NIO上做了很多异步封装。

深入理解Java三种IO模式和Epoll模型相关推荐

  1. java 三种工厂模式

    java 三种工厂模式 一.简单工厂模式 一个栗子:  我喜欢吃面条,抽象一个面条基类,(接口也可以),这是产品的抽象类. public abstract class INoodles {/*** 描 ...

  2. 【Netty】IO 模型简介 ( Netty 特点 | Netty 应用场景 | Java 三种 IO 模型 | BIO 模型 )

    文章目录 I . Netty 简介 II . Netty 应用场景 III . Java I/O 模型 IV . BIO 概念 V . BIO 开发流程 VI . BIO 实例 VII . BIO 模 ...

  3. 从同步阻塞聊到Java三种IO方式

    对于刚刚成为程序猿不久的人,可能常常会被以下几个概念所混淆: 同步,异步,阻塞,非阻塞?以及从这几个概念中衍生出的几个概念,同步阻塞,同步非阻塞,异步阻塞,异步非阻塞? 小编从网上查了一些资料,发现对 ...

  4. java 三种工厂模式(简单工厂+工厂方法+抽象工厂)

    一.简单工厂模式 概述   简单工厂模式:定义一个工厂类,它可以根据参数的不同返回不同类的 实例,被创建的实例通常都具有共同的父类.因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因 ...

  5. VMware虚拟机三种网络模式

    Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/cou ...

  6. Java的三种代理模式简述

    本文着重讲述三种代理模式在java代码中如何写出,为保证文章的针对性,暂且不讨论底层实现原理,具体的原理将在下一篇博文中讲述. 代理模式是什么 代理模式是一种设计模式,简单说即是在不改变源码的情况下, ...

  7. Java的三种代理模式完整源码分析

    Java的三种代理模式&完整源码分析 Java的三种代理模式&完整源码分析 参考资料: 博客园-Java的三种代理模式 简书-JDK动态代理-超详细源码分析 [博客园-WeakCach ...

  8. Java的三种代理模式【附源码分析】

    Java的三种代理模式&完整源码分析 代理模式分为两种,静态代理和动态代理,动态代理包括JDK动态代理和Cglib动态代理. 静态代理 静态代理在使用时,需要定义接口或者父类,被代理对象与代理 ...

  9. 什么java工厂模式_java的三种工厂模式是什么?

    java的三种工厂模式:1.简单工厂模式,提供一个创建对象实例的功能,而无须关心其具体实现:2.工厂方法模式:3.抽象工厂模式,提供一个创建一系列相关或相互依赖对象的接口,无须指定它们具体的类. 一. ...

  10. zmq java 消息阻塞_ZMQ的三种消息模式

    一. ZMQ是什么? 普通的socket是端对端(1:1)的关系,ZMQ是N:M的关系,socket的连接需要显式地建立连接,销毁连接,选择协议(TCP/UDP)和 错误处理,ZQM屏蔽了这些细节,像 ...

最新文章

  1. 抛出运行时异常的目的_「JAVA」运行时异常、编译时异常、自定义异常,通过案例实践转译和异常链...
  2. web.config文件中的特殊字符处理
  3. Ubuntu 安装SVN服务器端
  4. python内建函数是什么意思_python内建函数是什么意思
  5. Spring容器创建流程(6)国际化支持
  6. LNMP+FARM+DNS
  7. spark加载数据的方式
  8. ubuntu之间传输文件
  9. 关于中国男女的一些私密数据......
  10. 摄像头(WebCam)在Linux操作系统中的驱动方法
  11. Cross-speaker Style Transfer with Prosody Bottleneck in Neural Speech Synthesis
  12. 抽象代数之S3的自同构群和S3的内自同构群
  13. 从TCL的40年变革史,看中国制造之路
  14. java 给pdf文档加水印
  15. H3C AP状态指示灯
  16. Java猫和狗(继承,多态,抽象,接口版)上
  17. 越睡越累,原因竟然是这个!
  18. 环境工程部门怎么实施自动化软件学习时间更多
  19. 编写一个截取字符串的函数
  20. JSON Editor 中文文档

热门文章

  1. 8路开关量输入8路继电器输出的网络模块
  2. 零基础 Java 学习笔记
  3. java时间的格式化_java如何给时间格式化
  4. kali linux查看局域网下所有IP,并对指定IP实施局域网内攻击
  5. PPAPI插件与浏览器的通信
  6. 丁磊推荐《你的灯亮着吗》为三大管理必读书
  7. 关于极限编程简单介绍
  8. PHP 中文手册-国内镜像
  9. 基于elementui的年月日周时间控件
  10. 百度前员工因内网发布“女优一览表”被辞退,自诉原因:想转岗鉴黄师.........