Java IO详解

什么是I/O流?

I:Input O:Output
通过IO可以完成硬盘文件的读和写。
IO流又叫输入输出流 ,输入和输出均是以内存作为参照物。
在这里插入图片描述

I/O流的分类?
1.按照流的方向进行分类:

以内存作为参照物,往内存中去,叫做输入,或者叫做读。从内存中出来,叫做输出,或者叫做写。

2.按照读取数据方式不同进行分类:
  1. 按照字节的方式读取数据,一次读取1个字节byte,等同于一次读取8个二进制位。这种流是万能的,什么类型的文件都可以读取。包括:文本文件,图片,声音文件,视频文件。
    example:假设文件file1.txt,采用字节流的话是这样读的:
    a中国bo张三fe:
    第一次读: 一个字节,正好读"a"。
    第二次读:一个字节,正好读到"中"字符的一半。
    第三次读: 一个字节,正好读到"中字符的另外一半。
  2. 按照字符的方式读取数据, 一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流不能读取:图片、声音、视频等文件。只能读取纯文本文件,连word文件都无法读取。
    example:假设文件file1.txt,采用字符流的话是这样读的:
    a中国bo张三fe:
    第一次读:'a’字符('a’字符在windows系统中占用1个字节。)
    第二次读:'中’字符('中’字符在windows系统中占用2个字节。)
    综上所述:流的分类
    输入流、输出流、字节流、字符流
Java IO流这块有四大家族(首领):(均是抽象类)

java.io.InputStream 字节输入流
java.io.OutputStream 字节输出流
java.io.Reader 字符输入流
java.io.Writer 字符输出流
所有的流都实现了:java.io.Colseable接口,都是可关闭的,都有close()方法. 流毕竟是一个管道,这个是内存和硬盘之间的通道,养成一个好习惯,用完流将其关闭。
所有的输出流都实现了:
java.io.Flushable接口,都是可刷新的,都有flush()方法。养成一个好习惯,输出流在最终输出之后,一定要记得flush()刷新一下。这个刷新表示将通道/管道当中剩余未输出的数据强行输出完(清空管道!)刷新的作用就是清空管道。如果没有flush()可能会导致数据丢失.
注意:在java中只要"类名"以Stream结尾的都是字节流。以"Reader/Writer"结尾的都是字符流 。

Java.io 包下需要掌握的流有16个:
文件专属
java.io.FileinputStream   (用得最多)
java.io.FileOutputStream  (用得最多)
java.io.FileReader
java.io.FileWriter

转换流:(将字节流转换成字符流)
java.io.InputStreamReader
java.io.outputStreamWriter

缓冲流专属:
java.io.BufferedReader
java.io.BufferedWriter
java.io.BufferedInputStream
java.io.BufferedOutputStream

数据流专属:
java.io.DataInpoutStream
java.io.DataOutputStream

标准输出流:
java.io.PrintWriter
java.io.PrintStream
    
对象专属流:(掌握)
java.io.ObjectInputStream
java.io.ObjectOutputStream
FileInputStream

IDEA的默认路径是整个工程下的包

  1. 文件字节输入流,万能的,任何类型的文件都可以采用这个流来读
  2. 字节的方式,完成输入的操作,完成读的操作(硬盘 —> 内存)
    example1:
      FileInputStream fis = new FileInputStream("D:\\Desktop\\temp.txt");
        // 开始读
        int read = fis.read(); // read是读到的字节大小
        int read1 = fis.read(); // read1是读到的字节大小
        // 如果读不到数据  则返回负一
        System.out.println(read);
        System.out.println(read1);
        fis.close();

note:

  1. idea会自动把\编程\,因为java中****表示转义,故路径处采用的是\
  2. 读到文件末尾时read()方法会返回-1
    example2: 对example1进行改进,采用循环的方式!
   FileInputStream fis = null;
        // 指向一个文件
        fis = new FileInputStream("D:\\Desktop\\temp.txt");
        while(true){
            int read = fis.read();
            if(read==-1){  // 读到末尾的时候返回值为-1
                break;
            }
            System.out.println(read);
        }
        if(fis != null){
            fis.close();
        }

该程序的缺点:一次读取一个字节byte,这样内存和硬盘交互太频繁,基本时间/资源都耗费在交互上面了,能不能一次性读取多个字节呢?
example3:

 * int read(byte[] b)
 *  一次最多读取b.length个字节
 *  减少硬盘和内存的交互,提高程序的执行效率
 *byte[]数组当中读
		FileInputStream fis = null; // 创建一个流
        // IDEA默认的当前路径在哪里?
        // 工程Project的根就是IDEA的默认当前路径。
        fis =  new FileInputStream("tempfile");
        // 开始读  采用byte数组  一次读取多个字节。最多读取数组.length个字节.
        byte[] bytes = new byte[4]; // 准备一个4长度的byte数组,一次最多读取4个字节
        // 返回读到的字节数量(不是字节本身)(把数据读取到了byte数组当中)
        int readCount = fis.read(bytes); // 4
        //System.out.println(new String(bytes)); // 将字节数组全部转换成字符串
        // 不应该全部转换,应该是读取了多少字节,转换多少个
        System.out.println(new String(bytes,0,readCount));
        System.out.println(readCount);
        readCount = fis.read(bytes); // 2
        System.out.println(new String(bytes,0,readCount));
        System.out.println(readCount);
        readCount = fis.read(bytes); // -1 (一个字节都没有读到)
        System.out.println(new String(bytes,0,readCount));
        System.out.println(readCount);

当read方法中的参数为byte[]数组时,该方法的返回值为byte[]数组在文件中读取的字节数量,而此时的byte[]数组中的内容为在目标文件中所读取的内容。
example4: 建议使用这种方式读取

 FileInputStream fis = null;
        fis = new FileInputStream("tempfile");
        // 准备一个byte数组
        byte[] bytes = new byte[4];
        int readCount;
        while((readCount = fis.read(bytes))!=-1){
            // 把byte数组转换成字符串,读到多少个转换多少个
            System.out.print(new String(bytes,0,readCount));
        }
        if(fis!=null){
            fis.close();
        }

补充:FileInputStream类的其它方法:

*      int available();  // 返回流当中的剩余的没有读到的字节数量
*      long skip(long n); // 跳过几个字节不读
 FileInputStream fis = null;
        fis = new FileInputStream("tempfile");
        System.out.println("总字节数量:"+fis.available());
         fis.skip(3);
        System.out.println(fis.read());
FIleOutputStream

文件字节输出流,负责写,从内存到硬盘
example:

  FileOutputStream fos = null;
         // myfile文件不存在的时候会自动新建!
//         fos = new FileOutputStream("myfile");
        // 不清空原文件的方式写入  则选择以追加的方式写入  不会清空原文件内容
        fos = new FileOutputStream("myfile1",true); // true表示追加
         // 开始写
        byte[] bytes = {97,98,99,100};
        // 将byte数组全部写出
        fos.write(bytes); // abcd
        // 将byte数组的一部分写出
        fos.write(bytes,0,2); //ab
        // 以上两种方式都是将原文件清空之后再写入的(谨慎使用)
         // 写完之后,一定要刷新
        String s = "我是一个中国人,我骄傲";
        // 将字符串转换为byte数组
        byte[] bytes1 = s.getBytes();
        fos.write(bytes1);
        fos.flush();

*** note :
以不清空原文件的方式写入。选择以追加的方式写入,需要在创建流对象的时候引入
参数true
*表示以追加的方式写入硬盘。

文件的复制

原理图如下:
在这里插入图片描述
文件的拷贝,是先从硬盘中读取到文件里面,再通过内存写入硬盘从而完成文件的复制。使用FileInputStream + FileOutputStream 完成文件的拷贝拷贝的过程应该一边读,一边写。使用以上字节流拷贝文件的时候,文件的类型随意,万能的,什么样的文件都能拷贝。
example1:使用FileInputStream + FileOutputStream 进行拷贝

FileInputStream fis = null;
        FileOutputStream fos = null;
        // 创建一个输入流对象
        fis = new FileInputStream("myfile");
        // 创建一个输出流对象
        fos = new FileOutputStream("myfile2");
        // 最核心的: 一边读,一边写(1024个字节是1kb)
        byte[] bytes = new byte[1024 * 1024]; // 一次拷贝1M
        int readCounts = 0;
        while((readCounts = fis.read(bytes)) != -1){
            fos.write(bytes,0,readCounts);
        }
        // 刷新,输出流最后要刷新
        fos.flush();
        fis.close();
        fos.close();

example2:使用FIleReader + FileWriter进行拷贝(只能拷贝普通文件)

        FileReader fileReader = null;
        FileWriter fileWriter = null;
        // 创建一个输入流
        fileReader = new FileReader("myfile");
        // 创建一个输出流(没有文件会先自己创建)
        fileWriter = new FileWriter("myfile00");
        char[] chars = new char[1024*1024];
        int readCounts = 0;
        while((readCounts = fileReader.read(chars))!=-1){
            fileWriter.write(chars,0,readCounts);
        }
        fileWriter.flush();
        fileReader.close();
        fileWriter.close();
FIleReader

文件字符输入流。只能读取普通文本
example:

FileReader reader = null;
        // 创建文件字符输入流
        reader = new FileReader("tempfile");
        // 开始读
        char[] chars = new char[10];
        int readCounts = 0;
        while((readCounts = reader.read(chars)) != -1){
            System.out.print(new String(chars,0,readCounts));
        }
        reader.close();

因为是字符输入流,故此处创建的是char[]数组而不是byte[]数组

FileWriterTest

文件字符输出流,写。只能输出普通文本
example:

        FileWriter out = null;
        // 初始化一个文件字符输出流对象
        out = new FileWriter("file"); // 不想清空 一直累加的话  可以在此处加true
        // 开始写
        char[] chars = {'我','是','中','国','人'};
        // 刷新
        out.write(chars);
        out.write(chars,0,2);
        out.write("我是一名java工程师!");
        out.flush();

note: FileReader和FileWriter操作普通文本挺在行,但是操作其它非普通文本文件不在行(word不是普通文本,普通文本也与文件后缀名无关)

BufferedReader

带有缓冲区的字符输入流,使用这个流的时候不需要自定义char数组,或者说不需要自定义byte数组,自带缓冲。
example1

  FileReader fileReader = new FileReader("myfile");
        // 当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做: 节点流.
        // 外部负责包装的这个流,叫做:包装流,还有一个名字叫做 : 处理流
        // 像当前这个程序来说: FileReader就是一个节点流。BufferedReader就是包装流/处理流。
        BufferedReader bufferedReader = new BufferedReader(fileReader);

//        // 读第一行
//        String s = bufferedReader.readLine();
//        System.out.println("第一行: "+s);
//
//        // 读第二行
//        String s1 = bufferedReader.readLine();
//        System.out.println("第二行:"+s1);

        // 循环读出来   这种方式  读到末尾返回值为null
        // readLine() 方法读取一个文本行,但不带换行符。
        String s ;
        while((s = bufferedReader.readLine())!=null){
            System.out.println(s);
        }
        // 关闭流
        // 对于包装流来说,只需要关闭最外层流就行,里面的节点流会自动关闭!
        bufferedReader.close();

节点流:当一个流的构造方法中需要一个流的时候,这个被传进来的流叫做
包装流:外部负责包装的这个流,又叫处理流
example2:

 BufferedReader br = new BufferedReader(new InputStreamReader((new FileInputStream("myfile"))));
        String line = null;
        while((line=br.readLine())!= null){ // 一行一行的读  没有读到null就代表读到了
            System.out.println(line);
        }
BufferedWriter

带有缓冲区的字符输出流

      BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("copy1",true)));
        bw.write("hello world");
        bw.write("\n");
        bw.write("i love you");
        // 刷新(输出流好习惯)
        bw.flush();
        // 关闭最外层
        bw.close();
DataOutputStream

这个流可以将数据连同数据的类型一并写入文件,但是这个文件不是普通文本文档。

 // 创建数据专属的字节输出流
        DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));
        // 写数据
        byte b =100;
        short s =200;
        int i =300;
        long l = 400L;
        float f =3.0f;
        double d = 3.14;
        boolean sex = false;
        char c = 'a';
        // 写
        dos.writeByte(b);
        dos.writeShort(s);
        dos.writeInt(i);
        dos.writeFloat(f);
        dos.writeBoolean(sex);
        dos.writeDouble(d);
        dos.flush();
        dos.close();
DataInputStream

数据字节写入流,DataOutputStream写的文件,只能使用DataInputStream去读,并且读的时候需要提前知道写入的顺序。读的顺序和写的顺序一致,才可以正确取出数据。
example:

 DataInputStream dis = new DataInputStream(new FileInputStream("data"));
        // 开始读
        Byte b = dis.readByte();
        Short s = dis.readShort();
        int i = dis.readInt();
        Float f = dis.readFloat();
        Boolean sex = dis.readBoolean();
        Double d = dis.readDouble();

        System.out.println(b);
        System.out.println(s);
        System.out.println(i);
        System.out.println(f);
        System.out.println(sex);
        System.out.println(d);
PrintStream

标准字节的输出流,默认输出到控制台,也就是我们常用的System.out.println()方法
example1:

 // 联合起来写
        System.out.println("hello world");
        // 分开写
        PrintStream out = System.out;
        out.println("hello zhangsan");
        out.println("hello lisi");
        out.println("hello wangwu");

example2: 日志框架的实现原理:

 PrintStream log = new PrintStream(new FileOutputStream("log"));
        // 修改输出方向,将输出方向修改到"log"文件。
        System.setOut(log);
        // 再输出
        System.out.println("hello jingjing");
        System.out.println("hello xiangzheng");
        System.out.println("hello gege");

setOut方法改变输出流的方向
example3:日志框架的实现原理:

public class LogUtil {
    /**
     * 自己实现 记录日志的方法
     * @param msg
     */
    public static void log(String msg) {
        try {
            // 指向一个日志文件
            PrintStream ps = new PrintStream(new FileOutputStream("log.txt",true));
            // 改变流的输出方向
            System.setOut(ps);
            // 获取系统当前时间
            Date nowTime = new Date();
            // 日期格式化
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String strTime = sdf.format(nowTime);
            System.out.println(strTime+": "+msg);

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

    }
}
public class LogTest {
    public static void main(String[] args) {
        // 测试工具类是否好用
        LogUtil.log("调用了System类的gc方法");

        LogUtil.log("努力学习,为了以后的生活");

        LogUtil.log("晚上什么时候回去啊!");
    }
}

运行结果:生成日志文件中的内容
在这里插入图片描述

File类(补充)
  1. File类和四大家族没有关系,所以File类不能完成文件的读和写
  2. 代表: 文件 和 目录路径名 的抽象表示形式,一个File对象有可能对应的是目录,也有可能是文件,File只是一个路径名的抽象表达形式
    example1:
        File file = new File("D:\\Desktop\\temp.txt");
        // 判断文件是否存在
        System.out.println(file.exists());

        // 如果D:\file不存在,则以文件的形式创建出来
        if(!file.exists()){
            // 以文件的方式新建出来
            file.createNewFile();
        }

        // 如果D:\file不存在,则以目录的形式创建出来
        if(!file.exists()){
            file.mkdir();
        }

        // 创建多重目录
        File f2 = new File("D:\\Desktop\\a\\b\\c\\d");
        if(!f2.exists()){
            f2.mkdirs();
        }

        File f3 = new File("D\\Desktop\\a\\b\\c\\d");
        // 获取文件的父路径
        String path = f3.getParent();
        System.out.println(path);
        File parentFile = f3.getParentFile(); // 获取文件
        System.out.println(parentFile);
        // 获取绝对路径
        System.out.println(
                "绝对路径: "+parentFile.getAbsolutePath()
        );

example2:

    File file = new File("D:\\Desktop\\打工人学习计划.md");
        System.out.println("文件名"+file.getName());
        // 判断是否是一个目录
        System.out.println("是否是一个目录"+file.isDirectory());
        // 判断是否是一个文件
        System.out.println("是否是一个文件"+file.isFile());
        // 获取文件最后一次修改时间
        // 该方法的最后一次返回时间是毫秒
        System.out.println("该文件最后一次修改时间是: "+file.lastModified());
        // 获取文件的字节大小
        System.out.println("该文件的大小为 "+file.length());
        // 获取当前目录下所有的子文件。
        File file = new File("D:\\Desktop");
        File[] files = file.listFiles();
        for(File i:files){
            System.out.println(i.getName());
        }
目录的拷贝
public class CopyAll {
    public static void main(String[] args) throws IOException {
        // 拷贝源
        File srcFile = new File("D:\\Desktop\\a\\b\\c\\d");
        // 拷贝目标
        File destFile = new File("D:\\Desktop\\c");
        // 调用方法拷贝
        copyDir(srcFile,destFile);
    }
    /**
     * 目录拷贝
     * @param srcFile       拷贝源
     * @param destFile      拷贝目标
     */
    private static void copyDir(File srcFile,File destFile) throws IOException {
        if(srcFile.isFile()){
            // 如果是一个文件的话,递归结束
            // 是文件的时候需要拷贝
            // 拷贝的时候  自我理解  内存 从硬盘1中读  然后再由内存向硬盘2中写
            // 一边读一边写
            FileInputStream in = null;
            FileOutputStream out = null;
            // 读这个文件
            in = new FileInputStream(srcFile);
            // 写到这个文件中
            String path = destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath(): destFile.getAbsolutePath()+"\\"+destFile.getAbsolutePath().substring(3);
            out = new FileOutputStream(path);
            // 一边读一边写
            byte[] bytes = new byte[1024 * 1024]; // 一次复制1M
            int readCount = 0;
            while((readCount=in.read(bytes))!=-1){
                out.write(bytes,0,readCount);
            }
            out.flush();
            in.close();
            out.close();
            return;
        }
        // 获取源下面的子目录
        File[] files = srcFile.listFiles();
        for (File file:files){ // 这个i可能是文件 也可能是目录
            // 获取所有文件的绝对路径
//            System.out.println(destFile.getAbsolutePath());
            if(file.isDirectory()) {
                String srcDir = srcFile.getAbsolutePath(); // 源目录
                String destDir = destFile.getAbsolutePath().endsWith("\\")? destFile.getAbsolutePath() : destFile.getAbsolutePath()+"\\"+srcDir.substring(3); // 目标目录
                System.out.println(destDir);
                File newFile = new File(destDir);
                if(!newFile.exists()){
                    newFile.mkdirs();
                }
            }
            // 递归调用
            copyDir(file, destFile);
        }
    }

}

序列化和反序列化

在这里插入图片描述
序列化:Serialize java对象存储到文件中,将java对象的状态保存下来的过程
反序列化:DeSerialize 将硬盘上的数据重新恢复到内存当中,恢复成java对象
这个过程会用到两个类: ObjectOutputStream 和 Objetc’InputStream
序列化
example1:
Student类:

// 实现可序列化的接口 接口中无任何内容 只是一个标志性接口
public class Student implements Serializable {
    /**
     * Java虚拟机看到Serializable接口后,会自动生成一个序列化版本号
     * 这里没有手动写出来,java虚拟机会吗,默认提供这个序列化版本号
     * 建议将序列化版本号写出来,不建议自动生成
     */
    private static  final long serialVersionUID = 1L; // 手动写出序列化版本号
    private int no;
    private String name;

    // 过了很久,Student这个类源代码改动了
    // 源代码改动之后,需要重新编译,编译之后生成了全新的字节码文件。
    // 并且class文件再次运行的时候,java虚拟机生成的序列化版本号也会发生相应的改变。
    private int age;
    }

测试类:

     // 创建java对象
        Student s = new Student(111, "张三");
        // 序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("students"));
        // 序列化对象
        oos.writeObject(s);
        // 刷新
        oos.flush();
        // 关闭
        oos.close();

example2:
User类:

public class User implements Serializable {
    private int no;
    /**
     * 如果你不希望该关键字参与 序列化   可以加:  transient 关键字 表示游离的
     * private transient String name; // 表示name不参加序列化
     */
    private String name;

测试类: 一次性序列化多个对象

    public static void main(String[] args) throws Exception {
        List<User> users = new ArrayList<>();
        users.add(new User(1,"zhangsan"));
        users.add(new User(2,"wangwu"));
        users.add(new User(3,"lisi"));
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("users"));
        // 一次序列化一个集合  一个集合中放了很多对象
        oos.writeObject(users);
        oos.flush();
        oos.close();

反序列化:
example1:

 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("students"));
        // 开始反序列化  从硬盘中读到内存当中
        Object obj = ois.readObject();
        // 反序列化回来是一个学生对象  所以会调用学生对象的toString方法
        System.out.println(obj);
        ois.close();

example2:一次性反序列化多个对象

  ObjectInputStream oos = new ObjectInputStream(new FileInputStream("users"));
        // 自己测试的时候  用 instanceOf 判断过返回的对象是 list集合
        List<User> userList = (List<User>)oos.readObject();
        for(User user : userList){
            System.out.println(user);
        }
        oos.close();
IO和Properties

IO流和Properties联合使用!
创建一个userinfo.properties的文件:
在这里插入图片描述

/**
         * Properties是一个Map集合,key和value都是String类型。
         * 想将userinfo文件中的数据加载到properties对象中
         */
        /**
         * java规范中有要求: 属性配置文件建议以.properties结尾,但这不是必须的。
         * 这种以.properties结尾的文件在java中被称为: 属性配置文件.
         * 其中Properties对象是专门存放属性配置文件中的类
         */
        // 新建一个输入流对象
        FileReader reader = new FileReader("userinfo");
        // 新建一个Map集合
        Properties pro = new Properties();
        // 调用Properties的load方法将文件中的数据加载到mao集合当中
        pro.load(reader); // 文件中的数据顺着管道加载到Map集合当中,等号左边是key 右边是value
        // 通过key来获取value
        // 程序中经常发生变化的信息最好别写到Java程序里  写到类似的文件中(配置文件)
        String username = pro.getProperty("username");
        System.out.println(username);
        String password = pro.getProperty("password");
        System.out.println(password);

运行结果:

admin123
123456

补充:

  1. Java是通过什么机制来区分类的?
    第一: 首先通过类名进行比对,如果类名不一样,肯定不是同一个类
    第二: 如果类名一样,再通过序列化版本号进行区分
  2. 自动生成序列化版本号有什么缺陷?
    一旦代码确定就不能进行后续的修改,因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java虚拟机会认为这是一个全新的类。
  3. 建议凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号,即使以后这个类代码修改了,但是版本号不变,java虚拟机会认为这是同一个类。
  4. transient关键字表示该属性不参与序列化,表示游离的
Socket.io是一个可以在服务器端和客户端之间建立实时双向通信的开源库。在Java后端,我们可以使用socket.io-java-client库来实现与客户端的通信。 首先,我们需要在Java后端创建一个服务器,用来监听客户端的连接请求和处理客户端的消息。我们可以使用socket.io-java-server库来快速搭建一个简单的服务器。 ```java import io.socket.server.*; public class Server { public static void main(String[] args) { SocketIOServer server = new SocketIOServer(8080); server.on("connection", (socket) -> { System.out.println("Client connected: " + socket.getId()); socket.on("message", (message) -> { System.out.println("Received message from client: " + message); // 处理消息逻辑 // 向客户端发送回复 socket.emit("reply", "Hello client!"); }); socket.on("disconnect", () -> { System.out.println("Client disconnected: " + socket.getId()); }); }); server.start(); } } ``` 在上面的代码中,我们创建了一个SocketIOServer对象,指定了服务器监听的端口号。然后通过调用server.on方法监听客户端的连接事件,并在连接事件中处理客户端发送的消息。 当客户端发送消息时,我们可以在服务器端通过对应的事件进行处理,例如上面代码中的"message"事件。处理完消息之后,我们可以通过socket.emit方法向客户端发送回复。 当客户端断开连接时,我们可以在服务器端通过监听"disconnect"事件来处理相应的逻辑。 以上就是使用socket.io-java-server库来实现socket.ioJava后端的基本步骤。当然,实际情况中,我们可能还需要处理异常、管理多个客户端连接等等。但是在基本原理上是类似的。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

午觉千万别睡过

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值