初識(shí)NIO
一、BIO
什么是BIO?其實(shí)就是初學(xué)java的時(shí)候老師講的I/O,比如InputStream、OutputStream等很多stream,它們都在java.io包下:

BIO的全稱叫Blocking IO,翻譯過(guò)來(lái)就是阻塞IO,為了讓你們有個(gè)直觀的認(rèn)識(shí),先看段程序:
package com.example.io;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
????????while?(true)?{
System.out.println("等待連接...");
Socket socket = serverSocket.accept();
System.out.println("有客戶端連接了...");
handle(socket);
}
}
private static void handle(Socket socket) throws IOException {
byte[] bytes = new byte[1024];
System.out.println("準(zhǔn)備read...");
????????int?read?=?socket.getInputStream().read(bytes);
System.out.println("read完畢...");
????????if?(read?!=?-1)?{
System.out.println("接收到客戶端的數(shù)據(jù): " + new String(bytes, 0, read));
}
socket.getOutputStream().flush();
}
}
這段程序就是java基礎(chǔ)中的網(wǎng)絡(luò)編程,它其實(shí)就是基于BIO開發(fā)的。
在第15行打個(gè)斷點(diǎn),運(yùn)行程序

但你會(huì)發(fā)現(xiàn)程序并沒有運(yùn)行到第15行,當(dāng)然肯定也不會(huì)卡在第12行,那就是卡在第13行了,BIO的B就是這個(gè)意思,它會(huì)阻塞住。

回車后你會(huì)發(fā)現(xiàn),程序走了
1.3、通過(guò)客戶端發(fā)送數(shù)據(jù)
上面的調(diào)試到了handle方法,意思就是要處理這個(gè)請(qǐng)求,在23行打個(gè)斷點(diǎn),放開請(qǐng)求

你會(huì)發(fā)現(xiàn)又卡住了,很明顯這次卡在了第22行,這行代碼的意思是在等待客戶端發(fā)送數(shù)據(jù),因?yàn)閯偛诺膖elnet只是連接上了客戶端,并沒有發(fā)送數(shù)據(jù),接下來(lái)通過(guò)telnet發(fā)送數(shù)據(jù),方法是按下ctrl+]

如果對(duì)telnet不熟悉的話,可以通過(guò)help命令查看一下:

接下來(lái)通過(guò)send發(fā)送一條數(shù)據(jù):send xiaoP

回車后你會(huì)發(fā)現(xiàn)程序走了

斷點(diǎn)放開后,服務(wù)端成功收到了客戶端發(fā)送的內(nèi)容:

以上就是簡(jiǎn)單的BIO的演示過(guò)程,可以看到,accept和read方法都是阻塞的,其實(shí)不僅阻塞,也是同步的,把斷點(diǎn)都去掉,然后運(yùn)行程序

然后打開兩個(gè)客戶端

然后先在左邊的窗口按ctrl+],再在右邊的窗口按ctrl+]

然后在右邊的窗口先send數(shù)據(jù)

可以看到,我右邊已經(jīng)回車發(fā)送了,但是服務(wù)端并沒有打印xiaoP2,

這時(shí)候我在左邊的窗口回車

這時(shí)候控制臺(tái)打印了

可以看到,服務(wù)端在處理請(qǐng)求的時(shí)候是串行的,也就是單線程,必須處理完第一個(gè)請(qǐng)求才會(huì)處理第二個(gè),這樣的話就會(huì)有一個(gè)嚴(yán)重的問(wèn)題,假如說(shuō)第一個(gè)請(qǐng)求因?yàn)槟承┰蜻t遲未處理,那后面的請(qǐng)求也都會(huì)被阻塞,那既然它是單線程,我們可以不可以通過(guò)多線程來(lái)處理,一個(gè)線程處理一個(gè)請(qǐng)求,這樣就不會(huì)被阻塞了?稍微改下程序
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(9000);
while (true) {
System.out.println("等待連接...");
Socket socket = serverSocket.accept();
System.out.println("有客戶端連接了...");
new Thread(new Runnable() {
@Override
public void run() {
try {
handle(socket);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
重新跑起來(lái),然后重復(fù)上面的步驟,你就會(huì)發(fā)現(xiàn)xiaoP2打印出來(lái)了。
總結(jié)下BIO的缺點(diǎn):
1、需要開大量的線程處理請(qǐng)求
2、存在阻塞問(wèn)題
3、線程切換造成的開銷
4、連接但不發(fā)送數(shù)據(jù),會(huì)占用線程不釋放
所以BIO的使用場(chǎng)景是很少的,除非你的項(xiàng)目需要的連接數(shù)很少。
那有沒有辦法解決這種問(wèn)題,肯定是有的,其實(shí)在jdk1.4之后,引入了nio,其中n表示none blocking,非阻塞。
二、初識(shí)NIO2.1、最簡(jiǎn)單的NIO程序
我們先用nio的API寫一個(gè)簡(jiǎn)單的程序并啟動(dòng)它
package com.example.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class NioServer {
/**
* 存放SocketChannel
*/
private static List<SocketChannel> list = new ArrayList<>();
public static void main(String[] args) throws IOException {
//nio中叫ServerSocketChannel,bio中叫ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//綁定到9000端口
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
//設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false);
System.out.println("服務(wù)啟動(dòng)成功!");
while (true) {
//如果上面的configureBlocking設(shè)置為true,那這行代碼和bio中的accept是一樣的,都是阻塞的
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
System.out.println("連接成功!");
//設(shè)置為非阻塞
socketChannel.configureBlocking(false);
list.add(socketChannel);
}
//遍歷連接進(jìn)行數(shù)據(jù)讀取
Iterator<SocketChannel> iterator = list.iterator();
while (iterator.hasNext()) {
SocketChannel next = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = next.read(byteBuffer);
//如果有數(shù)據(jù),把數(shù)據(jù)打印出來(lái)
if(len > 0) {
System.out.println("接收到數(shù)據(jù):" + new String(byteBuffer.array()));
} else if (len == -1) {
//如果客戶端斷開,把socket從集合中移除
iterator.remove();
System.out.println("客戶端斷開連接");
}
}
}
}
}
2.2、通過(guò)客戶端連接
還是和bio中的調(diào)試一樣,開兩個(gè)客戶端,先連接第一個(gè)客戶端,但是先在第二個(gè)客戶端中發(fā)送數(shù)據(jù)


可以看到,沒有bio那種阻塞的問(wèn)題了,不管你開再多的連接,都可以處理。為什么?最本質(zhì)的區(qū)別就是accept和read方法不再是阻塞的了,這樣的話主線程其實(shí)一直在做循環(huán),不斷的在找客戶端連接,不斷的在讀數(shù)據(jù)(如果客戶端發(fā)送有的話)。你是不是覺得這樣的程序就沒問(wèn)題了?怎么可能,試想一下,假如有1萬(wàn)個(gè)連接,但是只有一個(gè)連接發(fā)數(shù)據(jù)了,那為了讀取這一個(gè)連接的數(shù)據(jù),每次都要循環(huán)一遍這一萬(wàn)個(gè)連接?合適嗎?那如何優(yōu)化??jī)?yōu)化的目的很明確,就是我們只需要找到有數(shù)據(jù)的那個(gè)連接就行了,具體怎么做?nio這么強(qiáng)大,它肯定已經(jīng)做好了,這個(gè)東西叫多路復(fù)用器,也就是大名鼎鼎的selector。
2.3、通過(guò)selector改良程序
package com.example.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NioSelectorServer {
public static void main(String[] args) throws IOException {
//nio中叫ServerSocketChannel,bio中叫ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//綁定到9000端口
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
//設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false);
/**
* 打開selector處理channel,即創(chuàng)建epoll
*/
Selector selector = Selector.open();
//將serverSocketChannel注冊(cè)到selector上,并且selector對(duì)客戶端的accept連接操作感興趣
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服務(wù)啟動(dòng)成功!");
while (true) {
//阻塞等待需要處理的事件發(fā)生,這里是阻塞的,有事件的話才會(huì)往下執(zhí)行
selector.select();
//獲取selector中注冊(cè)的全部事件的SelectionKey實(shí)例
Set selectionKeys = selector.selectedKeys();
Iterator iterator = selectionKeys.iterator();
//遍歷selectionKey對(duì)事件進(jìn)行處理
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
//如果是OP_ACCEPT事件,則進(jìn)行連接獲取和事件注冊(cè)
if (next.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) next.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
//這里只注冊(cè)了讀事件,如果需要給客戶端發(fā)送數(shù)據(jù),可以注冊(cè)寫事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客戶端連接成功!");
} else if (next.isReadable()) {
//如果是OP_READ事件,則進(jìn)行讀取和打印
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = channel.read(byteBuffer);
//如果有數(shù)據(jù),把數(shù)據(jù)打印出來(lái)
if (len > 0) {
System.out.println("接收到數(shù)據(jù):" + new String(byteBuffer.array()));
} else if(len == -1) {
//如果客戶端斷開連接,關(guān)閉Socket
System.out.println("客戶端斷開連接");
channel.close();
}
}
//從事件集合里刪除本次處理的key,防止下次selector重復(fù)處理
iterator.remove();
}
}
}
}
啟動(dòng)程序,觀察

可以看到,程序阻塞在了第31行,selector解決了程序一直空跑的情況,這時(shí)候打開一個(gè)客戶端,運(yùn)行telnet localhost 9000,會(huì)發(fā)現(xiàn)程序往下執(zhí)行了,telnet相當(dāng)于一個(gè)accept事件,往下debug會(huì)進(jìn)入next.isAcceptable判斷中,放開斷點(diǎn),你會(huì)發(fā)現(xiàn)程序又阻塞在31行,這時(shí)候發(fā)送數(shù)據(jù)


程序又往下走了

放開斷點(diǎn),這時(shí)候會(huì)進(jìn)入到next.isReadable中,因?yàn)榭蛻舳私o服務(wù)端發(fā)數(shù)據(jù),對(duì)服務(wù)端來(lái)說(shuō)是一個(gè)讀事件

放開斷點(diǎn),讓程序運(yùn)行

打印了數(shù)據(jù),并且程序繼續(xù)阻塞在selector.select這行代碼。這是不是就實(shí)現(xiàn)了我們想要的功能,有數(shù)據(jù)的時(shí)候才會(huì)處理,那原理是什么?看一張圖

對(duì)照著代碼看這一張圖
注:selectionKey其實(shí)就是對(duì)應(yīng)的ServerSocketChannel或者SocketChannel,通過(guò)selectionKey可以拿到ServerSocketChannel或SocketChannel 以上就是nio運(yùn)行的大概流程。
那底層是怎么實(shí)現(xiàn)的?下篇文章見!
