diff --git a/src/main/java/com/rosetta/im/Boot.java b/src/main/java/com/rosetta/im/Boot.java new file mode 100644 index 0000000..0549359 --- /dev/null +++ b/src/main/java/com/rosetta/im/Boot.java @@ -0,0 +1,42 @@ +package com.rosetta.im; + +import com.rosetta.im.executors.Executor0Handshake; +import com.rosetta.im.packet.Packet0Handshake; +import com.rosetta.im.protocol.packet.PacketManager; + +public class Boot { + + private PacketManager packetManager; + + public Boot(PacketManager packetManager) { + this.packetManager = packetManager; + } + + public PacketManager getPacketManager() { + return this.packetManager; + } + + /** + * Инициализация всех пакетов и их обработчиков + */ + public void bootstrap() { + this.registerAllPackets(); + this.registerAllExecutors(); + this.printBootMessage(); + } + + private void registerAllPackets() { + this.packetManager.registerPacket(0, Packet0Handshake.class); + } + + private void registerAllExecutors() { + this.packetManager.registerExecutor(0, Executor0Handshake.class); + } + + private void printBootMessage() { + System.out.println("Bootstrapping completed. All packets and executors are registered."); + System.out.println("Total packets registered: " + this.packetManager.totalPackets()); + System.out.println("Total executors registered: " + this.packetManager.totalExecutors()); + } + +} diff --git a/src/main/java/com/rosetta/im/Main.java b/src/main/java/com/rosetta/im/Main.java index f09e66a..20f20de 100644 --- a/src/main/java/com/rosetta/im/Main.java +++ b/src/main/java/com/rosetta/im/Main.java @@ -1,20 +1,28 @@ package com.rosetta.im; - -import com.rosetta.im.packet.Packet0Handshake; import com.rosetta.im.protocol.Server; +import com.rosetta.im.protocol.Settings; import com.rosetta.im.protocol.packet.PacketManager; public class Main { public static void main(String[] args) { PacketManager manager = new PacketManager(); - manager.registerPacket(0, Packet0Handshake.class); + /** + * Регистрация всех пакетов и их обработчиков + */ + Boot boot = new Boot(manager); + boot.bootstrap(); - Server server = new Server(8881, manager); + /** + * Загрузка настроек сервера + */ + Settings settings = new Settings(8881, 30); + + /** + * Запуск сервера на порту 8881 + */ + Server server = new Server(settings, manager); server.start(); - - - System.out.println("Rosetta server started..."); } } \ No newline at end of file diff --git a/src/main/java/com/rosetta/im/executors/Executor0Handshake.java b/src/main/java/com/rosetta/im/executors/Executor0Handshake.java index 349fab1..eda8ba5 100644 --- a/src/main/java/com/rosetta/im/executors/Executor0Handshake.java +++ b/src/main/java/com/rosetta/im/executors/Executor0Handshake.java @@ -1,13 +1,18 @@ package com.rosetta.im.executors; +import com.rosetta.im.packet.Packet0Handshake; +import com.rosetta.im.protocol.Client; import com.rosetta.im.protocol.packet.Packet; import com.rosetta.im.protocol.packet.PacketExecutor; public class Executor0Handshake implements PacketExecutor { @Override - public void onPacketReceived(Class packet) { - + public void onPacketReceived(Packet packet, Client client) { + Packet0Handshake handshake = (Packet0Handshake) packet; + String publicKey = handshake.getPublicKey(); + String privateKey = handshake.getPrivateKey(); + int b = 0; } } diff --git a/src/main/java/com/rosetta/im/packet/Packet0Handshake.java b/src/main/java/com/rosetta/im/packet/Packet0Handshake.java index 6389fce..f21ec45 100644 --- a/src/main/java/com/rosetta/im/packet/Packet0Handshake.java +++ b/src/main/java/com/rosetta/im/packet/Packet0Handshake.java @@ -26,6 +26,11 @@ public class Packet0Handshake extends Packet { * Публичный и приватный ключи клиента */ private String publicKey; + /** + * Приватный ключ клиента + * Это не совсем приватный ключ, а лишь необратимо зашифрованная его версия + * для идентификации клиента на сервере. + */ private String privateKey; /** * Версия протокола клиента @@ -80,4 +85,52 @@ public class Packet0Handshake extends Packet { ); } + public String getPublicKey() { + return publicKey; + } + + public String getPrivateKey() { + return privateKey; + } + + public int getProtocolVersion() { + return protocolVersion; + } + + public int getHeartbeatInterval() { + return heartbeatInterval; + } + + public Device getDevice() { + return device; + } + + public HandshakeStage getHandshakeStage() { + return handshakeStage; + } + + public void setHandshakeStage(HandshakeStage handshakeStage) { + this.handshakeStage = handshakeStage; + } + + public void setDevice(Device device) { + this.device = device; + } + + public void setHeartbeatInterval(int heartbeatInterval) { + this.heartbeatInterval = heartbeatInterval; + } + + public void setProtocolVersion(int protocolVersion) { + this.protocolVersion = protocolVersion; + } + + public void setPrivateKey(String privateKey) { + this.privateKey = privateKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + } diff --git a/src/main/java/com/rosetta/im/protocol/Client.java b/src/main/java/com/rosetta/im/protocol/Client.java index 58b04a0..a285afe 100644 --- a/src/main/java/com/rosetta/im/protocol/Client.java +++ b/src/main/java/com/rosetta/im/protocol/Client.java @@ -7,40 +7,133 @@ import org.java_websocket.WebSocket; import com.rosetta.im.protocol.util.StringUtil; +/** + * Клиент, подключенный к серверу. + */ public class Client { public WebSocket socket; public String clientId; public Map clientData; + /** + * Интервал отправки heartbeat пакетов в миллисекундах. + */ + public long heartbeatInterval = 0; + /** + * Время последнего полученного heartbeat в миллисекундах. + */ + private volatile long lastHeartbeatTime; - public Client(WebSocket socket) { + /** + * Создает нового клиента с указанным сокетом. + * Этот метод используется внутри протокола для управления подключениями клиентов. + * @param socket Веб-сокет клиента. + * + */ + public Client(WebSocket socket, long heartbeatInterval) { this.socket = socket; this.clientId = StringUtil.randomString(32); this.clientData = new HashMap<>(); + this.heartbeatInterval = heartbeatInterval; + this.lastHeartbeatTime = System.currentTimeMillis(); } + /** + * Проверяет жив ли клиент на основе времени последнего heartbeat. + * Если с момента последнего heartbeat прошло больше, чем указанный интервал, клиент считается неактивным. + * + * Для того чтобы исключить сетевые задержки, проверка умножает интервал на 2. + * @return + */ + public boolean isAlive() { + return (System.currentTimeMillis() - this.lastHeartbeatTime) * 2 <= this.heartbeatInterval * 1000; + } + + /** + * Обновляет время последнего полученного heartbeat на текущее время. + */ + public void updateHeartbeat() { + this.lastHeartbeatTime = System.currentTimeMillis(); + } + + /** + * Получает уникальный идентификатор клиента. + * @return Идентификатор клиента. + */ public String getClientId() { return clientId; } + /** + * Получает данные, связанные с клиентом. + * @return Данные клиента. + */ public Map getClientData() { return clientData; } + /** + * Устанавливает уникальный идентификатор клиента. + * @param clientId Идентификатор клиента. + */ public void setClientId(String clientId) { this.clientId = clientId; } - + + /** + * Устанавливает данные для клиента по указанному ключу. + * @param key Ключ данных. + * @param value Значение данных. + */ public void setData(String key, Object value) { this.clientData.put(key, value); } + /** + * Устанавливает данные клиента. + * @param data Данные клиента. + */ public void setData(Map data) { this.clientData = data; } + /** + * Получает данные клиента по указанному ключу. + * @param key Ключ данных. + * @return Значение данных. + */ public Object getData(String key) { return this.clientData.get(key); } + /** + * Получает веб-сокет клиента. + * @return Веб-сокет. + */ + public WebSocket getSocket() { + return socket; + } + + /** + * Отключает клиента с указанным кодом. + * @param code Код отключения. + */ + public void disconnect(int code) { + this.socket.close(code); + } + /** + * Отключает клиента с указанным кодом отказа. + * @param code Код отказа. + */ + public void disconnect(Failures code) { + this.disconnect(code.getCode()); + } + + /** + * Отключает клиента с неизвестной причиной. + */ + public void disconnect() { + this.disconnect(Failures.UNKNOWN_FAILURE); + } + } diff --git a/src/main/java/com/rosetta/im/protocol/Failures.java b/src/main/java/com/rosetta/im/protocol/Failures.java new file mode 100644 index 0000000..ac36bbd --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/Failures.java @@ -0,0 +1,55 @@ +package com.rosetta.im.protocol; + +/** + * Перечисление кодов ошибок, используемых в протоколе. + */ +public enum Failures { + /** + * Код ошибки, указывающий на несоответствие данных. + */ + DATA_MISSMATCH(3001), + /** + * Код ошибки, указывающий на незавершенное рукопожатие. + */ + HANDSHAKE_NOT_COMPLETED(3002), + /** + * Код ошибки, указывающий на некорректный пакет. + */ + BAD_PACKET(3003), + /** + * Код ошибки, указывающий на некорректный пакет. + */ + INVALID_PACKET(3003), + /** + * Код ошибки, указывающий на тайм-аут бездействия. + */ + INACTIVITY_TIMEOUT(3004), + /** + * Код ошибки, указывающий на неизвестный тип пакета. + */ + PACKET_ID_FAILURE(3998), + /** + * Код ошибки, указывающий на неизвестный тип пакета. + */ + UNSUPPORTED_PACKET(3998), + /** + * Код ошибки, указывающий на неизвестную ошибку. + */ + UNKNOWN_FAILURE(3999); + + + private final int code; + + Failures(int code) { + this.code = code; + } + + /** + * Получает код ошибки. + * @return Код ошибки. + */ + public int getCode() { + return code; + } + +} diff --git a/src/main/java/com/rosetta/im/protocol/Server.java b/src/main/java/com/rosetta/im/protocol/Server.java index f1bfaf0..3f99a24 100644 --- a/src/main/java/com/rosetta/im/protocol/Server.java +++ b/src/main/java/com/rosetta/im/protocol/Server.java @@ -2,20 +2,27 @@ package com.rosetta.im.protocol; import java.net.InetSocketAddress; import java.nio.ByteBuffer; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import org.java_websocket.WebSocket; import org.java_websocket.handshake.ClientHandshake; import org.java_websocket.server.WebSocketServer; import com.rosetta.im.protocol.packet.Packet; +import com.rosetta.im.protocol.packet.PacketExecutor; import com.rosetta.im.protocol.packet.PacketManager; public class Server extends WebSocketServer { private PacketManager packetManager; + private Settings settings; + private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); - public Server(int port, PacketManager packetManager) { - super(new InetSocketAddress(port)); + public Server(Settings settings, PacketManager packetManager) { + super(new InetSocketAddress(settings.port)); + this.settings = settings; this.packetManager = packetManager; } @@ -30,41 +37,97 @@ public class Server extends WebSocketServer { } @Override - public void onMessage(WebSocket arg0, String arg1) { - + public void onMessage(WebSocket socket, String message) { + /** + * Обновляем время последнего полученного heartbeat. + * Так как клиент отпраивл нам пакет, он живой. + */ + Client client = socket.getAttachment(); + client.updateHeartbeat(); } @Override public void onMessage(WebSocket socket, ByteBuffer byteBuffer) { Client client = socket.getAttachment(); - byte[] bytes = byteBuffer.array(); Stream stream = new Stream(bytes); int packetId = stream.readInt16(); - Class packetClass = this.packetManager.getPacketClass(packetId); - if(packetClass == null){ - System.out.println("Received unknown packet with id: " + packetId); + /** + * Обновляем время последнего полученного heartbeat. + * Так как клиент отпраивл нам пакет, он живой. + */ + client.updateHeartbeat(); + + if(!this.packetManager.hasPacketSupported(packetId)){ + /** + * Если пакет не поддерживается, отключаем клиента с соответствующим кодом ошибки. + */ + client.disconnect(Failures.UNSUPPORTED_PACKET); return; } + if(!this.packetManager.hasExecutorDelegated(packetId)){ + /** + * Если для пакета не назначен обработчик, отключаем клиента с соответствующим кодом ошибки. + */ + client.disconnect(Failures.UNSUPPORTED_PACKET); + return; + } + Class packetClass = this.packetManager.getPacketClass(packetId); + try { Packet packet = packetClass.getConstructor().newInstance(); packet.packetId = packetId; + /** + * Читаем данные пакета из потока. + */ packet.read(stream); - System.out.println("Received packet: " + packetClass.getSimpleName()); + /** + * Получаем обработчик пакета и вызываем его метод обработки. + */ + Class executorClass = this.packetManager.getExecutors().get(packetId); + PacketExecutor executor = executorClass.getConstructor().newInstance(); + executor.onPacketReceived(packet, client); } catch (Exception e) { - e.printStackTrace(); + System.out.println("Error while processing packet " + packetClass.getName()); + System.out.println(e.getStackTrace()); } } @Override public void onOpen(WebSocket socket, ClientHandshake arg1) { - Client client = new Client(socket); + /** + * Создаем нового клиента при открытии соединения. + * Передаем интервал heartbeat из настроек сервера. + * Если клиент не отправляет heartbeat в указанный интервал, его можно отключить. + */ + Client client = new Client(socket, this.settings.heartbeatInterval); socket.setAttachment(client); } @Override public void onStart() { - + /** + * Настраиваем планировщик для проверки активности клиентов. + */ + this.inactivityShedulerTweaker(); + + int port = this.getPort(); + System.out.println("\u001B[32mServer started at x.x.x.x:" + port + "\u001B[0m"); + } + + /** + * Планировщик для проверки активности клиентов. + * Если планировщик обнаруживает неактивного клиента, он отключает его с соответствующим кодом ошибки. + */ + public void inactivityShedulerTweaker() { + this.scheduler.scheduleAtFixedRate(() -> { + for(WebSocket socket : this.getConnections()) { + Client client = socket.getAttachment(); + if(!client.isAlive()) { + client.disconnect(Failures.INACTIVITY_TIMEOUT); + } + } + }, this.settings.heartbeatInterval, this.settings.heartbeatInterval, TimeUnit.MILLISECONDS); } } diff --git a/src/main/java/com/rosetta/im/protocol/Settings.java b/src/main/java/com/rosetta/im/protocol/Settings.java new file mode 100644 index 0000000..dbf9163 --- /dev/null +++ b/src/main/java/com/rosetta/im/protocol/Settings.java @@ -0,0 +1,18 @@ +package com.rosetta.im.protocol; + +public class Settings { + /** + * Порт сервера + */ + public int port = 8881; + /** + * Интервал отправки heartbeat пакетов в секундах. + * Если клиент не отправляет heartbeat пакеты в течение этого времени, сервер может считать его отключенным. + */ + public long heartbeatInterval = 30; + + public Settings(int port, long heartbeatInterval) { + this.port = port; + this.heartbeatInterval = heartbeatInterval; + } +} diff --git a/src/main/java/com/rosetta/im/protocol/packet/PacketExecutor.java b/src/main/java/com/rosetta/im/protocol/packet/PacketExecutor.java index bca1a0a..c1bac51 100644 --- a/src/main/java/com/rosetta/im/protocol/packet/PacketExecutor.java +++ b/src/main/java/com/rosetta/im/protocol/packet/PacketExecutor.java @@ -1,5 +1,7 @@ package com.rosetta.im.protocol.packet; +import com.rosetta.im.protocol.Client; + public interface PacketExecutor { - public void onPacketReceived(Class packet); + public void onPacketReceived(Packet packet, Client client); } diff --git a/src/main/java/com/rosetta/im/protocol/packet/PacketManager.java b/src/main/java/com/rosetta/im/protocol/packet/PacketManager.java index cabd5dd..2118fe5 100644 --- a/src/main/java/com/rosetta/im/protocol/packet/PacketManager.java +++ b/src/main/java/com/rosetta/im/protocol/packet/PacketManager.java @@ -2,20 +2,48 @@ package com.rosetta.im.protocol.packet; import java.util.HashMap; +/** + * Менеджер сетевых пакетов и их обработчиков. + */ public class PacketManager { private HashMap> packets; + private HashMap> executors; public PacketManager() { this.packets = new HashMap<>(); + this.executors = new HashMap<>(); } + /** + * Регистрирует пакет с указанным ID. + * @param packetId ID пакета + * @param packet Класс пакета + */ public void registerPacket(int packetId, Class packet) { this.packets.put(packetId, packet); } - public boolean isPacketRegistred(Packet packet) { - return this.packets.containsValue(packet.getClass()); + /** + * Проверяет, зарегистрирован ли обработчик для пакета с указанным ID. + * @param packetId ID пакета + * @return true, если обработчик зарегистрирован, иначе false. + */ + public boolean hasExecutorDelegated(int packetId) { + return this.executors.containsKey(packetId); + } + + /** + * Проверяет, поддерживается ли пакет с указанным ID. + * @param packetId ID пакета + * @return true, если пакет поддерживается, иначе false. + */ + public boolean hasPacketSupported(int packetId) { + return this.packets.containsKey(packetId); + } + + public HashMap> getExecutors() { + return this.executors; } public Integer getPacketIdByClass(Class packetClass) { @@ -31,5 +59,33 @@ public class PacketManager { return this.packets.get(packetId); } + /** + * Регистрирует обработчик пакета с указанным ID. + * @param packetId ID пакета + * @param executor Обработчик пакета + */ + public void registerExecutor(int packetId, Class executor) { + if (this.executors == null) { + this.executors = new HashMap<>(); + } + this.executors.put(packetId, executor); + } + + /** + * Возвращает общее количество зарегистрированных обработчиков пакетов. + * @return Количество обработчиков пакетов. + */ + public Integer totalExecutors() { + return this.executors.size(); + } + + /** + * Возвращает общее количество зарегистрированных пакетов. + * @return Количество пакетов. + */ + public Integer totalPackets() { + return this.packets.size(); + } + } diff --git a/target/classes/com/rosetta/im/Main.class b/target/classes/com/rosetta/im/Main.class index 33955d8..4dd96c5 100644 Binary files a/target/classes/com/rosetta/im/Main.class and b/target/classes/com/rosetta/im/Main.class differ diff --git a/target/classes/com/rosetta/im/executors/Executor0Handshake.class b/target/classes/com/rosetta/im/executors/Executor0Handshake.class index 1b3aead..bd5c676 100644 Binary files a/target/classes/com/rosetta/im/executors/Executor0Handshake.class and b/target/classes/com/rosetta/im/executors/Executor0Handshake.class differ diff --git a/target/classes/com/rosetta/im/packet/Packet0Handshake.class b/target/classes/com/rosetta/im/packet/Packet0Handshake.class index 20b0d02..3210067 100644 Binary files a/target/classes/com/rosetta/im/packet/Packet0Handshake.class and b/target/classes/com/rosetta/im/packet/Packet0Handshake.class differ diff --git a/target/classes/com/rosetta/im/protocol/Client.class b/target/classes/com/rosetta/im/protocol/Client.class index 0a642fb..f718965 100644 Binary files a/target/classes/com/rosetta/im/protocol/Client.class and b/target/classes/com/rosetta/im/protocol/Client.class differ diff --git a/target/classes/com/rosetta/im/protocol/Server.class b/target/classes/com/rosetta/im/protocol/Server.class index ca113bb..c49548f 100644 Binary files a/target/classes/com/rosetta/im/protocol/Server.class and b/target/classes/com/rosetta/im/protocol/Server.class differ diff --git a/target/classes/com/rosetta/im/protocol/packet/PacketExecutor.class b/target/classes/com/rosetta/im/protocol/packet/PacketExecutor.class index a4c2f31..7706236 100644 Binary files a/target/classes/com/rosetta/im/protocol/packet/PacketExecutor.class and b/target/classes/com/rosetta/im/protocol/packet/PacketExecutor.class differ diff --git a/target/classes/com/rosetta/im/protocol/packet/PacketManager.class b/target/classes/com/rosetta/im/protocol/packet/PacketManager.class index 972ceaf..5428209 100644 Binary files a/target/classes/com/rosetta/im/protocol/packet/PacketManager.class and b/target/classes/com/rosetta/im/protocol/packet/PacketManager.class differ