From 36867b57dbf4bc3b7ea16f7f1fd4536ce07faf59 Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Thu, 26 Mar 2026 22:05:28 +0200 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B8=D0=B2=D0=B5=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20Stream=20=D0=BA=20=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=B4=D0=B0=D1=80=D1=82=D1=83,=20=D0=BD=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D1=80=D0=B5=D0=B7=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B8=20=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F,?= =?UTF-8?q?=20Unsigned=20=D0=B7=D0=BD=D0=B0=D1=87=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/io/orprotocol/Stream.java | 241 ++++++++++++++++-------- 1 file changed, 167 insertions(+), 74 deletions(-) diff --git a/src/main/java/io/orprotocol/Stream.java b/src/main/java/io/orprotocol/Stream.java index c796ad9..17ed755 100644 --- a/src/main/java/io/orprotocol/Stream.java +++ b/src/main/java/io/orprotocol/Stream.java @@ -2,6 +2,17 @@ package io.orprotocol; import java.util.Arrays; + +/** + * Это класс для чтения и записи данных в виде потока байтов. + * Он предоставляет методы для записи и чтения различных типов данных, таких как целые числа, + * строки и массивы байтов. Поток может быть расширен динамически при записи данных, и он обеспечивает + * эффективное управление битами для оптимального использования пространства. Этот класс используется в + * протоколе для сериализации и десериализации пакетов данных, которые передаются между клиентами и сервером. + * + * writeInt<> - это методы для записи signed чисел (с учетом знака), которые используют дополнительный бит для хранения информации о знаке числа. + * writeUInt<> - это методы для записи unsigned чисел (без учета знака), которые используют все биты для хранения значения числа. + */ public class Stream { private byte[] stream; @@ -28,54 +39,82 @@ public class Stream { this.writePointer = this.stream.length << 3; } - // Fast path: write 9 bits (sign + value) in <= 2 byte writes - public void writeInt8(int value) { - 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; + public void writeByte(byte b){ + writeUInt8(b); } - // Fast path: read 9 bits (sign + value) from <= 2 bytes - public int readInt8() { - if (remainingBits() < 9L) { - throw new IllegalStateException("Not enough bits to read Int8"); + public byte readByte() { + return (byte) readUInt8(); + } + + 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; - int byteIndex = rp >> 3; - int bitOffset = rp & 7; + public void writeUInt16(int value) { + reserveBits(16); + writeUInt8(value >> 8); + writeUInt8(value & 0xFF); + } - int firstCount = 8 - bitOffset; - int secondCount = 9 - firstCount; + public int readUInt16() { + int high = readUInt8(); + int low = readUInt8(); + return (high << 8) | low; + } - int firstMask = (1 << firstCount) - 1; - int firstPart = stream[byteIndex] & firstMask; - int secondPart = ((stream[byteIndex + 1] & 0xFF) >> (8 - secondCount)); + public void writeUInt32(int value) { + reserveBits(32); + writeUInt16((int) (value >> 16)); + writeUInt16((int) (value & 0xFFFF)); + } - int packed9 = (firstPart << secondCount) | secondPart; - readPointer = rp + 9; + public int readUInt32() { + int high = readUInt16(); + int low = readUInt16(); + return (high << 16) | low; + } - int negationBit = (packed9 >> 8) & 1; - int int8Value = packed9 & 0xFF; - return negationBit == 1 ? -int8Value : int8Value; + public void writeUInt64(long value) { + reserveBits(64); + 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) { @@ -103,36 +142,27 @@ public class Stream { } public void writeInt16(int value) { - writeInt8(value >> 8); - writeInt8(value & 0xFF); + writeUInt16(value); } public int readInt16() { - int value = readInt8() << 8; - return value | readInt8(); + return (short) readUInt16(); } public void writeInt32(int value) { - writeInt16(value >> 16); - writeInt16(value & 0xFFFF); + writeUInt32(value); } public int readInt32() { - int value = readInt16() << 16; - return value | readInt16(); + return readUInt32(); } public void writeInt64(long value) { - int high = (int) (value >> 32); - int low = (int) value; - writeInt32(high); - writeInt32(low); + writeUInt64(value); } public long readInt64() { - long high = readInt32(); - long low = readInt32() & 0xFFFFFFFFL; - return (high << 32) | low; + return readUInt64(); } public void writeFloat32(float value) { @@ -145,77 +175,114 @@ public class Stream { return Float.intBitsToFloat(floatValue); } + /** + * Записывает строку в поток. Сначала записывается длина строки (как unsigned 32-битное целое), а затем записываются символы строки, которые кодируются как 16-битные значения. + * @param value строка для записи в поток. Если строка null, она будет записана как пустая строка. Длина строки ограничена максимальным значением unsigned 32-битного целого. + */ public void writeString(String value) { if (value == null) { value = ""; } + /** + * Пишем длину строки как unsigned 32-битное целое, чтобы при чтении знать, сколько символов нужно прочитать для восстановления строки + */ int length = value.length(); - writeInt32(length); + writeUInt32(length); if (length == 0) { 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++) { - writeInt16(value.charAt(i)); + writeUInt16(value.charAt(i)); } } + /** + * Читает строку из потока. Сначала читается длина строки (как unsigned 32-битное целое), а затем читаются символы строки, которые кодируются как 16-битные значения. + * Если длина отрицательная или если в потоке недостаточно битов для чтения указанного количества символов, возвращается пустая строка. + * @return строка, прочитанная из потока + */ public String readString() { - int length = readInt32(); + /** + * Читаем длину как UInt32 + */ + int length = readUInt32(); if (length < 0) { return ""; } - long requiredBits = (long) length * 18L; + long requiredBits = (long) length * 16L; if (requiredBits > remainingBits()) { - return ""; + throw new IllegalStateException("Not enough bits to read string"); } StringBuilder value = new StringBuilder(length); for (int i = 0; i < length; i++) { - value.append((char) readInt16()); + /** + * Читаем символы строки как UInt16 и добавляем их в StringBuilder. Каждый символ кодируется 16 битами, + * что позволяет поддерживать широкий диапазон символов Unicode. + */ + value.append((char) readUInt16()); } return value.toString(); } + /** + * Записывает массив байтов в поток. + * Сначала записывается длина массива (как unsigned 32-битное целое), + * а затем записываются сами байты. Если массив null, он будет записан как массив нулевой длины. + * @param value массив байтов для записи в поток + */ public void writeBytes(byte[] value) { if (value == null) { value = new byte[0]; } - - writeInt32(value.length); + /** + * Пишем длинну массива как unsigned 32-битное целое, чтобы при чтении знать, сколько байтов нужно прочитать для восстановления массива. + */ + writeUInt32(value.length); 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) { - writeInt8(b); + writeByte(b); } } + /** + * Читает массив байтов из потока. Сначала читается длина массива (как unsigned 32-битное целое), + * а затем читаются сами байты. Если длина отрицательная или если в потоке недостаточно битов для чтения указанного количества байтов, возвращается пустой массив. + * @return массив байтов, прочитанный из потока + */ public byte[] readBytes() { - int length = readInt32(); + int length = readUInt32(); - if (length < 0) { + if (length <= 0) { return new byte[0]; } - long requiredBits = (long) length * 9L; + long requiredBits = (long) length * 8L; if (requiredBits > remainingBits()) { return new byte[0]; } byte[] value = new byte[length]; for (int i = 0; i < length; i++) { - value[i] = (byte) readInt8(); + value[i] = readByte(); } return value; } @@ -223,9 +290,16 @@ public class Stream { public boolean isEmpty() { return writePointer == 0; } - - // useful bytes without reserved tail + + /** + * Возвращает количество реально использованного места (то есть только используемые байты, а не весь буфер включая аллокации) + * @return количество реально использованных байтов + */ public int length() { + /** + * Так как writePointer - это индекс в битах, то для получения количества байтов нужно разделить его на 8. + * Но так как writePointer может быть не кратен 8, то нужно округлить его в большую сторону, чтобы учесть все использованные биты. + */ return (writePointer + 7) >> 3; } @@ -233,10 +307,24 @@ public class Stream { return getStream(); } + /** + * Возвращает количество оставшихся битов для чтения. Это разница между указателем записи и указателем чтения, + * которая показывает, сколько битов еще доступно для чтения в потоке. + * @return количество оставшихся битов для чтения + */ private long remainingBits() { return (long) writePointer - readPointer; } + /** + * Резервирует место в потоке для записи указанного количества битов. Этот метод проверяет, достаточно ли места в текущем буфере для записи новых данных, + * и если нет, то расширяет буфер до необходимого размера. + * Это позволяет избежать переполнения, а вызывающему коду не беспокоится о дополнительных аллокациях при записи данных в поток. + * @param bitsToWrite количество битов, которые нужно зарезервировать для записи + * @internal Этот метод используется внутри класса Stream для управления размером внутреннего массива байтов, + * который хранит данные потока. Он гарантирует, что при записи данных в поток всегда будет достаточно места для хранения + * этих данных, и при необходимости расширяет буфер. + */ private void reserveBits(long bitsToWrite) { if (bitsToWrite <= 0) { return; @@ -255,6 +343,11 @@ public class Stream { ensureCapacity((int) byteIndexLong); } + /** + * Выделяет нужное количество места в массиве байтов для записи данных, если текущий размер массива недостаточен. + * @param byteIndex индекс байта, который нужно зарезервировать для записи данных + * @internal Этот метод используется внутри класса Stream для управления размером внутреннего массива байтов, который хранит данные потока. + */ private void ensureCapacity(int byteIndex) { int requiredSize = byteIndex + 1; if (requiredSize <= stream.length) {