© 老男孩 / 2017-07-13
这里主要围绕着Java NIO展开,从Java NIO的基本使用,到介绍Linux下NIO API,再到Java Selector其底层的实现原理。
Java NIO基本使用
Linux下的NIO系统调用介绍
Selector原理
Channel和Buffer之间的堆外内存
Java NIO基本使用
从JDK NIO文档里面可以发现,Java将其划分成了三大块:Channel,Buffer以及多路复用Selector。Channel的存在,封装了对什么实体的连接通道(如网络/文件);Buffer封装了对数据的缓冲存储,最后对于Selector则是提供了一种可以以单线程非阻塞的方式,来处理多个连接。
基本应用示例
NIO的基本步骤是,创建Selector和ServerSocketChannel,然后注册channel的ACCEPT事件,调用select方法,等待连接的到来,以及接收连接后将其注册到Selector中。下面的为Echo Server的示例:
public class SelectorDemo {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.bind(new InetSocketAddress(8080));
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int ready = selector.select();
if (ready == 0) {
continue;
} else if (ready < 0) {
break;
}
Set
Iterator
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel accept = channel.accept();
if (accept == null) {
continue;
}
accept.configureBlocking(false);
accept.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 读事件
deal((SocketChannel) key.channel(), key);
} else if (key.isWritable()) {
// 写事件
resp((SocketChannel) key.channel(), key);
}
// 注:处理完成后要从中移除掉
iterator.remove();
}
}
selector.close();
socketChannel.close();
}
private static void deal(SocketChannel channel, SelectionKey key) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read > 0) {
buffer.flip();
responseBuffer.put(buffer);
} else if (read == -1) {
System.out.println("socket close");
channel.close();
return;
}
key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
key.attach(responseBuffer);
}
private static void resp(SocketChannel channel, SelectionKey key) throws IOException {
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.flip();
channel.write(buffer);
if (!buffer.hasRemaining()) {
key.attach(null);
key.interestOps(SelectionKey.OP_READ);
}
}
}
Linux下的NIO系统调用介绍
在Linux环境下,提供了几种方式可以实现NIO,如epoll,poll,select等。对于select/poll,每次调用,都是从外部传入FD和监听事件,这就导致每次调用的时候,都需要将这些数据从用户态复制到内核态,就导致了每次调用代价比较大,而且每次从select/poll返回回来,都是全量的数据,需要自行去遍历检查哪些是READY的。对于epoll,则为增量式的,系统内部维护了所需要的FD和监听事件,要注册的时候,调用epoll_ctl即可,而每次调用,不再需要传入了,返回的时候,只返回READY的监听事件和FD。下面作个简单的伪代码:
// 1. 创建server socket
// 2. 绑定地址
// 3. 监听端口
// 4. 创建epoll
int epollFd = epoll_create(1024);
// 5. 注册监听事件
struct epoll_event event;
event.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
event.data.fd = serverFd;
epoll_ctl(epollFd, EPOLL_CTL_ADD, serverFd, &event);
while(true) {
readyNums = epoll_wait( epollFd, events, 1024, -1 );
if ( readyNums < 0 )
{
printf("epoll_wait error\n");
exit(-1);
}
for ( i = 0; i < readyNums; ++i)
{
if ( events[i].data.fd == serverFd )
{
clientFd = accept( serverFd, NULL, NULL );
// 注册监听事件
...
}else if ( events[i].events & EPOLLIN )
{
// 处理读事件
}else if ( events[i].events & EPOLLRDHUP )
(0)