方法实现

方法实现

java里面一些经典的模式和方法的实现

深浅拷贝

参考原文:https://blog.csdn.net/w605283073/article/details/103639889

深浅区别

在 Java 中,除了基本数据类型(元类型)之外,还存在 类的实例对象 这个引用数据类型。而一般使用 『 = 』号做赋值操作的时候。对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际上还是指向的同一个对象。

而浅拷贝和深拷贝就是在这个基础之上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行了引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝。

浅拷贝

定义的属性不是int 就是 String, 只包含基本类型和不可变类型,此时可以用浅拷贝

// 要创建拷贝构造方法,首先需要声明带有和本类相同类型的参数构造函数
public class Employee {
    private int id;
    private String name;

    public Employee(Employee employee) {
    }
}

// 将参数对象的每个属性都复制给新的实例
public class Employee {
    private int id;
    private String name;

    public Employee(Employee employee) {
        this.id = employee.id;
        this.name = employee.name;
    }
}

深拷贝

类中包含可变类型就要通过该构造函数实现深拷贝

public class Employee {
    private int id;
    private String name;
    private Date startDate;

    public Employee(Employee employee) {
        this.id = employee.id;
        this.name = employee.name;
        this.startDate = new Date(employee.startDate.getTime());
    }
}

比Clone的优势

  • 拷贝构造方法实现更简单。不需要实现 Cloneable 接口,也不需要处理 CloneNotSupportedException
  • clone 函数返回一个普通的 Object 类型的引用。还需要转成特定的类型。
  • 在 clone 方法中不能为 final 属性赋值,但是在拷贝构造方法中就可以。

继承问题

Java 中的拷贝构造方法不会被子类继承

如果我们尝试初始化一个带有父类引用的子类对象,就会面临着类型转换问题

public class Manager extends Employee {
    private List<Employee> directReports;
    // ... 其他构造方法

    public Manager(Manager manager) {
        super(manager.id, manager.name, manager.startDate);
        this.directReports = directReports.stream()
          .collect(Collectors.toList());
    }
}

// 然后,我们声明一个 Employee 类型的引用指向通过 Manager 构造方法构造的 Manager 实例。
Employee source = new Manager(1, "Baeldung Manager", startDate, directReports);

// 由于引用类型为 Employee, 如果我们想使用 Manager 的拷贝构造函数就必须将 source 强转为 Manager 类型。
Employee clone = new Manager((Manager) source);

// 避免使用拷贝构造方法时类型转换的方法是创建一个继承的拷贝函数
public class Employee {
   public Employee copy() {
        return new Employee(this);
    }
}

public class Manager extends Employee {
    @Override
    public Employee copy() {
        return new Manager(this);
    }
}

JAVA IO

参考文档:https://zhuanlan.zhihu.com/p/28286559

图片说明

根据操作来分类

  • 字节流和字符流
    • 字节流:以字节为单位,每次次读入或读出是8位数据。可以读任何类型数据。
    • 字符流:以字符为单位,每次次读入或读出是16位数据。其只能读取字符类型数据。
  • 输入流和输出流
    • 输出流:从内存读出到文件。只能进行写操作。
    • 输入流:从文件读入到内存。只能进行读操作。
  • 节点流和处理流
    • 节点流:直接与数据源相连,读入或读出。
    • 处理流:与节点流一块使用,在节点流的基础上,再套接一层,套接在节点流上的就是处理流。

分类说明
图片说明

    1. 输入字节流InputStream:
    • FileInputStream: 基本的介质流,从本地文件中读取数据。
    • ByteArrayInputStream:基本的介质流,从Byte 数组中读取数据。
    • PipedInputStream: 是从与其它线程共用的管道中读取数据。PipedInputStream的一个实例要和PipedOutputStream的一个实例共同使用,共同完成管道的读取写入操作。主要用于线程操作。
    • ObjectInputStream 和所有FilterInputStream 的子类都是装饰流(装饰器模式的主角)
    1. 输出字节流OutputStream:
    • FIleOutputStream:基本的介质流,向本地文件中写入数据
    • ByteArrayOutputStream: 基本的介质流,它向Byte 数组中写入数据。
    • PipedOutputStream:是向与其它线程共用的管道中写入数据。
    • ObjectOutputStream 和所有FilterOutputStream 的子类都是装饰流。

图片说明

    1. 字符输入流Reader:
    • FileReader:从本地文件中读取字符流
    • PipedReader:是从与其它线程共用的管道中读取数据
    • CharArrayReader:基本的介质流,它们分别将从Char 数组中读取数据。
    • StringReader:基本的介质流,它们分别将从String中读取数据。
    • BufferedReader 很明显就是一个装饰器,它和其子类负责装饰其它Reader 对象。
    • FilterReader 是所有自定义具体装饰流的父类,其子类PushbackReader 对Reader 对象进行装饰,会增加一个行号。
    • InputStreamReader: 是一个连接字节流和字符流的桥梁,它将字节流转变为字符流。FileReader 可以说是一个达到此功能、常用的工具类,在其源代码中明显使用了将FileInputStream 转变为Reader 的方法。我们可以从这个类中得到一定的技巧。Reader 中各个类的用途和使用方法基本和InputStream 中的类使用一致。后面会有Reader 与InputStream 的对应关系。
    1. 字符输出流Writer:
    • FileWriter:向本地文件中写入字符流
    • PipedWriter:是向与其它线程共用的管道中写入数据
    • CharArrayWriter:基本的介质流,它们分别将向Char 数组中写入数据。
    • StringWriter :基本的介质流,它向String 中写入数据。
    • BufferedWriter:是一个装饰器,为Writer 提供缓冲功能。
    • PrintWriter 和PrintStream 极其类似,功能和使用也非常相似。
    • OutputStreamWriter: 是OutputStream 到Writer 转换的桥梁,它的子类FileWriter 其实就是一个实现此功能的具体类(具体可以研究一SourceCode)。功能和使用和OutputStream 极其类似,后面会有它们的对应图。
    1. 字节流转化为字符流
  • 转换流的特点:
  1. 其是字符流和字节流之间的桥梁;

  2. 可对读取到的字节数据经过指定编码转换成字符;

  3. 可对读取到的字符数据经过指定编码转换成字节;
    何时使用转换流?

  4. 当字节和字符之间有转换动作时;

  5. 流操作的数据需要编码或解码时。

    // 这两个流对象是字符体系中的成员,它们有转换作用,本身又是字符流,所以在构造的时候需要传入字节流对象进来。
    // InputStreamReader:输入流转到读流;
    String fileName= "d:"+File.separator+"hello.txt";
    File file=new File(fileName);
    Writer out=new OutputStreamWriter(new FileOutputStream(file));
    out.write("hello");
    out.close();

    // OutputStreamWriter:输出流转到写流;
    String fileName= "d:"+File.separator+"hello.txt";
    File file=new File(fileName);
    Reader read=new InputStreamReader(new FileInputStream(file));
    char[] b=new char[100];
    int len=read.read(b);
    System.out.println(new String(b,0,len));
    read.close();

按操作对象分类
图片说明

分类说明

分类说明:

  • 对文件进行操作(节点流):
    • FileInputStream(字节输入流),
    • FileOutputStream(字节输出流),
    • FileReader(字符输入流),
    • FileWriter(字符输出流)
  • 对管道进行操作(节点流):
    • PipedInputStream(字节输入流),
    • PipedOutputStream(字节输出流),
    • PipedReader(字符输入流),
    • PipedWriter(字符输出流)。
  • 字节/字符数组流(节点流):
    • ByteArrayInputStream,
    • ByteArrayOutputStream,
    • CharArrayReader,
    • CharArrayWriter;
      是在内存中开辟了一个字节或字符数组。

除了上述三种是节点流,其他都是处理流,需要跟节点流配合使用。

  • Buffered缓冲流(处理流):
    • BufferedInputStream,
    • BufferedOutputStream,
    • BufferedReader,
    • BufferedWriter,
      是带缓冲区的处理流,缓冲区的作用的主要目的是:避免每次和硬盘打交道,提高数据访问的效率。
  • 转化流(处理流):
    • InputStreamReader:把字节转化成字符;
    • OutputStreamWriter:把字节转化成字符。
  • 基本类型数据流(处理流):用于操作基本数据类型值。
    • DataInputStream,
    • DataOutputStream。
      因为平时若是我们输出一个8个字节的long类型或4个字节的float类型,那怎么办呢?可以一个字节一个字节输出,也可以把转换成字符串输出,但是这样转换费时间,若是直接输出该多好啊,因此这个数据流就解决了我们输出数据类型的困难。数据流可以直接输出float类型或long类型,提高了数据读写的效率。
  • 打印流(处理流):
    • PrintStream,
    • PrintWriter,
      一般是打印到控制台,可以进行控制打印的地方。
  • 对象流(处理流):
    • ObjectInputStream,对象反序列化;
    • ObjectOutputStream,对象序列化;
      把封装的对象直接输出,而不是一个个在转换成字符串再输出。
  • 合并流(处理流):
    • SequenceInputStream:可以认为是一个工具类,将两个或者多个输入流当成一个输入流依次读取。

具体使用

// 复制文件
import java.io.*;

/**
 * 这个类是利用输入输出流复制文件
 * 一遍读一遍写
 * 这个方法是对这个流一个一个字节的读,返回的结果就是这个字节的int表示方式;
 */
public class CopyFile {
    public static void main(String[] args) throws IOException {
        String inPath = "E:/softprograme/java/in.txt";
        String outPath = "E:/softprograme/java/out.txt";

        File file1 = new File(inPath);
        File file2 = new File(outPath);

        if (!file1.exists()){
            System.out.println("被复制的文件不存在!");
            System.exit(1);
        }

        InputStream in = new FileInputStream(file1);
        OutputStream out = new FileOutputStream(file2);

        if (in != null && out != null){
            int temp = 0;
            while ((temp = in.read()) != -1){
                out.write(temp);
            }
        }

        // 最后要记得关闭输入输出流
        in.close();
        out.close();
    }
}


// 把流中的字符从大写改为小写
import java.io.*;

public class ToLower {
    public static void main(String[] args) throws IOException {
        String str = "HELLO WORLD";
        InputStream in = new ByteArrayInputStream(str.getBytes());
        OutputStream out = new ByteArrayOutputStream();

        int temp = 0;
        while ((temp = in.read()) != -1){
            out.write(Character.toLowerCase((char)temp));
        }

        System.out.println(out.toString());
        in.close();
        out.close();
    }
}


// 使用管道流在多个线程间通信
// 消息发送类
class Send implements Runnable{
    private PipedOutputStream out = null;

    public Send() {
        out = new PipedOutputStream();
    }

    public PipedOutputStream getOut() {
        return out;
    }

    @Override
    public void run() {
        String message = "send you a message";
        try {
            // 消息写入管道输出流
            out.write(message.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            out.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

// 消息接受类
class Receive implements Runnable{

    private PipedInputStream in = null;

    public Receive() {
        in = new PipedInputStream();
    }

    public PipedInputStream getIn() {
        return in;
    }

    @Override
    public void run() {
        byte[] b = new byte[2000];
        int len = 0;
        try {
            len = in.read(b);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println("接受的内容是: " + new String(b, 0, len));
    }
}

// 测试的主程序
public class Pipe {
    public static void main(String[] args) {
        Send send = new Send();
        Receive receive = new Receive();

        // 管道连接
        try {
            send.getOut().connect(receive.getIn());
        } catch (IOException e) {
            e.printStackTrace();
        }

        new Thread(send).start();
        new Thread(receive).start();
    }
}

// 利用缓冲字符流输出键盘输入
public class BufferRead {
    public static void main(String[] args) {
        // 装饰器
        BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("请输入打印的字符串:");

        String str = null;

        try {
            str = reader.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println(str);
    }
}

// 将系统输出定向到文件
public class RedirectSystemOut {
    public static void main(String[] args) throws FileNotFoundException {
        File file = new File("E:/softprograme/java/out.txt");

        System.out.println("这句话直接打印在控制台上!");

        System.setOut(new PrintStream(new FileOutputStream(file)));

        System.out.println("这句话打印在对应路径的文件中。");
    }
}

NIO

“旧”的I/O包已经使用NIO重新实现过,即使我们不显式的使用NIO编程,也能从中受益。

图片说明

  • 可简单认为:IO是面向流的处理,NIO是面向块(缓冲区)的处理
    • 面向流的I/O 系统一次一个字节地处理数据。
    • 一个面向块(缓冲区)的I/O系统以块的形式处理数据。
  • NIO主要有三个核心部分组成:
    • buffer缓冲区
    • Channel管道
    • Selector选择器

Buffer & Channel

在NIO中并不是以流的方式来处理数据的,而是以Buffer缓冲区和Channel管道配合使用来处理数据。

NIO就是通过Channel管道运输着存储数据的Buffer缓冲区的来实现数据的处理

Buffer与数据打交道,然后Channel只负责传输,对于传统IO,流是单向的。但是Channel管道是双向的。

Buffer

// 是缓冲区的抽象类
public abstract class Buffer {
    static final Unsafe UNSAFE = Unsafe.getUnsafe();
    static final int SPLITERATOR_CHARACTERISTICS = 16464;
    private int mark = -1; // 一个备忘位置。用于记录上一次读写的位置。
    private int position = 0; // 下一个要被读或写的元素的位置。Position会自动由相应的 get( )和 put( )函数更新。
    private int limit; // 缓冲区里的数据的总数,代表了当前缓冲区中一共有多少数据。
    private int capacity; //缓冲区能够容纳的数据元素的最大数量。容量在缓冲区创建时被设定,并且永远不能被改变。(不能被改变的原因也很简单,底层是数组嘛)
    long address;
    ....
}
;

它有直接或者间接的实现类73个:

图片说明

可以看到,最上面,也是最常用的是ByteBuffer,也是用得最多的实现类(在管道中读写字节数据)

  • get():获取缓冲区内容
  • put():写入缓冲区内容

API

// 创建Buffer
ByteBuffer byteBuffer=ByteBuffer.allocate(48)
// 通过该种方式将生成一个limit=capacity=bytes.length的新缓存区,如果bytes里含有数据,则会用该数据填充缓冲区
// 要注意的是通过该种方式创建的ByteBuffer其position初始值是0.
ByteBuffer byteBuffer=ByteBuffer.wrap(new Byte[1024]);

// 像Buffer上写数据
// 通过Channel的read方法,将数据写到Buffer
// 通过Buffer自身的put()方法,将数据放入Buffer
int bytesRead=fileChannel.read(byteBuffer);
byteBuffer.put(2);    

// 通过filp()进行模式转换
// flip方法将Buffer从写模式切换到读模式。
// 调用flip()方***首先将limit的值设为当前position的值,然后将position设为0,相当执行了以下语句:
// limit=position; position=0;
byteBuffer.flip();

// 从Buffer读取数据到Channel
// 使用Buffer的get()方法读取Buffer的数据
fileChannel.write(byteBuffer);
byteBuffer.get();    

// Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。
// limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
byteBuffer.rewind();

// 一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。
// 如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。
// 如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。
// compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。
byteBuffer.clear();
byteBuffer.compact();

// 通过mark()标记position位置,在进行其他操作后可通过reset() 方法恢复到标记的position
buffer.mark();
...
buffer.reset();

代码实现

// 核心变量的值的变化
public static void main(String[] args) {

        // 创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 看一下初始时4个核心变量的值
        System.out.println("初始时-->limit--->"+byteBuffer.limit());
        System.out.println("初始时-->position--->"+byteBuffer.position());
        System.out.println("初始时-->capacity--->"+byteBuffer.capacity());
        System.out.println("初始时-->mark--->" + byteBuffer.mark());

        System.out.println("--------------------------------------");

        // 添加一些数据到缓冲区中
        String s = "Wextree";
        byteBuffer.put(s.getBytes());

        // 看一下初始时4个核心变量的值
        System.out.println("put完之后-->limit--->"+byteBuffer.limit());
        System.out.println("put完之后-->position--->"+byteBuffer.position());
        System.out.println("put完之后-->capacity--->"+byteBuffer.capacity());
        System.out.println("put完之后-->mark--->" + byteBuffer.mark());
}
/**
*    初始时-->limit--->1024
*    初始时-->position--->0
*    初始时-->capacity--->1024
*    初始时-->mark--->java.nio.HeapByteBuffer[pos=0 lim=1024 cap=1024]
*    --------------------------------------
*    put完之后-->limit--->1024
*    put完之后-->position--->7
*    put完之后-->capacity--->1024
*    put完之后-->mark--->java.nio.HeapByteBuffer[pos=7 lim=1024 cap=1024]
*/


public static void main(String[] args) {

        // 创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 添加一些数据到缓冲区中
        String s = "Wextree";
        byteBuffer.put(s.getBytes());

        // 看一下4个核心变量的值
        System.out.println...

        System.out.println("--------------------------------------");

        // 这个方法可以改动position和limit的位置!
        // 一般我们称flip()为“切换成读模式”
        byteBuffer.flip();

        // 看一下初始时4个核心变量的值
        System.out.println...
}

/**
*     limit变成了position的位置了,而position变成了0
*
*    put完之后-->limit--->1024
*    put完之后-->position--->7
*    put完之后-->capacity--->1024
*    put完之后-->mark--->java.nio.HeapByteBuffer[pos=7 lim=1024 cap=1024]
*    --------------------------------------
*    flip后-->limit--->7
*    flip后-->position--->0
*    flip后-->capacity--->1024
*    flip后-->mark--->java.nio.HeapByteBuffer[pos=0 lim=7 cap=1024]
*/

图片说明

// flip()之后可以开始读数据了,用get()也会导致position的变化
public static void main(String[] args) {
        // 创建一个缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 添加一些数据到缓冲区中
        String s = "Wextree";
        byteBuffer.put(s.getBytes());
        byteBuffer.flip();

        System.out.println...

        System.out.println("--------------------------------------");

        byte[] bytes = new byte[byteBuffer.limit()];
        byteBuffer.get(bytes);
        System.out.println(new String(bytes, 0, bytes.length));

        System.out.println...
}

/**
    flip后-->limit--->7
    flip后-->position--->0
    flip后-->capacity--->1024
    flip后-->mark--->java.nio.HeapByteBuffer[pos=0 lim=7 cap=1024]
    --------------------------------------
    get完之后-->limit--->7
    get完之后-->position--->7
    get完之后-->capacity--->1024
    get完之后-->mark--->java.nio.HeapByteBuffer[pos=7 lim=7 cap=1024]
*/

// 读完我们还想写数据到缓冲区,那就使用clear()函数,这个函数会“清空”缓冲区:
// 其实数据并没有被清空,只是被遗忘了

/**
    flip后-->limit--->7
    flip后-->position--->0
    flip后-->capacity--->1024
    flip后-->mark--->java.nio.HeapByteBuffer[pos=0 lim=7 cap=1024]
    --------------------------------------
    clear完之后-->limit--->1024
    clear完之后-->position--->0
    clear完之后-->capacity--->1024
    clear完之后-->mark--->java.nio.HeapByteBuffer[pos=7 lim=7 cap=1024]
*/

Channel

Channel通道只负责传输数据、不直接操作数据的。操作数据都是通过Buffer缓冲区来进行操作!

图片说明

// 1. 通过本地IO的方式来获取通道
FileInputStream fileInputStream = new FileInputStream("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是这么简单.md");

// 得到文件的输入通道
FileChannel inchannel = fileInputStream.getChannel();

// 2. jdk1.7后通过静态方法.open()获取通道
FileChannel.open(Paths.get("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是这么简单2.md"), StandardOpenOption.WRITE);

// 使用FileChannel配合缓冲区实现文件复制的功能:
public static void main(String[] args) {

        FileInputStream in = null;
        FileOutputStream out = null;

        FileChannel fin = null;
        FileChannel fout = null;

        try {
            in = new FileInputStream(new File("E:/softprograme/java/in.txt"));
            out = new FileOutputStream(new File("E:/softprograme/java/out.txt"));

            fin = in.getChannel();
            fout = out.getChannel();

            ByteBuffer buf = ByteBuffer.allocate(1024);

            while (fin.read(buf) != -1){
                buf.flip();
                fout.write(buf);
                buf.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();

        }finally {
            fin.close();
            fout.close();
            in.close();
            out.close();
        }

    }

// 使用内存映射文件的方式实现文件复制的功能(直接操作缓冲区):
public static void main(String[] args) throws IOException {
        FileChannel inChannel = FileChannel.open(Paths.get("E:/softprograme/java/in.txt"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("E:/softprograme/java/out.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE, StandardOpenOption.READ);

        // 内存映射文件
        MappedByteBuffer in = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
        MappedByteBuffer out = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());

        byte[] bytes = new byte[in.limit()];
        in.get(bytes);
        out.put(bytes);

        inChannel.close();
        outChannel.close();
}

// 通道之间通过transfer()实现数据的传输(直接操作缓冲区):
public static void main(String[] args) throws IOException {
        FileChannel inChannel = FileChannel.open(Paths.get("E:/softprograme/java/in.txt"), StandardOpenOption.READ);
        FileChannel outChannel = FileChannel.open(Paths.get("E:/softprograme/java/out.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE, StandardOpenOption.READ);

        inChannel.transferTo(0, inChannel.size(), outChannel);
        inChannel.close();
        outChannel.close();
}

直接和非直接缓冲区

  • 非直接缓冲区是需要经过一个:copy的阶段的(从内核空间copy到用户空间)
  • 直接缓冲区不需要经过copy阶段,也可以理解成--->内存映射文件,(上面的图片也有过例子)。

使用直接缓冲区有两种方式:

  • 缓冲区创建的时候分配的是直接缓冲区
  • 在FileChannel上调用map()方法,将文件直接映射到内存中创建

图片说明
图片说明

scatter & gather

  • 分散读取(scatter):将一个通道中的数据分散读取到多个缓冲区中
  • 聚集写入(gather):将多个缓冲区中的数据集中写入到一个通道中

IO模型

在UNIX可以归纳成5种I/O模型:

  • 阻塞I/O
  • 非阻塞I/O
  • I/O多路复用
  • 信号驱动I/O
  • 异步I/O

文件描述符

Linux 的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令(api),返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有响应的描述符,称为socket fd(socket文件描述符),描述符就是一个数字,指向内核中的一个结构体(文件路径,数据区等一些属性)。

  • 所以说:在Linux下对文件的操作是利用文件描述符(file descriptor)来实现的。

阻塞型IO

在进程(用户)空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间一直等待。

非阻塞型IO

recvfrom从应用层到内核的时候,如果没有数据就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。

复用型IO

在Linux下它是这样子实现I/O复用模型的:

  • 调用select/poll/epoll/pselect其中一个函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。
  • (1)当用户进程调用了select,那么整个进程会被block;
  • (2)而同时,kernel会“监视”所有select负责的socket;
  • (3)当任何一个socket中的数据准备好了,select就会返回;
  • (4)这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程(空间)。
  • 所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读就绪状态,select()函数就可以返回。

网络IO

所以说:我们通常使用NIO是在网络中使用的,网上大部分讨论NIO都是在网络通信的基础之上的!说NIO是非阻塞的NIO也是网络中体现的!

NIO阻塞状态

是阻塞的就没有Selector选择器了,就直接使用Channel和Buffer就完事了。

/**
 * 向服务端发送本地文件
 */
public class BlockingClient {
    public static void main(String[] args) throws IOException {
        // 1. 获取网络通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("1270.0.0.1", 6666));

        // 2. 创建本地文件通道
        FileChannel fileChannel = FileChannel.open(Paths.get("E:/softprograme/java/in.txt"), StandardOpenOption.READ);

        // 3. 创建buffer
        ByteBuffer buf = ByteBuffer.allocate(1024);

        // 4. 写入网络通道
        while (fileChannel.read(buf) != -1){
            buf.flip();
            socketChannel.write(buf);
            buf.clear();
        }

        // 告诉服务器我已经发送完毕了,避免阻塞
        socketChannel.shutdownOutput();

        // 接受服务端发送的确认请求
        int len = 0;
        while ((len = socketChannel.read(buf)) != -1){
            buf.flip();
            System.out.println(new String(buf.array(), 0, len));
            buf.clear();
        }

        // 5. 关闭流
        socketChannel.close();
        fileChannel.close();
    }
}

/**
 * 接收客户端数据
 */
public class BlockingServer {
    public static void main(String[] args) throws IOException {
        ServerSocketChannel server = ServerSocketChannel.open();
        FileChannel fileChannel = FileChannel.open(Paths.get("E:/softprograme/java/out.txt"), StandardOpenOption.CREATE, StandardOpenOption.WRITE);

        // 绑定客户端
        server.bind(new InetSocketAddress(6666));

        // 获取客户端通道
        SocketChannel client = server.accept();

        ByteBuffer buf = ByteBuffer.allocate(1024);

        while (client.read(buf) != -1){
            buf.flip();
            fileChannel.write(buf);
            buf.clear();
        }

        // 向客户端发送确认请求
        buf.put("已经发送完毕!".getBytes());
        buf.flip();
        client.write(buf);
        buf.clear();

        client.close();
        fileChannel.close();
        server.close();
    }
}

NIO非阻塞状态

如果使用非阻塞模式的话,那么我们就可以不显式告诉服务器已经发完数据了。我们下面来看看怎么写:

// 客户端
public class NoBlockClient {

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

        // 1. 获取通道
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));

        // 1.1切换成非阻塞模式
        socketChannel.configureBlocking(false);

        // 1.2获取选择器
        Selector selector = Selector.open();

        // 1.3将通道注册到选择器中,获取服务端返回的数据
        socketChannel.register(selector, SelectionKey.OP_READ);

        // 2. 发送一张图片给服务端吧
        FileChannel fileChannel = FileChannel.open(Paths.get("E:/softprograme/java/in.txt"), StandardOpenOption.READ);

        // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // 4.读取本地文件(图片),发送到服务器
        while (fileChannel.read(buffer) != -1) {

            // 在读之前都要切换成读模式
            buffer.flip();

            socketChannel.write(buffer);

            // 读完切换成写模式,能让管道继续读取文件的数据
            buffer.clear();
        }


        // 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪
        while (selector.select() > 0) {
            // 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件)
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

            // 7. 获取已“就绪”的事件,(不同的事件做不同的事)
            while (iterator.hasNext()) {

                SelectionKey selectionKey = iterator.next();

                // 8. 读事件就绪
                if (selectionKey.isReadable()) {

                    // 8.1得到对应的通道
                    SocketChannel channel = (SocketChannel) selectionKey.channel();

                    ByteBuffer responseBuffer = ByteBuffer.allocate(1024);

                    // 9. 知道服务端要返回响应的数据给客户端,客户端在这里接收
                    int readBytes = channel.read(responseBuffer);

                    if (readBytes > 0) {
                        // 切换读模式
                        responseBuffer.flip();
                        System.out.println(new String(responseBuffer.array(), 0, readBytes));
                    }
                }

                // 10. 取消选择键(已经处理过的事件,就应该取消掉了)
                iterator.remove();
            }
        }
    }
}

// 服务器
public class NoBlockServer {

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

        // 1.获取通道
        ServerSocketChannel server = ServerSocketChannel.open();

        // 2.切换成非阻塞模式
        server.configureBlocking(false);

        // 3. 绑定连接
        server.bind(new InetSocketAddress(6666));

        // 4. 获取选择器
        Selector selector = Selector.open();

        // 4.1将通道注册到选择器上,指定接收“监听通道”事件
        server.register(selector, SelectionKey.OP_ACCEPT);

        // 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪
        while (selector.select() > 0) {
            // 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件)
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

            // 7. 获取已“就绪”的事件,(不同的事件做不同的事)
            while (iterator.hasNext()) {

                SelectionKey selectionKey = iterator.next();

                // 接收事件就绪
                if (selectionKey.isAcceptable()) {

                    // 8. 获取客户端的链接
                    SocketChannel client = server.accept();

                    // 8.1 切换成非阻塞状态
                    client.configureBlocking(false);

                    // 8.2 注册到选择器上-->拿到客户端的连接为了读取通道的数据(监听读就绪事件)
                    client.register(selector, SelectionKey.OP_READ);

                } else if (selectionKey.isReadable()) { // 读事件就绪

                    // 9. 获取当前选择器读就绪状态的通道
                    SocketChannel client = (SocketChannel) selectionKey.channel();

                    // 9.1读取数据
                    ByteBuffer buffer = ByteBuffer.allocate(1024);

                    // 9.2得到文件通道,将客户端传递过来的图片写到本地项目下(写模式、没有则创建)
                    FileChannel outChannel = FileChannel.open(Paths.get("E:/softprograme/java/in.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);

                    while (client.read(buffer) > 0) {
                        // 在读之前都要切换成读模式
                        buffer.flip();

                        outChannel.write(buffer);

                        // 读完切换成写模式,能让管道继续读取文件的数据
                        buffer.clear();
                    }

                    // 向客户端发送确认请求
                    buffer.put("已经发送完毕!".getBytes());
                    buffer.flip();
                    client.write(buffer);
                    buffer.clear();
                }
                // 10. 取消选择键(已经处理过的事件,就应该取消掉了)
                iterator.remove();
            }
        }

    }
}

管道和DataGramChannel
图片说明).webp "图片标题")

图片说明).webp "图片标题")

图片说明).webp "图片标题")

图片说明).jpg "图片标题")

全部评论

相关推荐

想去夏威夷的小哥哥在度假:5和6才是重点
点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务