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

- 前言 -

- BIO hello word -
/**
* 歡迎關(guān)注公眾號(hào)“種代碼“,獲取博主微信深入交流
*
* @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();
}
}
/**
* 歡迎關(guān)注公眾號(hào)“種代碼“,獲取博主微信深入交流
*
* @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();
}
}
/**
* 歡迎關(guān)注公眾號(hào)“種代碼“,獲取博主微信深入交流
*
* @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];
//阻塞操作,因?yàn)椴恢纈nputStream中什么時(shí)候會(huì)有數(shù)據(jù)可讀,只能阻塞在這里等待
//每一個(gè)連接都要消耗一個(gè)線程
int readLength = inputStream.read(buffer);
if (readLength != -1) {
String name = new String(buffer, 0, readLength);
System.out.println(name);
//打印客戶端發(fā)送過(guò)來(lái)的數(shù)據(jù)
socket.getOutputStream().write(("hello, " + name).getBytes());
}
} catch (Throwable e) {
try {
socket.close();
} catch (IOException ioException) {
}
}
}
}
});
thread.start();
}
}
HelloBioServer:?jiǎn)?dòng)類,創(chuàng)建一個(gè)ServerSocket,并交給BioServerConnetor處理 BioServerConnector:接受新連接的類,其中創(chuàng)建一個(gè)線程,循環(huán)阻塞在ServerSocket上等待新連接的到來(lái),每建立一條新連接,就創(chuàng)建一個(gè)BioServerHandler,并將該連接交給BioServerHandler處理 BioServerHandler:處理連接上數(shù)據(jù)的類,每個(gè)BioServerHandler都創(chuàng)建一個(gè)線程循環(huán)阻塞在Socket的InputStream上,讀取數(shù)據(jù),再為該數(shù)據(jù)拼上“Hello, ”發(fā)送回去。
1.2 BIO客戶端
/**
* 歡迎關(guān)注公眾號(hào)“種代碼“,獲取博主微信深入交流
*
* @author wangjianxin
*/
public class HelloBioClient {
//創(chuàng)建多少個(gè)客戶端
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();
//向服務(wù)端發(fā)送”zhongdaima" + 客戶端編號(hào)
outputStream.write(("zhongdaima" + clientIndex).getBytes());
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int readLength = inputStream.read(buffer);
//打印服務(wù)端返回?cái)?shù)據(jù)
System.out.println(new String(buffer, 0, readLength));
Thread.sleep(1000);
}
} catch (Throwable e) {
}
}
});
client.start();
}
}
}
BIO客戶端共1個(gè)類,HelloBioClient,在該客戶端中創(chuàng)建了兩個(gè)線程、兩條連接,每個(gè)線程處理一條連接,循環(huán)向服務(wù)端發(fā)送“zhongdaima" + 客戶端編號(hào),并打印服務(wù)端返回的數(shù)據(jù)。

- NIO hello word -
2.1 NIO服務(wù)端
/**
* 歡迎關(guān)注公眾號(hào)“種代碼“,獲取博主微信深入交流
* @author wangjianxin
*/
public class HelloNioServer {
public static void main(String[] args) throws IOException {
//創(chuàng)建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//設(shè)置Channel為非阻塞
serverSocketChannel.configureBlocking(false);
//綁定到8000端口
serverSocketChannel.bind(new InetSocketAddress(8000));
//交給Connector
new NioServerConnector(serverSocketChannel).start();
}
}
/**
* 歡迎關(guān)注公眾號(hào)“種代碼“,獲取博主微信深入交流
* @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注冊(cè)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();
}
}
/**
* 歡迎關(guān)注公眾號(hào)“種代碼“,獲取博主微信深入交流
* @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) {
//這里為什么不直接注冊(cè)呢,因?yàn)楫?dāng)有線程在selector上select時(shí),register操作會(huì)阻塞
//從未注冊(cè)過(guò)channel時(shí),start方法中的線程會(huì)一直阻塞,在這里調(diào)用register的線程也會(huì)一直阻塞
//所以我們把待注冊(cè)的channel放入隊(duì)列中,并且換醒start方法中的線程,讓start方法中的線程去注冊(cè)
//放入待注冊(cè)隊(duì)列
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個(gè)線程就可以監(jiān)視所有連接
//當(dāng)select方法返回值大于0時(shí),說(shuō)明注冊(cè)到selector的Channels有我們感興趣的事件發(fā)生
//返回值代表有多少Channel發(fā)生了我們感興趣的事件
if (NioServerHandler.this.selector.select() > 0) {
//緊接著調(diào)用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) {
//注冊(cè)待注冊(cè)的channel,感興趣事件為OP_READ(即可讀事件)
socketChannel.register(NioServerHandler.this.selector, SelectionKey.OP_READ, socketChannel);
}
}
} catch (IOException e) {
}
}
});
serverHandler.start();
}
}
和BIO服務(wù)端類似,NIO服務(wù)端也有3個(gè)類,分別是HelloNioServer、NioServerConnector和NioServerHandler。
HelloNioServer:?jiǎn)?dòng)類,創(chuàng)建一個(gè)ServerSocketChannel,將Channel設(shè)置為非阻塞的,綁定到8000端口,交給Connector處理。到這里我們應(yīng)該明白了為什么NIO是none blocking io,這里比BIO多了一步操作,即將Channel設(shè)置為非阻塞的。具體哪里體現(xiàn)出了非阻塞,我們繼續(xù)往下看。
NioServerConnector:處理新連接的類,該類接收一個(gè)ServerSocketChannel,創(chuàng)建一個(gè)Selector,并向Selector注冊(cè)Channel,感興趣事件為OP_ACCEPT(即新連接接入),并創(chuàng)一個(gè)NioServerHandler的實(shí)例。NioServerConnector的start方法中創(chuàng)建一個(gè)線程,循環(huán)向selector詢問(wèn)是否有新連接接入,一旦發(fā)現(xiàn)有新連接接入,就把新連接交給NioServerHandler處理。這里與BIOServerConnector中不同的是,有新連接接入時(shí),不必再創(chuàng)建一個(gè)新的Handler,而是所有連接共用一個(gè)Handler。
NioServerHandler:處理連接數(shù)據(jù)上類,該類start方法中創(chuàng)建一個(gè)線程循環(huán)向Selector詢問(wèn)是否有可讀事件發(fā)生。一旦某些連接上有可讀事件發(fā)生,就讀取這些連接上的數(shù)據(jù),并為該數(shù)據(jù)添加上“Hello, ”再發(fā)送回去。然后再處理新的連接注冊(cè),將新連接注冊(cè)到Selector上,感興趣事件為OP_READ(即可讀事件)。與BioServerHandler不同的是BioServerHandler中一個(gè)線程只能處理一條連接,而NioServerHandler中一個(gè)線程可以處理多條連接。
好了,至此我們已經(jīng)看到了NIO的非阻塞體現(xiàn)在socketChannel.read()方法是非阻塞的,而BIO的阻塞體現(xiàn)在inputstream.read()方法是阻塞的。
2.2 NIO客戶端
/**
* 歡迎關(guān)注公眾號(hào)“種代碼“,獲取博主微信深入交流
*
* @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)建兩個(gè)客戶端
for (int i = 0; i < CLIENTS; i++) {
try {
//連接8000端口
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(8000));
//將channel設(shè)置為非阻塞的
socketChannel.configureBlocking(false);
//注冊(cè)到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 {
//向服務(wù)端發(fā)送“zhongdaima" + 客戶端編號(hào)
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();
//打印服務(wù)端返回?cái)?shù)據(jù)
System.out.println(new String(buffer.array(), 0, read));
}finally {
iterator.remove();
}
}
}
Thread.sleep(1000);
}catch (Throwable e){
}
}
}
});
client.start();
}
}
NIO客戶端只有一個(gè)類HelloNioClient,其中創(chuàng)建一個(gè)線程、兩條連接,循環(huán)向服務(wù)端發(fā)送“zhongdaima" + 客戶端編號(hào),然后向Selctor循問(wèn)是否有可讀事件發(fā)生,一旦有可讀事件發(fā)生,就讀取數(shù)據(jù),并打印。與HelloBioClient不同的是HelloNioClient創(chuàng)建了兩條連接,卻只使用了一個(gè)線程,而HelloBioClient中創(chuàng)建了兩條連接,使用了兩個(gè)線程。

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

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

