这是《水煮 JDK 源码》系列 的第4篇文章,计划撰写100篇关于JDK源码相关的文章

GZIPInputStream 类位于 java.util.zip 包下,继承于 InflaterInputStream 类,它实现了一个流式过滤器,主要用于读取GZIP文件格式的压缩数据,其UML类图如下:

::: hljs-center

image20220730091637542.png

:::

类声明如下:

public class GZIPInputStream extends InflaterInputStream

1、成员变量

GZIPInputStream 定义了3个成员变量,分别如下:

/** CRC-32 用于未压缩的数据 */
protected CRC32 crc = new CRC32();
/** 表示输入流的结尾状态 */
protected boolean eos;
/** 输入流是否已关闭的状态 */
private boolean closed = false;

2、构造函数

创建 GZIPInputStream 压缩输入流主要有以下的两种方式:

/** 使用默认大小的缓冲区创建新的输入流 */
public GZIPInputStream(InputStream in) throws IOException {
    // 默认缓冲区大小为512
    this(in, 512);
}

/** 使用指定大小的缓冲区创建新的输入流 */
public GZIPInputStream(InputStream in, int size) throws IOException {
    // 调用父类 InflaterInputStream 的构造函数
    super(in, new Inflater(true), size);
    // 设置父类 InflaterInputStream 的 usesDefaultInflater
    // 表示使用默认的解压缩器
    usesDefaultInflater = true;
    // 读取 GZIP 的成员头信息,并返回头信息的总字节数
    readHeader(in);
}

由于 GZIPInputStream 是由于读取压缩数据的输入流,因此需要用到解压缩器 Inflater

3、读取数据方法

GZIPInputStream 主要提供了1个用于读取流数据的方法,如下:

public int read(byte[] buf, int off, int len) throws IOException {
    // 在正式读取流数据之前,要确保流没有被关闭
    ensureOpen();
    // 如果已经到了流的结尾,说明没有可读的数据了,直接返回 -1
    if (eos) {
        return -1;
    }
    // 调用父类 InflaterInputStream 的 read() 方法读取数据
    int n = super.read(buf, off, len);
    // 如果实际读取到的数据为 -1,说明没有可读数据
    if (n == -1) {
        // 读取 GZIP 的成员尾部信息,判断是否读取到了 eos
        // 如果是,则将 eos 置为 true,表示已读取到了尾部
        if (readTrailer())
            eos = true;
        else
            // 否则,继续调用本方法进行读取
            return this.read(buf, off, len);
    } else {
        // 如果读取了数据,则使用指定的字节数据更新 CRC32 的校验和
        crc.update(buf, off, n);
    }
    // 返回实际读取的字节数
    return n;
}

在读取数据之前,需要先检查流是否被关闭,如果流已经被关闭了,说明是不可读的,ensureOpen() 方法就是作此用途,其定义如下:

private void ensureOpen() throws IOException {
    if (closed) {
        // 如果流被关闭了,直接抛出 IOException 异常
        throw new IOException("Stream closed");
    }
}

在创建 GZIPInputStream 输入流的时候,需要去读取 GZIP 的成员头信息,readHeader() 方法定义如下:

/** GZIP 头魔法数 */
public final static int GZIP_MAGIC = 0x8b1f;

/** 文件头标识 */
private final static int FTEXT      = 1;    // Extra text
private final static int FHCRC      = 2;    // Header CRC
private final static int FEXTRA     = 4;    // Extra field
private final static int FNAME      = 8;    // File name
private final static int FCOMMENT   = 16;   // File comment

private int readHeader(InputStream this_in) throws IOException {
    // 创建 CheckedInputStream,用于维护数据的校验和
    CheckedInputStream in = new CheckedInputStream(this_in, crc);
    // 重置 CRC32 校验
    crc.reset();
    // 检查头部魔法数,判断是否为 GZIP 格式
    if (readUShort(in) != GZIP_MAGIC) {
        throw new ZipException("Not in GZIP format");
    }
    // 检查压缩方法,判断是否为支持的压缩方法
    if (readUByte(in) != 8) {
        throw new ZipException("Unsupported compression method");
    }
    // 读取标识
    int flg = readUByte(in);
    // Skip MTIME, XFL, and OS fields
    // 跳过特殊的字段
    skipBytes(in, 6);
    int n = 2 + 2 + 6;
    // Skip optional extra field
    if ((flg & FEXTRA) == FEXTRA) {
        int m = readUShort(in);
        skipBytes(in, m);
        n += m + 2;
    }
    // Skip optional file name
    if ((flg & FNAME) == FNAME) {
        do {
            n++;
        } while (readUByte(in) != 0);
    }
    // Skip optional file comment
    if ((flg & FCOMMENT) == FCOMMENT) {
        do {
            n++;
        } while (readUByte(in) != 0);
    }
    // Check optional header CRC
    if ((flg & FHCRC) == FHCRC) {
        int v = (int)crc.getValue() & 0xffff;
        if (readUShort(in) != v) {
            throw new ZipException("Corrupt GZIP header");
        }
        n += 2;
    }
    // 重置 CRC32 校验
    crc.reset();
    // 返回头信息的总字节数
    return n;
}

在读取数据的时候,需要读取GZIP的尾部信息,并以此来判断是否已读取结束了,readTrailer() 方法如下:

private boolean readTrailer() throws IOException {
    InputStream in = this.in;
    // 获取余下可读的总字节长度,调用的是 Inflater 的 getRemaining 方法
    int n = inf.getRemaining();
    // 如果可读字节数大于0
    if (n > 0) {
        // 创建序列输入流
        in = new SequenceInputStream(
            new ByteArrayInputStream(buf, len - n, n),
            new FilterInputStream(in) {
                public void close() throws IOException {}
            });
    }
    // Uses left-to-right evaluation order
    if ((readUInt(in) != crc.getValue()) ||
        // rfc1952; ISIZE is the input size modulo 2^32
        (readUInt(in) != (inf.getBytesWritten() & 0xffffffffL)))
        throw new ZipException("Corrupt GZIP trailer");

    // If there are more bytes available in "in" or
    // the leftover in the "inf" is > 26 bytes:
    // this.trailer(8) + next.header.min(10) + next.trailer(8)
    // try concatenated case
    if (this.in.available() > 0 || n > 26) {
        int m = 8;                  // this.trailer
        try {
            m += readHeader(in);    // next.header
        } catch (IOException ze) {
            return true;  // ignore any malformed, do nothing
        }
        inf.reset();
        if (n > m)
            inf.setInput(buf, len - n + m, n - m);
        return false;
    }
    return true;
}

不论是在读取头信息或者尾信息的时候,都会去读取指定的标识位长度,比如 readUIntreadUShortreadUByte 方法,分别用于读取无符号整型、无符号短整型、无符号字节数据,其定义如下:

private long readUInt(InputStream in) throws IOException {
    long s = readUShort(in);
    return ((long)readUShort(in) << 16) | s;
}

private int readUShort(InputStream in) throws IOException {
    int b = readUByte(in);
    return (readUByte(in) << 8) | b;
}

private int readUByte(InputStream in) throws IOException {
    // 读取字节数据,获取所读取的总字节数
    int b = in.read();
    // 如果总字节数为 -1,说明已经读完了
    if (b == -1) {
        throw new EOFException();
    }
    if (b < -1 || b > 255) {
        // Report on this.in, not argument in; see read{Header, Trailer}.
        throw new IOException(this.in.getClass().getName()
                              + ".read() returned value out of range -1..255: " + b);
    }
    return b;
}

4、其他方法

在读取压缩数据流的时候,也可以跳过指定的字节数,其方法定义如下:

private byte[] tmpbuf = new byte[128];
/** 跳过输入流中指定长度的字节数据,该方法是阻塞的,直到所有字节都跳过 */
private void skipBytes(InputStream in, int n) throws IOException {
    while (n > 0) {
        int len = in.read(tmpbuf, 0, n < tmpbuf.length ? n : tmpbuf.length);
        if (len == -1) {
            throw new EOFException();
        }
        n -= len;
    }
}

GZIPInputStreamclose() 方法如下:

public void close() throws IOException {
    // 首先判断输入流是否已关闭
    if (!closed) {
        // 如果没有关闭,调用父类的 close 方法对输入流进行关闭
        super.close();
        // 同时设置 eos 为 ture,标识该输入流已结束
        eos = true;
        // 修改已关闭的状态
        closed = true;
    }
}