• CoCo   2014/7/14 13:34:00
  • 开源搜索引擎框架Lucene.net学习之-InputStream
  • 关键字: Lucene.net 搜索引擎 搜索框架
  • InputStream这个类在Java基本类库里就有,但是Lucene选择了自己来实现,翻译到dotnet版本,名称保持没变。InputStream实现了ICloneable接口,就是能支持拷贝出新对象。

    代码2-1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
            public virtual System.Object Clone()
            {
                InputStream clone = null;
                try
                {
                    clone = (InputStream) this.MemberwiseClone();
                }
                catch (System.Exception e)
                {
                    throw new Exception("Can't clone InputStream.", e);
                }
                if (buffer != null)
                {
                    clone.buffer = new byte[BUFFER_SIZE];
                    Array.Copy(buffer, 0, clone.buffer, 0, bufferLength);
                }
                clone.chars = null;
                return clone;
            }
     

    从2-1代码可以看出,显示定义了一个InputStream 变量,然后用MemberwiseClone方法拷贝一个浅表副本,然后,判断如果buffer 内有数据,就把数据拷贝到浅副本里。而这个过程呢,实际上就是深拷贝。从中可以看出,浅拷贝就是克隆出新对象,但是不带数据,深拷贝呢就是浅副本带上数据。

     
    InputStream 类有一个静态构造函数,给BUFFER_SIZE赋了一下值,这个值是默认buffer的大小。由OutputStream类提供,初始值是1024大小。下来在看看每个方法是干嘛的。

    代码 2-2

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    private void  Refill()
    {
        long start = bufferStart + bufferPosition;
        long end = start + BUFFER_SIZE;
        if (end > length)
        // don't read past EOF
            end = length;
        bufferLength = (int) (end - start);
        if (bufferLength == 0)
            throw new System.IO.IOException("read past EOF");
        if (buffer == null)
            buffer = new byte[BUFFER_SIZE]; // allocate buffer lazily
        ReadInternal(buffer, 0, bufferLength);
        bufferStart = start;
        bufferPosition = 0;
    }

    要理解Refill方法一定要看看另外三个变量。

    1
    2
    3
    private long bufferStart = 0; // position in file of buffer
    private int bufferLength = 0; // end of valid bytes
    private int bufferPosition = 0; // next byte to read

    从注释上可以看到,bufferStart 代表文件缓冲区的位置,bufferLength 代表文件缓冲区的结束点,bufferPosition 表示当前读取到文件缓冲区的位置。而Refill方法也只在ReadByte方法中被使用了。

    代码2-3

    1
    2
    3
    4
    5
    6
    public byte ReadByte()
    {
        if (bufferPosition >= bufferLength)
            Refill();
        return buffer[bufferPosition++];
    }

    可以看出,是当读取的指针达到了文件缓冲区的最大长度,才调用了Refill方法。大体上就可以猜测,Refill是用来把下一段数据载入缓冲区的。Refill为什么要在第一个语句中计算

    long start = bufferStart + bufferPosition;

    这个start 变量到底代表什么呢?InputStream类有一个自身的缓冲区域,private byte[] buffer;在静态构造函数中设置了这个区域的大小。而用这个类去读取文件,文件的大小一般都是大于1024字节,所以,InputStream每次读取最多1024个字节的话,在InputStream读取文件的方式就是不连续读取的,是一段一段的载入的,这个start就代表了当前读取到了文件流的位置。long end = start + BUFFER_SIZE;语句则是计算出缓冲区域相对于文件流的位置。然后判断当前缓冲区相对于文件流位置如果大于length,那么就做一个调整。在InputStream的子类RAMInputStream中,可以看到length代表了文件的总长度。这个判断就是为了限制不会超出。

    bufferLength = (int) (end - start);计算出了当前缓冲区域的长度。因为读入的字节如果不满1024字节的话,实际上在任何地方都没有对buffer进行清空,因此,只能用这种方式来处理在读取的字节数小于缓冲区大小的时候,防止读取超出文件实际长度。接着用到一个ReadInternal方法,这个方法在InputStream类中是抽象方法。等讲到子类的时候再来看。

    看完了Refill方法,继续关注一下ReadByte,可以看到ReadByte方法相对于是做遍历操作的,只要调用ReadByte方法,就是获取下一个字节。

    代码2-4

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public void  ReadBytes(byte[] b, int offset, int len)
        {
            if (len < BUFFER_SIZE)
            {
                for (int i = 0; i < len; i++)
                // read byte-by-byte
                    b[i + offset] = (byte) ReadByte();
            }
            else
            {
                // read all-at-once
                long start = GetFilePointer();
                SeekInternal(start);
                ReadInternal(b, offset, len);
                bufferStart = start + len; // adjust stream variables
                bufferPosition = 0;
                bufferLength = 0; // trigger refill() on read
            }
        }

    而ReadBytes方法则是一次读取指定数量的字节。如果读取的字节小于缓冲区域,则按字节读取,而如果超出了,则会先计算当前缓冲区相对文件缓冲区的位置。

    代码 2-5

    1
    2
    3
    4
    public long GetFilePointer()
    {
        return bufferStart + bufferPosition;
    }

    然后SeekInternal方法也是一个抽象方法,是把读取位置跳转到指定的位置。然后用抽象方法ReadInternal实际读取。读取出的b变量因为是引用类型,所以值直接就发生了变化。

    bufferLength = 0; // trigger refill() on read

    把bufferLength 设置为0,则ReadByte方法一定会触发refill方法,这个注释也是这么写的。

    代码2-6

    1
    2
    3
    4
    public int ReadInt()
    {
        return ((ReadByte() & 0xFF) << 24) | ((ReadByte() & 0xFF) << 16) | ((ReadByte() & 0xFF) << 8) | (ReadByte() & 0xFF);
    }

    ReadInt方法,用读出的每个字节和0xFF做与操作。这个用法前面也介绍到过,在.net中实际上是无必要的,这是Java和dotnet的差异造成的,翻译过来的时候译者可能并未深究。事实上在Java中也只需要 0x7F就够了,不需要0xFF。

    假如ReadByte得出的值是 byte b = 244 那么 做了与操作之后还是244。左移24位,244的2进制是1111 1010,就是在后面加 24个0,位数就变成了32位。后面的操作和这个类似,做了或操作,相当于是把ReadByte读取到的4个byte值,排列得到一个数字。因为数字是32位,所以只要这个byte值是大于127的,都会产生负数。因为int类型的最高位是代表符号的,127的2进制是0111 1111,左移24位,最高位是0,而如果是128,就会变成1111 1111   1000 0000,自然就变成了负数了。这个方法读取了4个字节。

    代码2-7

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public int ReadVInt()
    {
        byte b = ReadByte();
        int i = b & 0x7F;
        for (int shift = 7; (b & 0x80) != 0; shift += 7)
        {
            b = ReadByte();
            i |= (b & 0x7F) << shift;
        }
        return i;
    }

    和ReadInt相比,运算的方式和ReadInt相似,但也有差别,每次位移是7位。


    代码2-8

    1
    2
    3
    4
    public long ReadLong()
    {
        return (((long) ReadInt()) << 32) | (ReadInt() & 0xFFFFFFFFL);
    }

    ReadLong方法读取了8个字节。对比看看ReadLong方法和ReadVInt方法有什么区别。ReadVInt方法每次位移的是7位的整数倍,那是因为

    int i = b & 0x7F;
    操作得到的值不会大于127,而这个值得2进制是0111 1111,把最高位的0省略,那么就是7个1。这样第一次没有位移,而第二次位移7位,相当于在第一次读取字节二进制的高位加上了第二读取的字节。一直循环N次,加上第一次,就是N+1次。而ReadLong则是和ReadInt方法一样的,把后面得到的字节放在低位。ReadVInt方法相当于把ReadLong方法的高位和低位调换了,这在系统中比较常见,Socket就是这么做的,做过游戏外挂的肯定都知道这个的。

    代码 2- 9

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public long ReadVLong()
    {
        byte b = ReadByte();
        long i = b & 0x7F;
        for (int shift = 7; (b & 0x80) != 0; shift += 7)
        {
            b = ReadByte();
            i |= (b & 0x7FL) << shift;
        }
        return i;
    }

    对比以上的理解就很容易理解ReadVLong方法了,和ReadVInt类似。ReadVLong,ReadVInt循环中的条件是(b & 0x80) != 0,0x80就是 1000 0000,所以一旦读取到b是1000 0000以上的数值,这个循环就终止了。也就是当b >= 128 。当然在java版本中和这个有差异,因为java的byte是 -128 - 127,所以,byte的最高位是符号,那么这个地方就是小于0的意思。

    代码 2- 10

    1
    2
    3
    4
    5
    6
    7
    8
    public System.String ReadString()
        {
            int length = ReadVInt();
            if (chars == null || length > chars.Length)
                chars = new char[length];
            ReadChars(chars, 0, length);
            return new System.String(chars, 0, length);
        }

    ReadString方法,在第一次运行时候,会设置为整个文件的长度,然后读取出了整个文件的字符。

    代码 2-11

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public void  ReadChars(char[] buffer, int start, int length)
        {
            int end = start + length;
            for (int i = start; i < end; i++)
            {
                byte b = ReadByte();
                if ((b & 0x80) == 0)