diff --git a/src/main/java/io/orprotocol/Context.java b/src/main/java/io/orprotocol/Context.java deleted file mode 100644 index 075a3b7..0000000 --- a/src/main/java/io/orprotocol/Context.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.orprotocol; - -public class Context extends Object {} diff --git a/src/main/java/io/orprotocol/Server.java b/src/main/java/io/orprotocol/Server.java index f446c08..92f40d3 100644 --- a/src/main/java/io/orprotocol/Server.java +++ b/src/main/java/io/orprotocol/Server.java @@ -13,11 +13,11 @@ import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; import io.orprotocol.client.Client; +import io.orprotocol.frame.FrameDecoder; import io.orprotocol.index.ClientIndexer; import io.orprotocol.lock.ThreadLocker; import io.orprotocol.packet.Packet; import io.orprotocol.packet.PacketExecutor; -import io.orprotocol.packet.PacketFactory; import io.orprotocol.packet.PacketManager; public class Server extends WebSocketServer { @@ -104,9 +104,9 @@ public class Server extends WebSocketServer { /** * Создаем пакет из полученных байтов. */ - PacketFactory packetFactory = new PacketFactory(bytes, this.packetManager); - Packet packet = packetFactory.createPacket(); - int packetId = packetFactory.getPacketId(); + FrameDecoder frameDecoder = new FrameDecoder(bytes, client, this.packetManager); + Packet packet = frameDecoder.decode(); + int packetId = packet.packetId; /** * Получаем обработчик пакета и вызываем его метод обработки. * @@ -127,7 +127,7 @@ public class Server extends WebSocketServer { if(listener != null && !listener.onPacketReceived(this, client, packet)) { /** * Если слушатель сервера вернул false, пакет не обрабатываем. - */ + */ return; } /** @@ -162,6 +162,13 @@ public class Server extends WebSocketServer { * Если клиент не отправляет heartbeat в указанный интервал, его можно отключить. */ Client client = new Client(socket, this.settings.heartbeatInterval, this); + /** + * Устанавливаем версию клиента + */ + client.setVersion(this.extractVersionFromHandshake(handshake)); + /** + * Получаем адрес клиента + */ String ipAddress = handshake.getFieldValue("X-Forwarded-For"); if (ipAddress == null || ipAddress.isEmpty()) { ipAddress = socket.getRemoteSocketAddress().getAddress().getHostAddress(); @@ -182,6 +189,23 @@ public class Server extends WebSocketServer { } } + /** + * Версия передается в адресной строке при подключении клиента, например ws://.../?v=1, + * если версия не передана - возвращается первая версия по умолчанию (1) + * @param handshake + */ + private int extractVersionFromHandshake(ClientHandshake handshake) { + String versionStr = handshake.getFieldValue("v"); + if(versionStr == null || versionStr.isEmpty()) { + return 1; + } + try { + return Integer.parseInt(versionStr); + } catch (NumberFormatException e) { + return 1; + } + } + @Override public void onStart() { /** diff --git a/src/main/java/io/orprotocol/Stream.java b/src/main/java/io/orprotocol/Stream.java deleted file mode 100644 index 1cf6ba4..0000000 --- a/src/main/java/io/orprotocol/Stream.java +++ /dev/null @@ -1,402 +0,0 @@ -package io.orprotocol; - -import java.util.Arrays; - -/** - * Поток побитовой/побайтовой записи и чтения. - * - * Поддержка: - * - signed: Int8/16/32/64 (классический two's complement) - * - unsigned: UInt8/16/32/64 - * - String: длина UInt32 + символы UInt16 - * - byte[]: длина UInt32 + сырые байты - */ -public class Stream { - - private byte[] stream; - private int readPointer = 0; // bits - private int writePointer = 0; // bits - - public Stream() { - this.stream = new byte[0]; - } - - public Stream(byte[] stream) { - this.stream = (stream == null) ? new byte[0] : stream; - this.readPointer = 0; - this.writePointer = this.stream.length << 3; - } - - public byte[] getStream() { - return Arrays.copyOf(this.stream, length()); - } - - public void setStream(byte[] stream) { - this.stream = (stream == null) ? new byte[0] : stream; - this.readPointer = 0; - this.writePointer = this.stream.length << 3; - } - - public byte[] getBuffer() { - return getStream(); - } - - public boolean isEmpty() { - return writePointer == 0; - } - - /** Количество реально записанных байт (без резервной емкости). */ - public int length() { - return (writePointer + 7) >> 3; - } - - // ---------- bit / boolean ---------- - - public void writeBit(int value) { - writeBits(value & 1L, 1); - } - - public int readBit() { - return (int) readBits(1); - } - - public void writeBoolean(boolean value) { - writeBit(value ? 1 : 0); - } - - public boolean readBoolean() { - return readBit() == 1; - } - - // ---------- byte ---------- - - public void writeByte(byte b) { - writeUInt8(b & 0xFF); - } - - public byte readByte() { - return (byte) readUInt8(); - } - - // ---------- UInt / Int 8 ---------- - - public void writeUInt8(int value) { - int v = value & 0xFF; - - // fast path when byte-aligned - if ((writePointer & 7) == 0) { - reserveBits(8); - stream[writePointer >> 3] = (byte) v; - writePointer += 8; - return; - } - - writeBits(v, 8); - } - - public int readUInt8() { - if (remainingBits() < 8L) { - throw new IllegalStateException("Not enough bits to read UInt8"); - } - - // fast path when byte-aligned - if ((readPointer & 7) == 0) { - int value = stream[readPointer >> 3] & 0xFF; - readPointer += 8; - return value; - } - - return (int) readBits(8); - } - - public void writeInt8(int value) { - writeUInt8(value); // lower 8 bits (two's complement) - } - - public int readInt8() { - return (byte) readUInt8(); // sign extension - } - - // ---------- UInt / Int 16 ---------- - - public void writeUInt16(int value) { - int v = value & 0xFFFF; - writeUInt8((v >>> 8) & 0xFF); - writeUInt8(v & 0xFF); - } - - public int readUInt16() { - int hi = readUInt8(); - int lo = readUInt8(); - return (hi << 8) | lo; - } - - public void writeInt16(int value) { - writeUInt16(value); // lower 16 bits (two's complement) - } - - public int readInt16() { - return (short) readUInt16(); // sign extension - } - - // ---------- UInt / Int 32 ---------- - - public void writeUInt32(long value) { - if (value < 0 || value > 0xFFFFFFFFL) { - throw new IllegalArgumentException("UInt32 out of range: " + value); - } - - writeUInt8((int) ((value >>> 24) & 0xFF)); - writeUInt8((int) ((value >>> 16) & 0xFF)); - writeUInt8((int) ((value >>> 8) & 0xFF)); - writeUInt8((int) (value & 0xFF)); - } - - public long readUInt32() { - long b1 = readUInt8() & 0xFFL; - long b2 = readUInt8() & 0xFFL; - long b3 = readUInt8() & 0xFFL; - long b4 = readUInt8() & 0xFFL; - return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4; - } - - public void writeInt32(int value) { - writeUInt32(value & 0xFFFFFFFFL); - } - - public int readInt32() { - return (int) readUInt32(); - } - - // ---------- UInt / Int 64 ---------- - - /** - * Записывает 64 бита как есть (для UInt64 это битовый паттерн в long). - */ - public void writeUInt64(long value) { - writeUInt8((int) ((value >>> 56) & 0xFF)); - writeUInt8((int) ((value >>> 48) & 0xFF)); - writeUInt8((int) ((value >>> 40) & 0xFF)); - writeUInt8((int) ((value >>> 32) & 0xFF)); - writeUInt8((int) ((value >>> 24) & 0xFF)); - writeUInt8((int) ((value >>> 16) & 0xFF)); - writeUInt8((int) ((value >>> 8) & 0xFF)); - writeUInt8((int) (value & 0xFF)); - } - - /** - * Читает 64 бита как есть (битовый паттерн в long). - */ - public long readUInt64() { - long high = readUInt32() & 0xFFFFFFFFL; - long low = readUInt32() & 0xFFFFFFFFL; - return (high << 32) | low; - } - - public void writeInt64(long value) { - writeUInt64(value); - } - - public long readInt64() { - return readUInt64(); - } - - // ---------- float ---------- - - public void writeFloat32(float value) { - writeInt32(Float.floatToIntBits(value)); - } - - public float readFloat32() { - return Float.intBitsToFloat(readInt32()); - } - - // ---------- string / bytes ---------- - - /** - * String: length(UInt32) + chars(UInt16). - */ - public void writeString(String value) { - if (value == null) { - value = ""; - } - - int length = value.length(); - writeUInt32(length); - - if (length == 0) { - return; - } - - reserveBits((long) length * 16L); - for (int i = 0; i < length; i++) { - writeUInt16(value.charAt(i)); - } - } - - public String readString() { - long lenLong = readUInt32(); - if (lenLong > Integer.MAX_VALUE) { - throw new IllegalStateException("String length too large: " + lenLong); - } - - int length = (int) lenLong; - long requiredBits = (long) length * 16L; - if (requiredBits > remainingBits()) { - throw new IllegalStateException("Not enough bits to read string"); - } - - StringBuilder sb = new StringBuilder(length); - for (int i = 0; i < length; i++) { - sb.append((char) readUInt16()); - } - return sb.toString(); - } - - /** - * byte[]: length(UInt32) + raw bytes. - */ - public void writeBytes(byte[] value) { - if (value == null) { - value = new byte[0]; - } - - writeUInt32(value.length); - - if (value.length == 0) { - return; - } - - reserveBits((long) value.length * 8L); - - // fast path when byte-aligned - if ((writePointer & 7) == 0) { - int byteIndex = writePointer >> 3; - ensureCapacity(byteIndex + value.length - 1); - System.arraycopy(value, 0, stream, byteIndex, value.length); - writePointer += (value.length << 3); - return; - } - - for (byte b : value) { - writeUInt8(b & 0xFF); - } - } - - public byte[] readBytes() { - long lenLong = readUInt32(); - if (lenLong == 0) { - return new byte[0]; - } - if (lenLong > Integer.MAX_VALUE) { - throw new IllegalStateException("Byte array too large: " + lenLong); - } - - int length = (int) lenLong; - long requiredBits = (long) length * 8L; - if (requiredBits > remainingBits()) { - return new byte[0]; - } - - byte[] out = new byte[length]; - - // fast path when byte-aligned - if ((readPointer & 7) == 0) { - int byteIndex = readPointer >> 3; - System.arraycopy(stream, byteIndex, out, 0, length); - readPointer += (length << 3); - return out; - } - - for (int i = 0; i < length; i++) { - out[i] = (byte) readUInt8(); - } - return out; - } - - // ---------- internals ---------- - - private long remainingBits() { - return (long) writePointer - readPointer; - } - - private void writeBits(long value, int bits) { - if (bits <= 0) { - return; - } - - reserveBits(bits); - - for (int i = bits - 1; i >= 0; i--) { - int bit = (int) ((value >>> i) & 1L); - int byteIndex = writePointer >> 3; - int shift = 7 - (writePointer & 7); - - if (bit == 1) { - stream[byteIndex] = (byte) (stream[byteIndex] | (1 << shift)); - } else { - stream[byteIndex] = (byte) (stream[byteIndex] & ~(1 << shift)); - } - - writePointer++; - } - } - - private long readBits(int bits) { - if (bits <= 0) { - return 0; - } - if (remainingBits() < bits) { - throw new IllegalStateException("Not enough bits to read"); - } - - long value = 0; - for (int i = 0; i < bits; i++) { - int bit = (stream[readPointer >> 3] >> (7 - (readPointer & 7))) & 1; - value = (value << 1) | bit; - readPointer++; - } - return value; - } - - private void reserveBits(long bitsToWrite) { - if (bitsToWrite <= 0) { - return; - } - - long lastBitIndex = (long) writePointer + bitsToWrite - 1; - if (lastBitIndex < 0) { - throw new IllegalStateException("Bit index overflow"); - } - - long byteIndexLong = lastBitIndex >> 3; - if (byteIndexLong > Integer.MAX_VALUE) { - throw new IllegalStateException("Stream too large"); - } - - ensureCapacity((int) byteIndexLong); - } - - /** - * Выделяет нужное количество места в массиве байтов для записи данных, если текущий размер массива недостаточен. - * @param byteIndex индекс байта, который нужно зарезервировать для записи данных - * @internal Этот метод используется внутри класса Stream для управления размером внутреннего массива байтов, который хранит данные потока. - */ - private void ensureCapacity(int byteIndex) { - int requiredSize = byteIndex + 1; - if (requiredSize <= stream.length) { - return; - } - - int newSize = (stream.length == 0) ? 32 : stream.length; - while (newSize < requiredSize) { - if (newSize > (Integer.MAX_VALUE / 2)) { - newSize = requiredSize; - break; - } - newSize <<= 1; - } - - stream = Arrays.copyOf(stream, newSize); - } -} \ No newline at end of file diff --git a/src/main/java/io/orprotocol/buffer/Buffer.java b/src/main/java/io/orprotocol/buffer/Buffer.java new file mode 100644 index 0000000..5f44866 --- /dev/null +++ b/src/main/java/io/orprotocol/buffer/Buffer.java @@ -0,0 +1,410 @@ +package io.orprotocol.buffer; + +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * Low-level бинарный буфер. + * + * Формат: + * - Int/UInt: big-endian + * - String: raw UTF-8 bytes (без длины) (убрана длинна) + * - byte[]: raw bytes (без длины) (убрана длинна) + * - boolean: 1 бит (0 или 1) + * - битовые поля: могут быть не выровнены по байтам, пишутся бит за битом + * - при записи буфер автоматически расширяется, при чтении проверяется + */ +public class Buffer { + + private static final int DEFAULT_CAPACITY = 32; + + private byte[] buffer; + private int readBitPos; // позиция чтения в битах + private int writeBitPos; // позиция записи в битах + + public Buffer() { + this.buffer = new byte[0]; + this.readBitPos = 0; + this.writeBitPos = 0; + } + + public Buffer(byte[] data) { + this.buffer = (data == null) ? new byte[0] : Arrays.copyOf(data, data.length); + this.readBitPos = 0; + this.writeBitPos = this.buffer.length << 3; + } + + public static Buffer wrap(byte[] data) { + return new Buffer(data); + } + + public static Buffer allocate(int capacity) { + Buffer buf = new Buffer(); + buf.ensureCapacity(capacity); + return buf; + } + + // ---------- state ---------- + + public boolean hasRemaining() { + return readBitPos < writeBitPos; + } + + public int remaining() { + return (int) ((writeBitPos - readBitPos + 7) >>> 3); + } + + public int length() { + return (writeBitPos + 7) >>> 3; + } + + public boolean isEmpty() { + return writeBitPos == 0; + } + + public byte[] toByteArray() { + return Arrays.copyOf(buffer, length()); + } + + public void resetRead() { + this.readBitPos = 0; + } + + public void clear() { + this.buffer = new byte[0]; + this.readBitPos = 0; + this.writeBitPos = 0; + } + + // ---------- bit / boolean ---------- + + public void writeBit(int value) { + writeBits(value & 1L, 1); + } + + public int readBit() { + return (int) readBits(1); + } + + public void writeBoolean(boolean value) { + writeBit(value ? 1 : 0); + } + + public boolean readBoolean() { + return readBit() != 0; + } + + // ---------- byte ---------- + + public void writeByte(byte value) { + writeUInt8(value & 0xFF); + } + + public byte readByte() { + return (byte) readUInt8(); + } + + // ---------- UInt8 / Int8 ---------- + + public void writeUInt8(int value) { + int v = value & 0xFF; + + if (isWriteByteAligned()) { + ensureCapacityBits(8); + buffer[writeBitPos >>> 3] = (byte) v; + writeBitPos += 8; + return; + } + + writeBits(v, 8); + } + + public int readUInt8() { + ensureReadableBits(8); + + if (isReadByteAligned()) { + int v = buffer[readBitPos >>> 3] & 0xFF; + readBitPos += 8; + return v; + } + + return (int) readBits(8); + } + + public void writeInt8(int value) { + writeUInt8(value); + } + + public int readInt8() { + return (byte) readUInt8(); + } + + // ---------- UInt16 / Int16 ---------- + + public void writeUInt16(int value) { + writeUInt8((value >>> 8) & 0xFF); + writeUInt8(value & 0xFF); + } + + public int readUInt16() { + int hi = readUInt8(); + int lo = readUInt8(); + return (hi << 8) | lo; + } + + public void writeInt16(int value) { + writeUInt16(value); + } + + public int readInt16() { + return (short) readUInt16(); + } + + // ---------- UInt32 / Int32 ---------- + + public void writeUInt32(long value) { + if (value < 0 || value > 0xFFFFFFFFL) { + throw new IllegalArgumentException("UInt32 out of range: " + value); + } + + writeUInt8((int) ((value >>> 24) & 0xFF)); + writeUInt8((int) ((value >>> 16) & 0xFF)); + writeUInt8((int) ((value >>> 8) & 0xFF)); + writeUInt8((int) (value & 0xFF)); + } + + public long readUInt32() { + long b1 = readUInt8() & 0xFFL; + long b2 = readUInt8() & 0xFFL; + long b3 = readUInt8() & 0xFFL; + long b4 = readUInt8() & 0xFFL; + return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4; + } + + public void writeInt32(int value) { + writeUInt32(value & 0xFFFFFFFFL); + } + + public int readInt32() { + return (int) readUInt32(); + } + + // ---------- UInt64 / Int64 ---------- + + public void writeUInt64(long value) { + writeUInt8((int) ((value >>> 56) & 0xFF)); + writeUInt8((int) ((value >>> 48) & 0xFF)); + writeUInt8((int) ((value >>> 40) & 0xFF)); + writeUInt8((int) ((value >>> 32) & 0xFF)); + writeUInt8((int) ((value >>> 24) & 0xFF)); + writeUInt8((int) ((value >>> 16) & 0xFF)); + writeUInt8((int) ((value >>> 8) & 0xFF)); + writeUInt8((int) (value & 0xFF)); + } + + public long readUInt64() { + long high = readUInt32() & 0xFFFFFFFFL; + long low = readUInt32() & 0xFFFFFFFFL; + return (high << 32) | low; + } + + public void writeInt64(long value) { + writeUInt64(value); + } + + public long readInt64() { + return readUInt64(); + } + + // ---------- floating point ---------- + + public void writeFloat(float value) { + writeInt32(Float.floatToIntBits(value)); + } + + public float readFloat() { + return Float.intBitsToFloat(readInt32()); + } + + public void writeDouble(double value) { + writeInt64(Double.doubleToLongBits(value)); + } + + public double readDouble() { + return Double.longBitsToDouble(readInt64()); + } + + // ---------- raw bytes (без префикса длины) ---------- + + /** + * Пишет ровно N байт без метаданных. + * Вызывающий код сам должен знать длину. + */ + public void writeBytes(byte[] data) { + if (data == null || data.length == 0) { + return; + } + + if (isWriteByteAligned()) { + ensureCapacityBits((long) data.length * 8L); + int byteIndex = writeBitPos >>> 3; + System.arraycopy(data, 0, buffer, byteIndex, data.length); + writeBitPos += data.length << 3; + return; + } + + for (byte b : data) { + writeUInt8(b & 0xFF); + } + } + + /** + * Читает ровно N байт без метаданных. + * Вызывающий код сам должен знать длину. + */ + public byte[] readBytes(int length) { + if (length < 0) { + throw new IllegalArgumentException("length < 0"); + } + if (length == 0) { + return new byte[0]; + } + + ensureReadableBits((long) length * 8L); + + byte[] out = new byte[length]; + + if (isReadByteAligned()) { + int byteIndex = readBitPos >>> 3; + System.arraycopy(buffer, byteIndex, out, 0, length); + readBitPos += length << 3; + return out; + } + + for (int i = 0; i < length; i++) { + out[i] = (byte) readUInt8(); + } + + return out; + } + + // ---------- string (raw UTF-8, без префикса длины) ---------- + + /** + * Пишет строку как raw UTF-8 bytes. + * Вызывающий код отвечает за то, как это разбирается на другом конце. + */ + public void writeString(String value) { + if (value == null || value.isEmpty()) { + return; + } + byte[] utf8 = value.getBytes(StandardCharsets.UTF_8); + writeBytes(utf8); + } + + /** + * Читает ровно N байт и интерпретирует как UTF-8 строку. + */ + public String readString(int length) { + byte[] bytes = readBytes(length); + return new String(bytes, StandardCharsets.UTF_8); + } + + // ---------- internals ---------- + + private boolean isWriteByteAligned() { + return (writeBitPos & 7) == 0; + } + + private boolean isReadByteAligned() { + return (readBitPos & 7) == 0; + } + + private long remainingBits() { + return (long) writeBitPos - readBitPos; + } + + private void ensureReadableBits(long bits) { + if (bits < 0) { + throw new IllegalArgumentException("bits < 0"); + } + if (remainingBits() < bits) { + throw new IllegalStateException( + String.format("Not enough bits: need %d, remaining %d", bits, remainingBits()) + ); + } + } + + private void writeBits(long value, int bits) { + if (bits <= 0) { + return; + } + + ensureCapacityBits(bits); + + for (int i = bits - 1; i >= 0; i--) { + int bit = (int) ((value >>> i) & 1L); + int byteIndex = writeBitPos >>> 3; + int bitShift = 7 - (writeBitPos & 7); + + if (bit == 1) { + buffer[byteIndex] = (byte) (buffer[byteIndex] | (1 << bitShift)); + } else { + buffer[byteIndex] = (byte) (buffer[byteIndex] & ~(1 << bitShift)); + } + + writeBitPos++; + } + } + + private long readBits(int bits) { + if (bits <= 0) { + return 0L; + } + + ensureReadableBits(bits); + + long value = 0L; + for (int i = 0; i < bits; i++) { + int bit = (buffer[readBitPos >>> 3] >>> (7 - (readBitPos & 7))) & 1; + value = (value << 1) | bit; + readBitPos++; + } + return value; + } + + private void ensureCapacityBits(long bitsToWrite) { + if (bitsToWrite <= 0) { + return; + } + + long requiredBits = (long) writeBitPos + bitsToWrite; + if (requiredBits < 0) { + throw new IllegalStateException("Bit position overflow"); + } + + long requiredBytesLong = (requiredBits + 7) >>> 3; + if (requiredBytesLong > Integer.MAX_VALUE) { + throw new IllegalStateException("Buffer too large"); + } + + ensureCapacity((int) requiredBytesLong); + } + + private void ensureCapacity(int requiredSize) { + if (requiredSize <= buffer.length) { + return; + } + + int newSize = (buffer.length == 0) ? DEFAULT_CAPACITY : buffer.length; + while (newSize < requiredSize) { + if (newSize > (Integer.MAX_VALUE >>> 1)) { + newSize = requiredSize; + break; + } + newSize <<= 1; + } + + buffer = Arrays.copyOf(buffer, newSize); + } +} \ No newline at end of file diff --git a/src/main/java/io/orprotocol/client/Client.java b/src/main/java/io/orprotocol/client/Client.java index 90e72fe..9968174 100644 --- a/src/main/java/io/orprotocol/client/Client.java +++ b/src/main/java/io/orprotocol/client/Client.java @@ -8,7 +8,7 @@ import io.orprotocol.BaseFailures; import io.orprotocol.ProtocolException; import io.orprotocol.Server; import io.orprotocol.ServerFailures; -import io.orprotocol.Stream; +import io.orprotocol.frame.FrameEncoder; import io.orprotocol.index.ClientIndexer; import io.orprotocol.packet.Packet; import io.orprotocol.packet.PacketManager; @@ -41,6 +41,11 @@ public class Client { private ClientIndexer clientIndexer; private PacketManager packetManager; + /** + * Версия, которую поддерживает клиент + */ + private int version; + /** * Создает нового клиента с указанным сокетом. * Этот метод используется внутри протокола для управления подключениями клиентов. @@ -57,6 +62,23 @@ public class Client { this.packetManager = server.getPacketManager(); } + /** + * Устанавливает версию клиента + * @internal + * @param version + */ + public void setVersion(int version) { + this.version = version; + } + + /** + * Получает версию клиента + * @return + */ + public int getVersion() { + return this.version; + } + /** * Проверяет жив ли клиент на основе времени последнего heartbeat. * Если с момента последнего heartbeat прошло больше, чем указанный интервал, клиент считается неактивным. @@ -228,10 +250,15 @@ public class Client { } packet.packetId = packetId; /** - * Записываем пакет в поток и отправляем его через сокет. + * Кодируем пакет в байты для отправки клиенту, c версии сервера (latest) до версии клиента, + * для дальнейшей отправки клиенту */ - Stream stream = packet.write(); - this.socket.send(stream.getBuffer()); + FrameEncoder frameEncoder = new FrameEncoder(packet, this, this.packetManager); + byte[] encodedData = frameEncoder.encode(); + /** + * Отправляем закодированные данные клиенту (уже закодированы под его версию) + */ + this.socket.send(encodedData); } /** diff --git a/src/main/java/io/orprotocol/codec/Codec.java b/src/main/java/io/orprotocol/codec/Codec.java new file mode 100644 index 0000000..ca9f330 --- /dev/null +++ b/src/main/java/io/orprotocol/codec/Codec.java @@ -0,0 +1,31 @@ +package io.orprotocol.codec; + +import io.orprotocol.ProtocolException; +import io.orprotocol.buffer.Buffer; +import io.orprotocol.packet.Packet; + +/** + * Интерфейс для кодирования/декодирования пакетов + * именно этот интерфейс под капотом отвечает за версионирование + */ +public interface Codec { + + /** + * Декодирует данные data от клиента у которого version версия, + * возвращает последнюю версию пакета для дальнейшей передачи в Executor + * @param data данные + * @param version версия клиента, от которого пришли данные + * @return декодированный пакет + * @throws ProtocolException если данные не могут быть декодированы в пакет или версия не поддерживается + */ + T decode(Buffer data, int version) throws ProtocolException; + /** + * Кодирует данные из последней версии в версию version для отправки клиенту, + * у клиента при этом должна быть версия version + * @param packet пакет для кодирования + * @param version версия, в которую нужно закодировать пакет + * @return закодированные данные + * @throws ProtocolException если пакет не может быть закодирован или версия не поддерживается + */ + byte[] encode(T packet, int version) throws ProtocolException; +} diff --git a/src/main/java/io/orprotocol/frame/FrameDecoder.java b/src/main/java/io/orprotocol/frame/FrameDecoder.java new file mode 100644 index 0000000..81c8903 --- /dev/null +++ b/src/main/java/io/orprotocol/frame/FrameDecoder.java @@ -0,0 +1,40 @@ +package io.orprotocol.frame; + +import io.orprotocol.ProtocolException; +import io.orprotocol.buffer.Buffer; +import io.orprotocol.client.Client; +import io.orprotocol.codec.Codec; +import io.orprotocol.packet.Packet; +import io.orprotocol.packet.PacketManager; + +/** + * Декодирует пакет(frame) пришедший от клиента + * с его версии до версии сервера (latest), для дальнейшей передачи в Executor + */ +public class FrameDecoder { + + private final byte[] frame; + private final Client sender; + private final PacketManager packetManager; + + public FrameDecoder(byte[] frame, Client sender, PacketManager packetManager) { + this.frame = frame; + this.sender = sender; + this.packetManager = packetManager; + } + + public Packet decode() throws ProtocolException { + int senderVersion = this.sender.getVersion(); + Buffer buf = Buffer.wrap(this.frame); + int packetId = buf.readUInt16(); + Codec codec = this.packetManager.getCodec(packetId); + Packet packet = codec.decode(buf, senderVersion); + packet.packetId = packetId; + /** + * Буфер уже смещен на 2 байта, так что декодирование в кодеке начинается с данных пакета, а не с ID + */ + return codec.decode(buf, senderVersion); + } + + +} diff --git a/src/main/java/io/orprotocol/frame/FrameEncoder.java b/src/main/java/io/orprotocol/frame/FrameEncoder.java new file mode 100644 index 0000000..7a9a5e3 --- /dev/null +++ b/src/main/java/io/orprotocol/frame/FrameEncoder.java @@ -0,0 +1,32 @@ +package io.orprotocol.frame; + +import io.orprotocol.ProtocolException; +import io.orprotocol.client.Client; +import io.orprotocol.codec.Codec; +import io.orprotocol.packet.Packet; +import io.orprotocol.packet.PacketManager; + +/** + * Кодирует пакет в байты для отправки клиенту, c версии сервера (latest) до версии клиента, + * для дальнейшей отправки клиенту + */ +public class FrameEncoder { + + private final Packet packet; + private final Client recipient; + private final PacketManager packetManager; + + public FrameEncoder(Packet packet, Client recipient, PacketManager packetManager) { + this.packet = packet; + this.recipient = recipient; + this.packetManager = packetManager; + } + + public byte[] encode() throws ProtocolException { + int recipientVersion = this.recipient.getVersion(); + @SuppressWarnings("unchecked") + Codec codec = (Codec) this.packetManager.getCodec(this.packet.packetId); + return codec.encode(this.packet, recipientVersion); + } + +} diff --git a/src/main/java/io/orprotocol/lock/ThreadLocker.java b/src/main/java/io/orprotocol/lock/ThreadLocker.java index 4a0b09d..c625b06 100644 --- a/src/main/java/io/orprotocol/lock/ThreadLocker.java +++ b/src/main/java/io/orprotocol/lock/ThreadLocker.java @@ -73,9 +73,7 @@ public class ThreadLocker { String fieldValue = (String) field.get(packet); String lockValue = packet.getClass().getName() + "_" + fieldValue; locks.remove(lockValue); - }catch(Exception e) { - // Игнорируем ошибки при разблокировке - } + }catch(Exception e) { } } } diff --git a/src/main/java/io/orprotocol/packet/Packet.java b/src/main/java/io/orprotocol/packet/Packet.java index 03b5378..3b16314 100644 --- a/src/main/java/io/orprotocol/packet/Packet.java +++ b/src/main/java/io/orprotocol/packet/Packet.java @@ -1,7 +1,5 @@ package io.orprotocol.packet; -import io.orprotocol.Stream; - /** * Представляет собой абстрактный класс для всех пакетов, используемых в протоколе. */ @@ -10,18 +8,4 @@ public abstract class Packet { public int packetId; public PacketManager packetManager; - /** - * Читает данные пакета из потока. Используется при получении - * - * @param stream Поток с данными пакета. - */ - public abstract void read(Stream stream); - - /** - * Записывает данные пакета в поток. Исползуется при отправке - * - * @return Поток с записанными данными пакета. - */ - public abstract Stream write(); - } diff --git a/src/main/java/io/orprotocol/packet/PacketFactory.java b/src/main/java/io/orprotocol/packet/PacketFactory.java deleted file mode 100644 index 4218f61..0000000 --- a/src/main/java/io/orprotocol/packet/PacketFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.orprotocol.packet; - -import io.orprotocol.ProtocolException; -import io.orprotocol.Stream; - -/** - * Фабрика для создания пакетов из байтового массива. Используется для создания пакетов при получении данных от клиента, - * а так же может быть использована приложением - */ -public class PacketFactory { - - private byte[] bytes; - private PacketManager packetManager; - - /** - * Создать фабрику для создания пакетов из байтового массива - * @param bytes байтовый массив для создания пакета - * @param packetManager менеджер пакетов для получения класса пакета по его id - */ - public PacketFactory(byte[] bytes, PacketManager packetManager) { - this.bytes = bytes; - this.packetManager = packetManager; - } - - /** - * Создает пакет из массива байт, сериализует и возвращает его. Если пакет с таким id не поддерживается, выбрасывает ProtocolException - * @return созданный пакет - * @throws ProtocolException - */ - public Packet createPacket() throws ProtocolException { - Stream stream = new Stream(this.bytes); - int packetId = stream.readInt16(); - if(!this.packetManager.hasPacketSupported(packetId)){ - throw new ProtocolException("Unsupported packet with id " + packetId); - } - Class packetClass = this.packetManager.getPacketClass(packetId); - try { - Packet packet = packetClass.getConstructor().newInstance(); - packet.packetId = packetId; - packet.read(stream); - return packet; - } catch (Exception e) { - throw new ProtocolException("Failed to create packet with id " + packetId); - } - } - - public int getPacketId() { - Stream stream = new Stream(this.bytes); - return stream.readInt16(); - } - -} diff --git a/src/main/java/io/orprotocol/packet/PacketManager.java b/src/main/java/io/orprotocol/packet/PacketManager.java index 2862c03..7fc110c 100644 --- a/src/main/java/io/orprotocol/packet/PacketManager.java +++ b/src/main/java/io/orprotocol/packet/PacketManager.java @@ -2,7 +2,7 @@ package io.orprotocol.packet; import java.util.HashMap; -import io.orprotocol.ProtocolException; +import io.orprotocol.codec.Codec; /** * Менеджер сетевых пакетов и их обработчиков. @@ -11,10 +11,17 @@ public class PacketManager { private HashMap> packets; private HashMap> executors; + private HashMap> codecs; + /** + * Ключ - класс пакета, значение - ID пакета, используется для получения ID по классу + */ + private HashMap, Integer> packetIds; public PacketManager() { this.packets = new HashMap<>(); this.executors = new HashMap<>(); + this.codecs = new HashMap<>(); + this.packetIds = new HashMap<>(); } /** @@ -24,6 +31,7 @@ public class PacketManager { */ public void registerPacket(int packetId, Class packet) { this.packets.put(packetId, packet); + this.packetIds.put(packet, packetId); } /** @@ -35,6 +43,19 @@ public class PacketManager { return this.executors.containsKey(packetId); } + /** + * Проверяет, зарегистрирован ли обработчик для пакета с указанным классом. + * @param packet Экземпляр пакета + * @return true, если обработчик зарегистрирован, иначе false. + */ + public boolean hasExecutorDelegated(Packet packet) { + Integer packetId = this.getPacketIdByClass(packet.getClass()); + if(packetId == null){ + return false; + } + return true; + } + /** * Проверяет, поддерживается ли пакет с указанным ID. * @param packetId ID пакета @@ -58,12 +79,7 @@ public class PacketManager { * @return ID пакета */ public Integer getPacketIdByClass(Class packetClass) { - for (var entry : this.packets.entrySet()) { - if (entry.getValue().equals(packetClass)) { - return entry.getKey(); - } - } - return null; + return this.packetIds.get(packetClass); } public Class getPacketClass(int packetId) { @@ -82,6 +98,35 @@ public class PacketManager { this.executors.put(packetId, executor); } + public void registerExecutor(Class packetClass, PacketExecutor executor) { + Integer packetId = this.getPacketIdByClass(packetClass); + if(packetId == null) { + throw new IllegalArgumentException("Packet class " + packetClass.getName() + " is not registered with a packet ID."); + } + this.registerExecutor(packetId, executor); + } + + /** + * Регистрирует кодек для пакета с указанным ID. + * @param packetId ID пакета + * @param codec Кодек для пакета + */ + public void registerCodec(int packetId, Codec codec) { + if (this.codecs == null) { + this.codecs = new HashMap<>(); + } + this.codecs.put(packetId, codec); + } + + /** + * Получает кодек для пакета с указанным ID. + * @param packetId ID пакета + * @return Кодек для пакета, или null если кодек не найден. + */ + public Codec getCodec(int packetId) { + return this.codecs.get(packetId); + } + /** * Возвращает общее количество зарегистрированных обработчиков пакетов. * @return Количество обработчиков пакетов. @@ -98,24 +143,4 @@ public class PacketManager { return this.packets.size(); } - /** - * Создает пакет из массива байт, сериализует и возвращает его. Если пакет с таким id не поддерживается, выбрасывает ProtocolException - * @param bytes байтовый массив для создания пакета - * @return созданный пакет - * @throws ProtocolException если пакет с таким id не поддерживается или произошла ошибка при создании пакета - */ - public Packet createPacket(byte[] bytes) throws ProtocolException { - PacketFactory packetFactory = new PacketFactory(bytes, this); - return packetFactory.createPacket(); - } - - /** - * Создает фабрику для создания пакетов из байтового массива - * @param bytes байтовый массив для создания пакета - * @return фабрика для создания пакетов из байтового массива - */ - public PacketFactory getPacketFactory(byte[] bytes) { - return new PacketFactory(bytes, this); - } - } diff --git a/src/main/java/io/orprotocol/serializer/Serializable.java b/src/main/java/io/orprotocol/serializer/Serializable.java deleted file mode 100644 index 7c1f90d..0000000 --- a/src/main/java/io/orprotocol/serializer/Serializable.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.orprotocol.serializer; - -import io.orprotocol.Stream; - -public interface Serializable { - /** - * Сериализует объект из потока данных. Используется при получении данных. - * @param stream Поток данных для сериализации - */ - void readFromStream(Stream stream); - /** - * Десериализует обьект в поток данных. Используется при отправке данных. - * @return Десериализованный объект - */ - void writeToStream(Stream stream); -} diff --git a/src/main/java/io/orprotocol/tlv/IoTlvReader.java b/src/main/java/io/orprotocol/tlv/IoTlvReader.java new file mode 100644 index 0000000..f5c769f --- /dev/null +++ b/src/main/java/io/orprotocol/tlv/IoTlvReader.java @@ -0,0 +1,130 @@ +package io.orprotocol.tlv; + +import java.util.List; + +/** + * Интерфейс для чтения данных в формате TLV (Type-Length-Value). + * Предоставляет методы для извлечения значений различных типов данных по идентификатору поля. + */ +public interface IoTlvReader { + + /** + * Получает строковое значение по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return строковое значение + */ + public String getString(int fieldId); + + /** + * Получает список строковых значений по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return список строк + */ + public List getStringList(int fieldId); + + /** + * Получает массив байтов по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return массив байтов + */ + public byte[] getBytes(int fieldId); + + /** + * Получает список массивов байтов по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return список массивов байтов + */ + public List getBytesList(int fieldId); + + /** + * Получает значение 8-битного целого числа со знаком по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение int8 + */ + public byte getInt8(int fieldId); + + /** + * Получает значение 8-битного целого числа без знака по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение uint8 + */ + public short getUInt8(int fieldId); + + /** + * Получает значение 16-битного целого числа со знаком по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение int16 + */ + public short getInt16(int fieldId); + + /** + * Получает значение 16-битного целого числа без знака по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение uint16 + */ + public int getUInt16(int fieldId); + + /** + * Получает значение 32-битного целого числа со знаком по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение int32 + */ + public int getInt32(int fieldId); + + /** + * Получает значение 32-битного целого числа без знака по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение uint32 + */ + public long getUInt32(int fieldId); + + /** + * Получает значение 64-битного целого числа со знаком по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение int64 + */ + public long getInt64(int fieldId); + + /** + * Получает значение 64-битного целого числа без знака по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение uint64 + */ + public long getUInt64(int fieldId); + + /** + * Получает значение 32-битного числа с плавающей точкой по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение float32 + */ + public float getFloat32(int fieldId); + + /** + * Получает значение 64-битного числа с плавающей точкой по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return значение float64 + */ + public double getFloat64(int fieldId); + + /** + * Получает логическое значение (true/false) по идентификатору поля. + * + * @param fieldId идентификатор поля + * @return логическое значение + */ + public boolean getBoolean(int fieldId); +} diff --git a/src/main/java/io/orprotocol/tlv/IoTlvWriter.java b/src/main/java/io/orprotocol/tlv/IoTlvWriter.java new file mode 100644 index 0000000..ad3454d --- /dev/null +++ b/src/main/java/io/orprotocol/tlv/IoTlvWriter.java @@ -0,0 +1,130 @@ +package io.orprotocol.tlv; + +import java.util.Collection; + +/** + * Интерфейс для записи TLV (Tag-Length-Value) данных. + * Предоставляет методы для сериализации различных типов данных. + */ +public interface IoTlvWriter { + + /** + * Записывает строковое значение. + * + * @param fieldId идентификатор поля + * @param value строковое значение для записи + */ + public void writeString(int fieldId, String value); + + /** + * Записывает коллекцию строк. + * + * @param fieldId идентификатор поля + * @param value коллекция строк для записи + */ + public void writeStringList(int fieldId, Collection value); + + /** + * Записывает массив байтов. + * + * @param fieldId идентификатор поля + * @param value массив байтов для записи + */ + public void writeBytes(int fieldId, byte[] value); + + /** + * Записывает коллекцию массивов байтов. + * + * @param fieldId идентификатор поля + * @param value коллекция массивов байтов для записи + */ + public void writeBytesList(int fieldId, Collection value); + + /** + * Записывает целое число (8-бит со знаком). + * + * @param fieldId идентификатор поля + * @param value значение от -128 до 127 + */ + public void writeInt8(int fieldId, byte value); + + /** + * Записывает целое число (8-бит без знака). + * + * @param fieldId идентификатор поля + * @param value значение от 0 до 255 + */ + public void writeUInt8(int fieldId, short value); + + /** + * Записывает целое число (16-бит со знаком). + * + * @param fieldId идентификатор поля + * @param value значение от -32768 до 32767 + */ + public void writeInt16(int fieldId, short value); + + /** + * Записывает целое число (16-бит без знака). + * + * @param fieldId идентификатор поля + * @param value значение от 0 до 65535 + */ + public void writeUInt16(int fieldId, int value); + + /** + * Записывает целое число (32-бит со знаком). + * + * @param fieldId идентификатор поля + * @param value значение от -2147483648 до 2147483647 + */ + public void writeInt32(int fieldId, int value); + + /** + * Записывает целое число (32-бит без знака). + * + * @param fieldId идентификатор поля + * @param value значение от 0 до 4294967295 + */ + public void writeUInt32(int fieldId, long value); + + /** + * Записывает целое число (64-бит со знаком). + * + * @param fieldId идентификатор поля + * @param value значение от -9223372036854775808 до 9223372036854775807 + */ + public void writeInt64(int fieldId, long value); + + /** + * Записывает целое число (64-бит без знака). + * + * @param fieldId идентификатор поля + * @param value значение от 0 до 18446744073709551615 + */ + public void writeUInt64(int fieldId, long value); + + /** + * Записывает число с плавающей точкой (32-бит). + * + * @param fieldId идентификатор поля + * @param value значение типа float + */ + public void writeFloat32(int fieldId, float value); + + /** + * Записывает число с плавающей точкой (64-бит). + * + * @param fieldId идентификатор поля + * @param value значение типа double + */ + public void writeFloat64(int fieldId, double value); + + /** + * Записывает логическое значение. + * + * @param fieldId идентификатор поля + * @param value логическое значение (true или false) + */ + public void writeBoolean(int fieldId, boolean value); +} diff --git a/src/main/java/io/orprotocol/tlv/TlvField.java b/src/main/java/io/orprotocol/tlv/TlvField.java new file mode 100644 index 0000000..6f5669f --- /dev/null +++ b/src/main/java/io/orprotocol/tlv/TlvField.java @@ -0,0 +1,24 @@ +package io.orprotocol.tlv; + +/** + * TLV Field + */ +public class TlvField { + + private final int wireType; + private final Object value; + + public TlvField(int wireType, Object value) { + this.wireType = wireType; + this.value = value; + } + + public int getWireType() { + return wireType; + } + + public Object getValue() { + return value; + } + +} diff --git a/src/main/java/io/orprotocol/tlv/TlvHeader.java b/src/main/java/io/orprotocol/tlv/TlvHeader.java new file mode 100644 index 0000000..e23bd02 --- /dev/null +++ b/src/main/java/io/orprotocol/tlv/TlvHeader.java @@ -0,0 +1,21 @@ +package io.orprotocol.tlv; + +public class TlvHeader { + + private final int fieldId; + private final int wireType; + + public TlvHeader(int fieldId, int wireType) { + this.fieldId = fieldId; + this.wireType = wireType; + } + + public int getFieldId() { + return fieldId; + } + + public int getWireType() { + return wireType; + } + +} diff --git a/src/main/java/io/orprotocol/tlv/read/TlvReader.java b/src/main/java/io/orprotocol/tlv/read/TlvReader.java new file mode 100644 index 0000000..87e0e33 --- /dev/null +++ b/src/main/java/io/orprotocol/tlv/read/TlvReader.java @@ -0,0 +1,263 @@ +package io.orprotocol.tlv.read; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.orprotocol.ProtocolException; +import io.orprotocol.buffer.Buffer; +import io.orprotocol.tlv.IoTlvReader; +import io.orprotocol.tlv.TlvField; +import io.orprotocol.tlv.TlvHeader; +import io.orprotocol.wire.WireType; + +public class TlvReader implements IoTlvReader { + + private final Map> fields = new HashMap<>(); + + public TlvReader(byte[] payload) throws ProtocolException { + this.read(payload); + } + + private void read(byte[] payload) throws ProtocolException { + Buffer buf = Buffer.wrap(payload); + while(buf.hasRemaining()) { + TlvHeader header = this.readHeader(buf); + TlvField field = switch(header.getWireType()) { + case WireType.INT8 -> new TlvField(WireType.INT8, buf.readInt8()); + case WireType.UINT8 -> new TlvField(WireType.UINT8, buf.readUInt8()); + case WireType.INT16 -> new TlvField(WireType.INT16, buf.readInt16()); + case WireType.UINT16 -> new TlvField(WireType.UINT16, buf.readUInt16()); + case WireType.INT32 -> new TlvField(WireType.INT32, buf.readInt32()); + case WireType.UINT32 -> new TlvField(WireType.UINT32, buf.readUInt32()); + case WireType.INT64 -> new TlvField(WireType.INT64, buf.readInt64()); + case WireType.UINT64 -> new TlvField(WireType.UINT64, buf.readUInt64()); + case WireType.FLOAT32 -> new TlvField(WireType.FLOAT32, buf.readFloat()); + case WireType.FLOAT64 -> new TlvField(WireType.FLOAT64, buf.readDouble()); + case WireType.BOOL -> new TlvField(WireType.BOOL, buf.readBoolean()); + case WireType.LENGTH_DELIMITED -> { + /** + * Строки, байты и другие данные которые могут содержать произвольную длинну + * в том числе другие TLV теги внутри себя, кодируются как length-delimited: сначала пишется длина данных в виде uint16, а затем сами данные + */ + int length = buf.readUInt16(); + byte[] data = buf.readBytes(length); + yield new TlvField(WireType.LENGTH_DELIMITED, data); + } + default -> throw new ProtocolException("tlv header is bad"); + }; + fields.computeIfAbsent(header.getFieldId(), k -> new ArrayList<>()).add(field); + } + } + + private TlvHeader readHeader(Buffer buf) { + int fieldId = buf.readUInt8(); + int wireType = buf.readUInt8(); + return new TlvHeader(fieldId, wireType); + } + + public List getField(int fieldId) { + return fields.get(fieldId); + } + + public boolean hasField(int fieldId) { + return fields.containsKey(fieldId); + } + + public String getString(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + return null; + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.LENGTH_DELIMITED) { + throw new IllegalStateException("field is not length-delimited"); + } + return new String((byte[]) field.getValue(), StandardCharsets.UTF_8); + } + + public List getStringList(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + return null; + } + List result = new ArrayList<>(); + for(TlvField field : fields) { + if(field.getWireType() != WireType.LENGTH_DELIMITED) { + throw new IllegalStateException("field is not length-delimited"); + } + result.add(new String((byte[]) field.getValue(), StandardCharsets.UTF_8)); + } + return result; + } + + public byte[] getBytes(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + return null; + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.LENGTH_DELIMITED) { + throw new IllegalStateException("field is not length-delimited"); + } + return (byte[]) field.getValue(); + } + + public List getBytesList(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + return null; + } + List result = new ArrayList<>(); + for(TlvField field : fields) { + if(field.getWireType() != WireType.LENGTH_DELIMITED) { + throw new IllegalStateException("field is not length-delimited"); + } + result.add((byte[]) field.getValue()); + } + return result; + } + + public byte getInt8(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.INT8) { + throw new IllegalStateException("field is not int8"); + } + return (byte) field.getValue(); + } + + public short getUInt8(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.UINT8) { + throw new IllegalStateException("field is not uint8"); + } + return (short) field.getValue(); + } + + public short getInt16(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.INT16) { + throw new IllegalStateException("field is not int16"); + } + return (short) field.getValue(); + } + + public int getUInt16(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.UINT16) { + throw new IllegalStateException("field is not uint16"); + } + return (int) field.getValue(); + } + + public int getInt32(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.INT32) { + throw new IllegalStateException("field is not int32"); + } + return (int) field.getValue(); + } + + public long getUInt32(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.UINT32) { + throw new IllegalStateException("field is not uint32"); + } + return (long) field.getValue(); + } + + public long getInt64(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.INT64) { + throw new IllegalStateException("field is not int64"); + } + return (long) field.getValue(); + } + + public long getUInt64(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.UINT64) { + throw new IllegalStateException("field is not uint64"); + } + return (long) field.getValue(); + } + + public boolean getBoolean(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.BOOL) { + throw new IllegalStateException("field is not bool"); + } + return (boolean) field.getValue(); + } + + public float getFloat32(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.FLOAT32) { + throw new IllegalStateException("field is not float32"); + } + return (float) field.getValue(); + } + + public double getFloat64(int fieldId) { + List fields = this.getField(fieldId); + if(fields == null || fields.isEmpty()) { + throw new IllegalStateException("field is not found"); + } + TlvField field = fields.get(0); + if(field.getWireType() != WireType.FLOAT64) { + throw new IllegalStateException("field is not float64"); + } + return (double) field.getValue(); + } + + public List getFields(int fieldId) { + return fields.get(fieldId); + } + + public Map> getAllFields() { + return fields; + } + +} \ No newline at end of file diff --git a/src/main/java/io/orprotocol/tlv/write/TlvWriter.java b/src/main/java/io/orprotocol/tlv/write/TlvWriter.java new file mode 100644 index 0000000..4570d5b --- /dev/null +++ b/src/main/java/io/orprotocol/tlv/write/TlvWriter.java @@ -0,0 +1,136 @@ +package io.orprotocol.tlv.write; + +import java.util.Collection; + +import io.orprotocol.buffer.Buffer; +import io.orprotocol.tlv.IoTlvWriter; +import io.orprotocol.tlv.TlvHeader; +import io.orprotocol.wire.WireType; + +public class TlvWriter implements IoTlvWriter { + + private Buffer buffer; + + public TlvWriter() { + this.buffer = new Buffer(); + } + + public TlvWriter(Buffer buffer) { + this.buffer = buffer; + } + + public TlvWriter(byte[] data) { + this.buffer = Buffer.wrap(data); + } + + public Buffer getBuffer() { + return buffer; + } + + public byte[] toByteArray() { + return buffer.toByteArray(); + } + + public void writeBoolean(int fieldId, boolean value) { + TlvHeader header = new TlvHeader(fieldId, WireType.BOOL); + this.writeHeader(header); + this.buffer.writeBoolean(value); + } + + public void writeInt8(int fieldId, byte value) { + TlvHeader header = new TlvHeader(fieldId, WireType.INT8); + this.writeHeader(header); + this.buffer.writeInt8(value); + } + + public void writeUInt8(int fieldId, short value) { + TlvHeader header = new TlvHeader(fieldId, WireType.UINT8); + this.writeHeader(header); + this.buffer.writeUInt8(value); + } + + public void writeInt16(int fieldId, short value) { + TlvHeader header = new TlvHeader(fieldId, WireType.INT16); + this.writeHeader(header); + this.buffer.writeInt16(value); + } + + public void writeUInt16(int fieldId, int value) { + TlvHeader header = new TlvHeader(fieldId, WireType.UINT16); + this.writeHeader(header); + this.buffer.writeUInt16(value); + } + + public void writeInt32(int fieldId, int value) { + TlvHeader header = new TlvHeader(fieldId, WireType.INT32); + this.writeHeader(header); + this.buffer.writeInt32(value); + } + + public void writeUInt32(int fieldId, long value) { + TlvHeader header = new TlvHeader(fieldId, WireType.UINT32); + this.writeHeader(header); + this.buffer.writeUInt32(value); + } + + public void writeInt64(int fieldId, long value) { + TlvHeader header = new TlvHeader(fieldId, WireType.INT64); + this.writeHeader(header); + this.buffer.writeInt64(value); + } + + public void writeUInt64(int fieldId, long value) { + TlvHeader header = new TlvHeader(fieldId, WireType.UINT64); + this.writeHeader(header); + this.buffer.writeUInt64(value); + } + + public void writeBytes(int fieldId, byte[] data) { + TlvHeader header = new TlvHeader(fieldId, WireType.LENGTH_DELIMITED); + this.writeHeader(header); + this.buffer.writeUInt16(data.length); + this.buffer.writeBytes(data); + } + + public void writeFloat32(int fieldId, float value) { + TlvHeader header = new TlvHeader(fieldId, WireType.FLOAT32); + this.writeHeader(header); + this.buffer.writeFloat(value); + } + + public void writeFloat64(int fieldId, double value) { + TlvHeader header = new TlvHeader(fieldId, WireType.FLOAT64); + this.writeHeader(header); + this.buffer.writeDouble(value); + } + + public void writeBytesList(int fieldId, Collection dataList) { + for (byte[] data : dataList) { + this.writeBytes(fieldId, data); + } + } + + /** + * Записывает список строк + * @param fieldId ID поля + * @param values список строк для записи + */ + public void writeStringList(int fieldId, Collection values) { + for (String value : values) { + this.writeString(fieldId, value); + } + } + + public void writeString(int fieldId, String value) { + TlvHeader header = new TlvHeader(fieldId, WireType.LENGTH_DELIMITED); + this.writeHeader(header); + this.buffer.writeUInt16(value.getBytes().length); + this.buffer.writeBytes(value.getBytes()); + } + + private void writeHeader(TlvHeader header) { + this.buffer.writeUInt8(header.getFieldId()); + this.buffer.writeUInt8(header.getWireType()); + } + +} diff --git a/src/main/java/io/orprotocol/wire/WireType.java b/src/main/java/io/orprotocol/wire/WireType.java new file mode 100644 index 0000000..f03cfda --- /dev/null +++ b/src/main/java/io/orprotocol/wire/WireType.java @@ -0,0 +1,23 @@ +package io.orprotocol.wire; + +public class WireType { + public static final int BOOL = 0; + public static final int INT8 = 1; + public static final int INT16 = 2; + public static final int INT32 = 3; + public static final int INT64 = 4; + public static final int UINT8 = 5; + public static final int UINT16 = 6; + public static final int UINT32 = 7; + public static final int UINT64 = 8; + public static final int FLOAT32 = 9; + public static final int FLOAT64 = 10; + /** + * int32 length + данные + * string, byte[] + */ + public static final int LENGTH_DELIMITED = 11; + + + private WireType() {} +}