很多人說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**。內核等待網卡數據到來,把數據從網卡拷貝到內核空間,接著把數據拷貝到用戶空間,再把用戶線程叫醒。

# 3. 同步非阻塞IO
用戶進程主動發起read調用,這是個系統調用,CPU由用戶態切換到內核態,執行內核代碼。
內核發現該socket上的數據已到內核空間,將用戶線程掛起,然后把數據從內核空間拷貝到用戶空間,再喚醒用戶線程,read調用返回。
**用戶線程不斷發起read調用(此時沒有阻塞,上邊那個調用read直接阻塞了)**,數據沒到內核空間時,每次都返回失敗,直到數據到了內核空間,這次read調用后,在等待數據從內核空間拷貝到用戶空間這段時間里,線程還是阻塞的,等數據到了用戶空間再把線程叫醒。
# 4. I/O多路復用
用戶線程的讀取操作分成兩步:
* **線程先發起select調用**,問內核:數據準備好了嗎?
* 等內核把數據準備好了,**用戶線程再發起read調用**
* 在等待數據從內核空間拷貝到用戶空間這段時間里,線程還是阻塞的
為什么叫I/O多路復用?
因為一次select調用可以向內核查多個數據通道(Channel)的狀態。

# 5. 異步I/O
用戶線程發起read調用的同時注冊一個回調函數,read立即返回,等內核將數據準備好后,再調用指定的回調函數完成處理。在這個過程中,用戶線程一直沒有阻塞。

# 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
- 規范
- 數據庫