BIO與NIO的區(qū)別
NIO 和 BIO 的區(qū)別主要體現(xiàn)在三個方面:
| NIO | BIO |
|---|---|
| 基于緩沖區(qū)( Buffer ) | 基于流( Stream ) |
| 非阻塞 IO | 阻塞 IO |
| 選擇器( Selector ) | 無 |
- 其中,選擇器( Selector )是 NIO 能實(shí)現(xiàn)非阻塞的基礎(chǔ)。
BIO是阻塞式IO,NIO是同步非阻塞IO。
這是Socket的模擬,基于BIO,可以看出以下IO流的傳輸是阻塞的,在不考慮多線程的情況下,BIO無法處理并發(fā)。
如果每個連接處理都開個線程,將會造成很大的資源浪費(fèi),因?yàn)橛泻芏嗟娜怂贿B接不發(fā)信息,這種連接視為浪費(fèi)資源。
/**
* 模擬一個客戶端
*/
public class Client {
public static void main(String[] args) throws IOException {
Socket socket=null;
try {
socket=new Socket("127.0.0.1",8080);
socket.getOutputStream().write("111".getBytes());
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
socket.close();
}
}
}
/**
* 通過Socket模擬一個客戶端
*/
public class QQServer {
/**
* 定義一個字節(jié)數(shù)組用于接收客戶端內(nèi)容
*/
static byte[] bytes=new byte[1024];
public static void main(String[] args) {
try {
ServerSocket serverSocket=new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080));
//這是一個阻塞方法,直到有客戶端進(jìn)行通信
Socket socket=serverSocket.accept();
//這是一個阻塞方法
socket.getInputStream().read(bytes);
System.out.println("讀到內(nèi)容打印");
String content=new String(bytes);
System.out.println(content);
}catch (IOException ex){
}
}
}
NIO代碼層原理:邏輯代碼
List<Socket> list=new ArrayList<>();
public void main() throws IOException {
ServerSocket serverSocket=new ServerSocket();
serverSocket.bind(new InetSocketAddress(8080));
/**
* 設(shè)置服務(wù)器的accept方法為非阻塞方法,防止沒人連接不會一直等待
*/
serverSocket.setConfig(false);
while (true){
//接受一個連接
Socket socket=serverSocket.accept();
//如果沒有連接
if(socket==null){
System.out.println("沒人來連接");
//查看前面的socket有沒有客戶端發(fā)送消息
for(Socket socket1:list){
int read=socket1.getInputStream().read(bytes);
//如果前面的客戶端有發(fā)送消息,則處理消息
if(read!=0){
//logic
}
}
}else {//如果有人來連接
//設(shè)置socket為非阻塞方法
socket.setConfig(false);
//將當(dāng)前連接的socket加入List集合
list.add(socket);
//同樣處理前面邏輯,看前面的socket是否發(fā)送消息以及現(xiàn)在的socket是否發(fā)送消息。
for(Socket socket1:list){
int read=socket1.getInputStream().read(bytes);
//如果前面的客戶端有發(fā)送消息,則處理消息
if(read!=0){
//logic
}
}
}
}
}
實(shí)現(xiàn)代碼
public class QQNIOServer {
/**
* 字節(jié)數(shù)組存儲讀取的數(shù)據(jù)
*/
static byte[] bytes=new byte[1024];
/**
* 非阻塞Socket對象集合,用于存儲所有的socket
*/
static List<SocketChannel> list=new ArrayList<>();
/**
* 申請一個堆外內(nèi)存,在虛擬機(jī)中也稱為直接內(nèi)存
*/
static ByteBuffer byteBuffer=ByteBuffer.allocate(512);
public static void main(String[] args) {
try {
//監(jiān)聽開始
//打開服務(wù)端非阻塞Socket
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
//綁定監(jiān)聽端口號
serverSocketChannel.bind(new InetSocketAddress(8080));
//設(shè)置為非阻塞,默認(rèn)為阻塞
serverSocketChannel.configureBlocking(false);
while (true){
//拿到客戶端的socket流
SocketChannel socketChannel=serverSocketChannel.accept();
//如果沒有客戶端連接服務(wù)器
if (socketChannel==null){
//處理業(yè)務(wù)邏輯
Thread.sleep(500);
System.out.println("no conn");
//查看前面連接的socket是否有發(fā)送消息
for(SocketChannel client:list){
int read=client.read(byteBuffer);
if(read>0){
//該方法表明將從緩存的頭開始讀,并且讀取所讀數(shù)據(jù)的長度,不讀取整個緩存,該方法必有?。?!
byteBuffer.flip();
System.out.println(byteBuffer.toString());
}
}
}else {//當(dāng)?shù)玫揭粋€連接
System.out.println("has a conn");
socketChannel.configureBlocking(false);
list.add(socketChannel);
//查看前面連接的socket是否有發(fā)送消息
for(SocketChannel client:list){
int read=client.read(byteBuffer);
if(read>0){
//該方法表明將從緩存的頭開始讀,并且讀取所讀數(shù)據(jù)的長度,不讀取整個緩存,該方法必有!?。? byteBuffer.flip();
System.out.println(byteBuffer.toString());
}
}
}
}
}catch (Exception e){
}
}
}
問題:List集合大部分連接沒用,做了沒必要的輪詢,故此,解決方案是輪詢不能交給我的系統(tǒng),要主動感知有數(shù)據(jù)的socket,在linux上有epoll函數(shù),它會告訴你當(dāng)前來的是什么socket,但在windows上使用的select函數(shù),同樣在內(nèi)核做輪詢。所以Socket的性能與操作系統(tǒng)有關(guān)
基于 Buffer 與基于 Stream
BIO 是面向字節(jié)流或者字符流的,而在 NIO 中,它摒棄了傳統(tǒng)的 IO 流,而是引入 Channel 和 Buffer 的概念:從 Channel 中讀取數(shù)據(jù)到 Buffer 中,或者將數(shù)據(jù)從 Buffer 中寫到 Channel 中。
① 那么什么是基于 Stream呢?
在一般的 Java IO 操作中,我們以流式的方式,順序的從一個 Stream 中讀取一個或者多個字節(jié),直至讀取所有字節(jié)。因?yàn)樗鼪]有緩存區(qū),所以我們就不能隨意改變讀取指針的位置。
② 那么什么是基于 Buffer 呢?參考http://ifeve.com/buffers/
基于 Buffer 就顯得有點(diǎn)不同了。我們在從 Channel 中讀取數(shù)據(jù)到 Buffer 中,這樣 Buffer 中就有了數(shù)據(jù)后,我們就可以對這些數(shù)據(jù)進(jìn)行操作了。并且不同于一般的 Java IO 操作那樣是順序操作,NIO 中我們可以隨意的讀取任意位置的數(shù)據(jù),這樣大大增加了處理過程中的靈活性。
阻塞與非阻塞 IO
Java IO 的各種流是阻塞的 IO 操作。這就意味著,當(dāng)一個線程執(zhí)行讀或?qū)?IO 操作時,該線程會被阻塞,直到有一些數(shù)據(jù)被讀取,或者數(shù)據(jù)完全寫入。
Java NIO 可以讓我們非阻塞的使用 IO 操作。例如:
當(dāng)一個線程執(zhí)行從 Channel 執(zhí)行讀取 IO 操作時,當(dāng)此時有數(shù)據(jù),則讀取數(shù)據(jù)并返回;當(dāng)此時無數(shù)據(jù),則直接返回而不會阻塞當(dāng)前線程。
當(dāng)一個線程執(zhí)行向 Channel 執(zhí)行寫入 IO 操作時,不需要阻塞等待它完全寫入,這個線程同時可以做別的事情。
也就是說,線程可以將非阻塞 IO 的空閑時間用于在其他 Channel 上執(zhí)行 IO 操作。所以,一個單獨(dú)的線程,可以管理多個 Channel 的讀取和寫入 IO 操作。
Selector
Java NIO 引入 Selector ( 選擇器 )的概念,它是 Java NIO 得以實(shí)現(xiàn)非阻塞 IO 操作的最最最關(guān)鍵。
我們可以注冊多個 Channel 到一個 Selector 中。而 Selector 內(nèi)部的機(jī)制,就可以自動的為我們不斷的執(zhí)行查詢( select )操作,判斷這些注冊的 Channel 是否有已就緒的 IO 事件( 例如可讀,可寫,網(wǎng)絡(luò)連接已完成 )。
通過這樣的機(jī)制,一個線程通過使用一個 Selector ,就可以非常簡單且高效的來管理多個 Channel 了。