Netty 源码解析 Part 0——第1篇:BIO vs NIO
- 前言 -
- BIO hello word -
/**
* 欢迎关注公众号“种代码“,获取博主微信深入交流
*
* @author wangjianxin
*/
public class HelloBioServer {
public static void main(String[] args) throws IOException {
//创建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中什么时候会有数据可读,只能阻塞在这里等待
//每一个连接都要消耗一个线程
int readLength = inputStream.read(buffer);
if (readLength != -1) {
String name = new String(buffer, 0, readLength);
System.out.println(name);
//打印客户端发送过来的数据
socket.getOutputStream().write(("hello, " + name).getBytes());
}
} catch (Throwable e) {
try {
socket.close();
} catch (IOException ioException) {
}
}
}
}
});
thread.start();
}
}
HelloBioServer:启动类,创建一个ServerSocket,并交给BioServerConnetor处理 BioServerConnector:接受新连接的类,其中创建一个线程,循环阻塞在ServerSocket上等待新连接的到来,每建立一条新连接,就创建一个BioServerHandler,并将该连接交给BioServerHandler处理 BioServerHandler:处理连接上数据的类,每个BioServerHandler都创建一个线程循环阻塞在Socket的InputStream上,读取数据,再为该数据拼上“Hello, ”发送回去。
1.2 BIO客户端
/**
* 欢迎关注公众号“种代码“,获取博主微信深入交流
*
* @author wangjianxin
*/
public class HelloBioClient {
//创建多少个客户端
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 {
//创建socket
Socket socket = new Socket();
//连接8000端口
socket.connect(new InetSocketAddress(8000));
while (true) {
OutputStream outputStream = socket.getOutputStream();
//向服务端发送”zhongdaima" + 客户端编号
outputStream.write(("zhongdaima" + clientIndex).getBytes());
InputStream inputStream = socket.getInputStream();
byte[] buffer = new byte[1024];
int readLength = inputStream.read(buffer);
//打印服务端返回数据
System.out.println(new String(buffer, 0, readLength));
Thread.sleep(1000);
}
} catch (Throwable e) {
}
}
});
client.start();
}
}
}
BIO客户端共1个类,HelloBioClient,在该客户端中创建了两个线程、两条连接,每个线程处理一条连接,循环向服务端发送“zhongdaima" + 客户端编号,并打印服务端返回的数据。
- NIO hello word -
2.1 NIO服务端
/**
* 欢迎关注公众号“种代码“,获取博主微信深入交流
* @author wangjianxin
*/
public class HelloNioServer {
public static void main(String[] args) throws IOException {
//创建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方法中创建的线程)
this.selector.wakeup();
}
public void start() {
Thread serverHandler = new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
//只需要1个线程就可以监视所有连接
//当select方法返回值大于0时,说明注册到selector的Channels有我们感兴趣的事件发生
//返回值代表有多少Channel发生了我们感兴趣的事件
if (NioServerHandler.this.selector.select() > 0) {
//紧接着调用selectedKeys方法获取发生事件的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:启动类,创建一个ServerSocketChannel,将Channel设置为非阻塞的,绑定到8000端口,交给Connector处理。到这里我们应该明白了为什么NIO是none blocking io,这里比BIO多了一步操作,即将Channel设置为非阻塞的。具体哪里体现出了非阻塞,我们继续往下看。
NioServerConnector:处理新连接的类,该类接收一个ServerSocketChannel,创建一个Selector,并向Selector注册Channel,感兴趣事件为OP_ACCEPT(即新连接接入),并创一个NioServerHandler的实例。NioServerConnector的start方法中创建一个线程,循环向selector询问是否有新连接接入,一旦发现有新连接接入,就把新连接交给NioServerHandler处理。这里与BIOServerConnector中不同的是,有新连接接入时,不必再创建一个新的Handler,而是所有连接共用一个Handler。
NioServerHandler:处理连接数据上类,该类start方法中创建一个线程循环向Selector询问是否有可读事件发生。一旦某些连接上有可读事件发生,就读取这些连接上的数据,并为该数据添加上“Hello, ”再发送回去。然后再处理新的连接注册,将新连接注册到Selector上,感兴趣事件为OP_READ(即可读事件)。与BioServerHandler不同的是BioServerHandler中一个线程只能处理一条连接,而NioServerHandler中一个线程可以处理多条连接。
好了,至此我们已经看到了NIO的非阻塞体现在socketChannel.read()方法是非阻塞的,而BIO的阻塞体现在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() {
//创建两个客户端
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 {
//向服务端发送“zhongdaima" + 客户端编号
for (int j = 0; j < clients.length; j++) {
this.clients[j].write(ByteBuffer.wrap(("zhongdaima" + j).getBytes()));
}
//监视Channel是否有可读事件发生
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();
//打印服务端返回数据
System.out.println(new String(buffer.array(), 0, read));
}finally {
iterator.remove();
}
}
}
Thread.sleep(1000);
}catch (Throwable e){
}
}
}
});
client.start();
}
}
NIO客户端只有一个类HelloNioClient,其中创建一个线程、两条连接,循环向服务端发送“zhongdaima" + 客户端编号,然后向Selctor循问是否有可读事件发生,一旦有可读事件发生,就读取数据,并打印。与HelloBioClient不同的是HelloNioClient创建了两条连接,却只使用了一个线程,而HelloBioClient中创建了两条连接,使用了两个线程。
- 相比 BIO,NIO 有哪些优势? -
读到这里,大家仔细品品NIO比BIO的优势在哪里。优势在哪里呢,要首先看看有什么区别,上面的代码具体有什么区别我都加粗表示了,看完之后第一感觉应该就是NIO比BIO节省线程。
3.1 BIO模型
这是BIO的示意图,比较简单,每条连接都需要一个线程来处理,因为无法得得连接中什么时候有数据可以读取,只能傻傻等待。
3.2 NIO模型
这是NIO的示意图,与BIO相比,其中多了一个组件Selector。正是由于Selector的存在让NIO与BIO产生了本质的不同。BIO中线程直接阻塞在1条连接上,直到有数据可读取才返回,而且NIO中线程首先阻塞在Selector上,而Selector上可以注册多条连接。
线程调用select方法向Selector询问是否有感兴趣的事件发生,阻塞在select方法上,直接到1条或者多条连接上有事件发生才返回。此时线程已经知道哪些连接上有事件发生了,于是去处理这些连接上的事件。处理完成之后再次阻塞在Selector的select方法上,如此往复。
至此我们已经发现,BIO和NIO的本质不同在于中间多了一层代理Selector,而Selector具备监视多条连接的能力。
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的非阻塞体现在socketChannel.read()方法是非阻塞的,而BIO的阻塞体现在inputstream.read()方法是阻塞的。
NIO一个线程可以处理多条连接,而BIO一个线程只能处理一条连接,NIO更节省线程资源。
作者:王建新,转转架构部资深Java工程师,主要负责服务治理、RPC框架、分布式调用跟踪、监控系统等。
来源公众号:种代码