转载

你知道Java中的流操作么?

你知道Java中的流操作么?
怎样都要生活,不如上进一些

1、同步、异步、阻塞、非阻塞

同步与异步: 同步就是一个任务的完成需要依赖另外一个任务时,只有等待被依赖的任务完成后,依赖的任务才能算完成,这是一种可靠的任务序列。要么成功都成功,失败都失败,两个任务的状态可以保持一致。而异步是不需要等待被依赖的任务完成,只是通知被依赖的任务要完成什么工作,依赖的任务也立即执行,只要自己完成了整个任务就算完成了。至于被依赖的任务最终是否真正完成,依赖它的任务无法确定,所以它是不可靠的任务序列。我们可以用打电话和发短信来很好的比喻同步与异步操作。

阻塞与非阻塞: 阻塞与非阻塞主要是从 CPU 的消耗上来说的,阻塞就是 CPU 停下来等待一个慢的操作完成 CPU 才接着完成其它的事。非阻塞就是在这个慢的操作在执行时 CPU 去干其它别的事,等这个慢的操作完成时,CPU 再接着完成后续的操作。虽然表面上看非阻塞的方式可以明显的提高 CPU 的利用率,但是也带了另外一种后果就是系统的线程切换增加。增加的 CPU 使用时间能不能补偿系统的切换成本需要好好评估。

BIO,一般称之为阻塞(block)IO、基本(basic)IO。就是传统的java.io包,它是基于流模型实现的,交互的方式是同步、阻塞方式,主要应用于文件IO和网络IO。

文件IO就是 InputStream、OutputStream 基于字节操作的 IO、 Writer、Reader 基于字符操作的 IO,网络IO就是 Socket 基于网络操作的 IO。文件IO就是读写操作,就不记录了,这里记录一下网络IO吧。

在JDK1.4之前,建议网络连接的时候只能采用BIO,需要先在服务端启动一个ServerSocket,然后在客户端启动ocket来对服务端进行通信。默认情况下服务端需要对每个请求建立一堆线程等待请求,而客户端发送请求后,会先咨询服务端是否有线程响应,如果没有则会一直等待或者遭到拒绝请求,如果有的话客户端线程会等待请求结束后才继续执行,这就是阻塞IO。

服务端代码:

/**
 * 服务端
 */
public class BioServer {

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

        // 创建ServerSocket对象,绑定端口:9000
        ServerSocket serverSocket = new ServerSocket(9000);

        for (;;){
            System.out.println("1");
            // 监听客户端程序,看是否有客户端来连接、发消息等(阻塞方式,如果客户端没有连接,会一直阻塞在这里,不会往下执行)
            Socket accept = serverSocket.accept();
            System.out.println("2");
            // 从连接中取出输入流来接收消息
            InputStream inputStream = accept.getInputStream();
            byte[] bytes = new byte[10];
            inputStream.read(bytes);
            // 打印接收到的消息
            System.out.println("对方说" + new String(bytes));
            // 回答消息
            OutputStream outputStream = accept.getOutputStream();
            System.out.println("请输入:");
            Scanner scanner = new Scanner(System.in);
            String data = scanner.nextLine();
            outputStream.write(data.getBytes());
            // 关闭
//            serverSocket.close();
        }
    }
}
复制代码

客户端代码:

/**
 * 客户端
 */
public class BioClient {

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

        for (;;){
            // 创建Socket对象
            Socket socket = new Socket("127.0.0.1", 9000);
            // 从连接中取出输出流并发送消息
            OutputStream outputStream = socket.getOutputStream();
            System.out.println("请输入:");
            Scanner scanner = new Scanner(System.in);
            String data = scanner.nextLine();
            outputStream.write(data.getBytes());
            System.out.println("3");
            // 从连接中取出输入流接收并回答(这里也是阻塞,如果没有接收到对方回复的消息,这里会一直阻塞下去)
            InputStream inputStream = socket.getInputStream();
            byte[] bytes = new byte[10];
            inputStream.read(bytes);
            System.out.println("4");
            System.out.println("对方说:" + new String(bytes).trim());
            // 关闭
//            socket.close();
        }
    }
}
复制代码
  • 1、首先启动服务端,控制台只打印了1,并没有继续往下执行: 你知道Java中的流操作么?

  • 2、启动客户端,输入aaa,如下图,服务端控制台继续执行了下去,但是可以看到客户端也阻塞了,控制台只打印了3: 你知道Java中的流操作么? 你知道Java中的流操作么?

  • 3、服务端回复bbb,客户端打印出4并循环执行: 你知道Java中的流操作么?

BIO 就是传统的 java.io 包,它是基于流模型实现的,交互的方式是同步、阻塞方式,也就是说在读入输入流或者输出流时,在读写动作完成之前,线程会一直阻塞在那里,它们之间的调用时可靠的线性顺序。它的优点就是代码比较简单、直观;缺点就是 IO 的效率和扩展性很低,容易成为应用性能瓶颈。

NIO,一般称之为新(new)IO、非阻塞(non-blocking)IO。就是java.nio包,是从 JDK1.4开始 提供的新api,Java提供了一系列改进的输入/输出的新特性,统称为NIO。新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中很多类进行改写,新增了满足NIO的功能,交互的方式是同步、非阻塞方式。

NIO和BIO有相同的目的和作用,但是他们的实现方式完全不同,BIO是以 的方式处理数据,而NIO是以 的方式处理数据,块IO的效率要比流IO高很多。另外, NIO是非阻塞式 的,这一点也和BIO很不相同,使用NIO可以提供非阻塞式的高伸缩性网络。

类型 输出方式 缓冲区 交互方式
BIO 数组(byte[]) 同步、阻塞
NIO Channel(通道) Buffer 同步、非阻塞(也可以是阻塞的,但没必要)

NIO主要有三大核心部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)。 传统的BIO是基于字节流和字符流进行操作而NIO基于Channel和Buffer进行操作 ,数据总是从通道读取到缓冲区,或者从缓冲区写入通道中。Selector(选择器)用于监听多个通道的事件(比如:连接打开、数据到达)。因此使用单个线程就可以监听多个数据通道。

前面说了NIO和BIO有相同的目的和作用,那么NIO的主要应用也是文件IO和网络IO。下面先解释一下NIO的核心部分概述和API,然后通过代码来看一下。

缓冲区(Buffer): 实际上是一个容器,是一个特殊的数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer。 你知道Java中的流操作么? 在NIO中,Buffer是一个顶层父类,它也是一个抽象类,常用的有以下子类: 你知道Java中的流操作么? 对于Java中的基本数据类型,都有一个具体的Buffer类型与之对应,最常用的自然是ByteBuffer类(二进制数据),该类的主要方法有以下几个:

  • public abstract ByteBuffer put(byte b):存储字节数据到缓冲区。

  • public abstract byte get():从缓冲区获得字节数据。

  • public final byte[] array():把缓冲区数据转换成字节数组。

  • public static ByteBuffer allocate(int capacity):设置缓冲区初始容量。

  • piblic static ByteBuffer wrap(byte[] array):把一个线程的数组放到缓冲区中使用。

  • public final Buffer flip():反转缓冲区,充值位置到初始位置。

通道(Channel): 类似于BIO中的stream,用来建立到目标的一个连接, 但是需要注意 :BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,既可以用来进行读操作,也可以用来进行写操作。常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel、SocketChannel。FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannel和SocketChannel用于TCP的数据读写。

FileChannel常用方法:

  • public int read(ByteBuffer dst):读取数据并放到缓冲区中。

  • public int write(ByteBuffer src):把缓冲区的数据写到通道中。

  • public long transferFrom(ReadableByteChannel src, long position, long count):从目标通道中复制数据。

  • public long transferTo(long position, long count, WritableByteChannel target):把数据从当前通道复制给目标通道。

示例:通过NIO往本地文件中写数据:

public class NioWrite {

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

        // 创建输出流
        FileOutputStream fileOutputStream = new FileOutputStream("test.txe");
        // 从流中得到一个通道
        FileChannel channel = fileOutputStream.getChannel();
        // 提供一个缓冲区
        ByteBuffer allocate = ByteBuffer.allocate(1024);
        // 往缓冲区存入数据
        String string = "我是要输入的数据";
        allocate.put(string.getBytes());
        // 翻转缓冲区,将位置设为初始位置
        allocate.flip();
        // 把缓冲区写到通道中
        channel.write(allocate);
        // 关闭
        fileOutputStream.close();
    }
}
复制代码

你知道Java中的流操作么? 可以看到上面代码有个翻转缓冲区的代码,当你把这一行代码去掉之后你会发现,test.txe文件内显示的内容是空的,这是为啥呢?

原因看下图: 你知道Java中的流操作么? 缓冲区存数据就是从头然后到尾,而1和2表示的就是指针位置,当我们把 我是要输入的数据 这一段话存到缓冲区中的时候,指针就到了2的位置,然后开始往通道写数据,实际上就是从2的位置开始往下写,而下面内容明显是空的,所以test.txe文件内显示的内容是空的,而缓冲区的flip()方法,就是将指针从2翻转到初始位置,也就是1的位置,再往通道写数据,这时候就有了内容。

示例:通过NIO从本地文件中读取数据:

public class NioRead {

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

        File file = new File("test.txe");
        // 创建输入流
        FileInputStream fileInputStream = new FileInputStream(file);
        // 得到一个通道
        FileChannel channel = fileInputStream.getChannel();
        // 准备一个缓冲区
        ByteBuffer allocate = ByteBuffer.allocate((int) file.length());
        // 从通道里读取数据并存到缓冲区
        channel.read(allocate);
        // 打印数据
        System.out.println(new String(allocate.array()));
        // 关闭
        fileInputStream.close();
    }
}
复制代码
你知道Java中的流操作么?
image

选择器(Selector): 选择器,能够检测多个注册的通道上是否有时间发生,如果有时间发生,便获取事件然后针对每个事件进行相应的响应处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正的读写事件发生时,才会调用函数来进行读写,就 大大的减少了系统开销 ,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。 你知道Java中的流操作么? 常用方法

  • public static Selector open():得到一个选择器对象。

  • public int select(long timeout):监控所有注册的channel,当其中有注册的IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间。

  • public Set SelectionKeys():从内部集合中得到所有的SelectionKey。

2、 SelectionKey :代表了Selector和serverSocketChannel的注册关系,一共四种:

  • int OP_ACCEPT:有新的网络连接可以accept,值为16。

  • int OP_CONNECT:代表连接已经建立,值为8。

  • int OP_READ和int OP_WRITE:代表了读、写操作,值为1和4。

常用方法

  • public abstract Selector selector():得到与之关联的Selector对象。

  • public abstract SelectableChannel channel():得到与之关联的通道。

  • public final Object attachment():得到与之关联的共享数据。

  • public abstract SelectionKey interestOps(int ops):设置或改变监听事件。

  • public final boolean isAccepyable():是否可以accept。

  • public final boolean isReadable():是否可以读。

  • public final boolean isWritable():是否可以写。

3、 ServerSocketChannel :用来在服务器端监听新的客户端Socket连接。

常用方法

  • public static ServerSocketChannel open():得到一个ServerSocketChannel通道。

  • public final ServerSocketChannel bind(SocketAddress local):设置服务器端口号。

  • public final SelectableChannel configureBlocking(boolean block):设置阻塞或非阻塞模式,取值false表示采用非阻塞模式。

  • public Socketchannel accept():接收一个连接,返回代表这个连接的通道对象。

  • public final SelectionKey register(Selector sel, int ops):注册一个选择器并设置监听事件。

4、 SocketChannel :网络IO通道,具体负责进行读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区。

常用方法

  • public static SocketChannel open():得到一个SocketChannel通道。

  • public final SelectableChannel configureBlocking(boolean block):设置阻塞或非阻塞模式,取值false表示采用非阻塞模式。

  • public boolean connect(SocketAddress remote):连接服务器。

  • public boolean finishConnect():如果上面的方法连接失败了,接下来就要通过该方法完成连接操作。

  • public int write(ByteBuffer src):往通道写数据。

  • public int read(ByteBuffer dst):从通道读数据。

  • public final SelectionKey register(Selector sel, int ops, Object att):注册一个选择器并设置监听时间,最后一个参数可以设置共享数据。

  • public final void close():关闭通道。

示例:NIO实现服务器端和客户端之间的数据通信(非阻塞、基础版):

客户端代码:

public class NioClient {

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

        // 得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置非阻塞模式
        socketChannel.configureBlocking(false);
        // 提供服务器端的ip地址和端口号
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9001);
        // 连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {
            // 循环-直到某一时刻连接到(在连接的时候可以做别的事情,比如下面打印数据,这就是NIO的优势)
            while (!socketChannel.finishConnect()){
                System.out.println("还没有连接到。。。");
            }
            System.out.println("连接到了。。。");
        }
        // 得到一个缓冲区并存入数据
        String string = "NIO测试,我是客户端传的数据";
        ByteBuffer writeBuffer = ByteBuffer.wrap(string.getBytes());
        // 发送数据
        socketChannel.write(writeBuffer);
        // 这里关闭通道,服务器端会报异常,所以这里就先设置阻塞
        System.in.read();
    }
}
复制代码

服务器端代码:

public class NioServer {

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

        // 得到一个ServerSocketChannel对象
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 得到一个Selector对象
        Selector selector = Selector.open();
        // 绑定一个端口号
        serverSocketChannel.bind(new InetSocketAddress(9001));
        // 设置非阻塞式
        serverSocketChannel.configureBlocking(false);
        // 把ServerSocketChannel对象注册给Selector对象
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 做事
        for (;;){
            // 监控客户端
            if (selector.select(2000) == 0){
                System.out.println("没有客户端连接我,我可以干别的事情");
                continue;
            }

            // 得到SelectionKey,判断通道里的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()){
                SelectionKey key = iterator.next();

                // 如果是客户端连接事件
                if (key.isAcceptable()){
                    System.out.println("连接事件");
                    // 接收连接
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 设置非阻塞式
                    socketChannel.configureBlocking(false);
                    // 注册
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                // 读取客户端数据事件
                if (key.isReadable()){
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    channel.read(buffer);
                    System.out.println("客户端发来数据:" + new String(buffer.array()));
                }

                // 手动从集合中移除当前key,防止重复处理
                iterator.remove();
            }
        }
    }
}
复制代码

1、先启动服务端,看到下图内容,没有客户端连接的时候,NIO可以做别的事情,而不像BIO阻塞在那里,这就是NIO的优势。 你知道Java中的流操作么? 2、启动客户端程序,服务端控制台可以看到下图内容,服务端接收到了客户端发送来的数据。( 先连接,才能读写数据,所以最开始发生的事件一定是连接事件,连接成功才会有读写事件 ) 你知道Java中的流操作么?

先写到这里吧,都是些基础的内容,之后的文章会写比较复杂的NIO案例、Netty框架等!

如果你觉得我的文章对你有帮助话,欢迎关注我的微信公众号:"一个快乐又痛苦的程序员"(无广告,单纯分享原创文章、已pj的实用工具、各种Java学习资源,期待与你共同进步)

你知道Java中的流操作么?
原文  https://juejin.im/post/5f0d1dc1e51d4534a049969d
正文到此结束
Loading...