Java NIO
想学Netty,所以先学下一直也没开始学的NIO。
传统的同步阻塞模型
下面是类似Java的伪代码编写的基于Java传统BIO的socket服务程序:
while(true) {
sock = serverSocket.accept(); // 请求到来前阻塞
ins = sock.getInputStream();
ins.read(buf); // 当有数据读取之前阻塞
result = handle(buf);
sock.getOutputStream().write(result); // 可写之前,阻塞
}
- 当调用
accept
时,产生阻塞,在请求到来之前,当前线程啥都不能再干了,只能干等着 - 当调用
read
时,如果流中并没有可读数据也会阻塞
- 调用
write
时,如果写缓冲满了,该方法也会阻塞
对于第三点,可以这样测试,不想看代码直接忽略即可。
public class ClientBIO {
public static void main(String[] args) throws IOException, InterruptedException {
Socket socket = new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", 11441));
// 循环写入大量数据,占满缓冲区
for (int i=0;i < 100; i++) {
socket.getOutputStream().write(new byte[1024 * 1024]);
System.out.println("send"+i);
}
Thread.sleep(10000);
socket.close();
}
}
public class ServerBIO {
public static void main(String[] args) throws IOException, InterruptedException {
ServerSocket ssock = new ServerSocket(11441);
while (true) {
Socket sock = ssock.accept();
// 暂停5s,不进行读取
Thread.sleep(5000);
InputStream ins = sock.getInputStream();
byte[] buf = new byte[1024];
long l = 0;
while ((l = ins.read(buf)) != -1) {
}
}
}
}
在我的机器上,write会在第三次循环时被阻塞,稍后当Server开始读取,程序开始正常运作。
可以看到传统的BIO在accept、read和write上都会阻塞,其过程如下:
- 发起IO调用
- 线程阻塞等待IO事件发生
- IO事件发生后的某个时间,之前阻塞的线程被唤醒
- 唤醒后继续执行下面的代码
所以传统的BIO都采用调度线程+工作线程+线程池的方式来调度
while(true) {
// dispatcher,不断接收新请求
sock = server.accept();
// 通过线程池开启一个请求处理器
executor.submit(new RequestHandler(sock));
}
但是线程创建销毁切换成本高,占用空间大这是一直被诟病的,大家都在寻求更好的办法。还有个最重要的问题,当线程因为外部IO事件阻塞时,这个线程就丧失了继续工作的能力。
NIO模型
NIO是非阻塞IO,它不再阻塞在accept
、read
这种IO操作上,它向系统注册一类事件,然后当注册的事件发生时,当前线程会接收到事件。
NIO有三种组件
- Buffer
- Channel
- Selector
Buffer就是传统意义上的字节缓冲区,Channel代表某种连接的通道,比如Socket,Selector中可以注册一系列Channel的事件,然后调用select
轮询是否有事件发生。
// 注册serverSocketChannel,ACCEPT事件
serverSocketChannel.register(selector, OP_ACCEPT);
while(true) {
selector.select(); // 当没有事件要处理时就阻塞,有就被唤醒
// 判断事件类型,处理...
}
下面是使用NIO编写SocketServer的一个示例:
public class ServerNIO {
public static void main(String[] args) {
new ServerNIO().serveForever();
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
SocketChannel sc = ssChannel.accept();
sc.configureBlocking(false);
System.out.println("accept " + sc.getLocalAddress().toString());
sc.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocateDirect(1024));
}
private void handleRead(SelectionKey key) throws IOException {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long br;
while ((br = sc.read(buf)) > 0) {
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char)buf.get());
}
System.out.println();
buf.clear();
}
if (br == -1) Utils.closeClosable(sc);
}
public void serveForever() {
Selector selector = null;
ServerSocketChannel ssc = null;
try {
selector = Selector.open();
ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
ssc.bind(new InetSocketAddress("0.0.0.0", 11441));
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
System.out.println("SELECT");
if (selector.select(10000) == 0) continue;
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
handleAccept(key);
} else if (key.isReadable()) {
handleRead(key);
}
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
Utils.closeClosable(selector, ssc);
}
}
}