Java IO

Java IO

java中IO流分类:

按数据流向:
输入流:数据流向程序
输出流:数据从程序流出。
按处理单位:
字节流和字符流
JDK 中后缀是 Stream 是字节流;后缀是 Reader,Writer 是字符流
按功能功能:
节点流和处理流
节点流:直接与数据源相连,读入或写出
处理流:与节点流一块使用,在节点流的基础上,再套接一层

最根本的四大类

InputStream(字节输入流),OutputStream(字节输出流),Reader(字符输入流),Writer(字符输出流)

四大类的扩展,按处理单位区分
InputStream:
FileInputStream、PipedInputStream、ByteArrayInputStream、BufferedInputstream、SequenceInputStream、DataInputStream、ObjectInputStream

OutputStream:FileOutputStream、PipedOutputStream、ByteArrayOutputStream、BufferedOutputStream、DataOutputStream、ObjectOutputStream、PrintStream

Reader:FileReader、PipedReader、CharArrayReader、BufferedReader、InputStreamReader

Writer:FileWriter、PipedWriter、CharArrayWriter、BufferedWriter、InputStreamWriter、PrintWriter

常用的流

对文件进行操作:FileInputStream(字节输入流)、FileOutputStream(字节输出流)、FileReader(字符输入流)、FileWriter(字符输出流)

对管道进行操作:PipedInputStream(字节输入流)、PipedOutStream(字节输出流)、PipedReader(字符输入流)、PipedWriter(字符输出流)

字节/字符数组:ByteArrayInputStream、ByteArrayOutputStream、CharArrayReader、CharArrayWriter

Buffered 缓冲流:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter

字节转化成字符流:InputStreamReader、OutputStreamWriter

数据流:DataInputStream、DataOutputStream

打印流:PrintStream、PrintWriter

对象流:ObjectInputStream、ObjectOutputStream

序列化流:SequenceInputStream

Java IO模型

Java共支持3种网络编程的I/O模型:BIO、NIO、AIO。
BIO 同步阻塞,NIO 同步非阻塞,AIO 异步非阻塞。

同步和异步
同步 是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;
异步 是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。

阻塞和非阻塞
阻塞 是指内核空间IO操作需要等待直到把数据返回到用户空间;
非阻塞 是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

同步异步是看调用者等结果不返回,还是先返回等通知; 阻塞非阻塞是调用者在这段时间能不能干别的事。

BIO

Java BIO就是传统的java io 编程,其相关的类和接口在java.io
适用场景:在活动连接数不是特别高的情况下使用

同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
请添加图片描述
工作模型
请添加图片描述
代码示例
服务器端
每检测到一个 socket链接,就会创建一个线程进行处理。

public class Server {
    public static void main(String[] args) {
        try {
            //注册 端口
            ServerSocket serverSocket = new ServerSocket(9999);
            //定义一个死循环,负责不断的去接受Client的Socket请求
            while (true){
                Socket socket = serverSocket.accept();
                new  Thread(()->{
                    try {
                        InputStream in = socket.getInputStream();
                        BufferedReader br = new BufferedReader(new InputStreamReader(in));
                        String msg;
                        while ((msg = br.readLine()) != null){
                            System.out.println(Thread.currentThread().getName()+"接收到消息---->"+msg);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端
连接服务器端,向服务器端发送数据

public class Client {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1",9999);
            PrintStream ps = new PrintStream(socket.getOutputStream());
            Scanner sc = new Scanner(System.in);
            while (true){
                System.out.print("说点啥吧:");
                String msg = sc.nextLine();
                ps.println(msg);
                ps.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

总结:

  1. 每个Socket接收到,都会创建一个线程,线程的竞争、切换上下文影响性能
  2. 每个线程都会占用栈空间和CPU资源;
  3. 并不是每个socket都进行IO操作,无意义的线程处理(等待)
  4. 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

伪异步BIO
当客户端接入时,将客户端的Socket封装成一个Task(该任务实现java.lang.Runnable线程任务接口)交给后端的线程池中进行处理。线程池维护一个消息队列和N个活跃的线程,对消息队列中Socket任务进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

代码示例
服务器端代码,采用线程池,来控制线程。

public class Server {
    public static void main(String[] args) {
        try {
            //对服务端端口进行注册 9999
            ServerSocket serverSocket = new ServerSocket(9999);
            /**
             * 手动创建线程池
             * 核心线程 3 ,最大 4,等待释放5s,任务队列大小4
             */
            ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3,4,5, TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(4), Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
            while (true){
                //监听客户端socket请求
                Socket socket = serverSocket.accept();
                //线程池执行
                threadPoolExecutor.execute(new Thread(()->{
                    try {
                        InputStream in = socket.getInputStream();
                        BufferedReader br = new BufferedReader(new InputStreamReader(in));
                        String msg;
                        while ((msg = br.readLine()) != null){
                            System.out.println(Thread.currentThread().getName()+"接收到客户端消息------>"+msg);
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端,连接服务端,向服务端发送消息。

public class Client {
    public static void main(String[] args) {
        try {
            //创建socket链接
            Socket socket = new Socket("127.0.0.1",9999);
            //从socket对象中获取一个输出流
            OutputStream out = socket.getOutputStream();
            //把字节输出流包装成打印流
            PrintStream printStream = new PrintStream(out);
            Scanner scanner = new Scanner(System.in);
            while (true){
                String msg = scanner.nextLine();
                printStream.println(msg);
                printStream.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

总结

  1. 伪异步采用了线程池实现,因此避免了为每个请求创建一个独立线程造成线程资源耗尽的问题,但由于底层依然是采用的同步阻塞模型,因此无法从根本上解决问题。
  2. 如果单个消息处理的缓慢,或者服务器线程池中的全部线程都被阻塞,那么后续套接字的I/O消息都将在队列中排队。新的Socket请求将被拒绝,客户端会发生大量连接超时。

NIO

NIO 是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO中可以配置socket为非阻塞模式。
NIO相关类都被放在java.nio包下。
NIO有三大核心部分:Buffer(缓冲区),Channel(通道),Selector(选择器)。

同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
一个线程通过选择器轮询多个连接
请添加图片描述

Java NlO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

NIO 与 BIO的比较:

BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流IO高很多
BIO是阻塞的,NIO则是非阻塞的
BlO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

NIOBIO
面向缓冲区(Buffer)面向流(Stream)
非阻塞(Non Blocking IO)阻塞IO(Blocking IO)
选择器(Selectors)

NIO可以先将数据写入到缓冲区,然后再有缓冲区写入通道,因此可以做到同步非阻塞。
BIO则是面向的流,读写数据都是单向的。因此是同步阻塞。

NIO有三大核心部分:Buffer(缓冲区),Channel(通道), Selector(选择器)。
Buffer 缓冲区
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer APl更加容易操作和管理。
Channel(通道)
Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。
Selector(选择器)
Selector 可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel。

Buffer 缓冲区

一个用于特定基本数据类型的容器。由java.nio包定义的,所有缓冲区都是Buffer抽象类的子类。Java NIO中的Buffer主要用于与NIO通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的。
请添加图片描述
Buffer类以及其子类:
ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer。

Buffer中的重要概念:
容量(capacity):作为一个内存块,Buffer具有一定的固定大小,也称为容量,创建后不能更改。
限制(limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。
位置(position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,不能大于其限制
标记(mark):与重置(reset):标记是一个索引,通过Buffer中的mark()方法指定Buffer中一个特定的position,之后可以通过调用reset()方法恢复到这个position。
标记、位置、限制、容量遵守以下不变式: 0<=mark <= position <= limit <= capacity

Buffer类中的方法:可以参考文档

// 返回此缓冲区的容量。
int capacity()
// 清除此缓冲区。
Buffer clear()
// 翻转这个缓冲区。
Buffer flip()
// 告诉当前位置和极限之间是否存在任何元素。
boolean	hasRemaining()
// 告诉这个缓冲区是否为 direct 。
abstract boolean isDirect()
// 告知这个缓冲区是否是只读的。
abstract boolean isReadOnly()
// 返回此缓冲区的限制。
int	limit()
// 设置此缓冲区的限制。
Buffer limit(int newLimit)
//将此缓冲区的标记设置在其位置。
Buffer mark()
// 返回此缓冲区的位置。
int	position()
// 设置这个缓冲区的位置。
Buffer position(int newPosition)
// 返回当前位置和限制之间的元素数。
int	remaining()
// 将此缓冲区的位置重置为先前标记的位置。
Buffer reset()
// 倒带这个缓冲区。
Buffer rewind()

在其子类中还有put方法 及其 get方法,用于向缓冲区读取和写入数据。

Channel 通道

通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

NIO 的通道类似于流,但有些区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲读数据,也可以写数据到缓冲。

Channel 在 NIO 中是一个接口

public interface Channel extends Closeable{}

常用的Channel实现类
FileChannel:用于读取、写入、映射和操作文件的通道。
DatagramChannel:通过 UDP 读写网络中的数据通道。
SocketChannel:通过 TCP 读写网络中的数据。
ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 (ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket)

Selector 选择器

选择器(Selector)是SelectableChannle对象的多路复用器,Selector可以同时监控多个SelectableChannel 的 IO状况,也就是说,利用Selector可使一个单独的线程管理多个Channel,Selector是非阻塞IO的核心。
创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。

Selector selector = Selector.open();

使用步骤

//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9999));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):
读 : SelectionKey.OP_READ (1)
写 : SelectionKey.OP_WRITE (4)
连接 : SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)
若注册时不止监听一个事件,则可以使用“位或”操作符连接。
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE

NIO 代码示例

场景:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。
服务端:

public class Server {
    public static void main(String[] args) {
        try {
            //1.获取管道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //2.设置非阻塞模式
            serverSocketChannel.configureBlocking(false);
            //3.绑定端口
            serverSocketChannel.bind(new InetSocketAddress(8888));
            //4.获取选择器
            Selector selector = Selector.open();
            //5.将通道注册到选择器上,并且开始指定监听的接收事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            //6.轮询已经就绪的事件
            while (selector.select() > 0){
                System.out.println("开启事件处理");
                //7.获取选择器中所有注册的通道中已准备好的事件
                Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                //8.开始遍历事件
                while (it.hasNext()){
                    SelectionKey selectionKey = it.next();
                    System.out.println("--->"+selectionKey);
                    //9.判断这个事件具体是啥
                    if (selectionKey.isAcceptable()){
                        //10.获取当前接入事件的客户端通道
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        //11.切换成非阻塞模式
                        socketChannel.configureBlocking(false);
                        //12.将本客户端注册到选择器
                        socketChannel.register(selector,SelectionKey.OP_READ);
                    }else if (selectionKey.isReadable()){
                        //13.获取当前选择器上的读
                        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
                        //14.读取
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int len;
                        while ((len = socketChannel.read(buffer)) > 0){
                            buffer.flip();
                            System.out.println(new String(buffer.array(),0,len));
                            //清除之前的数据(覆盖写入)
                            buffer.clear();
                        }
                    }
                    //15.处理完毕后,移除当前事件
                    it.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端:

public class Client {
    public static void main(String[] args) {
        try {
        	// 连接服务端
            SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",8888));
            // 设置非阻塞
            socketChannel.configureBlocking(false);
            // 设置缓冲区大小
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 获得输入流
            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.print("请输入:");
                String msg = scanner.nextLine();
                // 写入缓冲区
                buffer.put(msg.getBytes());
                buffer.flip();
                // 将缓冲区数据经过通道发送
                socketChannel.write(buffer);
                buffer.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

要点:

  1. 每个channel都会对应一个 Buffer
  2. 一个线程对应Selector ,一个Selector对应多个channel(连接)程序
  3. 切换到哪个channel是由事件决定的
  4. Selector 会根据不同的事件,在各个通道上切换
  5. Buffer 就是一个内存块,底层是一个数组
  6. 数据的读取写入是通过 Buffer完成的,BlO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写。
  7. Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到 lO设备的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。Channel负责传输,Buffer负责存取数据。

AIO

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理(回调)。
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可, 这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序

即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:
​ AsynchronousSocketChannel
​ AsynchronousServerSocketChannel
​ AsynchronousFileChannel
​ AsynchronousDatagramChannel

应用场景

并发连接数不多时采用BIO,因为它编程和调试都非常简单,但如果涉及到高并发的情况,应选择NIO或AIO,更好的建议是采用成熟的网络通信框架Netty。

参考资料:
视频链接
博客参考

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值