Java NIO使用教程

文中示例代码:https://github.com/q200737056/Java-Course

一、概述

Java NIO是java 1.4之后新出的一套IO接口,这里的新是相对于原有标准的Java IO和Java Networking接口。NIO提供了一种完全不同的操作方式。NIO中的N可以理解为Non-blocking(非阻塞)。

同步(Sync)vs 异步(Async)

  • 同步,发出一个功能调用时,在没有得到结果之前,不会执行后续操作。
  • 异步,发出一个功能调用时,会立即返回,不管有没有得到结果。继续执行后续操作。一般通过状态、通知和回调来通知调用者。

阻塞 vs 非阻塞

  • 阻塞,调用结果返回之前,当前线程会被阻塞。只有在得到结果之后才会返回。
  • 非阻塞,不能立刻得到调用结果时,当前线程不会被阻塞。

其实同步异步与阻塞非阻塞区别是关注的角度不同,同步和异步关注调用后是否等待结果返回。而阻塞和非阻塞关注的是调用时当前的线程是否阻塞。

NIO vs IO

  • IO是面向流的,而NIO是面向缓冲区的。
  • IO的各种流都是阻塞的。这意味着一个线程一旦调用了read(),write()方法,那么该线程就被阻塞住了;NIO的非阻塞模式使得线程可以通过channel来读数据,并且返回当前已有的数据,或者返回空的(可能没有数据)。这样一来线程不会被阻塞住,它可以继续向下执行。

NIO包含3个核心的组件

  • Channel通道。如FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel。
  • Buffer缓冲区。如ByteBuffer,CharBuffer,IntBuffer,MappedByteBuffer。
  • Selector多路复用器。

二、Channel,Buffer,Selector

Channel通道和流非常相似,主要有以下几点区别

  • 通道可以读也可以写,流一般来说是单向的(只能读或者写)。
  • 通道可以异步读写。
  • 通道总是基于缓冲区Buffer来读写。

通过一个FileChannel的例子来了解如何使用

RandomAccessFile file = new RandomAccessFile("d:/test.txt", "rw");
FileChannel inChannel = file.getChannel();
//开辟一块48字节的缓冲区
ByteBuffer buf = ByteBuffer.allocate(48);
//从通道中写入到缓冲区
int bytesRead = inChannel.read(buf);
    while(bytesRead != -1) {
      //写模式转读模式
      buf.flip();
      //如果position与limit上限之间有元素,则一个一个读取
      while(buf.hasRemaining()){
          System.out.print((char) buf.get());
      }
     //清空数据,准备下次写入,实际上只是重置了位置标识
      buf.clear();
     //继续读
      bytesRead = inChannel.read(buf);
    }
    file.close();

Buffer最重要的3个属性

  • capacity容量
  • position位置
  • limit上限


容量(Capacity)
作为一块内存,buffer有一个固定的大小,叫做capacity容量。也就是最多只能写入容量值得字节,整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据。

位置(Position)
当写入数据到Buffer的时候需要中一个确定的位置开始,默认初始化时这个位置position为0,一旦写入了数据比如一个字节,整形数据,那么position的值就会指向数据之后的一个单元,position最大可以到capacity-1.
当从Buffer读取数据时,也需要从一个确定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。

上限(Limit)
在写模式,limit的含义是我们所能写入的最大数据量。它等同于buffer的容量。
一旦切换到读模式,limit则代表我们所能读取的最大数据量,他的值等同于写模式下position的位置。
数据读取的上限时buffer中已有的数据,也就是limit的位置(原position所指的位置)。

Buffer主要的几个方法介绍
flip()
flip方法可以把Buffer从写模式切换到读模式。会把position归零,并设置limit为之前的position的值。
rewind()
rewind方法将position设置为0,limit保持不变,这样我们可以重复读取buffer中的数据。
clear() 与compact()
clear方法会重置position为0,limit为capacity,也就是把整个Buffer清空,为下次写入做好准备。实际上Buffer中数据并没有清空,我们只是把标记重置了。
compact方法保留未读数据,即重置position为limit-原来的position,limit为capacity,清空了已读部分。
mark()与reset()
通过mark方法可以标记当前的position,通过reset来恢复到原来的position位置。

Selector多路复用器实现了可以用单线程来处理多个channel。对高并发来说大大减少了对线程的开销。


如何使用Selector

  1. 创建Selector。Selector selector = Selector.open();

  2. 注册Channel到Selector上。Channel必须是非阻塞的。FileChannel不能切换为非阻塞模式,所以不适用。SocketChannel channel= SocketChannel.open(); channel.configureBlocking(false);SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
    register方法第二个参数为关注集合。

    • SelectionKey.OP_CONNECT(1 << 3)连接就绪状态
    • SelectionKey.OP_ACCEPT(1 << 4)可连接就绪状态
    • SelectionKey.OP_READ(1 << 0)读就绪状态
    • SelectionKey.OP_WRITE(1 << 2)写就绪状态

    我们发现这些状态值都使用了位运算,所以想监听多个状态时,可使用位或运算结合多个常量。SelectionKey.OP_READ | SelectionKey.OP_WRITE。取出来的时候可以按位与运算。int val = key.interestOps();int op_accept = val & SelectionKey.OP_ACCEPT;

register重载方法还有第3个参数。可以给SelectionKey附加一个Object对象,这样做一方面可以方便我们识别某个特定的channel,同时也增加了channel相关的附加信息。channel.register(selector, SelectionKey.OP_READ, obj);,也可以调用SelectionKey对象的attach方法添加。key.attach(theObject);,取出附加信息Object obj= key.attachment();

  1. 调用selector.select()阻塞等待,select方法底层实现是一个轮询,跟操作系统有关。select方法会返回所有处于就绪状态的channel。
  2. 返回了有channel就绪之后,获取SelectionKey集合。Set<SelectionKey> selectedKeys = selector.selectedKeys();,遍历SelectionKey集合,根据各个就绪状态,进行相应的处理。

三、NIO套接字通道简单示例

服务端代码

public class HelloServer {  
     
    private String name = "";  
    private Selector selector;
    //开出一块1024字节的缓冲区
    private ByteBuffer buffer = ByteBuffer.allocate(1024);  
    private CharsetDecoder decoder = Charset.forName("GB2312").newDecoder();  
    private CharsetEncoder encoder = Charset.forName("GB2312").newEncoder();  
  
    public HelloServer(int port) throws IOException {  
        selector = this.getSelector(port);  
        
    }  
  
    private Selector getSelector(int port) throws IOException {
         //获得一个ServerSocketChannel通道  
        ServerSocketChannel server = ServerSocketChannel.open();
        // 设置通道为非阻塞
        server.configureBlocking(false); 
        // 绑定端口 
        server.socket().bind(new InetSocketAddress(port));  
        
        // 创建多路复用器
        Selector sel = Selector.open();  
        //将该通道绑定到Selector,并为该通道注册SelectionKey.OP_ACCEPT事件(可连    接事件监听)  
        //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。  
        server.register(sel, SelectionKey.OP_ACCEPT);  
        return sel;  
    }  
  
    public void listen() {
        System.out.println("服务端启动成功!"); 
        try {  
            while(true) {
                //当注册的事件到达时,方法返回;否则,该方法会一直阻塞 
                selector.select();
                // 获得selectedKey集合的迭代器
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();  
                while (iter.hasNext()) {  
                    SelectionKey key =  iter.next();
                    // 必须手工移除,以防重复处理
                    iter.remove();
                    
                    process(key);  
                }  
            }  
        } catch (IOException e) {  
            e.printStackTrace();  
        }  
    }  
  
    // 处理事件  
    private void process(SelectionKey key) throws IOException {  
        if (key.isAcceptable()) { // 接收请求  
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            // 获得和客户端连接的通道 
            SocketChannel channel = server.accept();  
            //设置非阻塞模式  
            channel.configureBlocking(false);
            //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读就绪状态。
            channel.register(selector, SelectionKey.OP_READ);  
        } else if (key.isReadable()) { // 读信息
           // 服务器可读取消息,得到事件发生的SocketChannel通道 
            SocketChannel channel = (SocketChannel) key.channel();
            //从Channel写到Buffer
            int count = channel.read(buffer);  
            if (count > 0) {
                //将Buffer从写模式切换到读模式
                buffer.flip();
                //解码
                CharBuffer charBuffer = decoder.decode(buffer);  
                name = charBuffer.toString();  
                System.out.println("收到客户端信息:"+name);
                //给通道设置写就绪
                SelectionKey sKey = channel.register(selector,  
                        SelectionKey.OP_WRITE);  
                sKey.attach(name);//附加name数据  
            } else {  
                channel.close();  
            }  
            //清空缓冲区
            buffer.clear();  
        } else if (key.isWritable()) { // 写事件  
            SocketChannel channel = (SocketChannel) key.channel();
            //获取附加的name
            String name = (String) key.attachment();
            System.out.println("向客户端发送消息:"+"Hello! " + name);
            // 编码
            ByteBuffer block = encoder.encode(CharBuffer.wrap("Hello! " + name));  
              
            //从Buffer读取数据到Channel
            channel.write(block);  
            //关闭客户端通道
            channel.close();  
        }  
    }  
  
    public static void main(String[] args) throws IOException {  
        //单线程 管理多个SocketChannel通道
        HelloServer server = new HelloServer(8888);  
        server.listen();  
    }  
} 

客户端代码

public class HelloClient {  
      
    private InetSocketAddress ip = new InetSocketAddress("localhost", 8888);
    private CharsetDecoder decoder = Charset.forName("GB2312").newDecoder();  
    private CharsetEncoder encoder = Charset.forName("GB2312").newEncoder();  
    
     class Message implements Runnable {  
         private String name;  
         private String msg = "";  
  
        public Message(String name) {  
            this.name = name;  
        }  
  
        public void run() {  
            try {  
                long start = System.currentTimeMillis();  
                //打开SocketChannel通道  
                SocketChannel client = SocketChannel.open();  
                //设置为非阻塞模式  
                client.configureBlocking(false);  
                // 创建多路复用器  
                Selector selector = Selector.open();  
                //注册连接就绪状态
                client.register(selector, SelectionKey.OP_CONNECT);  
                //去连接  
                client.connect(ip);  
                //创建读取的缓冲区 
                ByteBuffer buffer = ByteBuffer.allocate(1024);  
               
                _FOR: while(true) {  
                    selector.select(); 
                    // 获得selectedKey集合的迭代器
                    Iterator<SelectionKey> iter = selector.selectedKeys().iterator();  
  
                    while (iter.hasNext()) {  
                        SelectionKey key =  iter.next();
                        // 必须手工移除,以防重复处理
                        iter.remove();  
                        if (key.isConnectable()) { // 连接事件监听   
                            SocketChannel channel = (SocketChannel) key.channel();
                            // 如果正在连接,则完成连接
                            if (channel.isConnectionPending())  
                                channel.finishConnect();
                            //向服务器发送数据
                            System.out.println("向服务端发送消息:"+name);
                            channel.write(encoder.encode(CharBuffer.wrap(name))); 
                            //在和服务端连接成功之后,给通道设置读就绪状态
                            channel.register(selector, SelectionKey.OP_READ);  
                        } else if (key.isReadable()) {  // 可读事件监听  
                            SocketChannel channel = (SocketChannel) key.channel();
                            //从Channel写到Buffer
                            int count = channel.read(buffer);  
                            if (count > 0) {  
                                //将Buffer从写模式切换到读模式
                                buffer.flip();  
                                CharBuffer charBuffer = decoder.decode(buffer);  
                                msg = charBuffer.toString(); 
                                System.out.println("收到服务端信息:"+msg);
                                //清空整个缓存,compact()方法只会清除已经读过的数据
                                buffer.clear();  
                            } else {
                                System.out.println(name+"没数据可读,关闭客户端");
                                client.close();  
                                break _FOR;  
                            }  
                        }  
                    } 
                    
                }  
                double last = (System.currentTimeMillis() - start) * 1.0 / 1000;  
                System.out.println(name+"使用时间 :" + last + "s.");  
                msg = "";  
            } catch (IOException e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
    public static void main(String[] args) throws IOException {  
    
        HelloClient cli = new HelloClient();
        for (int index = 0; index < 10; index++) {
            System.out.println("开启客户端"+index);
            new Thread(cli.new Message("client[" + index + "]").start();  
        }  
    }  
} 

四、异步文件通道

Java7中新增了AsynchronousFileChannel异步文件通道,使得数据可以进行异步读写。
以下简单例子

public class AIOTest {
    public static void main(String[] args) throws IOException {
        AIOTest test  = new AIOTest();
        test.writeTest("d:/1.txt");
        test.readTest("d:/1.txt");
    }
    
    public void readTest(String filename) throws IOException{
        Path path = Paths.get(filename);
        AsynchronousFileChannel fileChannel = 
                AsynchronousFileChannel.open(path, StandardOpenOption.READ);

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int position = 0;

        Future<Integer> operation = fileChannel.read(buffer, position);
        //是否操作完成
        while(!operation.isDone());

        buffer.flip();
        byte[] data = new byte[buffer.limit()];
        buffer.get(data);
        System.out.println(new String(data));
        buffer.clear();
    }
    public void writeTest(String filename) throws IOException{
        Path path = Paths.get(filename);
                if(!Files.exists(path)){
            Files.createFile(path);
        }
        AsynchronousFileChannel fileChannel = 
            AsynchronousFileChannel.open(path, StandardOpenOption.WRITE);

        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int position = 0;
        //是否操作完成
        buffer.put("112345678".getBytes());
        buffer.flip();
        
        Future<Integer> operation = fileChannel.write(buffer, position);
        buffer.clear();
        while(!operation.isDone());
        System.out.println("Write done");
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容