Java 网络编程之 IO 流

2017/01/02 Java 网络编程

网络程序做的很大一部分工作就是将数据从 A 读取,然后写到 B。

java 的 I/O 建立在 stream(流)之上。提供了 三种类型的流:

  1. 输入/输出流
  2. 过滤流
  3. 面向字符的流

不同的输入输出流读取/写入到不同数据文件中; 然后通过过滤流进行额外的操作,比如加密、压缩、转换为其他类型的数据; 最后面向字符的 Reader 和 Writer 可以允许程序读/写文本,而不是字节,这将需要指定不同的编码字符集。

流是同步的,也就是说会阻塞当前线程。 Java 还支持非阻塞 IO (使用通道和缓冲区),它在高吞吐量的环境中(Web 服务器)要快得多。

字节流

InputStream 是输入流的基类,read() 方法会阻塞当前线程,从输入流源中读取一个字节,然后返回一个 0~255 的数返回,如果返回 -1 表示结束。

返回的是一个 int,最终要的是 byte,需要强转:

public static void read(InputStream in) throws IOException {
    byte[] bytes = new byte[1024];
    for (int i = 0; i < bytes.length; i++) {
        int b = in.read();
        if (b == -1) break;
        bytes[i] = (byte) b;
    }
}

与一次只写入一个字节一样,一次只读取一个字节,效率不高

因此 InputStream 提供了多个重载方法,提供一个数组,让读取多个字节尝试填充到这个数组中

读取多个字节,可能会因为网络、读取出错导致读取不完整,需要重复去读;由于读取的方法会返回读取的字节数,可以判断返回的字节数和期望的是否一致,是否需要重新读取:

public static void readBuffered(InputStream in) throws IOException {
    int bytesRead = 0;
    int bytesToRead = 1024;

    byte[] input = new byte[bytesToRead];
    while (bytesRead < bytesToRead) {
        bytesRead += in.read(input, bytesRead, bytesToRead - bytesRead);
    }
}

含多个参数的 read() 方法在没有数据时会返回 0,但流没有关闭;流结束时都会返回 -1。

上面的读取代码有一个 bug,应该先判断是否结束,否则可能出现无限循环。

public static void readBuffered(InputStream in) throws IOException {
    int bytesRead = 0;
    int bytesToRead = 1024;

    byte[] input = new byte[bytesToRead];
    while (bytesRead < bytesToRead) {
        int result = in.read(input, bytesRead, bytesToRead - bytesRead);
        if (result == -1) break;    //流结束
        bytesRead += result;
    }
}

调用 close() 关闭流,这会释放这个流关联的所有资源,比如句柄和端口。一旦输入流关闭,进一步读取会导致 IOException。

标记和重置

mark 方法表示这次读到哪儿了,下次可以调用 reset() 回到标记的位置开始读取。

public void mark(int readAheadLimit)
public void reset() throws IOException
public boolean markSupported()

不是所有输入流都支持 mark 和 reset,我们需要先调用它的 markSupported() 检测一下,返回 true 才可以进行标记。

java.io 中只有两个类支持标记:

  • BufferedInputStream
  • ByteArrayInputStream

其他流需要先串联到缓冲的输入流才支持标记。

过滤器流

过滤器以链的形式进行组织。

BufferedInputStream 有一个作为缓冲区的字节数组,名为 buf,调用 read() 时,先去缓冲区请求数据,没数据时采取底层读取; 同时从源中读取尽可能多的数据存入缓冲区中,方便以后调用时读取。

BufferedOutputStream 没有任何新方法,区别在于写入时会把数据放在缓冲区中,当满了或者主动刷新时才会发射。

System.out 是一个 PrintStream

不建议使用 PrintStream,原因有三:

  1. 它的 println() 会在每一行后加上平台相关的行分隔符,这会导致另外一个平台读取时发生错误
  2. 编码方式也是跟平台本地化相关,不同国家的编码方式不同
  3. 吞掉了所有异常,不方便做异常处理

DataOutputStream 中各种数据类型占的字节数:

  • byte: 1
  • boolean: 1
  • char: 2
  • short: 2
  • int: 4
  • long: 8
  • float: 4
  • double: 8

DataInputStream 还有个 readFully() 发你发,可以重复的从底层输入流读取数据,知道读取所请求的字节数为止,如果不能读取到足够的数据,会抛出异常,所以需要提前知道要读取多少字节。

比如从 HTTP 首部读取 Content-length 字段,就能知道有多少字节的数据。

//DataInputStream.readFully()
public final void readFully(byte b[], int off, int len) throws IOException {
    if (len < 0)
        throw new IndexOutOfBoundsException();
    int n = 0;
    while (n < len) {
        int count = in.read(b, off + n, len - n);    //重复的读取
        if (count < 0)
            throw new EOFException();    //和指定的长度不符,就会抛出异常,这需要我们提前知道要读取多少字节
        n += count;
    }
}

DataInputStream.readLine() 被废弃了,因为它有 2 个 bug:

  1. 无法准确地将非 ASCII 码字符转为字节
  2. 无法准确地将一个回车识别为结束符 (即无法准确结束读取)
    • 文件读取时还好,主要是在持久网络连接时会有问题,无法识别为结束会导致超时、永远挂起

字符流

Reader 和 Writer 使用 Unicode 字符 而不是字节来进行读/写。

  • InputStreamReader 类中包含一个底层输入流,从中读取原始字节,然后以特定的解码形式将它们转为 Unicode 字符;
  • OutputStreamWriter 从运行的程序中接收 Unicode 字符,然后使用指定的编码方式将他们转为字节,然后写入到底层输出流中

java.io 包中还提供了几个 Reader 和 Writer 的实现类,它们不需要底层读写流就可以工作:

  • FileReader, FileWriter
  • StringReader, StringWriter
  • CharArrayReader, CharArrayWriter

Writer

Writerjava.io.OutputStream 的映射,用于向字符流写入,抽象类。

public abstract class Writer implements Appendable, Closeable, Flushable {

    //临时的缓冲区,保存字符串和单字符
    private char[] writeBuffer;
    //缓冲区的大小,默认 1K
    private static final int WRITE_BUFFER_SIZE = 1024;
    //对这个流的同步锁对象,为了效率使用一个对象而不是自己做锁,那样子类可以使用这个对象,而不是需要使用当前对象
    protected Object lock;

    //默认以当前对象作为同步对象
    protected Writer() {
        this.lock = this;
    }

    protected Writer(Object lock) {
        if (lock == null) {
            throw new NullPointerException();
        }
        this.lock = lock;
    }

    public void write(int c) throws IOException {
        synchronized (lock) {
            if (writeBuffer == null){
                writeBuffer = new char[WRITE_BUFFER_SIZE];
            }
            writeBuffer[0] = (char) c;
            write(writeBuffer, 0, 1);
        }
    }


    public void write(char cbuf[]) throws IOException {
        write(cbuf, 0, cbuf.length);
    }


    abstract public void write(char cbuf[], int off, int len) throws IOException;

    public void write(String str) throws IOException {
        write(str, 0, str.length());
    }

    public void write(String str, int off, int len) throws IOException {
        synchronized (lock) {
            char cbuf[];
            if (len <= WRITE_BUFFER_SIZE) {
                if (writeBuffer == null) {
                    writeBuffer = new char[WRITE_BUFFER_SIZE];
                }
                cbuf = writeBuffer;
            } else {    // Don't permanently allocate very large buffers.
                cbuf = new char[len];
            }
            str.getChars(off, (off + len), cbuf, 0);
            write(cbuf, 0, len);
        }
    }
   public Writer append(CharSequence csq) throws IOException {
        if (csq == null)
            write("null");
        else
            write(csq.toString());
        return this;
    }

    public Writer append(CharSequence csq, int start, int end) throws IOException {
        CharSequence cs = (csq == null ? "null" : csq);
        write(cs.subSequence(start, end).toString());
        return this;
    }

    public Writer append(char c) throws IOException {
        write(c);
        return this;
    }

    //将缓冲区中的字符都直接写出去
    abstract public void flush() throws IOException;

    //在 flush 之后调用,一旦 close,再 flush,就会抛出异常
    abstract public void close() throws IOException;

}

几种 write() 方法分别用于写入不同类型的数据。

具体的输出取决于编码方式。

  • Mac, Linux 上默认是 UTF-8 编码
  • Windows 会根据国家和配置改变

OutputStreamWriterWriter 最重要的子类,参数中接受一个输出流,另一个参数指定编码方式

public OutputStreamWriter(OutputStream out, String charsetName)
    throws UnsupportedEncodingException{
    super(out);
    if (charsetName == null)
        throw new NullPointerException("charsetName");
    se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
}

此外它还有一个方法返回编码方式:

public String getEncoding() {
    return se.getEncoding();
}

Reader

Readerjava.io.InputStream 的镜像,也是一个抽象类。

read() 方法会将读取的 Unicode 字符字符数作为一个 int 返回,范围是 0 ~ 65535,或者在流结束时返回 -1。

Reader 有一个 ready() 方法,表示这个流是否准备好被读取:

public boolean ready() throws IOException {    //当下一个 read() 方法不会被阻塞时,就会返回 true
    return false;
}

其他与 InputStream 类似。

InputStreamReaderReader 最重要的一个子类,读取字符并解码,实现了 Reader 的关键方法 read(), ready(), close():

public class InputStreamReader extends Reader {

    private final StreamDecoder sd;    //解码器

    public InputStreamReader(InputStream in) {
        super(in);
        try {
            sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
        } catch (UnsupportedEncodingException e) {
            // The default encoding should always be available
            throw new Error(e);
        }
    }

    public InputStreamReader(InputStream in, String charsetName)
        throws UnsupportedEncodingException
    {
        super(in);
        if (charsetName == null)
            throw new NullPointerException("charsetName");
        sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
    }

    public InputStreamReader(InputStream in, Charset cs) {
        super(in);
        if (cs == null)
            throw new NullPointerException("charset");
        sd = StreamDecoder.forInputStreamReader(in, this, cs);
    }

    public InputStreamReader(InputStream in, CharsetDecoder dec) {
        super(in);
        if (dec == null)
            throw new NullPointerException("charset decoder");
        sd = StreamDecoder.forInputStreamReader(in, this, dec);
    }

    //返回这个流的字符编码方式
    public String getEncoding() {
        return sd.getEncoding();
    }

    //读取单个字符,返回读取的字符
    public int read() throws IOException {
        return sd.read();
    }


    //读取字符到一个字符数组中,返回读取的字符数
    public int read(char cbuf[], int offset, int length) throws IOException {
        return sd.read(cbuf, offset, length);
    }

    public boolean ready() throws IOException {
        return sd.ready();
    }

    public void close() throws IOException {
        sd.close();
    }
}

可以看到,读取单个字符时,read() 返回的是读取的字符;有字符数组作为参数时,读取的数据存到了字符数组中,返回的是读取的字符数。

Filter Reader and Filter Writer

InputStreamReaderOutputStreamWriter 相当于输入流、输出流之上的装饰器,将面向字节的接口转为面向字符的接口。

然后 过滤 Reader 和 Writer 就可以基于这个面向字符的接口进行附加操作了,可以完成过滤操作的子类:

  • BufferedReader, BufferedWriter
  • PushbackReader
  • PrintWriter

BufferedReader, BufferedWriter 是基于字符的,作用类似 BufferedInputStream,都是为了提高效率,使用内部字符数组作为缓冲区。

读取时会先从缓冲区读,然后再去底层流;写入时,会先写到缓冲区,当缓冲区填满或者主动刷新时,才会写到底层,这比直接写入也要快一些。

public static String readBufferedReader(InputStream in) throws IOException {
    Reader r = new InputStreamReader(in, "UTF-8");
    r = new BufferedReader(r, 1024);
    StringBuilder sb = new StringBuilder();
    int c;
    while ((c = r.read()) != -1) {
        sb.append((char) c);
    }
    return sb.toString();
}

现在上面的代码你明白什么意思了吧,参数是一个底层输入流,第一个 InputStreamReader 将参数转为面向字符流,然后 BufferedReader 将使用缓冲区来进行读,这样直接操作最上层,效率会快很多。

因为 BufferedReader 也继承自 Reader,所以直接将 r 指向 BufferedReader 对象,也不会影响后面的代码。

BufferedReader 有一个 readLine() 方法,用于读取一行,它建议使用,因为我们可以给它指定字符集,而不是使用平台默认的。 BufferedWriter 新增一个 newLine() 方法,输出一个与平台相关的行分隔符。

\n: UNIX 系统行末结束符 \n\r: window 系统行末结束符 \r: MAC OS 系统行末结束符 大多数情况下,需要的结束符都是 回车/换行对 \r\n

PrintWriter

PrintWriter 用于取代 PrintSteam,它可以正确处理多字符字符集和国际化文本(其实就是处理好字符编码格式)。

Search

    Table of Contents