Netty 源碼解析 Part 0——第1篇:BIO vs NIO

- 前言 -

- BIO hello word -
/**
* 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
*
* @author wangjianxin
*/
public class HelloBioServer {
public static void main(String[] args) throws IOException {
//創(chuàng)建ServerSocket
ServerSocket serverSocket = new ServerSocket();
//綁定到8000端口
serverSocket.bind(new InetSocketAddress(8000));
new BioServerConnector(serverSocket).start();
}
}
/**
* 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
*
* @author wangjianxin
*/
public class BioServerConnector {
private final ServerSocket serverSocket;
public BioServerConnector(ServerSocket serverSocket) {
this.serverSocket = serverSocket;
}
public void start() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
Socket newSocket = null;
try {
//阻塞在這里等待新連接,返回值為一條新的連接
newSocket = serverSocket.accept();
} catch (IOException e) {
}
//將新連接交給handler處理
new BioServerHandler(newSocket).start();
}
}
});
thread.start();
}
}
/**
* 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
*
* @author wangjianxin
*/
public class BioServerHandler {
private final Socket socket;
public BioServerHandler(Socket socket) {
this.socket = socket;
}
public void start() {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
//阻塞操作,因為不知道inputStream中什么時候會有數(shù)據(jù)可讀,只能阻塞在這里等待
//每一個連接都要消耗一個線程
int readLength = inputStream.read(buffer);
if (readLength != -1) {
String name = new String(buffer, 0, readLength);
System.out.println(name);
//打印客戶端發(fā)送過來的數(shù)據(jù)
socket.getOutputStream().write(("hello, " + name).getBytes());
}
} catch (Throwable e) {
try {
socket.close();
} catch (IOException ioException) {
}
}
}
}
});
thread.start();
}
}
HelloBioServer:啟動類,創(chuàng)建一個ServerSocket,并交給BioServerConnetor處理 BioServerConnector:接受新連接的類,其中創(chuàng)建一個線程,循環(huán)阻塞在ServerSocket上等待新連接的到來,每建立一條新連接,就創(chuàng)建一個BioServerHandler,并將該連接交給BioServerHandler處理 BioServerHandler:處理連接上數(shù)據(jù)的類,每個BioServerHandler都創(chuàng)建一個線程循環(huán)阻塞在Socket的InputStream上,讀取數(shù)據(jù),再為該數(shù)據(jù)拼上“Hello, ”發(fā)送回去。
1.2 BIO客戶端
/**
* 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
*
* @author wangjianxin
*/
public class HelloBioClient {
//創(chuàng)建多少個客戶端
private static final int CLIENTS = 2;
public static void main(String[] args) throws IOException {
for (int i = 0; i < CLIENTS; i++) {
final int clientIndex = i;
Thread client = new Thread(new Runnable() {
@Override
public void run() {
try {
//創(chuàng)建socket
Socket socket = new Socket();
//連接8000端口
socket.connect(new InetSocketAddress(8000));
while (true) {
OutputStream outputStream = socket.getOutputStream();
//向服務端發(fā)送”zhongdaima" + 客戶端編號
outputStream.write(("zhongdaima" + clientIndex).getBytes());
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int readLength = inputStream.read(buffer);
//打印服務端返回數(shù)據(jù)
System.out.println(new String(buffer, 0, readLength));
Thread.sleep(1000);
}
} catch (Throwable e) {
}
}
});
client.start();
}
}
}
BIO客戶端共1個類,HelloBioClient,在該客戶端中創(chuàng)建了兩個線程、兩條連接,每個線程處理一條連接,循環(huán)向服務端發(fā)送“zhongdaima" + 客戶端編號,并打印服務端返回的數(shù)據(jù)。

- NIO hello word -
2.1 NIO服務端
/**
* 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
* @author wangjianxin
*/
public class HelloNioServer {
public static void main(String[] args) throws IOException {
//創(chuàng)建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//設置Channel為非阻塞
serverSocketChannel.configureBlocking(false);
//綁定到8000端口
serverSocketChannel.bind(new InetSocketAddress(8000));
//交給Connector
new NioServerConnector(serverSocketChannel).start();
}
}
/**
* 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
* @author wangjianxin
*/
public class NioServerConnector {
private final ServerSocketChannel serverSocketChannel;
private final Selector selector;
private final NioServerHandler nioServerHandler;
public NioServerConnector(ServerSocketChannel serverSocketChannel) throws IOException {
this.selector = Selector.open();
this.serverSocketChannel = serverSocketChannel;
//向selector注冊Channel,感興趣事件為OP_ACCEPT(即新連接接入)
this.serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT, serverSocketChannel);
this.nioServerHandler = new NioServerHandler();
this.nioServerHandler.start();
}
public void start() {
Thread serverConnector = new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
if (NioServerConnector.this.selector.select() > 0) {
Set<SelectionKey> selectionKeys = NioServerConnector.this.selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
try {
if (key.isAcceptable()) {
//新連接接入
SocketChannel socketChannel = ((ServerSocketChannel) key.attachment()).accept();
socketChannel.configureBlocking(false);
//把新連接交給serverHandler
NioServerConnector.this.nioServerHandler.register(socketChannel);
}
} finally {
iterator.remove();
}
}
}
} catch (IOException e) {
}
}
}
});
serverConnector.start();
}
}
/**
* 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
* @author wangjianxin
*/
public class NioServerHandler {
private final Selector selector;
private final BlockingQueue<SocketChannel> prepareForRegister = new LinkedBlockingDeque<>();
public NioServerHandler() throws IOException {
this.selector = Selector.open();
}
public void register(SocketChannel socketChannel) {
//這里為什么不直接注冊呢,因為當有線程在selector上select時,register操作會阻塞
//從未注冊過channel時,start方法中的線程會一直阻塞,在這里調用register的線程也會一直阻塞
//所以我們把待注冊的channel放入隊列中,并且換醒start方法中的線程,讓start方法中的線程去注冊
//放入待注冊隊列
try {
this.prepareForRegister.put(socketChannel);
} catch (InterruptedException e) {
}
//喚醒阻塞在selector上的線程(即下面start方法中創(chuàng)建的線程)
this.selector.wakeup();
}
public void start() {
Thread serverHandler = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
//只需要1個線程就可以監(jiān)視所有連接
//當select方法返回值大于0時,說明注冊到selector的Channels有我們感興趣的事件發(fā)生
//返回值代表有多少Channel發(fā)生了我們感興趣的事件
if (NioServerHandler.this.selector.select() > 0) {
//緊接著調用selectedKeys方法獲取發(fā)生事件的Key集合
Set<SelectionKey> selectionKeys = NioServerHandler.this.selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
//遍歷Key集合,處理Channel io事件
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
try {
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.attachment();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int readLength = socketChannel.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, readLength));
socketChannel.write(ByteBuffer.wrap(("hello, " + new String(buffer.array(), 0, readLength)).getBytes()));
}
} finally {
iterator.remove();
}
}
}
SocketChannel socketChannel;
while ((socketChannel = NioServerHandler.this.prepareForRegister.poll()) != null) {
//注冊待注冊的channel,感興趣事件為OP_READ(即可讀事件)
socketChannel.register(NioServerHandler.this.selector, SelectionKey.OP_READ, socketChannel);
}
}
} catch (IOException e) {
}
}
});
serverHandler.start();
}
}
和BIO服務端類似,NIO服務端也有3個類,分別是HelloNioServer、NioServerConnector和NioServerHandler。
HelloNioServer:啟動類,創(chuàng)建一個ServerSocketChannel,將Channel設置為非阻塞的,綁定到8000端口,交給Connector處理。到這里我們應該明白了為什么NIO是none blocking io,這里比BIO多了一步操作,即將Channel設置為非阻塞的。具體哪里體現(xiàn)出了非阻塞,我們繼續(xù)往下看。
NioServerConnector:處理新連接的類,該類接收一個ServerSocketChannel,創(chuàng)建一個Selector,并向Selector注冊Channel,感興趣事件為OP_ACCEPT(即新連接接入),并創(chuàng)一個NioServerHandler的實例。NioServerConnector的start方法中創(chuàng)建一個線程,循環(huán)向selector詢問是否有新連接接入,一旦發(fā)現(xiàn)有新連接接入,就把新連接交給NioServerHandler處理。這里與BIOServerConnector中不同的是,有新連接接入時,不必再創(chuàng)建一個新的Handler,而是所有連接共用一個Handler。
NioServerHandler:處理連接數(shù)據(jù)上類,該類start方法中創(chuàng)建一個線程循環(huán)向Selector詢問是否有可讀事件發(fā)生。一旦某些連接上有可讀事件發(fā)生,就讀取這些連接上的數(shù)據(jù),并為該數(shù)據(jù)添加上“Hello, ”再發(fā)送回去。然后再處理新的連接注冊,將新連接注冊到Selector上,感興趣事件為OP_READ(即可讀事件)。與BioServerHandler不同的是BioServerHandler中一個線程只能處理一條連接,而NioServerHandler中一個線程可以處理多條連接。
好了,至此我們已經看到了NIO的非阻塞體現(xiàn)在socketChannel.read()方法是非阻塞的,而BIO的阻塞體現(xiàn)在inputstream.read()方法是阻塞的。
2.2 NIO客戶端
/**
* 歡迎關注公眾號“種代碼“,獲取博主微信深入交流
*
* @author wangjianxin
*/
public class HelloNioClient {
private static final int CLIENTS = 2;
public static void main(String[] args) throws IOException {
Thread client = new Thread(new Runnable() {
final Selector selector = Selector.open();
final SocketChannel[] clients = new SocketChannel[CLIENTS];
@Override
public void run() {
//創(chuàng)建兩個客戶端
for (int i = 0; i < CLIENTS; i++) {
try {
//連接8000端口
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(8000));
//將channel設置為非阻塞的
socketChannel.configureBlocking(false);
//注冊到selector
socketChannel.register(this.selector, SelectionKey.OP_READ, socketChannel);
//保存channel
clients[i] = socketChannel;
}catch (Throwable e){
}
}
for (int i = 0; i < Integer.MAX_VALUE; i++) {
try {
//向服務端發(fā)送“zhongdaima" + 客戶端編號
for (int j = 0; j < clients.length; j++) {
this.clients[j].write(ByteBuffer.wrap(("zhongdaima" + j).getBytes()));
}
//監(jiān)視Channel是否有可讀事件發(fā)生
if (this.selector.select() > 0) {
Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
try {
SocketChannel channel = (SocketChannel) key.attachment();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
buffer.flip();
//打印服務端返回數(shù)據(jù)
System.out.println(new String(buffer.array(), 0, read));
}finally {
iterator.remove();
}
}
}
Thread.sleep(1000);
}catch (Throwable e){
}
}
}
});
client.start();
}
}
NIO客戶端只有一個類HelloNioClient,其中創(chuàng)建一個線程、兩條連接,循環(huán)向服務端發(fā)送“zhongdaima" + 客戶端編號,然后向Selctor循問是否有可讀事件發(fā)生,一旦有可讀事件發(fā)生,就讀取數(shù)據(jù),并打印。與HelloBioClient不同的是HelloNioClient創(chuàng)建了兩條連接,卻只使用了一個線程,而HelloBioClient中創(chuàng)建了兩條連接,使用了兩個線程。

- 相比 BIO,NIO 有哪些優(yōu)勢? -
讀到這里,大家仔細品品NIO比BIO的優(yōu)勢在哪里。優(yōu)勢在哪里呢,要首先看看有什么區(qū)別,上面的代碼具體有什么區(qū)別我都加粗表示了,看完之后第一感覺應該就是NIO比BIO節(jié)省線程。
3.1 BIO模型
這是BIO的示意圖,比較簡單,每條連接都需要一個線程來處理,因為無法得得連接中什么時候有數(shù)據(jù)可以讀取,只能傻傻等待。
3.2 NIO模型
這是NIO的示意圖,與BIO相比,其中多了一個組件Selector。正是由于Selector的存在讓NIO與BIO產生了本質的不同。BIO中線程直接阻塞在1條連接上,直到有數(shù)據(jù)可讀取才返回,而且NIO中線程首先阻塞在Selector上,而Selector上可以注冊多條連接。
線程調用select方法向Selector詢問是否有感興趣的事件發(fā)生,阻塞在select方法上,直接到1條或者多條連接上有事件發(fā)生才返回。此時線程已經知道哪些連接上有事件發(fā)生了,于是去處理這些連接上的事件。處理完成之后再次阻塞在Selector的select方法上,如此往復。
至此我們已經發(fā)現(xiàn),BIO和NIO的本質不同在于中間多了一層代理Selector,而Selector具備監(jiān)視多條連接的能力。
3.3 舉個例子
開一家BIO模型的飯店,飯店里只有1個廚師(相當于Thread),有1位顧客(相當于連接)來吃飯,廚師就一直為這1位顧客做飯,直到這個顧客結賬走了(連接關閉),廚師才開始為下1位顧客做飯。如果需要同時滿足10個顧客吃飯,就要10個廚師。
開一家NIO模型的飯店,飯店里有1個廚師(相當于Thread),還有1個服務員(相當于Selector),有10位顧客來吃飯,服務員就為這10位顧客點餐(向Selector注冊),并且需要知道顧客你們都點什么菜(向Selector注冊時的興趣事件)。廚師問服務員顧客都點了什么菜(Selector.select()),開始做菜,做完菜之后再問服務員顧客們又點了什么菜,如此往復。只需要1個廚師、1個服務員就可以為多個顧客提供服務。
很顯然,如果你開飯店,你是開BIO飯店呢,還是NIO飯店呢。

- 總結 -
NIO的非阻塞體現(xiàn)在socketChannel.read()方法是非阻塞的,而BIO的阻塞體現(xiàn)在inputstream.read()方法是阻塞的。
NIO一個線程可以處理多條連接,而BIO一個線程只能處理一條連接,NIO更節(jié)省線程資源。
作者:王建新,轉轉架構部資深Java工程師,主要負責服務治理、RPC框架、分布式調用跟蹤、監(jiān)控系統(tǒng)等。
來源公眾號:種代碼

