很多人说BIO不好,会“block”,但到底什么是IO的Block呢?考虑下面两种情况:
* 用系统调用`read`从socket里读取一段数据
* 用系统调用`read`从一个磁盘文件读取一段数据到内存
如果你的直觉告诉你,这两种都算“Block”,那么很遗憾,你的理解与[Linux](https://so.csdn.net/so/search?from=pc_blog_highlight&q=Linux)不同。Linux认为:
* 对于第一种情况,算作block,因为Linux无法知道网络上对方是否会发数据。如果没数据发过来,对于调用`read`的程序来说,就只能“等”。
* 对于第二种情况,**不算做block**。
是的,对于磁盘文件IO,Linux总是不视作Block。
你可能会说,这不科学啊,磁盘读写偶尔也会因为硬件而卡壳啊,怎么能不算Block呢?但实际就是不算。
> 一个解释是,**所谓“Block”是指操作系统可以预见这个Block会发生才会主动Block**。例如当读取TCP连接的数据时,如果发现Socket buffer里没有数据就可以确定定对方还没有发过来,于是Block;而对于普通磁盘文件的读写,也许磁盘运作期间会抖动,会短暂暂停,但是操作系统无法预见这种情况,只能视作不会Block,照样执行。
基于这个基本的设定,在讨论IO时,一定要严格区分网络IO和磁盘文件IO。NIO和后文讲到的IO多路复用只对网络IO有意义。
> 严格的说,O\_NONBLOCK和IO多路复用,对标准输入输出描述符、管道和FIFO也都是有效的。但本文侧重于讨论高性能网络服务器下各种IO的含义和关系,所以本文做了简化,只提及网络IO和磁盘文件IO两种情况。
# 1. 为什么需要IO模型
1.cpu基于内存计算(内存存取速度远高于外部设备),所以在进行数据读取时,首先将数据从外部设备读取到内存,这个拷贝过程可能比较耗时,这时cpu应该空闲等待,还是处理其他请求,这时io模型需要解决的事情。
## 1.2 网络IO
对于一个网络I/O通信过程,比如网络数据读取,会涉及两个对象:
* 调用这个I/O操作的用户线程
* 操作系统内核
一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。
当用户线程发起I/O操作后(Selector发出的select调用就是一个I/O操作),网络数据读取操作会经历两个步骤:
1. 用户线程等待内核将数据从网卡拷贝到内核空间
2. 内核将数据从内核空间拷贝到用户空间
有人会好奇,内核数据从内核空间拷贝到用户空间,这样会不会有点浪费?
毕竟实际上只有一块内存,能否直接把内存地址指向用户空间可以读取?
Linux中有个叫mmap的系统调用,可以将磁盘文件映射到内存,省去了内核和用户空间的拷贝,但不支持网络通信场景!各种I/O模型的区别就是这两个步骤的方式不一样。
# 2. 同步阻塞I/O
**因为IO,阻塞线程**
**用户线程发起read调用后就阻塞了**,**让出CPU**。内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒。
![](https://img.kancloud.cn/43/9e/439e641ca3d7a606dc88729e1b276803_640x549.png)
# 3. 同步非阻塞IO
用户进程主动发起read调用,这是个系统调用,CPU由用户态切换到内核态,执行内核代码。
内核发现该socket上的数据已到内核空间,将用户线程挂起,然后把数据从内核空间拷贝到用户空间,再唤醒用户线程,read调用返回。
**用户线程不断发起read调用(此时没有阻塞,上边那个调用read直接阻塞了)**,数据没到内核空间时,每次都返回失败,直到数据到了内核空间,这次read调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程叫醒。![](https://img.kancloud.cn/f8/b6/f8b6ae5b0823c22f360b61d5c11be331_640x561.png)
# 4. I/O多路复用
用户线程的读取操作分成两步:
* **线程先发起select调用**,问内核:数据准备好了吗?
* 等内核把数据准备好了,**用户线程再发起read调用**
* 在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的
为什么叫I/O多路复用?
因为一次select调用可以向内核查多个数据通道(Channel)的状态。
![](https://img.kancloud.cn/7c/15/7c159c14a7cfb3e0720b3f521626455f_640x565.png)
# 5. 异步I/O
用户线程发起read调用的同时注册一个回调函数,read立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。
![](https://img.kancloud.cn/e8/ea/e8eaad8656f2cb652445cdb4ae0e8e97_640x578.png)
# 6. Java nio
## 6.1 NIOClient
```
public class NIOClient {
/*标识数字*/
private static int flag = 0;
/*缓冲区大小*/
private static int BLOCK = 4096;
/*接受数据缓冲区*/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/*服务器端地址*/
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"localhost", 8888);
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
// 打开socket通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞方式
socketChannel.configureBlocking(false);
// 打开选择器
Selector selector = Selector.open();
// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 连接
socketChannel.connect(SERVER_ADDRESS);
// 分配缓冲区大小内存
Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count=0;
while (true) {
//选择一组键,其相应的通道已为 I/O 操作准备就绪。
//此方法执行处于阻塞模式的选择操作。
selector.select();
//返回此选择器的已选择键集。
selectionKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("client connect");
client = (SocketChannel) selectionKey.channel();
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("完成连接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count=client.read(receivebuffer);
if(count>0){
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("客户端接受服务器端数据--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--" + (flag++);
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客户端向服务器端发送数据--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
selectionKeys.clear();
}
}
}
```
## 6.2 NIOServer
```
public class NIOServer {
/*标识数字*/
private int flag = 0;
/*缓冲区大小*/
private int BLOCK = 4096;
/*接受数据缓冲区*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
private Selector selector;
public NIOServer(int port) throws IOException {
/**
* 以下的所有说明均已linux系统底层进行说明:
* nio 的底层实现是 epoll 模式,采用多路复用技术,对nio的代码进行深入分析,结合epoll的底层实现
* 进行详细的说明
* 1.linux网络编程是两个进程之间的通信,跨集群合网络
* 2.开启一个socket线程,在linux系统上任何操作均以文件句柄数表示,默认情况下
* 一个线程可以打开1024个句柄,也就说最多同时支持1024个网络连接请求。阿里云默认打开65535个文件
* 句柄,通常情况下,1G内存最多可以打开10w个句柄数
*
*
*/
// 打开服务器套接字通道
// 底层: 在linux上面开启socket服务,启动一个线程。绑定ip地址和端口号
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 服务器配置为非阻塞
serverSocketChannel.configureBlocking(false);
// 检索与此通道关联的服务器套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// 进行服务的绑定
serverSocket.bind(new InetSocketAddress(port));
// 通过open()方法找到Selector
// 底层: 开启epoll,为当前socket服务创建epoll服务,epoll_create
selector = Selector.open();
// 注册到selector,等待连接
/**
* 底层:
* 1.将当前的epoll,服务器地址,端口号绑定,如果有连接请求,直接添加到epoll中,epoll的底层是红黑树,
* 可以快速的实现连接的查找和状态更新。如果有新的连接过来,直接存放到epoll中。如果有连接过期,中断,
* 会从epoll中删除。
* 2.通过epoll_ctl添加到epoll的同时,会注册一个回调函数给内核,当网卡有数据来的时候,会通知内核,内核
* 调用回调函数,将当前内核数据的事件状态添加到list链表中
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server Start----8888:");
}
// 监听
private void listen() throws IOException {
while (true) {
// 选择一组键,并且相应的通道已经打开
/**
* epoll底层维护一个链表,rdlist,基于事件驱动模式,当网卡有数据请求过来,会发起硬件中断,通知内核已经有来了。内核调用
* 回调函数,将当前的事件添加到rdlist中,将当前可用的rdlist列表发送给用户态,用户去遍历rdlist中的事件,进行处理
*/
selector.select();
// 返回此选择器的已选择键集。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 获得当前epoll的rdlist复制到用户态,遍历,同事删除当前rdlist中的事件
iterator.remove();
handleKey(selectionKey);
}
}
}
// 处理请求
private void handleKey(SelectionKey selectionKey) throws IOException {
// 接受请求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count=0;
// 测试此键的通道是否已准备好接受新的套接字连接。
if (selectionKey.isAcceptable()) {
// 返回为之创建此键的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的连接。
// 此方法返回的套接字通道(如果有)将处于阻塞模式。
client = server.accept();
// 配置为非阻塞
client.configureBlocking(false);
// 注册到selector,等待连接
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("服务器端接受客户端数据--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
//将缓冲区清空以备下次写入
sendbuffer.clear();
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
sendText="message from server--" + flag++;
//向缓冲区中输入数据
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为向里面put了数据标志被改变要想从中读取数据发向服务器,就要复位
sendbuffer.flip();
//输出到通道
client.write(sendbuffer);
System.out.println("服务器端向客户端发送数据--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
/**
* @param args
* @throws IOException
*/
public static void main(String[] args) throws IOException {
// TODO Auto-generated method stub
int port = 8888;
NIOServer server = new NIOServer(port);
server.listen();
}
}
```
- 计算机网络
- 基础_01
- tcp/ip
- http转https
- Let's Encrypt免费ssl证书(基于haproxy负载)
- what's the http?
- 网关
- 网络IO
- http
- 工具
- Git
- 初始本地仓库并上传
- git保存密码
- Gitflow
- maven
- 1.生命周期命令
- 聚合与继承
- 插件管理
- assembly
- 资源管理插件
- 依赖范围
- 分环境打包
- dependencyManagement
- 版本分类
- 找不到主类
- 无法加载主类
- 私服
- svn
- gradle
- 手动引入第三方jar包
- 打包exe文件
- Windows
- java
- 设计模式
- 七大原则
- 1.开闭原则
- 2. 里式替换原则
- 3. 依赖倒置原则
- 4. 单一职责原则
- 单例模式
- 工厂模式
- 简单工厂
- 工厂方法模式
- 抽象工厂模式
- 观察者模式
- 适配器模式
- 建造者模式
- 代理模式
- 适配器模式
- 命令模式
- json
- jackson
- poi
- excel
- easy-poi
- 规则
- 模板
- 合并单元格
- word
- 读取
- java基础
- 类路径与jar
- 访问控制权限
- 类加载
- 注解
- 异常处理
- String不可变
- 跨域
- transient关键字
- 二进制编码
- 泛型1
- 与或非
- final详解
- Java -jar
- 正则
- 读取jar
- map
- map计算
- hashcode计算原理
- 枚举
- 序列化
- URLClassLoader
- 环境变量和系统变量
- java高级
- java8
- 1.Lambda表达式和函数式接口
- 2.接口的默认方法和静态方法
- 3.方法引用
- 4.重复注解
- 5.类型推断
- 6.拓宽注解的应用场景
- java7-自动关闭资源机制
- 泛型
- stream
- 时区的正确理解
- StringJoiner字符串拼接
- 注解
- @RequestParam和@RequestBody的区别
- 多线程
- 概念
- 线程实现方法
- 守护线程
- 线程阻塞
- 笔试题
- 类加载
- FutureTask和Future
- 线程池
- 同步与异步
- 高效简洁的代码
- IO
- ThreadLocal
- IO
- NIO
- 图片操作
- KeyTool生成证书
- 压缩图片
- restful
- 分布式session
- app保持session
- ClassLoader.getResources 能搜索到的资源路径
- java开发规范
- jvm
- 高并发
- netty
- 多线程与多路复用
- 异步与事件驱动
- 五种IO模型
- copy on write
- code style
- 布隆过滤器
- 笔试
- 数据库
- mybatis
- mybatis与springboot整合配置
- pagehelper
- 分页数据重复问题
- Java与数据库之间映射
- 拦截器
- 拦截器应用
- jvm
- 堆内存测试
- 线程栈
- 直接内存
- 内存结构
- 内存模型
- 垃圾回收
- 调优
- 符号引用
- 运行参数
- 方法区
- 分带回收理论
- 快捷开发
- idea插件
- 注释模板
- git
- pull冲突
- push冲突
- Excel处理
- 图片处理
- 合并单元格
- easypoi
- 模板处理
- 响应式编程
- reactor
- reactor基础
- jingyan
- 规范
- 数据库