Skip to content

Latest commit

 

History

History
1016 lines (762 loc) · 37.4 KB

JavaIO之BIO.md

File metadata and controls

1016 lines (762 loc) · 37.4 KB
title date order categories tags permalink
Java I/O 之工具类
2020-06-30 14:34:59 -0700
5
Java
JavaCore
IO
Java
JavaCore
IO
BIO
InputStream
OutputStream
Reader
Writer
/pages/a4dba16e/

Java I/O 之 BIO

BIO

BIO(blocking IO) 即阻塞 IO。指的主要是传统的 java.io 包,它基于流模型实现。流从概念上来说是一个连续的数据流。当程序需要读数据的时候就需要使用输入流读取数据,当需要往外写数据的时候就需要输出流。

java.io 包提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。很多时候,人们也把 java.net 下面提供的部分网络 API,比如 SocketServerSocketHttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。

BIO 中操作的流主要有两大类,字节流和字符流,两类根据流的方向都可以分为输入流和输出流。

  • 字节流
    • 输入字节流:InputStream
    • 输出字节流:OutputStream
  • 字符流
    • 输入字符流:Reader
    • 输出字符流:Writer

img

字节流

字节流主要操作字节数据或二进制对象。

字节流有两个核心抽象类:InputStreamOutputStream。所有的字节流类都继承自这两个抽象类。

img

InputStream

InputStream用于从源头(通常是文件)读取数据(字节信息)到内存中,java.io.InputStream抽象类是所有字节输入流的父类。

InputStream 常用方法:

  • read():返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回 -1 ,表示文件结束。
  • read(byte b[ ]) : 从输入流中读取一些字节存储到数组 b 中。如果数组 b 的长度为零,则不读取。如果没有可用字节读取,返回 -1。如果有可用字节读取,则最多读取的字节数最多等于 b.length , 返回读取的字节数。这个方法等价于 read(b, 0, b.length)
  • read(byte b[], int off, int len):在read(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)。
  • skip(long n):忽略输入流中的 n 个字节 , 返回实际忽略的字节数。
  • available():返回输入流中可以读取的字节数。
  • close():关闭输入流释放相关的系统资源。

从 Java 9 开始,InputStream 新增加了多个实用的方法:

  • readAllBytes():读取输入流中的所有字节,返回字节数组。
  • readNBytes(byte[] b, int off, int len):阻塞直到读取 len 个字节。
  • transferTo(OutputStream out):将所有字节从一个输入流传递到一个输出流。

OutputStream

OutputStream 用于将数据(字节信息)写入到目的地(通常是文件),java.io.OutputStream抽象类是所有字节输出流的父类。

OutputStream 常用方法:

  • write(int b):将特定字节写入输出流。
  • write(byte b[ ]) : 将数组b 写入到输出流,等价于 write(b, 0, b.length)
  • write(byte[] b, int off, int len) : 在write(byte b[ ]) 方法的基础上增加了 off 参数(偏移量)和 len 参数(要读取的最大字节数)。
  • flush():刷新此输出流并强制写出所有缓冲的输出字节。
  • close():关闭输出流释放相关的系统资源。

文件字节流

FileOutputStreamFileInputStream 提供了读写字节到文件的能力。

文件流操作一般步骤:

  1. 使用 File 类绑定一个文件。
  2. File 对象绑定到流对象上。
  3. 进行读或写操作。
  4. 关闭流

FileOutputStreamFileInputStream 示例:

public class FileStreamDemo {

    private static final String FILEPATH = "temp.log";

    public static void main(String[] args) throws Exception {
        write(FILEPATH);
        read(FILEPATH);
    }

    public static void write(String filepath) throws IOException {
        // 第 1 步、使用 File 类找到一个文件
        File f = new File(filepath);

        // 第 2 步、通过子类实例化父类对象
        OutputStream out = new FileOutputStream(f);
        // 实例化时,默认为覆盖原文件内容方式;如果添加 true 参数,则变为对原文件追加内容的方式。
        // OutputStream out = new FileOutputStream(f, true);

        // 第 3 步、进行写操作
        String str = "Hello World\n";
        byte[] bytes = str.getBytes();
        out.write(bytes);

        // 第 4 步、关闭输出流
        out.close();
    }

    public static void read(String filepath) throws IOException {
        // 第 1 步、使用 File 类找到一个文件
        File f = new File(filepath);

        // 第 2 步、通过子类实例化父类对象
        InputStream input = new FileInputStream(f);

        // 第 3 步、进行读操作
        // 有三种读取方式,体会其差异
        byte[] bytes = new byte[(int) f.length()];
        int len = input.read(bytes); // 读取内容
        System.out.println("读入数据的长度:" + len);

        // 第 4 步、关闭输入流
        input.close();
        System.out.println("内容为:\n" + new String(bytes));
    }

}

内存字节流

ByteArrayInputStreamByteArrayOutputStream 是用来完成内存的输入和输出功能。

内存操作流一般在生成一些临时信息时才使用。 如果临时信息保存在文件中,还需要在有效期过后删除文件,这样比较麻烦。

ByteArrayInputStreamByteArrayOutputStream 示例:

public class ByteArrayStreamDemo {

    public static void main(String[] args) {
        String str = "HELLOWORLD"; // 定义一个字符串,全部由大写字母组成
        ByteArrayInputStream bis = new ByteArrayInputStream(str.getBytes());
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        // 准备从内存 ByteArrayInputStream 中读取内容
        int temp = 0;
        while ((temp = bis.read()) != -1) {
            char c = (char) temp; // 读取的数字变为字符
            bos.write(Character.toLowerCase(c)); // 将字符变为小写
        }
        // 所有的数据就全部都在 ByteArrayOutputStream 中
        String newStr = bos.toString(); // 取出内容
        try {
            bis.close();
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println(newStr);
    }

}

管道流

管道流的主要作用是可以进行两个线程间的通信。

如果要进行管道通信,则必须把 PipedOutputStream 连接在 PipedInputStream 上。为此,PipedOutputStream 中提供了 connect() 方法。

public class PipedStreamDemo {

    public static void main(String[] args) {
        Send s = new Send();
        Receive r = new Receive();
        try {
            s.getPos().connect(r.getPis()); // 连接管道
        } catch (IOException e) {
            e.printStackTrace();
        }
        new Thread(s).start(); // 启动线程
        new Thread(r).start(); // 启动线程
    }

    static class Send implements Runnable {

        private PipedOutputStream pos = null;

        Send() {
            pos = new PipedOutputStream(); // 实例化输出流
        }

        @Override
        public void run() {
            String str = "Hello World!!!";
            try {
                pos.write(str.getBytes());
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                pos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        /**
         * 得到此线程的管道输出流
         */
        PipedOutputStream getPos() {
            return pos;
        }

    }

    static class Receive implements Runnable {

        private PipedInputStream pis = null;

        Receive() {
            pis = new PipedInputStream();
        }

        @Override
        public void run() {
            byte[] b = new byte[1024];
            int len = 0;
            try {
                len = pis.read(b);
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                pis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println("接收的内容为:" + new String(b, 0, len));
        }

        /**
         * 得到此线程的管道输入流
         */
        PipedInputStream getPis() {
            return pis;
        }

    }

}

对象字节流

ObjectInputStream 和 ObjectOutputStream 是对象输入输出流,一般用于对象序列化。

这里不展开叙述,想了解详细内容和示例可以参考:[Java 序列化](03.Java 序列化。md)

数据操作流

数据操作流提供了格式化读入和输出数据的方法,分别为 DataInputStreamDataOutputStream

DataInputStreamDataOutputStream 格式化读写数据示例:

public class DataStreamDemo {

    public static final String FILEPATH = "temp.log";

    public static void main(String[] args) throws IOException {
        write(FILEPATH);
        read(FILEPATH);
    }

    private static void write(String filepath) throws IOException {
        // 1. 使用 File 类绑定一个文件
        File f = new File(filepath);

        // 2. 把 File 对象绑定到流对象上
        DataOutputStream dos = new DataOutputStream(new FileOutputStream(f));

        // 3. 进行读或写操作
        String[] names = { "衬衣", "手套", "围巾" };
        float[] prices = { 98.3f, 30.3f, 50.5f };
        int[] nums = { 3, 2, 1 };
        for (int i = 0; i < names.length; i++) {
            dos.writeChars(names[i]);
            dos.writeChar('\t');
            dos.writeFloat(prices[i]);
            dos.writeChar('\t');
            dos.writeInt(nums[i]);
            dos.writeChar('\n');
        }

        // 4. 关闭流
        dos.close();
    }

    private static void read(String filepath) throws IOException {
        // 1. 使用 File 类绑定一个文件
        File f = new File(filepath);

        // 2. 把 File 对象绑定到流对象上
        DataInputStream dis = new DataInputStream(new FileInputStream(f));

        // 3. 进行读或写操作
        String name = null; // 接收名称
        float price = 0.0f; // 接收价格
        int num = 0; // 接收数量
        char[] temp = null; // 接收商品名称
        int len = 0; // 保存读取数据的个数
        char c = 0; // '\u0000'
        try {
            while (true) {
                temp = new char[200]; // 开辟空间
                len = 0;
                while ((c = dis.readChar()) != '\t') { // 接收内容
                    temp[len] = c;
                    len++; // 读取长度加 1
                }
                name = new String(temp, 0, len); // 将字符数组变为 String
                price = dis.readFloat(); // 读取价格
                dis.readChar(); // 读取、t
                num = dis.readInt(); // 读取 int
                dis.readChar(); // 读取、n
                System.out.printf("名称:%s;价格:%5.2f;数量:%d\n", name, price, num);
            }
        } catch (EOFException e) {
            System.out.println("结束");
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 4. 关闭流
        dis.close();
    }

}

合并流

合并流的主要功能是将多个 InputStream 合并为一个 InputStream 流。合并流的功能由 SequenceInputStream 完成。

public class SequenceInputStreamDemo {

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

        InputStream is1 = new FileInputStream("temp1.log");
        InputStream is2 = new FileInputStream("temp2.log");
        SequenceInputStream sis = new SequenceInputStream(is1, is2);

        int temp = 0; // 接收内容
        OutputStream os = new FileOutputStream("temp3.logt");
        while ((temp = sis.read()) != -1) { // 循环输出
            os.write(temp); // 保存内容
        }

        sis.close(); // 关闭合并流
        is1.close(); // 关闭输入流 1
        is2.close(); // 关闭输入流 2
        os.close(); // 关闭输出流
    }

}

字符流

字符流主要操作字符,一般用于处理文本数据。

字符流有两个核心类:Reader 类和 Writer 。所有的字符流类都继承自这两个抽象类。

文件字符流

文件字符流 FileReaderFileWriter 可以向文件读写文本数据。

FileReaderFileWriter 读写文件示例:

public class FileReadWriteDemo {

    private static final String FILEPATH = "temp.log";

    public static void main(String[] args) throws IOException {
        write(FILEPATH);
        System.out.println("内容为:" + new String(read(FILEPATH)));
    }

    public static void write(String filepath) throws IOException {
        // 1. 使用 File 类绑定一个文件
        File f = new File(filepath);

        // 2. 把 File 对象绑定到流对象上
        Writer out = new FileWriter(f);
        // Writer out = new FileWriter(f, true); // 追加内容方式

        // 3. 进行读或写操作
        String str = "Hello World!!!\r\n";
        out.write(str);

        // 4. 关闭流
        // 字符流操作时使用了缓冲区,并在关闭字符流时会强制将缓冲区内容输出
        // 如果不关闭流,则缓冲区的内容是无法输出的
        // 如果想在不关闭流时,将缓冲区内容输出,可以使用 flush 强制清空缓冲区
        out.flush();
        out.close();
    }

    public static char[] read(String filepath) throws IOException {
        // 1. 使用 File 类绑定一个文件
        File f = new File(filepath);

        // 2. 把 File 对象绑定到流对象上
        Reader input = new FileReader(f);

        // 3. 进行读或写操作
        int temp = 0; // 接收每一个内容
        int len = 0; // 读取内容
        char[] c = new char[1024];
        while ((temp = input.read()) != -1) {
            // 如果不是-1 就表示还有内容,可以继续读取
            c[len] = (char) temp;
            len++;
        }
        System.out.println("文件字符数为:" + len);

        // 4. 关闭流
        input.close();

        return c;
    }

}

字节流转换字符流

我们可以在程序中通过 InputStreamReader 从数据源中读取数据,然后也可以在程序中将数据通过 OutputStreamWriter 输出到目标媒介中

使用 InputStreamReader 可以将输入字节流转化为输入字符流;使用OutputStreamWriter可以将输出字节流转化为输出字符流。

OutputStreamWriter 示例:

public class OutputStreamWriterDemo {

    public static void main(String[] args) throws IOException {
        File f = new File("temp.log");
        Writer out = new OutputStreamWriter(new FileOutputStream(f));
        out.write("hello world!!");
        out.close();
    }

}

InputStreamReader 示例:

public class InputStreamReaderDemo {

    public static void main(String[] args) throws IOException {
        File f = new File("temp.log");
        Reader reader = new InputStreamReader(new FileInputStream(f));
        char[] c = new char[1024];
        int len = reader.read(c);
        reader.close();
        System.out.println(new String(c, 0, len));
    }

}

字节流 vs. 字符流

相同点:

字节流和字符流都有 read()write()flush()close() 这样的方法,这决定了它们的操作方式近似。

不同点:

  • 数据类型
    • 字节流的数据是字节(二进制对象)。主要核心类是 InputStream 类和 OutputStream 类。
    • 字符流的数据是字符。主要核心类是 Reader 类和 Writer 类。
  • 缓冲区
    • 字节流在操作时本身不会用到缓冲区(内存),是文件直接操作的。
    • 字符流在操作时是使用了缓冲区,通过缓冲区再操作文件。

选择:

所有的文件在硬盘或传输时都是以字节方式保存的,例如图片,影音文件等都是按字节方式存储的。字符流无法读写这些文件。

所以,除了纯文本数据文件使用字符流以外,其他文件类型都应该使用字节流方式。

I/O 工具类

File

File 类是 java.io 包中唯一对文件本身进行操作的类。它可以对文件、目录进行增删查操作。

createNewFille

可以使用 createNewFille() 方法创建一个新文件

注:

Windows 中使用反斜杠表示目录的分隔符 \。~~~~~~~~

Linux 中使用正斜杠表示目录的分隔符 /

最好的做法是使用 File.separator 静态常量,可以根据所在操作系统选取对应的分隔符。

【示例】创建文件

File f = new File(filename);
boolean flag = f.createNewFile();

mkdir

可以使用 mkdir() 来创建文件夹,但是如果要创建的目录的父路径不存在,则无法创建成功。

如果要解决这个问题,可以使用 mkdirs(),当父路径不存在时,会连同上级目录都一并创建。

【示例】创建目录

File f = new File(filename);
boolean flag = f.mkdir();

delete

可以使用 delete() 来删除文件或目录

需要注意的是,如果删除的是目录,且目录不为空,直接用 delete() 删除会失败。

【示例】删除文件或目录

File f = new File(filename);
boolean flag = f.delete();

list 和 listFiles

File 中给出了两种列出文件夹内容的方法:

  • list(): 列出全部名称,返回一个字符串数组
  • listFiles(): 列出完整的路径,返回一个 File 对象数组

list() 示例:

File f = new File(filename);
String str[] = f.list();

listFiles() 示例:

File f = new File(filename);
File files[] = f.listFiles();

RandomAccessFile

注:RandomAccessFile 类虽然可以实现对文件内容的读写操作,但是比较复杂。所以一般操作文件内容往往会使用字节流或字符流方式。

RandomAccessFile 类是随机读取类,它是一个完全独立的类。

它适用于由大小已知的记录组成的文件,所以我们可以使用 seek() 将记录从一处转移到另一处,然后读取或者修改记录。

文件中记录的大小不一定都相同,只要能够确定哪些记录有多大以及它们在文件中的位置即可。

RandomAccessFile 写操作

当用 rw 方式声明 RandomAccessFile 对象时,如果要写入的文件不存在,系统将自行创建。

r 为只读;w 为只写;rw 为读写。

【示例】文件随机读写

public class RandomAccessFileDemo01 {

    public static void main(String args[]) throws IOException {
        File f = new File("d:" + File.separator + "test.txt"); // 指定要操作的文件
        RandomAccessFile rdf = null; // 声明 RandomAccessFile 类的对象
        rdf = new RandomAccessFile(f, "rw");// 读写模式,如果文件不存在,会自动创建
        String name = null;
        int age = 0;
        name = "zhangsan"; // 字符串长度为 8
        age = 30; // 数字的长度为 4
        rdf.writeBytes(name); // 将姓名写入文件之中
        rdf.writeInt(age); // 将年龄写入文件之中
        name = "lisi    "; // 字符串长度为 8
        age = 31; // 数字的长度为 4
        rdf.writeBytes(name); // 将姓名写入文件之中
        rdf.writeInt(age); // 将年龄写入文件之中
        name = "wangwu  "; // 字符串长度为 8
        age = 32; // 数字的长度为 4
        rdf.writeBytes(name); // 将姓名写入文件之中
        rdf.writeInt(age); // 将年龄写入文件之中
        rdf.close(); // 关闭
    }
}

RandomAccessFile 读操作

读取是直接使用 r 的模式即可,以只读的方式打开文件。

读取时所有的字符串只能按照 byte 数组方式读取出来,而且长度必须和写入时的固定大小相匹配。

public class RandomAccessFileDemo02 {

    public static void main(String args[]) throws IOException {
        File f = new File("d:" + File.separator + "test.txt");    // 指定要操作的文件
        RandomAccessFile rdf = null;        // 声明 RandomAccessFile 类的对象
        rdf = new RandomAccessFile(f, "r");// 以只读的方式打开文件
        String name = null;
        int age = 0;
        byte b[] = new byte[8];    // 开辟 byte 数组
        // 读取第二个人的信息,意味着要空出第一个人的信息
        rdf.skipBytes(12);        // 跳过第一个人的信息
        for (int i = 0; i < b.length; i++) {
            b[i] = rdf.readByte();    // 读取一个字节
        }
        name = new String(b);    // 将读取出来的 byte 数组变为字符串
        age = rdf.readInt();    // 读取数字
        System.out.println("第二个人的信息 --> 姓名:" + name + ";年龄:" + age);
        // 读取第一个人的信息
        rdf.seek(0);    // 指针回到文件的开头
        for (int i = 0; i < b.length; i++) {
            b[i] = rdf.readByte();    // 读取一个字节
        }
        name = new String(b);    // 将读取出来的 byte 数组变为字符串
        age = rdf.readInt();    // 读取数字
        System.out.println("第一个人的信息 --> 姓名:" + name + ";年龄:" + age);
        rdf.skipBytes(12);    // 空出第二个人的信息
        for (int i = 0; i < b.length; i++) {
            b[i] = rdf.readByte();    // 读取一个字节
        }
        name = new String(b);    // 将读取出来的 byte 数组变为字符串
        age = rdf.readInt();    // 读取数字
        System.out.println("第三个人的信息 --> 姓名:" + name + ";年龄:" + age);
        rdf.close();                // 关闭
    }
}

System

System 类中提供了大量的静态方法,可以获取系统相关的信息或系统级操作,其中提供了三个常用于 IO 的静态成员:

  • System.out - 一个 PrintStream 流。System.out 一般会把你写到其中的数据输出到控制台上。System.out 通常仅用在类似命令行工具的控制台程序上。System.out 也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。
  • System.err - 一个 PrintStream 流。System.err 与 System.out 的运行方式类似,但它更多的是用于打印错误文本。一些类似 Eclipse 的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过 System.err 输出到控制台上。
  • System.in - 一个典型的连接控制台程序和键盘输入的 InputStream 流。通常当数据通过命令行参数或者配置文件传递给命令行 Java 程序的时候,System.in 并不是很常用。图形界面程序通过界面传递参数给程序,这是一块单独的 Java IO 输入机制。

【示例】重定向 System.out 输出流

import java.io.*;
public class SystemOutDemo {

    public static void main(String args[]) throws Exception {
        OutputStream out = new FileOutputStream("d:\\test.txt");
        PrintStream ps = new PrintStream(out);
        System.setOut(ps);
        System.out.println("人生若只如初见,何事秋风悲画扇");
        ps.close();
        out.close();
    }
}

【示例】重定向 System.err 输出流

public class SystemErrDemo {

    public static void main(String args[]) throws IOException {
        OutputStream bos = new ByteArrayOutputStream();        // 实例化
        PrintStream ps = new PrintStream(bos);        // 实例化
        System.setErr(ps);    // 输出重定向
        System.err.print("此处有误");
        System.out.println(bos);    // 输出内存中的数据
    }
}

【示例】System.in 接受控制台输入信息

import java.io.*;
public class SystemInDemo {

    public static void main(String args[]) throws IOException {
        InputStream input = System.in;
        StringBuffer buf = new StringBuffer();
        System.out.print("请输入内容:");
        int temp = 0;
        while ((temp = input.read()) != -1) {
            char c = (char) temp;
            if (c == '\n') {
                break;
            }
            buf.append(c);
        }
        System.out.println("输入的内容为:" + buf);
        input.close();
    }
}

Scanner

Scanner 可以获取用户的输入,并对数据进行校验

【示例】校验输入数据是否格式正确

import java.io.*;
public class ScannerDemo {

    public static void main(String args[]) {
        Scanner scan = new Scanner(System.in);    // 从键盘接收数据
        int i = 0;
        float f = 0.0f;
        System.out.print("输入整数:");
        if (scan.hasNextInt()) {    // 判断输入的是否是整数
            i = scan.nextInt();    // 接收整数
            System.out.println("整数数据:" + i);
        } else {
            System.out.println("输入的不是整数!");
        }

        System.out.print("输入小数:");
        if (scan.hasNextFloat()) {    // 判断输入的是否是小数
            f = scan.nextFloat();    // 接收小数
            System.out.println("小数数据:" + f);
        } else {
            System.out.println("输入的不是小数!");
        }

        Date date = null;
        String str = null;
        System.out.print("输入日期(yyyy-MM-dd):");
        if (scan.hasNext("^\\d{4}-\\d{2}-\\d{2}$")) {    // 判断
            str = scan.next("^\\d{4}-\\d{2}-\\d{2}$");    // 接收
            try {
                date = new SimpleDateFormat("yyyy-MM-dd").parse(str);
            } catch (Exception e) {}
        } else {
            System.out.println("输入的日期格式错误!");
        }
        System.out.println(date);
    }
}

输出:

输入整数:20
整数数据:20
输入小数:3.2
小数数据:3.2
输入日期(yyyy-MM-dd):1988-13-1
输入的日期格式错误!
null

网络编程

关键词:SocketServerSocketDatagramPacketDatagramSocket

网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。

java.net 包中提供了低层次的网络通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。

java.net 包中提供了两种常见的网络协议的支持:

  • TCP - TCP 是传输控制协议的缩写,它保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP/ IP。
  • UDP - UDP 是用户数据报协议的缩写,一个无连接的协议。提供了应用程序之间要发送的数据的数据包。

Socket 和 ServerSocket

套接字(Socket)使用 TCP 提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。

Java 通过 Socket 和 ServerSocket 实现对 TCP 的支持。Java 中的 Socket 通信可以简单理解为:java.net.Socket 代表客户端,java.net.ServerSocket 代表服务端,二者可以建立连接,然后通信。

以下为 Socket 通信中建立建立的基本流程:

  • 服务器实例化一个 ServerSocket 对象,表示服务器绑定一个端口。
  • 服务器调用 ServerSocketaccept() 方法,该方法将一直等待,直到客户端连接到服务器的绑定端口(即监听端口)。
  • 服务器监听端口时,客户端实例化一个 Socket 对象,指定服务器名称和端口号来请求连接。
  • Socket 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。
  • 在服务器端,accept() 方法返回服务器上一个新的 Socket 引用,该引用连接到客户端的 Socket

连接建立后,可以通过使用 IO 流进行通信。每一个 Socket 都有一个输出流和一个输入流。客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。

TCP 是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送,以下是一些类提供的一套完整的有用的方法来实现 sockets。

ServerSocket

服务器程序通过使用 java.net.ServerSocket 类以获取一个端口,并且监听客户端请求连接此端口的请求。

ServerSocket 构造方法

ServerSocket 有多个构造方法:

方法 描述
ServerSocket() 创建非绑定服务器套接字。
ServerSocket(int port) 创建绑定到特定端口的服务器套接字。
ServerSocket(int port, int backlog) 利用指定的 backlog 创建服务器套接字并将其绑定到指定的本地端口号。
ServerSocket(int port, int backlog, InetAddress address) 使用指定的端口、监听 backlog 和要绑定到的本地 IP 地址创建服务器。
ServerSocket 常用方法

创建非绑定服务器套接字。 如果 ServerSocket 构造方法没有抛出异常,就意味着你的应用程序已经成功绑定到指定的端口,并且侦听客户端请求。

这里有一些 ServerSocket 类的常用方法:

方法 描述
int getLocalPort() 返回此套接字在其上侦听的端口。
Socket accept() 监听并接受到此套接字的连接。
void setSoTimeout(int timeout) 通过指定超时值启用/禁用 SO_TIMEOUT,以毫秒为单位。
void bind(SocketAddress host, int backlog) ServerSocket 绑定到特定地址(IP 地址和端口号)。

Socket

java.net.Socket 类代表客户端和服务器都用来互相沟通的套接字。客户端要获取一个 Socket 对象通过实例化 ,而 服务器获得一个 Socket 对象则通过 accept() 方法 a 的返回值。

Socket 构造方法

Socket 类有 5 个构造方法:

方法 描述
Socket() 通过系统默认类型的 SocketImpl 创建未连接套接字
Socket(String host, int port) 创建一个流套接字并将其连接到指定主机上的指定端口号。
Socket(InetAddress host, int port) 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。
Socket(String host, int port, InetAddress localAddress, int localPort) 创建一个套接字并将其连接到指定远程主机上的指定远程端口。
Socket(InetAddress host, int port, InetAddress localAddress, int localPort) 创建一个套接字并将其连接到指定远程地址上的指定远程端口。

当 Socket 构造方法返回,并没有简单的实例化了一个 Socket 对象,它实际上会尝试连接到指定的服务器和端口。

Socket 常用方法

下面列出了一些感兴趣的方法,注意客户端和服务器端都有一个 Socket 对象,所以无论客户端还是服务端都能够调用这些方法。

方法 描述
void connect(SocketAddress host, int timeout) 将此套接字连接到服务器,并指定一个超时值。
InetAddress getInetAddress() 返回套接字连接的地址。
int getPort() 返回此套接字连接到的远程端口。
int getLocalPort() 返回此套接字绑定到的本地端口。
SocketAddress getRemoteSocketAddress() 返回此套接字连接的端点的地址,如果未连接则返回 null。
InputStream getInputStream() 返回此套接字的输入流。
OutputStream getOutputStream() 返回此套接字的输出流。
void close() 关闭此套接字。

Socket 通信示例

服务端示例:

public class HelloServer {

    public static void main(String[] args) throws Exception {
        // Socket 服务端
        // 服务器在 8888 端口上监听
        ServerSocket server = new ServerSocket(8888);
        System.out.println("服务器运行中,等待客户端连接。");
        // 得到连接,程序进入到阻塞状态
        Socket client = server.accept();
        // 打印流输出最方便
        PrintStream out = new PrintStream(client.getOutputStream());
        // 向客户端输出信息
        out.println("hello world");
        client.close();
        server.close();
        System.out.println("服务器已向客户端发送消息,退出。");
    }

}

客户端示例:

public class HelloClient {

    public static void main(String[] args) throws Exception {
        // Socket 客户端
        Socket client = new Socket("localhost", 8888);
        InputStreamReader inputStreamReader = new InputStreamReader(client.getInputStream());
        // 一次性接收完成
        BufferedReader buf = new BufferedReader(inputStreamReader);
        String str = buf.readLine();
        buf.close();
        client.close();
        System.out.println("客户端接收到服务器消息:" + str + ",退出");
    }

}

DatagramSocket 和 DatagramPacket

Java 通过 DatagramSocketDatagramPacket 实现对 UDP 协议的支持。

  • DatagramPacket:数据包类
  • DatagramSocket:通信类

UDP 服务端示例:

public class UDPServer {

    public static void main(String[] args) throws Exception { // 所有异常抛出
        String str = "hello World!!!";
        DatagramSocket ds = new DatagramSocket(3000); // 服务端在 3000 端口上等待服务器发送信息
        DatagramPacket dp =
            new DatagramPacket(str.getBytes(), str.length(), InetAddress.getByName("localhost"), 9000); // 所有的信息使用 buf 保存
        System.out.println("发送信息。");
        ds.send(dp); // 发送信息出去
        ds.close();
    }

}

UDP 客户端示例:

public class UDPClient {

    public static void main(String[] args) throws Exception { // 所有异常抛出
        byte[] buf = new byte[1024]; // 开辟空间,以接收数据
        DatagramSocket ds = new DatagramSocket(9000); // 客户端在 9000 端口上等待服务器发送信息
        DatagramPacket dp = new DatagramPacket(buf, 1024); // 所有的信息使用 buf 保存
        ds.receive(dp); // 接收数据
        String str = new String(dp.getData(), 0, dp.getLength()) + "from " + dp.getAddress().getHostAddress() + ":"
            + dp.getPort();
        System.out.println(str); // 输出内容
    }

}

InetAddress

InetAddress 类表示互联网协议 (IP) 地址。

没有公有的构造函数,只能通过静态方法来创建实例。

InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);

URL

可以直接从 URL 中读取字节流数据。

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

    URL url = new URL("http://www.baidu.com");

    /* 字节流 */
    InputStream is = url.openStream();

    /* 字符流 */
    InputStreamReader isr = new InputStreamReader(is, "utf-8");

    /* 提供缓存功能 */
    BufferedReader br = new BufferedReader(isr);

    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }

    br.close();
}

参考资料