深入理解Java三种IO模式和Epoll模型
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模型相关推荐
- java 三种工厂模式
java 三种工厂模式 一.简单工厂模式 一个栗子: 我喜欢吃面条,抽象一个面条基类,(接口也可以),这是产品的抽象类. public abstract class INoodles {/*** 描 ...
- 【Netty】IO 模型简介 ( Netty 特点 | Netty 应用场景 | Java 三种 IO 模型 | BIO 模型 )
文章目录 I . Netty 简介 II . Netty 应用场景 III . Java I/O 模型 IV . BIO 概念 V . BIO 开发流程 VI . BIO 实例 VII . BIO 模 ...
- 从同步阻塞聊到Java三种IO方式
对于刚刚成为程序猿不久的人,可能常常会被以下几个概念所混淆: 同步,异步,阻塞,非阻塞?以及从这几个概念中衍生出的几个概念,同步阻塞,同步非阻塞,异步阻塞,异步非阻塞? 小编从网上查了一些资料,发现对 ...
- java 三种工厂模式(简单工厂+工厂方法+抽象工厂)
一.简单工厂模式 概述 简单工厂模式:定义一个工厂类,它可以根据参数的不同返回不同类的 实例,被创建的实例通常都具有共同的父类.因为在简单工厂模式中用于创建实例的方法是静态(static)方法,因 ...
- VMware虚拟机三种网络模式
Python微信订餐小程序课程视频 https://edu.csdn.net/course/detail/36074 Python实战量化交易理财系统 https://edu.csdn.net/cou ...
- Java的三种代理模式简述
本文着重讲述三种代理模式在java代码中如何写出,为保证文章的针对性,暂且不讨论底层实现原理,具体的原理将在下一篇博文中讲述. 代理模式是什么 代理模式是一种设计模式,简单说即是在不改变源码的情况下, ...
- Java的三种代理模式完整源码分析
Java的三种代理模式&完整源码分析 Java的三种代理模式&完整源码分析 参考资料: 博客园-Java的三种代理模式 简书-JDK动态代理-超详细源码分析 [博客园-WeakCach ...
- Java的三种代理模式【附源码分析】
Java的三种代理模式&完整源码分析 代理模式分为两种,静态代理和动态代理,动态代理包括JDK动态代理和Cglib动态代理. 静态代理 静态代理在使用时,需要定义接口或者父类,被代理对象与代理 ...
- 什么java工厂模式_java的三种工厂模式是什么?
java的三种工厂模式:1.简单工厂模式,提供一个创建对象实例的功能,而无须关心其具体实现:2.工厂方法模式:3.抽象工厂模式,提供一个创建一系列相关或相互依赖对象的接口,无须指定它们具体的类. 一. ...
- zmq java 消息阻塞_ZMQ的三种消息模式
一. ZMQ是什么? 普通的socket是端对端(1:1)的关系,ZMQ是N:M的关系,socket的连接需要显式地建立连接,销毁连接,选择协议(TCP/UDP)和 错误处理,ZQM屏蔽了这些细节,像 ...
最新文章
- 抛出运行时异常的目的_「JAVA」运行时异常、编译时异常、自定义异常,通过案例实践转译和异常链...
- web.config文件中的特殊字符处理
- Ubuntu 安装SVN服务器端
- python内建函数是什么意思_python内建函数是什么意思
- Spring容器创建流程(6)国际化支持
- LNMP+FARM+DNS
- spark加载数据的方式
- ubuntu之间传输文件
- 关于中国男女的一些私密数据......
- 摄像头(WebCam)在Linux操作系统中的驱动方法
- Cross-speaker Style Transfer with Prosody Bottleneck in Neural Speech Synthesis
- 抽象代数之S3的自同构群和S3的内自同构群
- 从TCL的40年变革史,看中国制造之路
- java 给pdf文档加水印
- H3C AP状态指示灯
- Java猫和狗(继承,多态,抽象,接口版)上
- 越睡越累,原因竟然是这个!
- 环境工程部门怎么实施自动化软件学习时间更多
- 编写一个截取字符串的函数
- JSON Editor 中文文档
热门文章
- 8路开关量输入8路继电器输出的网络模块
- 零基础 Java 学习笔记
- java时间的格式化_java如何给时间格式化
- kali linux查看局域网下所有IP,并对指定IP实施局域网内攻击
- PPAPI插件与浏览器的通信
- 丁磊推荐《你的灯亮着吗》为三大管理必读书
- 关于极限编程简单介绍
- PHP 中文手册-国内镜像
- 基于elementui的年月日周时间控件
- 百度前员工因内网发布“女优一览表”被辞退,自诉原因:想转岗鉴黄师.........