Java中NIO详解

NIO是当前Java中最流行的IO方式,大名鼎鼎的网络框架Netty就是基于NIO的。本文将仔细介绍NIO的工作方式。

NIO简介

NIO的底层原理就是对IO进行多路复用,对IO多路复用不太了解的可以看我之前写的文章Linux中网络IO模型详解

通过多路复用可以在一个线程中监听多个连接,节省了线程资源。

NIO详解

NIO中有三个核心:
1、Buffer简介
Buffer就是缓冲池。Buffer和Channel配合使用。
1、将Channel中的数据读取到Buffer中。
2、将Buffer中的数据写入到Channel中。

Java NIO 有以下Buffer类型

ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
MappedByteBuffer

Buffer详解
Buffer就是一个数组缓冲池。Buffer有两种模式,往Buffer中写数据,或者从Buffer中读取数据。读写模式通过flip()函数切换。

Buffer默认为写模式

Buffer主要通过三个变量和一个切换函数flip()来维护。
1、position
当前第一个可以读或者写的下标
2、limit
当前可读或者可写的最后一位下标+1
3、capacity
Buffer的容量,也就是数组长度。
4、flip()
flip()用来切换读写模式。
当从写切换读之后,position和limit之间的数据是之前写的数据,可以进行读取。
当从读切换写之后,postion和limit之间才可以被写数据。

public final Buffer flip() {
    limit = position;
    position = 0;
    return this;
}

主要方法
get(): 读取一个byte,将position++
get(byte[] bytes,int offeset,int length): 将buffer中的[offset,offset+length)复制到bytes中,position+=length
put(byte[] bytes): 往buffer中写入bytes,position += bytes.length
clear(): 将buffer清除,position=0,limit=capacity
compact(): 假设还有n个字节未被读取,就将这n个字节搬运到数组头部,从数组的第n位开始写。
remaining(): 返回limit - position,也就是当前还是多少字节未读取或者未写

2、Channel
Channel是基于流的改进,
Channel是面向缓冲区的,并且需要与另一个Buffer配和使用,我的猜测是将数据弄了一个缓冲区,然后通过Buffer将数据拷贝到缓冲区或者从缓冲区里将数据拷贝到Buffer中。

与流有以下区别

Channel
是否支持异步支持异步不支持异步
是否支持双向储传输数据双向单向
原理缓冲区
性能
是否集合Buffer使用结合Buffer使用不用

网络IO中Channel主要有以下两大类:
1、ServerSocketChannel
用于服务器的Channel,负责接收客户端SocketChannel的连接

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(9999));

while(true){
    //聆听新连接,如果调用configureBlocking(false)的话,accept不阻塞,如果没有相关数据就返回null
    //默认为阻塞
    SocketChannel socketChannel = serverSocketChannel.accept();

    //非阻塞模式下
	if(socketChannel != null){
        //do something with socketChannel...
    }

}

2、SocketChannel
用于接收客户端SocketChannel的数据传输

SocketChannel socketChannel = SocketChannel.open();
//连接
socketChannel.connect(new InetSocketAddress("http://jenkov.com", 80));

//读取数据
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);


//写入数据
String newData = "New String to write to file..." + 
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

3、Selector

NIO的优势就是利用单线程来处理多个Socket。其原理就是依靠Selector。具体的原理可以查看我之前写的文章Linux中网络IO模型详解中的IO复用模型。

Selector就是IO复用中的监视器,一个Selector可以监视多个Socket,Socket需要将其感兴趣的事件注册给Selector。当对应的Socket事件发生后,会将其加入到Selector对应的队列中,然后会将Selector唤醒,执行其业务代码。

Socket共有四个事件:
1、CONNECT
2、ACCEPT
3、READ
4、WRITE

通道触发了一个事件意思是该事件已经就绪。所以,某个channel成功连接到另一个服务器称为“CONNECT”。一个server socket channel准备好接收新进入的连接称为“ACCEPT”。一个有数据可读的通道可以说是“READ”。等待写数据的通道可以说是“WRITE”。

具体的步骤就看下面的代码吧。

代码

服务器端


public class ServerConnect
{
    private static final int BUF_SIZE=1024;
    private static final int PORT = 8080;
    private static final int TIMEOUT = 3000;
    public static void main(String[] args)
    {
        selector();
    }
	//如果是新连接
    public static void handleAccept(SelectionKey key) throws IOException{
        //获取其请求的ServerSocketChannel
        ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();
        //为客户端新建SocketChannel
        SocketChannel sc = ssChannel.accept();
        //配置非阻塞
        sc.configureBlocking(false);
        //为其注册Selector,监听事件为READ
        sc.register(key.selector(), SelectionKey.OP_READ);
    }

   //读取数据
    public static void handleRead(SelectionKey key) throws IOException{
        SocketChannel sc = (SocketChannel)key.channel();
        ByteBuffer buf = ByteBuffer.allocateDirect(BUF_SIZE);
        long bytesRead = sc.read(buf);
        while(bytesRead>0){
            buf.flip();
            while(buf.hasRemaining()){
                System.out.print((char)buf.get());
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        if(bytesRead == -1){
            sc.close();
        }

	    //如果要是往客户端返回数据的话,可以在此处直接发送,也可以注册写事件,因为如果在此处发的话,有可能缓冲区不可用,可以绑定一个事件,让Selector监控,在缓冲区可用的时候,发送
	    key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
	    //绑定要发送的Buffer
	    key.attach(buffer);
    }
    
    //写数据
    public static void handleWrite(SelectionKey key) throws IOException{
       ByteBuffer buffer = (ByteBuffer) key.attachment();
    	SocketChannel channel = (SocketChannel) key.channel();
    	if (buffer.hasRemaining()) {
        	channel.write(buffer)
    	} else {
        //发送完了就取消写事件,否则下次还会进入该分支
        key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
    	}
    }
    
    public static void selector() {
        Selector selector = null;
        ServerSocketChannel ssc = null;
        try{
            //生成Selector监视器
            selector = Selector.open();
            //生成ServerSocketChannel
            ssc= ServerSocketChannel.open();
            //为ServerSocket绑定端口
            ssc.socket().bind(new InetSocketAddress(PORT));
            //ServerSocket与Selector搭配使用时,必须使用非阻塞模式
            ssc.configureBlocking(false);
            //将ServerSocketChannel注册到Selector上,其关心的事件时ACCEPT,也就是有新的SocketChannel连接的时候
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            //死循环来监视Socket
            while(true){
                //如果超时了还没有Socket事件发生,就继续
                if(selector.select(TIMEOUT) == 0){
                    System.out.println("==");
                    continue;
                }
                //获取发生的Socket事件
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
                //遍历Socket事件
                while(iter.hasNext()){
                    //获得对应的事件
                    SelectionKey key = iter.next();
                    //如果是客户端SocketChannel来连接
                    if(key.isAcceptable()){
                        handleAccept(key);
                    }
                    //如果是有客户端SocketChannel发来数据
                    if(key.isReadable()){
                        handleRead(key);
                    }
                    //如果是写数据
                    if(key.isWritable() && key.isValid()){
                        handleWrite(key);
                    }
                    
                    if(key.isConnectable()){
                        System.out.println("isConnectable = true");
                    }
                    iter.remove();
                }
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(selector!=null){
                    selector.close();
                }
                if(ssc!=null){
                    ssc.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}

客户端代码

//打开通道
SocketChannel socketChannel = SocketChannel.open();

//连接远程主机
socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
//循环处理
ByteBuffer buffer = ByteBuffer.allocate(1024);
int r = 0;
while ( (r = channel.read(buffer)) > 0){
    buffer.flip();
    byte[] bytes1 = new byte[buffer.remaining()];
    buffer.get(bytes1);
    System.out.println(new String(bytes1));
}
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值