java NIO,非阻塞IO

《java网络编程》关于NIO的一个笔记

NIO介绍

在java中,通过传统的IO进行操作的时候,比如文件,网络读写数据的时候,流程都是阻塞的,也就是说,在一个线程通过IO调用 read() 和 write() 方法去读/写数据的时候,该线程往往是被阻塞的,直到数据可以读取或数据完全写入,此时线程不能做其他的事情。而使用NIO在读写数据的时候,如果没有数据可读或可写,这时,线程也不会阻塞而去做一些其他的事情,从而能够提升效率。

在java NIO中,有三个比核心的概念:

Buffer:缓冲区

Channel:通道

Selecrot:选择器

它们之间的关系如下:

在操作数据的时候,通过Channel来操作Buffer来实现的,也就是说,通过Channel来读取Buffer中的数据,通过Channel把数据写到Buffer中去。

下面依次介绍这几个概念:

Buffer,缓冲区:

 在NIO模型中,不再向输出流写入数据和从输入流中读取数据,而是从缓冲区中读写数据。

从编程的角度看,流和通道之间的关键区别在于流是基于字节的,而通道是基于块的,流设计为按顺序一个字节一个字节的传输数据(也可以传输字节数组),而通道会传输缓冲区中的数据块,可以在读写通道的字节之前,这些字节必须已经存储在缓冲区中,而且一次会读写一个缓冲区的数据。

Buffer是一个抽象类,提供了一些公用的方法,除了boolean外,java的所有基本数据类型都有特定的Buffer子类:
ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer和DoubleBuffer, 每个方法都有相应类型的返回值和参数列表。

网络程序几乎只会用ByteBuffer,但有时也会使用其他类型的Buffer.

Buffer有4个关键部分:

position(位置):缓冲区中可以读取或写入的下一个位置,这个位置从0开始,最大值等于缓冲区的大小。当从Buffer中读取或写入一个字节的时候,position加1;可以使用下面方法来获取和设置:

public final int position()
public final Buffer position(int newPosition)

capacity(容量):缓冲区中可以保存的元素的最大数目。容量值在创建缓冲区的时候设置,之后不能在改变,可以用以下方法读取:
public final int capacity()

limit(限度):缓冲区中可以访问数据的末尾位置,只要不改变限度,就无法读写超过这个位置的数据,即使缓冲区有更大的容量也没有用。可以用以下方法获取设置:
public final int limit() 
public final Buffer limit(int newLimit)

mark(标记):缓冲区中客户端指定的索引,也就是一个临时存放位置的下标,通过调用mark()方法,可以将标记设置为当前位置,调用reset()方法,可以将当前位置设置为所标记的位置:
public final Buffer mark() //  mark = position;
public final Buffer reset() // position = mark;

与读取InputStream不同,读取缓冲区实际上不会以任何方式改变缓冲区中的数据,只可能向前或向后设置位置,从而可以从缓冲区从某个特定位置开始读取或写入,类似的,程序可以调整限度,从而可以控制将要读取的数据的末尾,只有容量是固定的。

这几个参数的大小关系如下:
mark <= position <= limit <= capacity

下面通过图来说明:

图一:

通过allocate()来创建一个10个元素的缓冲区的时候,mark, position, limit, capacity的关系如上图一所示,

ByteBuffer buff = ByteBuffer.allocate(10);

此时,limit和capacity为9,position为0,mark为-1,

之后调用put()方法添加5个元素,mark, position, limit, capacity的位置变为下图:

		ByteBuffer buff = ByteBuffer.allocate(10);
		buff.put((byte)'a');
		buff.put((byte)'b');
		buff.put((byte)'c');
		buff.put((byte)'d');
		buff.put((byte)'e');

limit和capacity不变,为9, position变为了4,此时缓冲区中有5个元素,这时候想去读取缓冲区中的内容,通过get()方法取读取:

		ByteBuffer buff = ByteBuffer.allocate(10);
		buff.put((byte)'a');
		buff.put((byte)'b');
		buff.put((byte)'c');
		buff.put((byte)'d');
		buff.put((byte)'e');
		System.out.println("get() value : " + (char)buff.get());
get() value : 

可以看出取出的值为空,这是因为,当我们调用 put() 或 get() 方法的时候,position加1,当上面调用 buff.get() 的时候,实际上是取 position为5的值,因为position=5没有值(添加了5个元素,position为4,下标从0开始),所以取出的值为空,

此时需要调用 flip() 方法来改变postion的值,调用 flip() 方法后,limite设置为position的值,position设置为0,mark为-1,flip()方法源码如下:

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

调用flip()方法,limit, position, capacity和mark的位置如下图

		ByteBuffer buff = ByteBuffer.allocate(10);
		buff.put((byte)'a');
		buff.put((byte)'b');
		buff.put((byte)'c');
		buff.put((byte)'d');
		buff.put((byte)'e');
		buff.flip();

这时limit表示的是可取读取的数据最大位置,之后调用 get( )方法就可以从position=0的位置,也就是从头开始获取我们刚才添加的到缓冲区中的值了:

		ByteBuffer buff = ByteBuffer.allocate(10);
		buff.put((byte)'a');
		buff.put((byte)'b');
		buff.put((byte)'c');
		buff.put((byte)'d');
		buff.put((byte)'e');
		buff.flip();
		System.out.println("get() value : " + (char)buff.get());
		System.out.println("get() value : " + (char)buff.get());
		System.out.println("get() value : " + (char)buff.get());
		System.out.println("get() value : " + (char)buff.get());
		System.out.println("get() value : " + (char)buff.get());
get() value : a
get() value : b
get() value : c
get() value : d
get() value : e

在这个过程中,capacity初始化完成后就不会在改变了。

接下来说说 mark 的一个使用:

mark用来临时存放一个位置的下标,通过 mark() 方法来把当前的position记录下来,之后通过 reset() 方法来恢复这个位置,这样就可以多次读取数据:

		ByteBuffer buff = ByteBuffer.allocate(10);
		buff.put((byte)'a');
		buff.put((byte)'b');
		buff.put((byte)'c');
		buff.put((byte)'d');
		buff.put((byte)'e');
		
		buff.flip(); // position = 0
		
		buff.position(2);
		buff.mark(); // 把mark设置为position, mark = position = 2
		
		while(buff.hasRemaining())
		{
			System.out.println("get() value : " + (char)buff.get());
		}
		
        buff.reset(); //position = mark = 2
		while(buff.hasRemaining())
		{
			System.out.println("get() value : " + (char)buff.get());
		}

当调用 buff.mark() 的时候,mark = position = 2:

当第一次调用循环的时候,可以输出 c, d, e,之后position变为5,第二次调用循环的时候就不会输出值的,所有使用 buff.reset() 方法来恢复position的位置2,从而可以多次的读取相同的数据。

创建缓冲区:

public static ByteBuffer allocate(int capacity)
allocate()方法只返回一个指定固定容量的新缓冲区,这个缓冲区是基于数组实现的。可以通过 array() 和 arrayOffset() 方法来访问。通过该方法来创建缓冲区的时候,mark设置为-1,position(设置为0),limit(限度)和capacity(容量)相等,之后创建一个byte数组(数组长度为capacity)来存放数据。

public static ByteBuffer allocateDirect(int capacity)
allocateDirect() 方法不会为缓冲区创建后备数组,VM会对以太网卡,核心内存或其他位置上的缓冲区
使用直接内存访问,以此来直接分配ByteBuffer,其他类型的Buffer没有此方法,如IntBuffer。
这个方法不是必须的的,但可以提升IO操作性能。不过创建直接缓冲区比间接缓冲区代价更高,所以只能在
缓冲区可能只持续较短时间时才分配这种直接缓冲区,其细节非常依赖与VM,不建议使用。

如果已经有了要输出的数据数组,一般要用缓冲区进行包装,而不是分配一个新的缓冲区。
public static ByteBuffer wrap(byte[] array)
在这里,缓冲区包含数组的一个引用,这个数组作为缓冲区的后备数组,修改数组会反映到缓冲区,所以对数组操作结束之前不要包装数组。

Buffer还有几个公共的方法:

clear():

将 position(位置)设置为0,并将limit(限度)设置为capacity(容量),从而清空缓冲区,这样一来,就可以重用缓冲区了。不过,clear()方法没有删除缓冲区中的数据,这些数据仍然存在,还可以获取到这些数据。

    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

rewind():

将position(位置)设置为0,但不改变limit(限度),这允许重新读取缓冲区
public final Buffer rewind()

    public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

flip()方法:

翻转Buffer,该方法将limit(限度)设置为position(当前位置),position(当前位置设置为0),调用该方法可以清空刚刚填充的缓冲区。
Flips this buffer.  The limit is set to the current position and then
the position is set to zero.  If the mark is defined then it is discarded.

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

remaining():

返回position(当前位置)和limit(限度)之间的元素个数,

hasRemaining()方法:

如果position(当前位置)和limit(限度)之间的元素个数大于0,则返回true
public final boolean hasRemaining()

填充和清空:

缓冲区是为顺序访问而设计的,每个缓冲区都有一个当前位置,即position变量,从缓冲区读取或
写入一个元素时,position加1,如下创建一个10个大小的缓冲区,添加3个元素

CharBuffer buff = CharBuffer.allocate(10);
buff.put('a');
buff.put('b');
buff.put('c');

此时,position为3,
当使用get()方法获取下一个位置,也就是position=4的时候,会返回null,因为position=4没有填充值,
所有要想读取刚才填充的值,需要调用 flip方法来反转缓冲区,调用flip()方法后,会把limit设置为position,把position设置为0,也就是从0的位置开始读取,最多能读取limit个元素。在这个例子中limit为3。

compact()方法:

压缩缓冲区,压缩时,将缓冲区中所有剩余的数据移到缓冲区的开头,
为元素释放更多的空间,这些位置上的任何元素都将被覆盖,缓冲区的位置设置为元素末尾,limit设置为容量

public IntBuffer compact() {
    //把元素复制到缓冲区的开头
    System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
    //设置position为元素的个数,remaininh()返回元素个数
    position(remaining());
    //设置limit为容量大小
    limit(capacity());
    discardMark();
    return this;
}

duplicate()方法:

建立缓冲区的副本,从而将相同的信息分发到多个通道中,修改原来的缓冲区会影响到副本。尽管共享相同的数据,但初始和复制的缓冲区有独立的标记,限度和位置。
如果希望通过多个通道大致并行的传输相同的数据时,复制非常有用,可以为每个通道建立主缓冲区的副本,让每个通道以其自己的速度运行。

通道

通道将缓冲区的数据块移入或移除到各种IO源中,如文件,socket,数据报等。
对于网络编程来说,主要有三个通道类:SocketChannel, ServerSocketChannel, DatagramChannel.

SocketChannel:

SocketChannel类可以读写TCP socket,数据必须编码到ByteBuffer对象中来完成读写。
连接:
使用两个静态方法来创建SocketChannel对象:
public static SocketChannel open()
public static SocketChannel open(SocketAddress remote)
带参数的方法建立连接的时候,将会阻塞,也就是说在连接建立或抛出异常之前,这个方法不会返回。如

        SocketAddress address = new InetSocketAddress("www.baidu.com", 80);
        SocketChannel socketChannel = SocketChannel.open(address);

无参的方法不立即建立连接,之后调用connect()方法进行连接,如: 

        SocketAddress address = new InetSocketAddress("www.baidu.com", 80);
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(address);

如果想在连接前配置通道,希望以无阻塞的方式打开通道,就要使用这种方法:

        SocketAddress address = new InetSocketAddress("www.baidu.com", 80);
        SocketChannel socketChannel = SocketChannel.open();
        //配置通道或其他选项
        socketChannel.configureBlocking(false);
        socketChannel.connect(address);
        if (socketChannel.finishConnect()) {
            // 进行操作
        }

对于非阻塞模式来说,在进行操作之前,必须要使用 finishConnect()方法来判断是否已完成连接。

如果想检查连接是否完成,可以调用以下两个方法:
public abstract boolean isConnected()
public abstract boolean isConnectionPending()

如果连接打开,isConnected()为true,如果连接仍在建立但还没有打开时,isConnectionPending()为true。

读取:
SocketChannel使用read(ByteBuffer)来读取数据。
如果通道是阻塞的,则在读取到流末尾的时候,返回-1或抛出异常,如果是非阻塞的,则可能返回0.
写入:
SocketChannel使用write(ByteBuffer)来写入数据。
如果通道是非阻塞的,这个方法不会保证写入缓冲区中的全部内容,不过可以用循环来完全写入

        SocketAddress address = new InetSocketAddress("www.baidu.com", 80);
        SocketChannel socketChannel = SocketChannel.open();
        ByteBuffer buff = ByteBuffer.allocateDirect(100);
        while(buff.hasRemaining() && socketChannel.write(buff) != -1){
            // 操作
        }

ServerSocketChannel:

ServerSocketChannel类只有一个目的,就是接受入站的连接,无法对其进行读取,写入或连接。
使用以下方法来创建对象:
public static ServerSocketChannel open()
之后调用socket()方法来创建ServerSocket.

        SocketAddress address = new InetSocketAddress("www.baidu.com", 80);
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        ServerSocket socket = serverChannel.socket();
        socket.bind(address);

一旦打开并绑定了ServerSocketChannel对象,就可以使用 accept()方法来监听入站连接了,
public abstract SocketChannel accept()
在阻塞模式下,accept()方法会等待入站连接,然后它接受一个连接,并返回连接到远程客户端的一个
SocketChannel对象,在建立连接之前,线程无法进行任何操作,这种策略适用于立即响应每一个请求
的简单服务,阻塞模式为默认模式。
在非阻塞模式下,如果没有入站连接,accept()方法会返回Null,非阻塞模式更适合于需要为每个连接
完成大量工作的服务,这样就可以并行的处理多个请求。非阻塞模式一般与Selector结合使用。

Channels:
Channels是一个简单的工具类,可以将传统的IO流包装在通道中,也可以从通道中转换为基本的IO流。

选择器 Selector:

为了完成就绪选择,需要将不同的通道注册到一个Selector对象中,每个通道分配一个SelectorKey,
然后程序可以询问这个Selector对象,哪些通道已经准备就绪可以无阻塞的完成操作,就可以请求
Selector对象返回相应的键集合。

通过open()方法来创建新的选择器:
public static Selector open() 

之后向选择器注册通道:
public final SelectionKey register(Selector sel, int ops)
public final SelectionKey register(Selector sel, int ops, Object att)

第二个参数ops标识通道所注册的操作,为SelectionKey定义的4个常量:
SelectionKey.OP_ACCEPT
SelectionKey.OP_CONNECT
SelectionKey.OP_READ
SelectionKey.OP_WRITE

如果一个通道需要在同一个选择器中注册多个操作,使用“或”操作符即可:
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE)

将通道注册到选择器之后,就可以随时查询选择器,找到已经准备好的通道来进行操作:

public abstract int selectNow() 
以非阻塞的方式进行选择,如果当前没有准备好的要处理连接,则返回0,否则返回准备好的键的个数。

public abstract int select()
以阻塞的方式进行选择,直到至少有一个注册的通道可用。

public abstract Set<SelectionKey> selectedKeys()
使用SelectedKeys()方法获取就绪通道,然后循环处理每个SelectionKey,处理完成某个键时,
应删除,否则选择器在以后循环时还会一直通知有这个键。
 

NIO客户端例子:

	public void client()
	{
		// 要绑定的地址
		SocketAddress address = new InetSocketAddress("127.0.0.1", 8080);
		
		try (SocketChannel channel = SocketChannel.open())
		{
			//设置为非阻塞模式
			channel.configureBlocking(false);
			channel.connect(address);
			
			// 申请Buffer空间
			ByteBuffer buff = ByteBuffer.allocate(10);
			if (channel.finishConnect()) 
			{
				String sendMsg = "public static void main(String[] args)";
				//往缓冲区里面放数据
				buff.put(sendMsg.getBytes());
				//反转缓冲区,从缓冲区里面读取数据
				buff.flip();
				while(buff.hasRemaining())
				{
					System.out.println(buff);
					channel.write(buff);
					// 清空缓冲区
					buff.clear();
				}
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

NIO服务器例子:

	public void server()
	{
		SocketAddress address = new InetSocketAddress(8080);
		try 
		(
			//打开选择器
			Selector selector = Selector.open();
			//打开服务端通道
			ServerSocketChannel serverChannel = ServerSocketChannel.open();
		)
		{
			ServerSocket serverSocket = serverChannel.socket();
			serverSocket.bind(address);
			serverChannel.configureBlocking(false);
			// 把通道注册到选择器中
			serverChannel.register(selector, SelectionKey.OP_ACCEPT);
			//选择准备好的通道进行处理
			for(;;)
			{
				try {
					if (selector.select(3000) == 0) 
					{
						continue;
					}
				} catch (Exception e) {
					break;
				}
				
				Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
				while(iterator.hasNext())
				{
					SelectionKey key = iterator.next();
					// 如果当前的通道是可接受入站连接,则是一个服务端通道,程序就会接受一个新的Socket,将其添加到选择器中
					if (key.isAcceptable()) 
					{
						ServerSocketChannel server = (ServerSocketChannel) key.channel();
						SocketChannel client = server.accept();
						System.out.println("接受客户端的连接:" + client);
						client.configureBlocking(false);
						SelectionKey key2 = client.register(selector, SelectionKey.OP_WRITE);
						// 申请缓冲区
						ByteBuffer buff = ByteBuffer.allocate(10);
						buff.put((byte)'\r');
						buff.put((byte)'\n');
						buff.flip();
						key2.attach(buff);
					}
					else if(key.isWritable())
					{
						SocketChannel client = (SocketChannel) key.channel();
						ByteBuffer buff = (ByteBuffer) key.attachment();
						buff.flip();
						while(buff.hasRemaining())
						{
							client.write(buff);
						}
					}
					else if (key.isReadable()) 
					{
						SocketChannel channel = (SocketChannel) key.channel();
						ByteBuffer buff = (ByteBuffer) key.attachment();
						long len = channel.read(buff);
						while(len > 0)
						{
							buff.flip();
							while(buff.hasRemaining())
							{
								System.out.println((char)buff.get());
							}
							buff.clear();
							channel.read(buff);
						}
					}
					
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

 

转载于:https://my.oschina.net/mengyuankan/blog/1606577

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值