您的位置:首页 > 其它

深入研究Netty框架之ByteBuf功能原理及源码分析

2016-09-04 00:00 826 查看
摘要: 主要介绍ByteBuf的功能原理、ByteBuf动态扩展、ByteBuf主要API等。

ByteBuf功能原理

ByteBuf是一个byte数组的缓冲区,通过两个位置指针完成缓冲区的读写操作,读操作使用readerIndex,写操作使用writeIndex。

readerIndex和writeIndex初始取值均为0,写入数据,writeIndex增加;读取数据则readerIndex增加。0~readerIndex之间的数据是已经读取的,调用discardReadBytes()可释放这部分空间,其作用类似于JDK ByteBuffer的compact()方法;readerIndex~writeIndex之间的数据是可读取的,等价于ByteBuffer position和limit之间的数据;writeIndex和capacity之间的空间是可写入的,等价于ByteBuffer limit和capacity之间的可用空间;调用clear()可重置readerIndex和writeIndex为0,但该操作不会清理buffer中的内容。

初始分配的ByteBuf:

+-------------------------------------------------------+
|                writable bytes                         |
+-------------------------------------------------------+
|                                                       |
0 = readerIndex = writerIndex                       capacity

写入N个字节后的ByteBuf:

+-------------------------------------+------------------+
|       readable bytes                |  writable bytes  |
|       (CONTENT)                     |                  |
+-------------------------------------+------------------+
|                                     |                  |
0 = readerIndex                N = writerIndex    <=  capacity

读取M(<=N)个字节后的ByteBuf:

+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
|                   |     (CONTENT)    |                  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0          M  = readerIndex  <=    N = writerIndex  <= capacity

调用discardReadBytes()方法之后的ByteBuf:

+-------------------+---------------------+
|   readable bytes  |  writable bytes     |
+-------------------+---------------------+
|                   |                     |
0 = readerIndex   N-M = writerIndex  <= capacity

调用clear()方法之后的ByteBuf:

+-------------------------------------------------------+
|                writable bytes                         |
+-------------------------------------------------------+
|                                                       |
0 = readerIndex = writerIndex                       capacity


ByteBuf 动态扩展

通常情况下,当对JDK ByteBuffer进行put操作时,如果缓冲区可写空间不够,就会抛出BufferOverflowException异常。为了避免这个问题,在进行put操作时,需要对可写空间进行判断,如果剩余可写空间不足,需要创建一个新ByteBuffer,并将之前ByteBuffer的内容复制到新创建的ByteBuffer中,然后释放老的ByteBuffer。

//needSize为需要写入的字节数
if(this.buffer.remaining()<needSize){
int realAllocateSize=needSize>128 ? needSize:128;
ByteBuffer newBuffer=ByteBuffer.allocate(this.buffer.capacity()+realAllocateSize);
this.buffer.flip();
newBuffer.put(this.buffer);
this.buffer=newBuffer;
}

为防止ByteBuffer溢出,每次进行put操作都需要进行可写空间校验,这导致了代冗余。

为了解决这个问题,ByteBuf对write方法进行了封装,由write操作负责进行剩余可用空间的校验,当空间不足时,由ByteBuf自动进行动态扩展(不超过maxCapacity),使用者无需关心底层的校验和动态扩展细节。

源码如下:

@Override
public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length) {
ensureWritable(length);
setBytes(writerIndex, src, srcIndex, length);
writerIndex += length;
return this;
}

当执行writeBytes时,先调用ensureWritable(length)进行可写空间的校验。

@Override
public ByteBuf ensureWritable(int minWritableBytes) {
if (minWritableBytes < 0) {
throw new IllegalArgumentException(String.format(
"minWritableBytes: %d (expected: >= 0)", minWritableBytes));
}

if (minWritableBytes <= writableBytes()) {
return this;
}

if (minWritableBytes > maxCapacity - writerIndex) {
throw new IndexOutOfBoundsException(String.format(
"writerIndex(%d) + minWritableBytes(%d) exceeds maxCapacity(%d): %s",
writerIndex, minWritableBytes, maxCapacity, this));
}

// Normalize the current capacity to the power of 2.
int newCapacity = calculateNewCapacity(writerIndex + minWritableBytes);

// Adjust to the new capacity.
capacity(newCapacity);
return this;
}

当需要写入的字节数大于缓冲区最大可写字节数时,ByteBuf自动进行动态扩展。calculateNewCapacity(writerIndex + minWritableBytes)方法用于计算缓冲区新的容量,capacity(newCapacity)则用于实现动态扩展,后面会详细介绍其源码。

ByteBuf 主要API

顺序读操作(read)

方法名称返回值功能说明抛出异常
readBoolean()boolean从readerIndex开始读取1字节的数据
throws IndexOutOfBoundsException
readableBytes<1
readByte()byte从readerIndex开始读取1字节的数据throws IndexOutOfBoundsException
readableBytes<1
readUnsignedByte()short从readerIndex开始读取1字节的数据(无符号字节值)throws IndexOutOfBoundsException:
readableBytes<1
readShort()short从readerIndex开始读取16位的短整形值throws IndexOutOfBoundsException:
readableBytes<2
readUnsignedShort()int从readerIndex开始读取16位的无符号短整形值throws IndexOutOfBoundsException:
readableBytes<2
readMedium()int从readerIndex开始读取24位的整形值,(该类型并非java基本类型,通常不用)throws IndexOutOfBoundsException:
readableBytes<3
readUnsignedMedium()int从readerIndex开始读取24位的无符号整形值,(该类型并非java基本类型,通常不用)throws IndexOutOfBoundsException:
readableBytes<3
readInt()int从readerIndex开始读取32位的整形值throws IndexOutOfBoundsException:
readableBytes<4
readUnsignedInt()long从readerIndex开始读取32位的无符号整形值throws IndexOutOfBoundsException:
readableBytes<4
readLong()long从readerIndex开始读取64位的整形值throws IndexOutOfBoundsException:
readableBytes<8
readChar()char从readerIndex开始读取2字节的字符值throws IndexOutOfBoundsException:
readableBytes<2
readFloat()float从readerIndex开始读取32位的浮点值throws IndexOutOfBoundsException:
readableBytes<4
readDouble()double从readerIndex开始读取64位的浮点值throws IndexOutOfBoundsException:
readableBytes<8
readBytes(int length)ByteBuf将当前ByteBuf中的数据读取到新创建的ByteBuf中,从readerIndex开始读取length字节的数据。返回的ByteBuf readerIndex 为0,writeIndex为length。
throws IndexOutOfBoundsException:
readableBytes<length
readSlice(int length)ByteBuf返回当前ByteBuf新创建的子区域,子区域和原ByteBuf共享缓冲区的内容,但独立维护自己的readerIndex和writeIndex,新创建的子区域readerIndex 为0,writeIndex为length。throws IndexOutOfBoundsException:
readableBytes<length
readBytes(ByteBuf dst)ByteBuf将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从当前ByteBuf readerIndex开始读取,直到目标ByteBuf无可写空间,从目标ByteBuf writeIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=读取的字节数。目标ByteBuf的writeIndex+=读取的字节数。
throws IndexOutOfBoundsException:
this.readableBytes<dst.writableBytes
readBytes(ByteBuf dst, int length)ByteBuf将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从当前ByteBuf readerIndex开始读取,长度为length,从目标ByteBuf writeIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=length,目标ByteBuf的writeIndex+=lengththrows IndexOutOfBoundsException:
this.readableBytes<length or
dst.writableBytes<length
readBytes(ByteBuf dst, int dstIndex, int length)ByteBuf将当前ByteBuf中的数据读取到目标ByteBuf (dst)中,从readerIndex开始读取,长度为length,从目标ByteBuf dstIndex开始写入数据。读取完成后,当前ByteBuf的readerIndex+=length,目标ByteBuf的writeIndex+=lengththrows IndexOutOfBoundsException:
dstIndex<0 or
this.readableBytes<length or
dst.capacity<dstIndex + length
readBytes(byte[] dst)ByteBuf将当前ByteBuf中的数据读取到byte数组dst中,从当前ByteBuf readerIndex开始读取,读取长度为dst.length,从byte数组dst索引0处开始写入数据。throws IndexOutOfBoundsException:
this.readableBytes<dst.length
readBytes(byte[] dst, int dstIndex, int length)ByteBuf将当前ByteBuf中的数据读取到byte数组dst中,从当前ByteBuf readerIndex开始读取,读取长度为length,从byte数组dst索引dstIndex处开始写入数据。throws IndexOutOfBoundsException:
dstIndex<0 or
this.readableBytes<length or
dst.length<dstIndex + length
readBytes(ByteBuffer dst)ByteBuf将当前ByteBuf中的数据读取到ByteBuffer dst中,从当前ByteBuf readerIndex开始读取,直到dst的位置指针到达ByteBuffer 的limit。读取完成后,当前ByteBuf的readerIndex+=dst.remaining()throws IndexOutOfBoundsException:
this.readableBytes<dst.remaining()
readBytes(OutputStream out, int length)ByteBuf将当前ByteBuf readerIndex读取数据到输出流OutputStream中,读取的字节长度为lengththrows IndexOutOfBoundsException:
this.readableBytes<length
throws IOException
readBytes(GatheringByteChannel out, int length)int将当前ByteBuf readerIndex读取数到GatheringByteChannel 中,写入out的最大字节长度为length。GatheringByteChannel为非阻塞Channel,调用其write方法不能够保存将全部需要写入的数据均写入成功,存在半包问题。因此其写入的数据长度为【0,length】,如果操作成功,readerIndex+=实际写入的字节数,返回实际写入的字节数throws IndexOutOfBoundsException:
this.readableBytes<length
throws IOException

顺序写操作(write)

方法名称返回值功能说明抛出异常
writeBoolean(boolean value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=1
throws IndexOutOfBoundsException:
this.writableBytes<1
writeByte(int value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=1throws IndexOutOfBoundsException:
this.writableBytes<1
writeShort(int value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=2throws IndexOutOfBoundsException:
this.writableBytes<2
writeMedium(int value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=3throws IndexOutOfBoundsException:
this.writableBytes<3
writeInt(int value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=4throws IndexOutOfBoundsException:
this.writableBytes<4
writeLong(long value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=8throws IndexOutOfBoundsException:
this.writableBytes<8
writeChar(int value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=2throws IndexOutOfBoundsException:
this.writableBytes<2
writeFloat(float value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=4throws IndexOutOfBoundsException:
this.writableBytes<4
writeDouble(double value)ByteBuf将value写入到当前ByteBuf中。写入成功,writeIndex+=8throws IndexOutOfBoundsException:
this.writableBytes<8
writeBytes(ByteBuf src)ByteBuf将源ByteBuf src中从readerIndex开始的所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.readableBytesthrows IndexOutOfBoundsException:
this.writableBytes<src.readableBytes
writeBytes(ByteBuf src, int length)ByteBuf将源ByteBuf src中从readerIndex开始,长度length的可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=lengththrows IndexOutOfBoundsException:
this.writableBytes<length or
src.readableBytes<length
writeBytes(ByteBuf src, int srcIndex, int length)ByteBuf将源ByteBuf src中从srcIndex开始,长度length的可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=lengththrows IndexOutOfBoundsException:
srcIndex<0 or
this.writableBytes<length or
src.capacity<srcIndex + length
writeBytes(byte[] src)ByteBuf将源字节数组src中所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.lengththrows IndexOutOfBoundsException:
this.writableBytes<src.length
writeBytes(byte[] src, int srcIndex, int length)ByteBuf将源字节数组src中srcIndex开始,长度为length可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=lengththrows IndexOutOfBoundsException:
srcIndex<0 or
this.writableBytes<src.length or
src.length<srcIndex + length
writeBytes(ByteBuffer mignsrc)ByteBuf将源ByteBuffer src中所有可读字节写入到当前ByteBuf。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=src.remaining()throws IndexOutOfBoundsException:
this.writableBytes<src.remaining()
writeBytes(InputStream in, int length)int将源InputStream in中的内容写入到当前ByteBuf,写入的最大长度为length,实际写入的字节数可能少于length。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=实际写入的字节数。返回实际写入的字节数throws IndexOutOfBoundsException:
this.writableBytes<length
writeBytes(ScatteringByteChannel in, int length)int将源ScatteringByteChannel in中的内容写入到当前ByteBuf,写入的最大长度为length,实际写入的字节数可能少于length。从当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=实际写入的字节数。返回实际写入的字节数throws IndexOutOfBoundsException:
this.writableBytes<length
writeZero(int length)ByteBuf将当前缓冲区的内容填充为NUL(0x00),当前ByteBuf writeIndex写入数据。写入成功,writeIndex+=lengththrows IndexOutOfBoundsException:
this.writableBytes<length

readerIndex 和 writeIndex

调用ByteBuf的read操作时,从readerIndex开始读取数据,调用ByteBuf的write操作时,从writeIndex开始写入数据,readerIndex和writeInde关系如下:

+-------------------+------------------+------------------+
| discardable bytes |  readable bytes  |  writable bytes  |
|                   |     (CONTENT)    |                  |
+-------------------+------------------+------------------+
|                   |                  |                  |
0      <=      readerIndex   <=   writerIndex    <=    capacity

方法名称返回值功能说明抛出异常
readerIndex()int返回当前ByteBuf的readerIndex
readerIndex(int readerIndex)ByteBuf修改当前ByteBuf的readerIndexthrows IndexOutOfBoundsException
this.writerIndex<readerIndex
writerIndex()int返回当前ByteBuf的writeIndex
writerIndex(int writerIndex)ByteBuf修改当前ByteBuf的writeIndexthrows IndexOutOfBoundsException
writeIndex<this.readerIndex or
this.capacity<writerIndex
readableBytes()int获取当前ByteBuf的可读字节数
this.writerIndex -this.readerIndex
writableBytes()int获取当前ByteBuf的可写字节数
this.capacity - this.writerIndex
setIndex(int readerIndex, int writerIndex)ByteBuf快捷设置当前ByteBuf的readerIndex和writerIndexthrows IndexOutOfBoundsException
readerIndex<0 or
this.writerIndex<readerIndex or
this.capacity<writerIndex
skipBytes(int length)ByteBuf更新当前ByteBuf的readerIndex,更新后将跳过length字节的数据读取。throws IndexOutOfBoundsException
this.readableBytes<length

释放空间和clear操作

方法名称返回值功能说明
discardReadBytes()ByteBuf释放0到readerIndex之间已经读取的空间;同时复制readerIndex和writerIndex之间的数据到0到writerIndex-readerIndex之间;修改readerIndex和writerIndex的值。该操作会发生字节数据的内存复制,频繁调用会导致性能下降。此外,相比其他java对象,缓冲区的分配和释放是个耗时的操作,缓冲区的动态扩张需要进行进行字节数据的复制,也是耗时的操作,因此应尽量提高缓冲区的重用率
discardSomeReadBytes()ByteBuf功能和discardReadBytes()相似,不同之处在于可定制要释放的空间,依赖于具体实现
clear()ByteBuf与JDK 的ByteBuffer clear操作相同,该操作不会清空缓冲区内容本身,其主要是为了操作位置指针,将readerIndex和writerIndex重置为0

mark和rest

当对缓冲区进行读写操作时,可能需要对之前的操作进行回滚。ByteBuf可通过调用mark操作将当前的位置指针备份到mark变量中,调用rest操作后,重新将指针的当前位置恢复为备份在mark变量的值。ByteBuf主要有以下相关方法:

markReaderIndex():将当前的readerIndex备份到markedReaderIndex中;

resetReaderIndex():将当前的readerIndex重置为markedReaderIndex的值;

markWriterIndex() :将当前的writerIndex备份到markedWriterIndex中;

resetWriterIndex():将当前的writerIndex重置为markedWriterIndex的值;

相关源码:

@Override
public ByteBuf markReaderIndex() {
markedReaderIndex = readerIndex;
return this;
}

@Override
public ByteBuf resetReaderIndex() {
readerIndex(markedReaderIndex);
return this;
}

@Override
public ByteBuf markWriterIndex() {
markedWriterIndex = writerIndex;
return this;
}

@Override
public ByteBuf resetWriterIndex() {
writerIndex = markedWriterIndex;
return this;
}


查找操作

方法名称返回值功能说明抛出异常
indexOf(int fromIndex, int toIndex, byte value)int从当前ByteBuf中查找首次出现value的位置,fromIndex<=查找范围<toIndex;查找成功返回位置索引,否则返回-1
bytesBefore(byte value)int从当前ByteBuf中查找首次出现value的位置,readerIndex<=查找范围<writerIndex;查找成功返回位置索引,否则返回-1
bytesBefore(int length, byte value)int从当前ByteBuf中查找首次出现value的位置,readerIndex<=查找范围<readerIndex+length;查找成功返回位置索引,否则返回-1IndexOutOfBoundsException:
this.readableBytes<length
bytesBefore(int index, int length, byte value)int从当前ByteBuf中查找首次出现value的位置,index<=查找范围<index+length;查找成功返回位置索引,否则返回-1IndexOutOfBoundsException:
this.readableBytes<index+length
forEachByte(ByteBufProcessor processor);int 遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从readerIndex开始遍历直到writerIndex。如果满足条件,返回位置索引,否则返回-1
forEachByte(int index, int length, ByteBufProcessor processor)遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从index开始遍历直到index+length。如果满足条件,返回位置索引,否则返回-1
forEachByteDesc(ByteBufProcessor processor)逆序遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从writerIndex-1开始遍历直到readerIndex。如果满足条件,返回位置索引,否则返回-1
forEachByteDesc(int index, int length, ByteBufProcessor processor)逆序遍历当前ByteBuf的可读字节数组,与ByteBufProcessor中设置的查找条件进行比对,从index+length-1开始遍历直到index。如果满足条件,返回位置索引,否则返回-1

Buffer视图

Derived Buffers类似于数据库视图,ByteBuf提供了多个接口用于创建某个ByteBuf的视图或者复制ByteBuf。主要操作如下:

方法名称返回值功能说明
duplicate()ByteBuf返回当前ByteBuf的复制对象,复制后的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。
copy()ByteBuf从当前ByteBuf复制一个新的ByteBuf对象,复制的新对象缓冲区的内容和索引均是独立的。该操作不修改原ByteBuf的readerIndex和writerIndex。(复制readerIndex到writerIndex之间的内容,其他属性与原ByteBuf相同,如maxCapacity,ByteBufAllocator)
copy(int index, int length)ByteBuf从当前ByteBuf 指定索引index开始,字节长度为length,复制一个新的ByteBuf对象,复制的新对象缓冲区的内容和索引均是独立的。该操作不修改原ByteBuf的readerIndex和writerIndex。(其他属性与原ByteBuf相同,,如maxCapacity,ByteBufAllocator)
slice()ByteBuf返回当前ByteBuf的可读子区域,起始位置从readerIndex到writerIndex,返回的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。返回ByteBuf对象的长度为readableBytes()
slice(int index, int length)ByteBuf返回当前ByteBuf的可读子区域,起始位置从index到index+length,返回的ByteBuf对象与当前ByteBuf对象共享缓冲区的内容,但是维护自己独立的readerIndex和writerIndex。该操作不修改原ByteBuf的readerIndex和writerIndex。返回ByteBuf对象的长度为length

转换为JDK ByteBuffer

当通过NIO的SocketChannel进行网络读写时,操作的对象为JDK的ByteBuffer,因此须在接口层支持netty ByteBuf到JDK的ByteBuffer的相互转换。

方法名称返回值功能说明抛出异常
nioBuffer()ByteBuffer将当前ByteBuf的可读缓冲区(readerIndex到writerIndex之间的内容)转换为ByteBuffer,两者共享共享缓冲区的内容。对ByteBuffer的读写操作不会影响ByteBuf的读写索引。注意:ByteBuffer无法感知ByteBuf的动态扩展操作。ByteBuffer的长度为readableBytes()UnsupportedOperationException
nioBuffer(int index, int length)ByteBuffer将当前ByteBuf的可读缓冲区(index到index+length之间的内容)转换为ByteBuffer,两者共享共享缓冲区的内容。对ByteBuffer的读写操作不会影响ByteBuf的读写索引。注意:ByteBuffer无法感知ByteBuf的动态扩展操作。ByteBuffer的长度为lengthUnsupportedOperationException

随机读写(set和get)

除顺序读写之外,ByteBuf还支持随机读写,其最大的区别在于可随机指定读写的索引位置。

关于随机读写的API这里不再详述。无论set或get,执行前都会进行索引和长度的合法性验证,此外,set操作不同于write的是不支持动态扩展。部分源码:

@Override
public ByteBuf getBytes(int index, byte[] dst) {
getBytes(index, dst, 0, dst.length);
return this;
}
//
@Override
public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) {
checkDstIndex(index, length, dstIndex, dst.length);
System.arraycopy(array, index, dst, dstIndex, length);
return this;
}

protected final void checkDstIndex(int index, int length, int dstIndex, int dstCapacity) {
checkIndex(index, length);
if (dstIndex < 0 || dstIndex > dstCapacity - length) {
throw new IndexOutOfBoundsException(String.format(
"dstIndex: %d, length: %d (expected: range(0, %d))", dstIndex, length, dstCapacity));
}
}

protected final void checkIndex(int index, int fieldLength) {
ensureAccessible();
if (fieldLength < 0) {
throw new IllegalArgumentException("length: " + fieldLength + " (expected: >= 0)");
}
if (index < 0 || index > capacity() - fieldLength) {
throw new IndexOutOfBoundsException(String.format(
"index: %d, length: %d (expected: range(0, %d))", index, fieldLength, capacity()));
}
}

@Override
public ByteBuf setByte(int index, int value) {
checkIndex(index);
_setByte(index, value);
return this;
}
//索引合法性验证
protected final void checkIndex(int index) {
ensureAccessible();
if (index < 0 || index >= capacity()) {
throw new IndexOutOfBoundsException(String.format(
"index: %d (expected: range(0, %d))", index, capacity()));
}
}
//确认ByteBuf对象可访问,引用计数器不为0
protected final void ensureAccessible() {
if (refCnt() == 0) {
throw new IllegalReferenceCountException(0);
}
}
//UnpooledHeapByteBuf 实现
@Override
protected void _setByte(int index, int value) {
array[index] = (byte) value;
}


关于ByteBuf继承结构请阅读:深入研究Netty框架之ByteBuf家族

欢迎指出本文有误的地方,转载请注明原文出处https://my.oschina.net/7001/blog/742236
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息