Java的IO操作

本文探讨了Java的NIO和NIO2中同步与异步、阻塞与非阻塞的区别,重点讲解了NIO的Buffer、Channel和Selector,以及NIO2引入的事件驱动编程模型,展示了如何利用这些技术提升网络编程的效率和扩展性。
摘要由CSDN通过智能技术生成

首先看两个概念

  • 区分同步或异步(synchronous/asynchronous)。同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。
  • 区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如 ServerSocket 新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管 IO 操作是否结束,直接返回,相应操作在后台继续处理。

关于IO

  • IO 不仅仅是对文件的操作,网络编程中,比如 Socket 通信,都是典型的 IO 操作目标。
  • 输入流、输出流(InputStream/OutputStream)是用于读取或写入字节的,例如操作图片文件。
  • Reader/Writer 则是用于操作字符,增加了字符编解码等功能,适用于类似从文件中读取或者写入文本信息,本质上计算机操作的都是字节。
  • BufferedOutputStream 等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高 IO 处理效率。这种设计利用了缓冲区,将批量数据进行一次操作。
  • 很多 IO 工具类都实现了 Closeable 接口,因为需要进行资源的释放。比如,打开 FileInputStream,它就会获取相应的文件描述符(FileDescriptor),需要利用 try-with-resources、 try-finally 等机制保证 FileInputStream 被明确关闭,进而相应文件描述符也会失效,否则将导致资源无法被释放。

按功能来分:输入流(input)、输出流(output)。

按类型来分:字节流和字符流。

字节流和字符流的区别是:字节流按8位传输以字节为单位输入输出数据,字符

流按16位传输以字符为单位输入输出数据。

NIO

NIO 的主要组成部分

  • Buffer,高效的数据容器,除了布尔类型,所有原始数据类型都有相应的 Buffer 实现。
  • Channel,类似在 Linux 之类操作系统上看到的文件描述符,是 NIO 中被用来支持批量式 IO 操作的一种抽象。File 或者 Socket,通常被认为是比较高层次的抽象,而 Channel 则是更加操作系统底层的一种抽象。
  • Selector,是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。

简单看一个NIO的例子

public class DemoServer extends Thread {

    private ServerSocket serverSocket;

    public int getPort() {

        return  serverSocket.getLocalPort();

    }

    public void run() {

        try {

            serverSocket = new ServerSocket(0);

            while (true) {

                Socket socket = serverSocket.accept();

                RequestHandler requestHandler = new RequestHandler(socket);

                requestHandler.start();

            }

        } catch (IOException e) {

            e.printStackTrace();

        } finally {

            if (serverSocket != null) {

                try {

                    serverSocket.close();

                } catch (IOException e) {

                    e.printStackTrace();

                }

                ;

            }

        }

    }

    public static void main(String[] args) throws IOException {

        DemoServer server = new DemoServer();

        server.start();

        try (Socket client = new Socket(InetAddress.getLocalHost(), server.getPort())) {

            BufferedReader bufferedReader = new BufferedReader(new                   InputStreamReader(client.getInputStream()));

            bufferedReader.lines().forEach(s -> System.out.println(s));

        }

    }

 }

// 简化实现,不做读取,直接发送字符串

class RequestHandler extends Thread {

    private Socket socket;

    RequestHandler(Socket socket) {

        this.socket = socket;

    }

    @Override

    public void run() {

        try (PrintWriter out = new PrintWriter(socket.getOutputStream());) {

            out.println("Hello world!");

            out.flush();

        } catch (Exception e) {

            e.printStackTrace();

        }

    }

 }
  • 服务器端启动 ServerSocket,端口 0 表示自动绑定一个空闲端口。
  • 调用 accept 方法,阻塞等待客户端连接。
  • 利用 Socket 模拟了一个简单的客户端,只进行连接、读取、打印。
  • 当连接建立后,启动一个单独线程负责回复客户端请求。

每一个 Client 启动一个线程似乎都有些浪费,除了引入线程池,NIO 采用多路复用机制

public class NIOServer extends Thread {

    public void run() {

        try (Selector selector = Selector.open();

             ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建 Selector 和 Channel

            serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888));

            serverSocket.configureBlocking(false);

            // 注册到 Selector,并说明关注点

            serverSocket.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {

                selector.select();// 阻塞等待就绪的 Channel,这是关键点之一

                Set<SelectionKey> selectedKeys = selector.selectedKeys();

                Iterator<SelectionKey> iter = selectedKeys.iterator();

                while (iter.hasNext()) {

                    SelectionKey key = iter.next();

                   // 生产系统中一般会额外进行就绪状态检查

                    sayHelloWorld((ServerSocketChannel) key.channel());

                    iter.remove();

                }

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

    private void sayHelloWorld(ServerSocketChannel server) throws IOException {

        try (SocketChannel client = server.accept();) {          client.write(Charset.defaultCharset().encode("Hello world!"));

        }

    }

   // 省略了与前面类似的 main

}
  • 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。
  • 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。

注意这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。

  • Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。
  • sayHelloWorld 方法中,通过 SocketChannel Buffer 进行数据操作,在本例中是发送了一段字符串。

 

NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高。

 

NIO 2

Java 7 引入的 NIO 2 中,又增添了一种额外的异步 IO 模式,利用事件和回调。

AsynchronousServerSocketChannel serverSock =        AsynchronousServerSocketChannel.open().bind(sockAddr);

serverSock.accept(serverSock, new CompletionHandler<>() { // 为异步操作指定 CompletionHandler 回调函数

    @Override

    public void completed(AsynchronousSocketChannel sockChannel, AsynchronousServerSocketChannel serverSock) {

        serverSock.accept(serverSock, this);

        // 另外一个 write(sock,CompletionHandler{})

        sayHelloWorld(sockChannel, Charset.defaultCharset().encode

                ("Hello World!"));

    }

  // 省略其他路径处理方法...

});

  • 基本抽象很相似,AsynchronousServerSocketChannel 对应于上面例子中的 ServerSocketChannelAsynchronousSocketChannel 则对应 SocketChannel
  • 业务逻辑的关键在于,通过指定 CompletionHandler 回调接口,在 accept/read/write 等关键节点,通过事件机制调用,这是非常不同的一种编程思路。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值