初识NIO
一、BIO
什么是BIO?其实就是初学java的时候老师讲的I/O,比如InputStream、OutputStream等很多stream,它们都在java.io包下:
BIO的全称叫Blocking IO,翻译过来就是阻塞IO,为了让你们有个直观的认识,先看段程序:
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("准备read...");
int read = socket.getInputStream().read(bytes);
System.out.println("read完毕...");
if (read != -1) {
System.out.println("接收到客户端的数据: " + new String(bytes, 0, read));
}
socket.getOutputStream().flush();
}
}
这段程序就是java基础中的网络编程,它其实就是基于BIO开发的。
在第15行打个断点,运行程序
但你会发现程序并没有运行到第15行,当然肯定也不会卡在第12行,那就是卡在第13行了,BIO的B就是这个意思,它会阻塞住。
回车后你会发现,程序走了
1.3、通过客户端发送数据上面的调试到了handle方法,意思就是要处理这个请求,在23行打个断点,放开请求
你会发现又卡住了,很明显这次卡在了第22行,这行代码的意思是在等待客户端发送数据,因为刚才的telnet只是连接上了客户端,并没有发送数据,接下来通过telnet发送数据,方法是按下ctrl+]
如果对telnet不熟悉的话,可以通过help命令查看一下:
接下来通过send发送一条数据:send xiaoP
回车后你会发现程序走了
断点放开后,服务端成功收到了客户端发送的内容:
以上就是简单的BIO的演示过程,可以看到,accept和read方法都是阻塞的,其实不仅阻塞,也是同步的,把断点都去掉,然后运行程序
然后打开两个客户端
然后先在左边的窗口按ctrl+],再在右边的窗口按ctrl+]
然后在右边的窗口先send数据
可以看到,我右边已经回车发送了,但是服务端并没有打印xiaoP2,
这时候我在左边的窗口回车
这时候控制台打印了
可以看到,服务端在处理请求的时候是串行的,也就是单线程,必须处理完第一个请求才会处理第二个,这样的话就会有一个严重的问题,假如说第一个请求因为某些原因迟迟未处理,那后面的请求也都会被阻塞,那既然它是单线程,我们可以不可以通过多线程来处理,一个线程处理一个请求,这样就不会被阻塞了?稍微改下程序
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();
}
}
重新跑起来,然后重复上面的步骤,你就会发现xiaoP2打印出来了。
总结下BIO的缺点:
1、需要开大量的线程处理请求
2、存在阻塞问题
3、线程切换造成的开销
4、连接但不发送数据,会占用线程不释放
所以BIO的使用场景是很少的,除非你的项目需要的连接数很少。
那有没有办法解决这种问题,肯定是有的,其实在jdk1.4之后,引入了nio,其中n表示none blocking,非阻塞。 二、初识NIO2.1、最简单的NIO程序
我们先用nio的API写一个简单的程序并启动它
2.2、通过客户端连接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));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
System.out.println("服务启动成功!");
while (true) {
//如果上面的configureBlocking设置为true,那这行代码和bio中的accept是一样的,都是阻塞的
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
System.out.println("连接成功!");
//设置为非阻塞
socketChannel.configureBlocking(false);
list.add(socketChannel);
}
//遍历连接进行数据读取
Iterator<SocketChannel> iterator = list.iterator();
while (iterator.hasNext()) {
SocketChannel next = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = next.read(byteBuffer);
//如果有数据,把数据打印出来
if(len > 0) {
System.out.println("接收到数据:" + new String(byteBuffer.array()));
} else if (len == -1) {
//如果客户端断开,把socket从集合中移除
iterator.remove();
System.out.println("客户端断开连接");
}
}
}
}
}
还是和bio中的调试一样,开两个客户端,先连接第一个客户端,但是先在第二个客户端中发送数据
可以看到,没有bio那种阻塞的问题了,不管你开再多的连接,都可以处理。为什么?最本质的区别就是accept和read方法不再是阻塞的了,这样的话主线程其实一直在做循环,不断的在找客户端连接,不断的在读数据(如果客户端发送有的话)。你是不是觉得这样的程序就没问题了?怎么可能,试想一下,假如有1万个连接,但是只有一个连接发数据了,那为了读取这一个连接的数据,每次都要循环一遍这一万个连接?合适吗?那如何优化?优化的目的很明确,就是我们只需要找到有数据的那个连接就行了,具体怎么做?nio这么强大,它肯定已经做好了,这个东西叫多路复用器,也就是大名鼎鼎的selector。
2.3、通过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));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
/**
* 打开selector处理channel,即创建epoll
*/
Selector selector = Selector.open();
//将serverSocketChannel注册到selector上,并且selector对客户端的accept连接操作感兴趣
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务启动成功!");
while (true) {
//阻塞等待需要处理的事件发生,这里是阻塞的,有事件的话才会往下执行
selector.select();
//获取selector中注册的全部事件的SelectionKey实例
Set
selectionKeys = selector.selectedKeys(); Iterator
iterator = selectionKeys.iterator(); //遍历selectionKey对事件进行处理
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
//如果是OP_ACCEPT事件,则进行连接获取和事件注册
if (next.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) next.channel();
SocketChannel socketChannel = channel.accept();
socketChannel.configureBlocking(false);
//这里只注册了读事件,如果需要给客户端发送数据,可以注册写事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客户端连接成功!");
} else if (next.isReadable()) {
//如果是OP_READ事件,则进行读取和打印
SocketChannel channel = (SocketChannel) next.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = channel.read(byteBuffer);
//如果有数据,把数据打印出来
if (len > 0) {
System.out.println("接收到数据:" + new String(byteBuffer.array()));
} else if(len == -1) {
//如果客户端断开连接,关闭Socket
System.out.println("客户端断开连接");
channel.close();
}
}
//从事件集合里删除本次处理的key,防止下次selector重复处理
iterator.remove();
}
}
}
}
启动程序,观察
可以看到,程序阻塞在了第31行,selector解决了程序一直空跑的情况,这时候打开一个客户端,运行telnet localhost 9000,会发现程序往下执行了,telnet相当于一个accept事件,往下debug会进入next.isAcceptable判断中,放开断点,你会发现程序又阻塞在31行,这时候发送数据
程序又往下走了
放开断点,这时候会进入到next.isReadable中,因为客户端给服务端发数据,对服务端来说是一个读事件
放开断点,让程序运行
打印了数据,并且程序继续阻塞在selector.select这行代码。这是不是就实现了我们想要的功能,有数据的时候才会处理,那原理是什么?看一张图
对照着代码看这一张图
注:selectionKey其实就是对应的ServerSocketChannel或者SocketChannel,通过selectionKey可以拿到ServerSocketChannel或SocketChannel 以上就是nio运行的大概流程。
那底层是怎么实现的?下篇文章见!