IO模型
io模型就是各種資料使用相應通道進行發送和接收,Java共支持三種網路編程IO模式 BIO、NIO、AIO
BIO (Blocking IO)
同步阻塞模型,一個客戶端連接對應一個處理執行緒,
缺點
- IO代碼里read是阻塞操作,如果連接不做讀寫操作會導致執行緒阻塞,浪費資源
- 如果讀寫很多,會導致服務器執行緒過多,壓力太大,
應用場景
BIO適用于連接數目較小且固定的架構,這種方式對服務器資源的要求比較高,但是程式簡單易理解,

示例代碼
/**
* 服務端
* @author 風信子
*/
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
while (true){
System.out.println("等待連接...");
//阻塞方法
final Socket socket = serverSocket.accept();
System.out.println("有客戶端連接...");
new Thread(new Runnable() {
public void run() {
try{
handler(socket);
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
}
public static void handler(Socket socket) throws IOException{
System.out.println("當前執行緒:"+Thread.currentThread().getId());
byte[] bytes = new byte[1024];
System.out.println("準備read...");
// 接受客戶端資料沒有就阻塞
int read = socket.getInputStream().read(bytes);
if(read!=-1){
System.out.println("接收到客戶端的資訊為:"+new String(bytes,0,read));
System.out.println("當前執行緒:"+Thread.currentThread().getId());
}
socket.getOutputStream().write("hello client".getBytes());
socket.getOutputStream().flush();
}
}
/**
* 客戶端
* @author 風信子
*/
public class SocketClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1",9000);
socket.getOutputStream().write("Hello BIO".getBytes());
socket.getOutputStream().flush();
System.out.println("資料發送結束!");
byte[] bytes = new byte[1024];
// 接受服務器傳回的資料
socket.getInputStream().read(bytes);
System.out.println("接收到的資訊為:"+new String(bytes));
socket.close();
}
}
NIO (Non Blocking IO)
同步非阻塞模型,服務實作模型為一個執行緒可以處理多個連接(請求),客戶端的連接都會注冊到多路復用器selector上面,多路復用輪詢到連接用IO請求就進行處理,I/O 多路復用底層一般用的是Linux API (select,poll,epoll)來實作,區別見下面表格:
| select | poll | epoll(jdk1.5及以上) | |
|---|---|---|---|
| 操作方式 | 遍歷 | 遍歷 | 回呼 |
| 底層實作 | 陣列 | 鏈表 | 哈希表 |
| IO效率 | 每次呼叫進行線性遍歷,時間復雜度為O(n) | 每次呼叫進行線性遍歷,時間復雜度為O(n) | 事件通知方式,每當有io事件就緒,系統注冊的回呼函式就會被呼叫,時間復雜度為O(1) |
| 最大連接 | 有上限 | 無上限 | 無上限 |
應用場景
NIO 適用于連接數目多且連接時間短(輕量級)的架構,比如聊天服務器,彈幕系統,服務間通信,編程比較復雜,jdk1.4開始支持,

NIO 有三大組件: Channel(通道)、Buffer(緩沖區),Selector(選擇器)

- channel 類似于流,每個channel對應一個buffer緩沖區,buffer底層是個陣列
- channel 會注冊到selector上面,由selector 根據channel讀寫事件的發生將其交給空閑的執行緒處理,
- selector 可以對應一個或多個執行緒
- NIO 的Buffer和Channel都是既可以讀也可以寫的
NIO 服務端程式分析
- 創建一個ServerSocketChannel和Selector,將serverSocketChannel注冊到Selector上
- selector通過select()方法監聽channel事件,當客戶端連接時selector監聽到連接事件,獲取到ServerSocketChannel注冊時系結的selectionKey
- selectionKey通過channel()方法可以獲取系結的ServerSocketChannel
- ServerSocketChannel通過accept()方法得到SocketChannel
- 將SocketChannel注冊到Selector上,關心read事件
- 注冊后回傳一個SelectionKey,會和該SocketChannel關聯
- selector繼續通過select()方法監聽事件,當客戶端發送資料給服務端,selector監聽到read事件,獲取到SocketChannel注冊時系結的selectionKey
- selectionKey通過channel()方法可以獲取系結的socketChannel
- 將socketChannel里的資料讀取出來
- 用socketChannel將服務端資料寫回客戶端
*總結:NIO模型的selector 就像一個大總管,負責監聽各種IO事件,然后轉交給后端執行緒去處理 ,
NIO相對于BIO非阻塞的體現就在,BIO的后端執行緒需要阻塞等待客戶端寫資料(比如read方法),如果客戶端不寫資料執行緒就要阻塞, NIO把等待客戶端操作的事情交給了大總管 selector,selector 負責輪詢所有已注冊的客戶端,發現有事件發生了才轉交給后端執行緒處 理,后端執行緒不需要做任何阻塞等待,直接處理客戶端事件的資料即可,處理完馬上結束,或回傳執行緒池供其他客戶端事件繼續使用,還 有就是 channel 的讀寫是非阻塞的,
Redis就是典型的NIO執行緒模型,selector收集所有連接的事件并且轉交給后端執行緒,執行緒連續執行所有事件命令并將結果寫回客戶端*
示例代碼
/**
* @author 風信子
* NIO 服務端實作
*/
public class NioServer {
public static void main(String[] args) throws IOException {
// 創建一個本地埠監聽的服務socket通道,并設定為非阻塞方式
ServerSocketChannel ssc = ServerSocketChannel.open();
// selector是非阻塞模式,必須設定為非阻塞才能在selector上注冊,否則會報錯.
ssc.configureBlocking(false);
ssc.socket().bind(new InetSocketAddress(9000));
// 創建一個selector
Selector selector = Selector.open();
// 把ServerSocketChannel 注冊到selector上面,并設定對客戶端的accept感興趣
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true){
System.out.println("等待事件發生..");
// 輪詢監聽channel里面的key,select 是阻塞的 accept也是阻塞的
selector.select();
System.out.println("有事件發生了..");
// 輪詢監聽到客戶端請求
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()){
SelectionKey key = it.next();
// 洗掉本次處理的key ,防止下次select 重復處理
it.remove();
handler(key);
}
}
}
public static void handler(SelectionKey key) throws IOException{
if(key.isAcceptable()){
System.out.println("有客戶端連接事件發生了..");
ServerSocketChannel ssc =(ServerSocketChannel) key.channel();
// NIO非阻塞體現:此處accept方法會阻塞 但它是連接事件所有很快就會執行完,不會阻塞
// 處理完連接請求不會繼續等待客戶端的資料發送
SocketChannel sc= ssc.accept();
sc.configureBlocking(false);
// 通過Selector 監聽Channel 時對讀事件感興趣
sc.register(key.selector(), SelectionKey.OP_READ);
}else if(key.isReadable()){
System.out.println("有客戶端可讀資料事件發生..");
SocketChannel sc =(SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// NIO非阻塞體現:首先read請求不會阻塞,其次這種事件回應模型,當呼叫到read方法是肯定是客戶端發生了發送資料的事件
int len = sc.read(byteBuffer);
if(len!=-1){
System.out.println("接收到了客戶端的訊息:"+new String(byteBuffer.array(),0,len));
}
ByteBuffer bufferWrite = ByteBuffer.wrap("hello client".getBytes());
sc.write(bufferWrite);
key.interestOps(SelectionKey.OP_READ|SelectionKey.OP_WRITE);
sc.close();
}
}
}
/**
* @author 風信子
* NIO 客戶端實作
*/
public class NioClient {
Selector selector;
public static void main(String[] args) throws IOException{
NioClient nioClient = new NioClient();
nioClient.initClient("127.0.0.1",9000);
nioClient.connection();
}
public void initClient(String ip,int port) throws IOException {
// 獲取一個socket 通道
SocketChannel socketChannel = SocketChannel.open();
// 設定通道為非阻塞
socketChannel.configureBlocking(false);
// 獲取一個通道管理器
this.selector = Selector.open();
// 客戶端連接服務器,其實方法執行并沒實作連接,需要在listen()方法中
// 呼叫channel.finishConnection才能完成連接
socketChannel.connect(new InetSocketAddress(ip,port));
// 將管道管理器和通道系結,并為該通道注冊SelectionKey.OP_CONNECT事件
socketChannel.register(this.selector, SelectionKey.OP_CONNECT);
}
public void connection() throws IOException{
// 輪詢訪問selector
while(true){
// 選擇一組可以進行I/O操作的事件,放在selector中,客戶端該方法不會阻塞
// 這里和服務端的方法不一樣,查看api注釋可以知道,服務端當至少一個通道被選中時
// selector的wakeup方法被呼叫,方法回傳,而對于客戶端來說,通道是一直被選中的
this.selector.select();
Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
while (it.hasNext()){
SelectionKey key = it.next();
it.remove();
// 連接事件發生
if(key.isConnectable()){
SocketChannel socketChannel =(SocketChannel) key.channel();
// 如果正在連接則完成連接
if(socketChannel.isConnectionPending()){
socketChannel.finishConnect();
}
// 設定成非阻塞
socketChannel.configureBlocking(false);
// 向服務器發送資訊
ByteBuffer byteBuffer = ByteBuffer.wrap("hello server".getBytes());
socketChannel.write(byteBuffer);
// 連接成功之后注冊讀取服務器資訊事件
socketChannel.register(this.selector,SelectionKey.OP_READ);
}else if(key.isReadable()){
read(key);
}
}
}
}
public void read(SelectionKey key) throws IOException{
SocketChannel channel = (SocketChannel)key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int len = channel.read(byteBuffer);
if(len!=-1){
System.out.println("接收到服務端資訊:"+new String(byteBuffer.array(),0,len));
}
}
}
AIO(NIO 2.0)
異步非阻塞模型,由作業系統完成后回呼通知服務端程式啟用執行緒去處理,一般使用于連接數較多且連接時間長的應用,
應用場景
AIO方式適用于連接數目多且連接比較長(重操作)的架構,jdk7開始支持,
代碼示例
/**
* 服務端
* @author 風信子
*/
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>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try{
// 此處接收客戶端請求,不寫這行代碼客戶端連接不上服務器
serverChannel.accept(attachment,this);
System.out.println(socketChannel.getRemoteAddress());
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 異步點: 通過鉤子函式處理資料接收操作
socketChannel.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
byteBuffer.flip();
System.out.println("接收到客戶端訊息:"+new String(byteBuffer.array(),0,result));
socketChannel.write(ByteBuffer.wrap("hello client".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}catch (IOException e){
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
Thread.sleep(Integer.MAX_VALUE);
}
}
/**
* 客戶端
* @author 風信子
*/
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));
}
}
}
BIO、NIO、AIO 對比
| BIO | NIO | AIO | |
|---|---|---|---|
| IO模型 | 同步阻塞 | 同步非阻塞(多路復用) | 異步非阻塞 |
| 編程難度 | 簡單 | 復雜 | 復雜 |
| 可靠性 | 低 | 高 | 高 |
| 吞吐量 | 低 | 高 | 高 |
網路段子
老張愛喝茶,廢話不說,煮開水, 出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺),
老張把水壺放到火上,立等水開,(同步阻塞) 老張覺得自己有點傻
老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有,(同步非阻塞) 老張還是覺得自己有點傻,于是變高端了,買了把會響笛的那種水壺,水開之后,能大聲發出嘀~~~~的噪音,
老張把響水壺放到火上,立等水開,(異步阻塞) 老張覺得這樣傻等意義不大
老張把響水壺放到火上,去客廳看電視,水壺響之前不再去看它了,響了再去拿壺,(異步非阻塞) 老張覺得自己聰明了,
所謂同步異步,只是對于水壺而言,
普通水壺,同步;
響水壺,異步,
雖然都能干活,但響水壺可以在自己完工之后,提示老張水開了,
這是普通水壺所不能及的,
同步只能讓呼叫者去輪詢自己(情況2中),造成老張效率的低下,
所謂阻塞非阻塞,僅僅對于老張而言,
立等的老張,阻塞;看電視的老張,非阻塞,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/263434.html
標籤:java
