Java Nio

≡放荡痞女 提交于 2020-01-27 06:19:04

Java Nio简介

java Nio指的是java在1.4版本后推出的new io,nio的主要特性提升是io操作不再是阻塞的了,通常io我们会分为文件io和网络io,对于文件io操作nio还是阻塞的,但是对于socket的操作则不是阻塞的了。

由于oio(旧io)是阻塞的所以是无法在单线程内实现并发的,只能依赖一个io操作一个线程的方式来实现并发,但是线程的开销对于系统来说是太大了,而nio的实现就可以解决这一问题,nio可以通过单线程管理多个网络连接,并且每一个连接都是非阻塞的,单线程通过判断每个连接的就绪状态来管理并发,这也是IO多路复用的模式。
三个请求

Java Nio的三大组件

  • Buffer 缓存区
  • Channel 通道
  • Selector 选择器

Buffer

buffer是缓存区的意思,就好像是一个容器,用来存储需要io操作的数据,Java Nio一共有8个buffer类型:
在这里插入图片描述
分别对应java的四类八种基本数据类型。

Buffer源码分析

四大关键属性

	private int mark = -1; // 记录的是position
    private int position = 0; // 当前写或者读的位置
    private int limit; // buffer的最大读写限制
    private int capacity; // 容量

三大关键方法

/**
* 清空buffer,buffer转为可写
*/
public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
/**
* 将当前位置转为buffer最大限制,并将位置重为0,buffer转为可读
*/
public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }
/**
* 将当前位置重置为0,buffer可重复读
*/
public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

有人会问buffer作为一个数据容器没有存取的操作吗?当然有,只是buffer的存取操作非常简单,就是简单的put和get方法,但是put和get方法的实现依赖于上述的四大属性,无论是从buffer中读数据还是像buffer中写数据,都要移动postion,即position+1,当position=limit时数据存取到头,最后要注意buffer默认是可写的

另外,需要理解的是buffer只是一个容器,对于IO操作来说还要把数据写出去,将数据写出去的操作是由Channel来完成的,而Channel在写数据时会先读取buffer里的数据,Channel在读取数据时会写进buffer,也即是buffer的读写和Channel的读写正好反过来形成一个闭合回路。

Channel

channel总共分为大类型:

  • 文件通道 FileChannel
  • socket通道 ServerSocketChannel SocketChannel
  • 数据通道 DatagramChannel

channel其实可以类比oio的stream,下面写一个Nio的文件拷贝demo

/**
     * 文件拷贝
     * @param src
     * @param desc
     */
    private static void fileCopy(String src,String desc){

        File srcFile = new File(src);
        File descFile = new File(desc);
        
        FileInputStream fis = null;
        FileOutputStream fos = null;
        FileChannel inChannel = null;
        FileChannel outChannel = null;

        try {
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(descFile);
            inChannel = fis.getChannel();
            outChannel = fos.getChannel();
            // 定义一个buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int length = -1;
            while ((length = inChannel.read(buffer)) != -1){
                // 此时将buffer转为可读 outChannel 读取buffer并写出去
                System.out.println("读入字节:"+length);
                buffer.flip();
                int outLength = 0;
                while ((outLength = outChannel.write(buffer)) != 0){
                    System.out.println("写入字节:"+outLength);
                }
                buffer.clear();
            }
            outChannel.force(true);
        }catch (Exception e){
            System.out.println(e.toString());
        }finally {
            close(fis);
            close(fos);
            close(inChannel);
            close(outChannel);
        }
    }

以上代码的流程可以拆分为一下四步:

  1. inChannel将数据读进buffer
  2. buffer.flip() 将buffer变成可读
  3. outChannel将buffer写出去
  4. buffer.clear() 将buffer变成可写

由于socket通道和数据通道都用到了选择器,所以放在下面讲,其实用法文件通道一样,所有IO操作无非是读和写。

Selector

selector故名选择器,是用来监控和管理channel的,之所以单线程实现并发就是基于selector的,原理是将channel注册进selector里,写个socket编程的demo

服务端:

public class NioReceiveServer {

    static class Client{
        // 文件名称
        String fileName;
        // 文件长度
        long length;
        // 文件开始传输时间
        long startTime;
        // 客户端地址
        InetSocketAddress remoteAddress;
        // 文件输出通道
        FileChannel fileChannel;
    }

    private ByteBuffer buffer = ByteBuffer.allocate(1024);

    private Charset charset = Charset.forName("UTF-8");

    Map<SelectableChannel,Client> clientMap = new HashMap<SelectableChannel,Client>();

    private static final String DESC_PATH = "/Users/wengyuzhu/Desktop/demo";

    public  void startServer() throws IOException {
        // 1.获取选择器
        Selector selector = Selector.open();
        // 2.获取socket通道
        ServerSocketChannel socketChannel = ServerSocketChannel.open();
        // 3.获取socket
        ServerSocket socket = socketChannel.socket();
        // 4.通道设置非阻塞
        socketChannel.configureBlocking(false);
        // 5.端口绑定
        socket.bind(new InetSocketAddress(18888));
        // 6.将通道注册到选择器上
        socketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("=========Server start=========");
        while (selector.select() > 0){
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
            while (it.hasNext()){
                SelectionKey key = it.next();
                if (key.isAcceptable()){
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel channel = server.accept();
                    if (channel == null) {
                        continue;
                    }
                    channel.configureBlocking(false);
                    channel.register(selector,SelectionKey.OP_READ);
                    // 初始化客户端
                    Client client = new Client();
                    client.remoteAddress =(InetSocketAddress) channel.getRemoteAddress();
                    clientMap.put(channel,client);
                    System.out.println("=========Client connect=========");
                }else if (key.isReadable()){
                    System.out.println("=========Client write=========");
                    processData(key);
                }
                it.remove();
            }
        }
    }

    private void processData(SelectionKey key){
        Client client = clientMap.get(key.channel());
        SocketChannel channel =(SocketChannel) key.channel();
        int length = -1;
        try {
            //
            buffer.clear();
            while ((length = channel.read(buffer)) > 0){
                buffer.flip();
                if (client.fileName == null) {
                    String fileName = "1.txt";
                    System.out.println("文件名称:"+fileName);
                    File descPath = new File(DESC_PATH);
                    if (!descPath.exists()) {
                        descPath.mkdir();
                    }
                    String fullName = descPath.getAbsolutePath() + File.separator + fileName;
                    File file = new File(fullName);
                    FileChannel fileChannel = new FileOutputStream(file).getChannel();
                    client.fileChannel = fileChannel;
                    client.fileName = fileName;
                    System.out.println("文件名称:"+fileName);
                }else if (client.length == 0){
                    long longa = buffer.getLong();
                    client.length = longa;
                    client.startTime = System.currentTimeMillis();
                    System.out.println("文件大小:"+longa);
                }else {
                    // 文件内容
                    client.fileChannel.write(buffer);
                    System.out.println("接收文件内容:"+length);
                }
                buffer.clear();
            }
            client.fileChannel.force(true);


            if (length == -1){
                // 传输完毕
                channel.close();
                client.fileChannel.close();
                key.cancel();
                System.out.println("========上传完毕========");
                System.out.println("======文件名称:"+client.fileName);
                System.out.println("======文件大小:"+client.length);
            }

        }catch (Exception e){
            key.cancel();
            System.out.println("服务器异常:error = "+e.toString());
        }
    }

    public static void main(String[] args) throws IOException {
        NioReceiveServer server = new NioReceiveServer();
        server.startServer();
    }
}

客户端:

public class NioClient {

    private static final String SRC = "/Users/wengyuzhu/Desktop/JAVA并发编程实战.pdf";

    private Charset charset = Charset.forName("UTF-8");

    private void sendFile() {
        try{
            File file = new File(SRC);
            if (!file.exists()){
                System.out.println("文件不存在");
                return;
            }


            // 文件通道
            FileChannel fileChannel = new FileInputStream(file).getChannel();
            // socket连接
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.socket().connect(new InetSocketAddress("127.0.0.1",18888));
            socketChannel.configureBlocking(false);
            System.out.println("开始连接服务器");
            // 自旋连接
            while (!socketChannel.finishConnect()){

            }



            // 写出文件名
            ByteBuffer bf = charset.encode(file.getName());
            System.out.println(file.getName());
            bf.flip();
            socketChannel.write(bf);

            // 写出文件大小
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.putLong(file.length());
            buffer.flip();
            socketChannel.write(buffer);
            buffer.clear();

            // 写出文件
            System.out.println("开始传输文件");
            int length = -1;
            long pos = 0;
            while ((length = fileChannel.read(buffer)) != -1){
                buffer.flip();
                socketChannel.write(buffer);
                buffer.clear();
                pos += length;
                System.out.println("传输文件大小:"+pos);
            }

            fileChannel.close();
            socketChannel.shutdownInput();
            socketChannel.close();
            System.out.println("文件传输完成");

        }catch (Exception e){
            System.out.println("文件传输失败");![在这里插入图片描述](https://img-blog.csdnimg.cn/20200114213255601.jpg)
        }
    }

    public static void main(String[] args) {
        NioClient client = new NioClient();
        client.sendFile();
    }
}

总结

nio的重点我觉得在于理解buffer的数据结构和学习selector的编程思想,至于channel的用法和之前流的用法并无明显区别。

扫码关注个人公众号

在这里插入图片描述

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!