Java NIO学习

一、为什么出现Java NIO

由于Java的OutputStream和InputStream没有提供异步I/O的能力,OutputStream上的写操作write()方法会阻塞直至数据被成功写入,InputStream上的读操作read()方法也会阻塞,直到有数据可读,还有ServerSocket的accept()方法也会阻塞直至有客户端进行连接。它们都是阻塞方法,那当服务器需要处理上千个客户请求时,往往就需要一个客户对应一个线程,大量的闲置客户端会限制系统可以同时服务的客户端总数,所以JDK1.4引入了Java NIO(New Input/Output)。

二、Java NIO 基础

非阻塞:NIO引入了选择器(Selector)和通道(Channel)来实现非阻塞,通过表示到设备、文件、网络套接字这种可以执行不同I/O操作的程序组件的开放连接,有的通道允许选择器对它们进行轮询。通道可以注册一个选择器实例,通过该实例的select()方法来询问在一个或一组通道中,哪一个当前需要服务(读、写、接受)。非阻塞指的就是“在一个准备好的通道上进行相应的I/O操作,就不需要等待(阻塞)了”。

1.缓冲区

NIO一个主要的特性就是java.nio.Buffer。缓冲区代表了一个有限容量的容器(本质是一个数组),通道Channel使用Buffer实例来传递数据。流和通道的主要区别就是通道在读写数据时,所有的内容都是先放入缓冲区之中,再通过缓冲区取得的,这也说明了为什么通道是双向操作的,而流是单向的。

缓冲区有几个比较重要的变量:position、limit、capacity等;

还有几个操作缓冲区常用的方法:allocate()、wrap()、get()、put()、flip()等;

这里先不对这些进行说明,先大概从方法名和变量名能有一定的理解。

2.通道

Channel实例代表一个和设备的连接,通过它可以进行输入输出操作。

Java NIO中,ServerSocketChannel和SocketChannel对应于传统的基本套接字ServerSocket和Socket。

(1)ServerSocketChannel和SocketChannel都通过工厂方法创建,就是常见的open()方法。SocketChannel创建后,可以通过connect()方法连接到远程机器,通过close()关闭连接,和原来的Socket的操作没什么差别。

(2)SocketChannel读写数据时和通过Socket获取输入输出流进行读写不同,它使用前面说的Buffer缓冲区作为参数。一般使用read()/write()及其扩展方法。

          ServerSocketChannel提供了和ServerSocket类似的accept()、close()方法;但不提供bind()方法,要实现绑定Socket到某端口,需要使用ServerSocket.socket().bind(...)方法。

(3)通过configureBlocking(false)方法可以可以把Channel设置为非阻塞式。

  非阻塞式SocketChannel和阻塞式Socket有一些不同,比如:非阻塞式SocketChannel的connect()方法会立即返回,用户必须通过isConnected()判断连接是否已经建立,或者通过finishConnect()方法在非阻塞套接字上阻塞等待连接成功;非阻塞的read()在Socket上没有数据时会立即返回(0),不会等待;非阻塞的accept()如果没有等待的连接,将立即返回null。

(4)ServerSocketChannel和SocketChannel能够跟选择器(Selector)配合工作,避免非阻塞式I/O操作中很浪费资源的忙等方法。例如在连接很多但需要处理的请求很少时,就需要一种方法阻塞等待,直到至少一个Channel可以进行I/O操作,并指出是哪个通道,选择器就是为了这个功能而设计:一个Selector实例可以同时检查(等待)一组通道的I/O方法。

3.选择器

选择器(Selector)创建实例后,通过Channel的注册方法注册到想要监控的Channel实例上,然后调用选择器的select()方法,该方法会阻塞等待,直到有一个或多个通道准备好I/O操作。select()方法会返回可进行I/O操作的通道数量。它使得一个线程就能检查多个通道是否可以进行I/O操作,而不是一个线程对应一个通道了。

这里很重要的一点是选择器和通道进行关联的SelectionKey(选择器注册标记),它维护了一个通道上感兴趣的事件类型信息,包括4种:OP_READ(通道上有数据可读)、OP_WRITE(通道上已经可写)、OP_CONNECT(通道连接已建立)、OP_ACCEPT(通道上有连接请求)。

调用通道的register()方法,可以将一个选择器注册到通道,并指定该通道的初始兴趣时间集,注册完毕后便可以开始等待通道I/O事件了。

当select()方法的返回值大于0,表明有需要处理的I/O事件发生,选择器会将对应的通道放入已选键集中,通过Selector.selectedKeys()可以获得该集合,并在上面迭代处理关联通道上的I/O操作。比如通过selectedKeys()方法获取可进行I/O操作的通道,通过便利通道,利用SelectionKey判断通道上等待的操作(如key.isAcceptable()表明有一个入站请求),并做相应处理。

三、Java NIO 实例

下面一个demo用于更好地理解,实现的就是客户端向服务端的连接和互相发送当前时间。

NIOyyjServer.java

package com.yyj.nio;

import java.net.InetSocketAddress;
import java.net.ServerSocket;
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.Date;
import java.util.Iterator;

/**
 * Created by GraceYang on 2017/4/6.
 */
public class NIOyyjServer {
    public static void main(String args[])throws Exception{

        ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();// 获得一个ServerSocket通道
        serverSocketChannel.configureBlocking(false);// 设置通道为非阻塞
        ServerSocket serverSocket=serverSocketChannel.socket();//获得该通道对应的ServerSocket
        InetSocketAddress address=new InetSocketAddress(8000);
        serverSocket.bind(address);// 将该通道对应的ServerSocket绑定到8080端口
        Selector selector=Selector.open();// 获得一个通道管理器
        //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while(true){
            //注册OP_ACCEPT事件后:
            //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
            selector.select();
            Iterator iterator=selector.selectedKeys().iterator();// 获得selector中选中的项的迭代器,选中的项为注册的事件
            while(iterator.hasNext()){
                SelectionKey key=(SelectionKey) iterator.next();
                iterator.remove();// 删除已选的key,以防重复处理
                // 客户端请求连接事件
                if(key.isAcceptable()){
                    ServerSocketChannel serverSocketChannel1=(ServerSocketChannel)key.channel();
                    SocketChannel socketChannel=serverSocketChannel1.accept();// 获得和客户端连接的通道
                    socketChannel.configureBlocking(false);// 设置成非阻塞
                    Thread.sleep(3000);
                    //给客户端发送信息当前时间
                    socketChannel.write(ByteBuffer.wrap(new String("向客户端发送当前时间"+new Date()).getBytes()));
                    //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                else if(key.isReadable()){// 获得了可读的事件 ;处理读取客户端发来的信息的事件
                    SocketChannel socketChannel=(SocketChannel)key.channel();// 服务器可读取消息:得到事件发生的Socket通道
                    ByteBuffer buf=ByteBuffer.allocate(100);// 创建读取的缓冲区
                    socketChannel.read(buf);//读取到了客户端发来的信息
                    byte[] data = buf.array();
                    String msg = new String(data).trim();
                    System.out.println("服务端收到信息:"+msg);

                    socketChannel.close();
                }
            }
        }
    }
}

NIOyyjClient.java

package com.yyj.nio;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Date;
import java.util.Iterator;

/**
 * Created by GraceYang on 2017/4/7.
 */
public class NIOyyjClient {
    public static void main(String args[]) throws Exception{
        SocketChannel socketChannel=SocketChannel.open();// 获得一个Socket通道
        socketChannel.configureBlocking(false);// 设置通道为非阻塞
        Selector selector=Selector.open();// 获得一个通道管理器
        InetSocketAddress address=new InetSocketAddress("localhost",8000);
        // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调用channel.finishConnect();才能完成连接
        socketChannel.connect(address);
        //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
        socketChannel.register(selector, SelectionKey.OP_CONNECT);

        while (true){
            selector.select();
            Iterator iterator=selector.selectedKeys().iterator(); // 获得selector中选中的项的迭代器
            while (iterator.hasNext()){
                SelectionKey key=(SelectionKey) iterator.next();
                iterator.remove();// 删除已选的key,以防重复处理
                if(key.isConnectable()){ // 连接事件发生
                    SocketChannel socketChannel1=(SocketChannel)key.channel();
                    if(socketChannel1.isConnectionPending()){// 如果正在连接,则完成连接
                        socketChannel1.finishConnect();
                    }
                    socketChannel1.configureBlocking(false);// 设置成非阻塞
                    //给服务端发送信息
                    socketChannel1.write(ByteBuffer.wrap(new String("向服务端发送当前时间"+new Date()).getBytes()));
                    //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
                    socketChannel1.register(selector,SelectionKey.OP_READ);
                }
               else if(key.isReadable()){// 获得了可读的事件,处理读取服务端发来的信息的事件
                    SocketChannel socketChannel1=(SocketChannel)key.channel();
                    ByteBuffer buf=ByteBuffer.allocate(100);
                    socketChannel1.read(buf);
                    byte[] data = buf.array();
                    String msg = new String(data).trim();
                    System.out.println("客户端收到信息:"+msg);
                }
            }
        }
    }
}


运行结果:

NIOyyjServer:

服务端收到信息:向服务端发送当前时间Mon Apr 10 15:11:46 CST 2017

NIOyyjClient:

客户端收到信息:向客户端发送当前时间Mon Apr 10 15:11:49 CST 2017



参考书籍:《Hadoop技术内幕:深入解析Hadoop Common和HDFS架构设计与实现原理》


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值