目錄
同步?異步?阻塞?非阻塞?
同步:用戶進程觸發IO操作並等待或者輪詢的去查看IO操作是否就緒。
異步:用戶進程觸發IO操作以後便開始做自己的事情,而當IO操作已經完成的時候會得到IO完成的通知(異步的特點就是通知)
阻塞:當試圖對該文件描述符進行讀寫時, 如果當時沒有東西可讀,或者暫時不可寫, 程序就進入等待 狀態, 直到有東西可讀或者可寫爲止
非阻塞: 如果沒有東西可讀, 或者不可寫, 讀寫函數馬上返回, 而不會等待
綜上所述:
同步和異步是針對應用程序和內核的交互而言的,同步指的是用戶進程觸發IO操作並等待或者輪詢的去查看IO操作是否就緒,而異步是指用戶進程觸發IO操作以後便開始做自己的事情,而當IO操作已經完成的時候會得到IO完成的通知。
阻塞和非阻塞是針對於進程在訪問數據的時候,根據IO操作的就緒狀態來採取的不同方式,說白了是一種讀取或者寫入操作函數的實現方式,阻塞方式下讀取或者寫入函數將一直等待,而非阻塞方式下,讀取或者寫入函數會立即返回一個狀態值。
同步和異步是目的,阻塞和非阻塞是實現方式。
=======================================================================
同步IO和異步IO,阻塞IO和非阻塞IO 區別
一個IO操作其實分成了兩個步驟:發起IO請求 和 實際的IO操作。
同步IO和異步IO的區別就在於第二個步驟是否阻塞,如果實際的IO讀寫阻塞請求進程,那麼就是同步IO
阻塞IO和非阻塞IO的區別在於第一步,發起IO請求是否會被阻塞,如果阻塞直到完成那麼就是傳統的阻塞IO,如果不阻塞,那麼就是非阻塞IO。
=======================================================================
概念
一、同步阻塞I/O(BIO):jdk1.4以前 面向流
用戶進程在發起一個IO操作以後,必須等待IO操作的完成,只有當真正完成了IO操作以後,用戶進程才能運行。JAVA傳統的IO模型屬於此種方式。
服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,可以通過線程池機制來改善。
BIO方式架適用於連接數目比較小且固定的構,這種方式對服務端資源要求比較高,併發侷限於應用中。
二、同步非阻塞I/O(NIO):jdk1.4開始支持 面向緩衝區
用戶進程發起一個IO操作以後便可返回做其它事情,但是用戶進程需要時不時的詢問IO操作是否就緒,這就要求用戶進程不停的去詢問,從而引入不必要的CPU資源浪費。
服務器實現模式爲一個請求一個線程,即客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有IO請求時才啓動一個線程進行處理。
NIO方式適用於連接數目多且連接比較短(輕操作)的架構,比如聊天服務器,併發侷限於應用中,編程比較複雜。
三、異步非阻塞I/O(AIO):jdk1.7開始支持
應用發起一個IO操作以後,不等待內核IO操作的完成,等內核完成IO操作以後會通知應用程序。
服務器實現模式爲一個有效請求一個線程,客戶端的IO請求都是由操作系統先完成了再通知服務器用其啓動線程進行處理。
AIO方式適用於連接數目多且連接比較長(重操作)的架構,比如相冊服務器,充分調用OS參與併發操作,編程比較複雜。
喻:
目標:燒十壺開水。
IO:需要10個人,一個人盯一壺。
BIO:需要1個人,輪流查看幾個壺。
AIO:需要1個人兼職,給壺裝個開關,水開後提示,水未開時人可以去做任何事。
=======================================================================
BIO(IO)
Socket又稱“套接字”,應用程序通常通過“套接字”向網絡發出請求或者應答網絡請求。
Socket和ServerSocket類庫位於java.net包中,serverSocket用於服務器端,Socket是建立網絡連接時使用的。
在連接成功時,應用程序兩端都會產生一個Socket實例,操作這個實例,完成所需的會話。
對於一個網絡連接來說,套接字是平等的,不因爲在服務器端或在客戶端而產生不同的級別,
Socket和ServerSocket都是通過SocketImpl類及其子類完成的
缺點:該模型最大的問題就是,當客戶端數量增加時,服務端的線程數與客戶端併發的數量是 1:1 關係,我們知道在jvm 中線程是非常寶貴的資源,當線程數不斷上升時,系統性能將不斷下降,可能會出現堆棧溢出,創建線程失敗等問題,並最終導致宕機或者僵死,不對外提供服務。
網絡編程的基本模型Client/Server模型,也就是兩個進程直接進行相互通信,其中服務端提供配置信息(綁定的IP地址和監聽端口),客戶端通過連接操作向服務端監聽的地址發起連接請求,通過三次握手建立連接,如果連接成功,則雙方即可進行通信(網絡套接字socket)
優化:
採用線程池和任務隊列可以實現一種僞異步的IO通信框架。
我們學過連接池的使用和隊列的使用,其實就是將客戶端的socket封裝成一個task任務(實現runnable接口的類)然後投遞到線程池中去,配置相應的隊列進行實現。
代碼演示
package com.io.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ServerHandler implements Runnable {
private Socket socket;
public ServerHandler(Socket socket) {
this.socket = socket;
}
public void run() {
BufferedReader in = null;
PrintWriter out = null;
try {
in = new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
out = new PrintWriter(this.socket.getOutputStream(), true);
String body = null;
while (true) {
body = in.readLine();
if (body == null) {
break;
}
System.out.println("Server :"+ body);
out.println("服務器端回送響的應數據.");
}
} catch (IOException e) {
if (in != null) {
try {
in.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (out != null) {
out.close();
out = null;
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e1) {
e1.printStackTrace();
}
this.socket = null;
}
}
}
}
package com.io.bio;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
public static void main(String[] args) throws IOException {
final int port = 8088;
ServerSocket server = null;
try {
server = new ServerSocket(port);
System.out.println("服務器端口號:" + port);
Socket socket;
while (true){
//進行阻塞
socket = server.accept();
//新建一個線程執行客戶端的任務
new Thread(new ServerHandler(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
}finally {
if(server != null){
System.out.println("The time server close");
server.close();
server = null;
}
}
}
}
package com.io.bio;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class Client {
public static void main(String[] args) {
final int port = 8088;
Socket socket = null;
BufferedReader reader = null;
PrintWriter writer = null;
try {
socket = new Socket("127.0.0.1", port);
} catch (IOException e) {
System.out.println("客戶端初始化失敗");
}
try {
writer = new PrintWriter(socket.getOutputStream(),true);
reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
} catch (IOException e) {
System.out.println("輸入輸出流獲取失敗");
}
writer.println("客戶端向服務器端發送的請求");
try {
String readLine = reader.readLine();
System.out.println("Client: " + readLine);
} catch (IOException e) {
System.out.println("讀取失敗");
}finally {
if(writer != null){
writer.close();
writer = null;
}
if(reader != null){
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
reader = null;
}
}
}
}
=======================================================================
NIO
NIO 叫做 new IO,也叫做 Non-block IO , 即非阻塞IO
傳統的IO流(inputstream,outputstream)都是單向的管道,要麼去讀,要麼去寫。
有一個server端,有n個client端。古老的socket編程,是每個客戶端直接向server端發起一個套接字,建立一個tcp連接。
NIO是在傳統的tcp之上進行一個抽象。不是client端和server端直接進行連接,而是,把client的通道註冊到server端。
在NIO中沒有Socket和ServerSocket的概念。
在NIO中服務端要實例化出一個ServerSocketChannel(把ServerSocket進行了一個抽象),客戶端使用SocketChannel。
在Server端會創建一個Selector(多路複用器)。所有客戶端的SocketChannel都要註冊到Selector。
Selector的機制是使用一個線程,去輪詢所有註冊到這個服務器端的SocketChannel,根據通道的狀態,執行相關的操作。
概念
(1)Buffer-緩衝區
(2)Channel(管道。通道)
(3)Selector(選擇器,多路複用器)
Buffer
是一個對象,它包含一些要寫入或者要讀取的數據。在NIO類庫中加入Buffer對象,體現了新庫與原IO的一個重要的區別。在面向流的IO中,可以將數據直接寫入或讀取到Stream對象中。在NIO庫中,所有的數據都是用緩衝區處理的(讀寫)。緩衝區實質上是一個數組,通常它是一個字節數組(ByteBuffer),也可以使用其他類型的數組,這個數組爲緩衝區提供了數據的訪問讀寫等操作屬性,如位置,容量,上限等概念,參考API文檔。Buffer類型:我們最常用的就是ByteBuffer,實際上每一種java基本類型都對應了一種緩衝區(除了Boolean類型)
ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBUffer、FloatBuffer、DoubleBUffer
使用:
flip() 將Buffer從寫模式切換到讀模式。調用flip()方法會將position設回0,並將limit設置成之前position的值。
get() 讀取數據
rewind() 將position設回0,所以你可以重讀Buffer中的所有數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素
mark() 可以標記Buffer中的一個特定position。之後可以通過調用。
reset() 恢復到Buffer.mark()標記時的position。
clear() 清空整個緩衝區。position將被設回0,limit被設置成 capacity的值
compact() 只會清除已經讀過的數據;任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。
將position設到最後一個未讀元素正後面,limit被設置成 capacity的值。
通道(Channel)
它就像自來水管道一樣,網絡數據通過Channel讀取和寫入,通道與流不同之處在於通道是雙向的,而流只是一個方向上移動(一個流必須是inputStream或者outputStream的子類),而通道可以用於讀,寫或者二者同時進行,最關鍵的是可以與多路複用器結合起來,有多種的狀態位,方便多路複用器去識別。
事實上通道分爲兩大類,一類是網絡讀寫的(SelectableChannel),一類是用於文件操作的(FileChannel),我們使用的SocketChannel和ServerSockerChannel都是SelectableChannel的子類
使用
FileChannel:用於讀取、寫入、映射和操作文件的通道。
DatagramChannel:通過 UDP 讀寫網絡中的數據通道。
SocketChannel:通過 TCP 讀寫網絡中的數據。
ServerSocketChannel:可以監聽新進來的 TCP 連接,對每一個新進來的連接都會創建一個 SocketChannel。
AbstractInterruptibleChannel
AbstractSelectableChannel
Pipe.SinkChannel
Pipe.SourceChannel
SelectableChannel
多路複用器(seletor)
他是NIO編程的基礎,非常重要,多路複用器提供選擇已經就緒的任務的能力。
Selector線程就類似一個管理者(Master),管理了成千上萬個管道,然後輪詢哪個管道的數據已經準備好,通知CPU執行IO的讀取或寫入操作。
Selector模式:當IO事件(管理)註冊到選擇器以後,selector會分配給每個管道一個key值,相當於標籤。selector選擇器是以輪詢的方式進行查找註冊的所有IO事件(管道)
如果某個通道發生了讀寫操作,這個通道就處於就緒狀態,select就會識別,會通過key值來找到相應的管道,進行相關的數據處理操作(從管道里讀或寫數據,寫道我們的數據緩衝區中)。
每個管道都會對選擇器進行註冊不同的事件狀態,以便選擇器查找。
SelectionKey.OP_CONNECT 連接狀態
SelectionKey.OP_ACCEPT 阻塞狀態
SelectionKey.OP_READ 可讀狀態
SelectionKey.OP_WRITE 可寫狀態
創建NIO服務器的主要步驟
(1)打開ServerSocketChannel,監聽客戶端連接
(2)綁定監聽端口,設置連接爲非阻塞模式
(3)創建Reactor線程,創建多路複用器並啓動線程
(4)將ServerSocketChannel註冊到Reactor線程中的Selector上,監聽ACCEPT事件
(5)Selector輪詢準備就緒的key
(6)Selector監聽到新的客戶端接入,處理新的接入請求,完成TCP三次握手,簡歷物理鏈路
(7)設置客戶端鏈路爲非阻塞模式
(8)將新接入的客戶端連接註冊到Reactor線程的Selector上,監聽讀操作,讀取客戶端發送的網絡消息
(9)異步讀取客戶端消息到緩衝區
(10)對Buffer編解碼,處理半包消息,將解碼成功的消息封裝成Task
(11)將應答消息編碼爲Buffer,調用SocketChannel的write將消息異步發送給客戶端
因爲應答消息的發送,SocketChannel也是異步非阻塞的,所以不能保證一次能吧需要發送的數據發送完,此時就會出現寫半包的問題。我們需要註冊寫操作,不斷輪詢Selector將沒有發送完的消息發送完畢,然後通過Buffer的hasRemain()方法判斷消息是否發送完成。
代碼演示
package com.io.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
public class Server implements Runnable {
//多路複用器(管理所有的通道)
private Selector seletor;
//2 建立緩衝區
private ByteBuffer readBuf = ByteBuffer.allocate(1024);
//3
private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
public Server(int port) {
try {
//步驟一: 打開服務器通道(ServerSocketChannel),用於監聽客戶端的連接,它是所有客戶端連接的副管道
ServerSocketChannel socketChannel = ServerSocketChannel.open();
//步驟二: 綁定監聽端口,設置連接爲非阻塞模式
socketChannel.bind(new InetSocketAddress(port));
socketChannel.configureBlocking(false);
//步驟三: 創建多路複用器(Selector)
this.seletor = Selector.open();
//步驟四: 把服務器通道(ServerSocketChannel)註冊到多路複用器(Selector)上,並且監聽阻塞(ACCEPT)事件
socketChannel.register(this.seletor, SelectionKey.OP_ACCEPT);
System.out.println("Server start, port :" + port);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
//步驟五:多路複用器在線程run方法的無線循環體內輪詢準備就緒的key
while (true) {
try {
//1 必須要讓多路複用器開始監聽
this.seletor.select();
//2 返回多路複用器已經選擇的結果集
Iterator<SelectionKey> keys = this.seletor.selectedKeys().iterator();
//3 進行遍歷
while (keys.hasNext()) {
//4 獲取一個選擇的元素
SelectionKey key = keys.next();
//5 直接從容器中移除就可以了
keys.remove();
//6 如果是有效的
if (key.isValid()) {
//7 如果爲阻塞狀態
if (key.isAcceptable()) {
//步驟六:多路複用器監聽到有新的客戶端接入,處理新的接入請求,完成TCP三次握手,建立物理連接
//步驟七:設置客戶端鏈路爲非阻塞模式 3
//步驟八:將新接入的客戶端連接註冊到的多路複用器(Selector)上,監聽讀操作,讀取客戶端發送的網絡消息 4
this.accept(key);
}
//8 如果爲可讀狀態
if (key.isReadable()) {
//步驟九:異步讀取客戶端請求消息到緩衝區
this.read(key);
}
//9 寫數據
if (key.isWritable()) {
this.write(key);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void write(SelectionKey key) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
try {
ssc.register(this.seletor, SelectionKey.OP_WRITE);
} catch (ClosedChannelException e) {
e.printStackTrace();
}
}
private void read(SelectionKey key) {
try {
//1 清空緩衝區舊的數據
this.readBuf.clear();
//2 獲取之前註冊的socket通道對象
SocketChannel sc = (SocketChannel) key.channel();
//3 讀取數據
int count = sc.read(this.readBuf);
//4 如果沒有數據
if (count == -1) {
key.channel().close();
key.cancel();
return;
}
//5 有數據則進行讀取 讀取之前需要進行復位方法(把position 和limit進行復位)
this.readBuf.flip();
//6 根據緩衝區的數據長度創建相應大小的byte數組,接收緩衝區的數據
byte[] bytes = new byte[this.readBuf.remaining()];
//7 接收緩衝區數據
this.readBuf.get(bytes);
//8 打印結果
String body = new String(bytes).trim();
// 9.可以寫回給客戶端數據
System.out.println("Server : " + body);
} catch (IOException e) {
e.printStackTrace();
}
}
private void accept(SelectionKey key) {
try {
//1 獲取服務通道
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
//2 執行阻塞方法
SocketChannel sc = ssc.accept();
//3 設置爲非阻塞模式
sc.configureBlocking(false);
sc.socket().setReuseAddress(true);
//4 註冊到多路複用器上,並設置讀取標識
sc.register(this.seletor, SelectionKey.OP_READ);
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(new Server(8081)).start();
}
}
package com.io.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class Client {
//需要一個Selector
public static void main(String[] args) {
//創建連接的地址
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8081);
//聲明連接通道
SocketChannel sc = null;
//建立緩衝區
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
//打開通道
sc = SocketChannel.open();
//進行連接
sc.connect(address);
while(true){
//定義一個字節數組,然後使用系統錄入功能:
byte[] bytes = new byte[1024];
System.in.read(bytes);
buf.put(bytes);//把數據放到緩衝區中
buf.flip();//對緩衝區進行復位
sc.write(buf);//寫出數據
buf.clear();//清空緩衝區數據
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if(sc != null){
try {
sc.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
推薦NIO底層講解網址:
=======================================================================
AIO
package com.io.aio;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.ExecutionException;
public class ServerHandler implements CompletionHandler<AsynchronousSocketChannel, Server> {
@Override
public void completed(AsynchronousSocketChannel asc, Server attachment) {
//當有下一個客戶端接入的時候 直接調用Server的accept方法,這樣反覆執行下去,保證多個客戶端都可以阻塞(沒有遞歸上限)
// 1.7以後AIO才實現了異步非阻塞
attachment.assc.accept(attachment, this);
read(asc);
}
private void read(final AsynchronousSocketChannel asc) {
//讀取數據
ByteBuffer buf = ByteBuffer.allocate(1024);
asc.read(buf, buf, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer resultSize, ByteBuffer attachment) {
//進行讀取之後,重置標識位
attachment.flip();
//獲得讀取的字節數
System.out.println("Server -> " + "收到客戶端的數據長度爲:" + resultSize);
//獲取讀取的數據
String resultData = new String(attachment.array()).trim();
System.out.println("Server -> " + "收到客戶端的數據信息爲:" + resultData);
String response = "服務器響應, 收到了客戶端發來的數據: " + resultData;
write(asc, response);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
exc.printStackTrace();
}
});
}
private void write(AsynchronousSocketChannel asc, String response) {
try {
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.put(response.getBytes());
buf.flip();
asc.write(buf).get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Server attachment) {
exc.printStackTrace();
}
}
package com.io.aio;
import java.net.InetSocketAddress;
import java.nio.channels.AsynchronousChannelGroup;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Server{
//線程池
private ExecutorService executorService;
//線程組
private AsynchronousChannelGroup threadGroup;
//服務器通道
public AsynchronousServerSocketChannel assc;
public Server(int port){
try {
//創建一個緩存池
executorService = Executors.newCachedThreadPool();
//創建線程組
threadGroup = AsynchronousChannelGroup.withCachedThreadPool(executorService, 1);
//創建服務器通道
assc = AsynchronousServerSocketChannel.open(threadGroup);
//進行綁定
assc.bind(new InetSocketAddress(port));
System.out.println("server start , port : " + port);
//進行阻塞
assc.accept(this, new ServerHandler());
//一直阻塞 不讓服務器停止
Thread.sleep(Integer.MAX_VALUE);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
Server server = new Server(8082);
}
}
package com.io.aio;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.concurrent.ExecutionException;
public class Client implements Runnable{
private AsynchronousSocketChannel asc ;
public Client() throws Exception {
asc = AsynchronousSocketChannel.open();
}
public void connect(){
asc.connect(new InetSocketAddress("127.0.0.1", 8082));
}
public void write(String request){
try {
asc.write(ByteBuffer.wrap(request.getBytes())).get();
read();
} catch (Exception e) {
e.printStackTrace();
}
}
private void read() {
ByteBuffer buf = ByteBuffer.allocate(1024);
try {
asc.read(buf).get();
buf.flip();
byte[] respByte = new byte[buf.remaining()];
buf.get(respByte);
System.out.println(new String(respByte,"utf-8").trim());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while(true){
}
}
public static void main(String[] args) throws Exception {
Client c1 = new Client();
c1.connect();
Client c2 = new Client();
c2.connect();
Client c3 = new Client();
c3.connect();
new Thread(c1, "c1").start();
new Thread(c2, "c2").start();
new Thread(c3, "c3").start();
Thread.sleep(1000);
c1.write("c1 aaa");
c2.write("c2 bbbb");
c3.write("c3 ccccc");
}
}
=======================================================================
參考地址