Приведение Stream к стандарту, нормальная резервация и сериализация, Unsigned значения
This commit is contained in:
@@ -2,6 +2,17 @@ package io.orprotocol;
|
|||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Это класс для чтения и записи данных в виде потока байтов.
|
||||||
|
* Он предоставляет методы для записи и чтения различных типов данных, таких как целые числа,
|
||||||
|
* строки и массивы байтов. Поток может быть расширен динамически при записи данных, и он обеспечивает
|
||||||
|
* эффективное управление битами для оптимального использования пространства. Этот класс используется в
|
||||||
|
* протоколе для сериализации и десериализации пакетов данных, которые передаются между клиентами и сервером.
|
||||||
|
*
|
||||||
|
* writeInt<> - это методы для записи signed чисел (с учетом знака), которые используют дополнительный бит для хранения информации о знаке числа.
|
||||||
|
* writeUInt<> - это методы для записи unsigned чисел (без учета знака), которые используют все биты для хранения значения числа.
|
||||||
|
*/
|
||||||
public class Stream {
|
public class Stream {
|
||||||
|
|
||||||
private byte[] stream;
|
private byte[] stream;
|
||||||
@@ -28,54 +39,82 @@ public class Stream {
|
|||||||
this.writePointer = this.stream.length << 3;
|
this.writePointer = this.stream.length << 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: write 9 bits (sign + value) in <= 2 byte writes
|
public void writeByte(byte b){
|
||||||
public void writeInt8(int value) {
|
writeUInt8(b);
|
||||||
int negationBit = value < 0 ? 1 : 0;
|
|
||||||
int int8Value = Math.abs(value) & 0xFF;
|
|
||||||
int packed9 = (negationBit << 8) | int8Value; // 9 bits total
|
|
||||||
|
|
||||||
reserveBits(9);
|
|
||||||
|
|
||||||
int wp = writePointer;
|
|
||||||
int byteIndex = wp >> 3;
|
|
||||||
int bitOffset = wp & 7; // 0..7
|
|
||||||
|
|
||||||
int firstCount = 8 - bitOffset; // bits into current byte
|
|
||||||
int secondCount = 9 - firstCount; // bits into next byte (1..8)
|
|
||||||
|
|
||||||
int firstPart = packed9 >> secondCount; // fits firstCount bits
|
|
||||||
stream[byteIndex] |= (byte) firstPart;
|
|
||||||
|
|
||||||
int secondMask = (1 << secondCount) - 1;
|
|
||||||
int secondPart = (packed9 & secondMask) << (8 - secondCount);
|
|
||||||
stream[byteIndex + 1] |= (byte) secondPart;
|
|
||||||
|
|
||||||
writePointer = wp + 9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fast path: read 9 bits (sign + value) from <= 2 bytes
|
public byte readByte() {
|
||||||
public int readInt8() {
|
return (byte) readUInt8();
|
||||||
if (remainingBits() < 9L) {
|
}
|
||||||
throw new IllegalStateException("Not enough bits to read Int8");
|
|
||||||
|
public void writeUInt8(int value) {
|
||||||
|
reserveBits(8);
|
||||||
|
int byteIndex = writePointer >> 3;
|
||||||
|
stream[byteIndex] = (byte) (value & 0xFF);
|
||||||
|
writePointer += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int readUInt8() {
|
||||||
|
if (remainingBits() < 8L) {
|
||||||
|
throw new IllegalStateException("Not enough bits to read UInt8");
|
||||||
}
|
}
|
||||||
|
int byteIndex = readPointer >> 3;
|
||||||
|
int value = stream[byteIndex] & 0xFF;
|
||||||
|
readPointer += 8;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
int rp = readPointer;
|
public void writeUInt16(int value) {
|
||||||
int byteIndex = rp >> 3;
|
reserveBits(16);
|
||||||
int bitOffset = rp & 7;
|
writeUInt8(value >> 8);
|
||||||
|
writeUInt8(value & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
int firstCount = 8 - bitOffset;
|
public int readUInt16() {
|
||||||
int secondCount = 9 - firstCount;
|
int high = readUInt8();
|
||||||
|
int low = readUInt8();
|
||||||
|
return (high << 8) | low;
|
||||||
|
}
|
||||||
|
|
||||||
int firstMask = (1 << firstCount) - 1;
|
public void writeUInt32(int value) {
|
||||||
int firstPart = stream[byteIndex] & firstMask;
|
reserveBits(32);
|
||||||
int secondPart = ((stream[byteIndex + 1] & 0xFF) >> (8 - secondCount));
|
writeUInt16((int) (value >> 16));
|
||||||
|
writeUInt16((int) (value & 0xFFFF));
|
||||||
|
}
|
||||||
|
|
||||||
int packed9 = (firstPart << secondCount) | secondPart;
|
public int readUInt32() {
|
||||||
readPointer = rp + 9;
|
int high = readUInt16();
|
||||||
|
int low = readUInt16();
|
||||||
|
return (high << 16) | low;
|
||||||
|
}
|
||||||
|
|
||||||
int negationBit = (packed9 >> 8) & 1;
|
public void writeUInt64(long value) {
|
||||||
int int8Value = packed9 & 0xFF;
|
reserveBits(64);
|
||||||
return negationBit == 1 ? -int8Value : int8Value;
|
writeUInt32((int) (value >> 32));
|
||||||
|
writeUInt32((int) (value & 0xFFFFFFFFL));
|
||||||
|
}
|
||||||
|
|
||||||
|
public long readUInt64() {
|
||||||
|
long high = readUInt32();
|
||||||
|
long low = readUInt32();
|
||||||
|
return (high << 32) | low;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Записывает signed 8-битное целое в классическом two's complement (ровно 8 бит).
|
||||||
|
*/
|
||||||
|
public void writeInt8(int value) {
|
||||||
|
writeUInt8(value); // берутся младшие 8 бит
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Читает signed 8-битное целое в классическом two's complement.
|
||||||
|
*/
|
||||||
|
public int readInt8() {
|
||||||
|
/**
|
||||||
|
* Приводим к byte чтобы сохранить знак
|
||||||
|
*/
|
||||||
|
return (byte) readUInt8();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeBit(int value) {
|
public void writeBit(int value) {
|
||||||
@@ -103,36 +142,27 @@ public class Stream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void writeInt16(int value) {
|
public void writeInt16(int value) {
|
||||||
writeInt8(value >> 8);
|
writeUInt16(value);
|
||||||
writeInt8(value & 0xFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int readInt16() {
|
public int readInt16() {
|
||||||
int value = readInt8() << 8;
|
return (short) readUInt16();
|
||||||
return value | readInt8();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeInt32(int value) {
|
public void writeInt32(int value) {
|
||||||
writeInt16(value >> 16);
|
writeUInt32(value);
|
||||||
writeInt16(value & 0xFFFF);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public int readInt32() {
|
public int readInt32() {
|
||||||
int value = readInt16() << 16;
|
return readUInt32();
|
||||||
return value | readInt16();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeInt64(long value) {
|
public void writeInt64(long value) {
|
||||||
int high = (int) (value >> 32);
|
writeUInt64(value);
|
||||||
int low = (int) value;
|
|
||||||
writeInt32(high);
|
|
||||||
writeInt32(low);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long readInt64() {
|
public long readInt64() {
|
||||||
long high = readInt32();
|
return readUInt64();
|
||||||
long low = readInt32() & 0xFFFFFFFFL;
|
|
||||||
return (high << 32) | low;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeFloat32(float value) {
|
public void writeFloat32(float value) {
|
||||||
@@ -145,77 +175,114 @@ public class Stream {
|
|||||||
return Float.intBitsToFloat(floatValue);
|
return Float.intBitsToFloat(floatValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Записывает строку в поток. Сначала записывается длина строки (как unsigned 32-битное целое), а затем записываются символы строки, которые кодируются как 16-битные значения.
|
||||||
|
* @param value строка для записи в поток. Если строка null, она будет записана как пустая строка. Длина строки ограничена максимальным значением unsigned 32-битного целого.
|
||||||
|
*/
|
||||||
public void writeString(String value) {
|
public void writeString(String value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
value = "";
|
value = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пишем длину строки как unsigned 32-битное целое, чтобы при чтении знать, сколько символов нужно прочитать для восстановления строки
|
||||||
|
*/
|
||||||
int length = value.length();
|
int length = value.length();
|
||||||
writeInt32(length);
|
writeUInt32(length);
|
||||||
|
|
||||||
if (length == 0) {
|
if (length == 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeInt16 -> 2 * writeInt8 -> 18 bits per char
|
/**
|
||||||
reserveBits((long) length * 18L);
|
* Резервируем по 16 бит на каждый символ строки, так как каждый символ кодируется как 16-битное значение
|
||||||
|
*/
|
||||||
|
reserveBits((long) length * 16L);
|
||||||
|
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
writeInt16(value.charAt(i));
|
writeUInt16(value.charAt(i));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Читает строку из потока. Сначала читается длина строки (как unsigned 32-битное целое), а затем читаются символы строки, которые кодируются как 16-битные значения.
|
||||||
|
* Если длина отрицательная или если в потоке недостаточно битов для чтения указанного количества символов, возвращается пустая строка.
|
||||||
|
* @return строка, прочитанная из потока
|
||||||
|
*/
|
||||||
public String readString() {
|
public String readString() {
|
||||||
int length = readInt32();
|
/**
|
||||||
|
* Читаем длину как UInt32
|
||||||
|
*/
|
||||||
|
int length = readUInt32();
|
||||||
|
|
||||||
if (length < 0) {
|
if (length < 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
long requiredBits = (long) length * 18L;
|
long requiredBits = (long) length * 16L;
|
||||||
if (requiredBits > remainingBits()) {
|
if (requiredBits > remainingBits()) {
|
||||||
return "";
|
throw new IllegalStateException("Not enough bits to read string");
|
||||||
}
|
}
|
||||||
|
|
||||||
StringBuilder value = new StringBuilder(length);
|
StringBuilder value = new StringBuilder(length);
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
value.append((char) readInt16());
|
/**
|
||||||
|
* Читаем символы строки как UInt16 и добавляем их в StringBuilder. Каждый символ кодируется 16 битами,
|
||||||
|
* что позволяет поддерживать широкий диапазон символов Unicode.
|
||||||
|
*/
|
||||||
|
value.append((char) readUInt16());
|
||||||
}
|
}
|
||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Записывает массив байтов в поток.
|
||||||
|
* Сначала записывается длина массива (как unsigned 32-битное целое),
|
||||||
|
* а затем записываются сами байты. Если массив null, он будет записан как массив нулевой длины.
|
||||||
|
* @param value массив байтов для записи в поток
|
||||||
|
*/
|
||||||
public void writeBytes(byte[] value) {
|
public void writeBytes(byte[] value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
value = new byte[0];
|
value = new byte[0];
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
writeInt32(value.length);
|
* Пишем длинну массива как unsigned 32-битное целое, чтобы при чтении знать, сколько байтов нужно прочитать для восстановления массива.
|
||||||
|
*/
|
||||||
|
writeUInt32(value.length);
|
||||||
|
|
||||||
if (value.length > 0) {
|
if (value.length > 0) {
|
||||||
// writeInt8 = 9 bits per byte
|
/**
|
||||||
reserveBits((long) value.length * 9L);
|
* Резервируем нужное количество бит для записи массива байт. На один байт нужно 8 бит, поэтому умножаем длину
|
||||||
|
* массива на 8, чтобы получить общее количество бит для резервирования.
|
||||||
|
*/
|
||||||
|
reserveBits((long) value.length * 8L);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (byte b : value) {
|
for (byte b : value) {
|
||||||
writeInt8(b);
|
writeByte(b);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Читает массив байтов из потока. Сначала читается длина массива (как unsigned 32-битное целое),
|
||||||
|
* а затем читаются сами байты. Если длина отрицательная или если в потоке недостаточно битов для чтения указанного количества байтов, возвращается пустой массив.
|
||||||
|
* @return массив байтов, прочитанный из потока
|
||||||
|
*/
|
||||||
public byte[] readBytes() {
|
public byte[] readBytes() {
|
||||||
int length = readInt32();
|
int length = readUInt32();
|
||||||
|
|
||||||
if (length < 0) {
|
if (length <= 0) {
|
||||||
return new byte[0];
|
return new byte[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
long requiredBits = (long) length * 9L;
|
long requiredBits = (long) length * 8L;
|
||||||
if (requiredBits > remainingBits()) {
|
if (requiredBits > remainingBits()) {
|
||||||
return new byte[0];
|
return new byte[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] value = new byte[length];
|
byte[] value = new byte[length];
|
||||||
for (int i = 0; i < length; i++) {
|
for (int i = 0; i < length; i++) {
|
||||||
value[i] = (byte) readInt8();
|
value[i] = readByte();
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
@@ -223,9 +290,16 @@ public class Stream {
|
|||||||
public boolean isEmpty() {
|
public boolean isEmpty() {
|
||||||
return writePointer == 0;
|
return writePointer == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
// useful bytes without reserved tail
|
/**
|
||||||
|
* Возвращает количество реально использованного места (то есть только используемые байты, а не весь буфер включая аллокации)
|
||||||
|
* @return количество реально использованных байтов
|
||||||
|
*/
|
||||||
public int length() {
|
public int length() {
|
||||||
|
/**
|
||||||
|
* Так как writePointer - это индекс в битах, то для получения количества байтов нужно разделить его на 8.
|
||||||
|
* Но так как writePointer может быть не кратен 8, то нужно округлить его в большую сторону, чтобы учесть все использованные биты.
|
||||||
|
*/
|
||||||
return (writePointer + 7) >> 3;
|
return (writePointer + 7) >> 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,10 +307,24 @@ public class Stream {
|
|||||||
return getStream();
|
return getStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Возвращает количество оставшихся битов для чтения. Это разница между указателем записи и указателем чтения,
|
||||||
|
* которая показывает, сколько битов еще доступно для чтения в потоке.
|
||||||
|
* @return количество оставшихся битов для чтения
|
||||||
|
*/
|
||||||
private long remainingBits() {
|
private long remainingBits() {
|
||||||
return (long) writePointer - readPointer;
|
return (long) writePointer - readPointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Резервирует место в потоке для записи указанного количества битов. Этот метод проверяет, достаточно ли места в текущем буфере для записи новых данных,
|
||||||
|
* и если нет, то расширяет буфер до необходимого размера.
|
||||||
|
* Это позволяет избежать переполнения, а вызывающему коду не беспокоится о дополнительных аллокациях при записи данных в поток.
|
||||||
|
* @param bitsToWrite количество битов, которые нужно зарезервировать для записи
|
||||||
|
* @internal Этот метод используется внутри класса Stream для управления размером внутреннего массива байтов,
|
||||||
|
* который хранит данные потока. Он гарантирует, что при записи данных в поток всегда будет достаточно места для хранения
|
||||||
|
* этих данных, и при необходимости расширяет буфер.
|
||||||
|
*/
|
||||||
private void reserveBits(long bitsToWrite) {
|
private void reserveBits(long bitsToWrite) {
|
||||||
if (bitsToWrite <= 0) {
|
if (bitsToWrite <= 0) {
|
||||||
return;
|
return;
|
||||||
@@ -255,6 +343,11 @@ public class Stream {
|
|||||||
ensureCapacity((int) byteIndexLong);
|
ensureCapacity((int) byteIndexLong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Выделяет нужное количество места в массиве байтов для записи данных, если текущий размер массива недостаточен.
|
||||||
|
* @param byteIndex индекс байта, который нужно зарезервировать для записи данных
|
||||||
|
* @internal Этот метод используется внутри класса Stream для управления размером внутреннего массива байтов, который хранит данные потока.
|
||||||
|
*/
|
||||||
private void ensureCapacity(int byteIndex) {
|
private void ensureCapacity(int byteIndex) {
|
||||||
int requiredSize = byteIndex + 1;
|
int requiredSize = byteIndex + 1;
|
||||||
if (requiredSize <= stream.length) {
|
if (requiredSize <= stream.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user