1、NIO简介
JAVA中传统的IO模式,也就是BIO模式,如果在线程中调用read,而此时如果还没有消息到来,这个线程就会被挂起。为了处理多个连接,就需要启动相应数量的线程,这样才能保证每个线程之间互不打扰,不会因为某个线程的挂起,而导致其他连接得不到处理。
但是这样就会出现另外一个问题:N个工作线程中的大部分线程都因为等待消息而挂起,这些线程由于CPU任务调度算法,而频繁的被唤醒,但是又什么都没做又被挂起。在线程数少的时候,这种情况可以接受。但是当线程数多起来,就会浪费大量的CPU时间,如果按照如下方式计算吞吐量
吞吐量 = 实际处理消息时间 / (实际处理消息时间 + 线程切换总时间)
会发现,吞吐量会很低。正是因为这么多的问题,才会有NIO的诞生。如果想了解NIO的底层实现,可以去看看epoll的实现。这里超出了本文的范畴,不做介绍了~。
来看看NIO是如何工作的。由一个线程(该线程充当了观察者的角色)不断的去查看用户注册的感兴趣事件是否触发(这里感兴趣的事件比如某个连接是否有数据到达),一旦发现有感兴趣的事件到达,就可以将该事件交由工作线程去处理。而这个工作线程可以直接取出数据,或者写入数据,然后做处理。这样每个工作线程都专注于数据的读取和写入,和计算,保证了每次线程切换都是有意义的。按照上面的公式,线程的大部分时间都是用来处理消息,因此吞吐量相应也会提高很多。
下面来看几个术语,
1)Channel,NIO中的通道,相当于Socket连接的抽象,数据的源头或者数据的目的地。并且Channle支持异步I/O。
2)Selector,上面提到的观察者。轮询查找注册在该Selector上的Channel,查看关注的事件是否发生。在同一个Selector上可以注册多个通道,也就是说Selector一次select(),取出的SelectionKey可能归属于不同的Channel。
3)Buffer,和Channel配合使用。从Channel读取数据到Buffer,从Buffer将数据写入Channel。
4)SelectionKey,Selector在某个Channel上关注的事件。
2、NIO应用
Server:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class Server {
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定地址
ssc.bind(new InetSocketAddress("127.0.0.1", 8765));
// 设置非阻塞模式
ssc.configureBlocking(false);
// acceptor
final Selector acceptor = Selector.open();
ssc.register(acceptor, SelectionKey.OP_ACCEPT);
// selector
final Selector selector = Selector.open();
// acceptor thread,关注accept事件
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
int i = 0;
try {
i = acceptor.select();
} catch (IOException e) {
}
if (i != 0) {
Set<SelectionKey> selected = acceptor.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext()) {
SelectionKey cur = (SelectionKey) it.next();
it.remove();
if (cur.isAcceptable()){
ServerSocketChannel ssc = (ServerSocketChannel) cur.channel();
try {
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} catch (IOException e) {
}
}
else{
System.out.println("error");
}
}
}
}
}
}).start();
// selector thread,关注read和write
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
int i = 0;
try {
i = selector.select(1000);
} catch (IOException e) {
}
if (i != 0) {
// 获取关注的事件
Set<SelectionKey> selected = selector.selectedKeys();
Iterator it = selected.iterator();
while (it.hasNext()) {
SelectionKey cur = (SelectionKey) it.next();
it.remove();
if (cur.isReadable()){
// 获取产生该事件的channel
SocketChannel sc = (SocketChannel) cur.channel();
ByteBuffer bb = ByteBuffer.allocate(100);
try {
sc.read(bb);
System.out.println(new String(bb.array()).trim());
} catch (IOException e) {
}
}
else if(cur.isWritable()){
System.out.println("writable");
}
else{
System.out.println("error");
}
}
}
}
}
}).start();
} catch (IOException e) {
}
}
}
启动两个线程,一个用于accept,另一个用于select处理write和read事件。
Client:
/**
*
*/
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
Selector selector = Selector.open();
channel.connect(new InetSocketAddress("127.0.0.1", 8765));
channel.register(selector, SelectionKey.OP_CONNECT);
while(!channel.finishConnect()){
System.out.println("Not connected");
}
ByteBuffer byb = ByteBuffer.wrap("abc".getBytes());
channel.write(byb);
}
}
客户端连接服务器,并且写入abc字符串。
运行程序,服务器打印abc。