Client: CommandValidator and initialRequest were added,fixed behaviour if server is unreachable; Server: GetCommandsCommand was added

This commit is contained in:
2026-04-13 21:54:43 +03:00
parent d97bbed00f
commit f088d05731
6 changed files with 128 additions and 92 deletions
+31 -52
View File
@@ -2,51 +2,43 @@ import network.NetworkManager
import input.HumanBeingBuilder import input.HumanBeingBuilder
import input.IOManager import input.IOManager
import network.Request import network.Request
import runner.CommandValidator
// BEGIN ---- Move it to CommandValidator
// Команды, которые требуют ввода HumanBeing
private val HUMAN_BEING_COMMANDS = setOf("add", "add_if_max", "add_if_min")
// Команды с аргументом (UUID) в строке команды
private val ARG_COMMANDS = setOf("update", "remove_by_id", "execute_script")
// Все команды, доступные клиенту (save — только серверная)
private val KNOWN_COMMANDS = setOf(
"help", "info", "show", "add", "update",
"remove_by_id", "clear", "execute_script",
"add_if_max", "add_if_min", "history",
"sum_of_minutes_of_waiting", "min_by_name",
"print_field_descending_minutes_of_waiting",
"exit"
)
// END ---- Move it to CommandValidator
fun main(args: Array<String>) { fun main(args: Array<String>) {
val host = if (args.isNotEmpty()) args[0] else "localhost" val host = if (args.isNotEmpty()) args[0] else "localhost"
val port = if (args.size >= 2) args[1].toIntOrNull() ?: 8080 else 8080 val port = if (args.size >= 2) args[1].toIntOrNull() ?: 8080 else 8080
val io = IOManager() val io = IOManager() // Name by Boris Bosenko
val client = NetworkManager(host, port) val client = NetworkManager(host, port)
val validator = CommandValidator()
io.print(""" // Shutdown hook
|╔══════════════════════════════════════╗
|║ Lab6 Client — подключение к $host:$port
|╚══════════════════════════════════════╝
|Введите 'help' для справки.
""".trimMargin())
// Shutdown hook — закрыть канал при выходе
Runtime.getRuntime().addShutdownHook(Thread { Runtime.getRuntime().addShutdownHook(Thread {
client.close() client.close()
}) })
runClientRepl(io, client) io.print("""
|════════════════════════════════════════════
|Lab6 Client — подключение к $host:$port
|════════════════════════════════════════════
""".trimMargin())
runClientRepl(io, client, validator)
}
fun runClientRepl(io: IOManager, client: NetworkManager, validator: CommandValidator) {
val initialRequest = Request(
commandName = "getCommandsCommand",
humanBeing = null
)
if (client.sendRequest(initialRequest) == null) {
io.print("[!] Сервер недоступен. Попробуйте позже.")
} else {
io.print("|Введите 'help' для справки.")
} }
fun runClientRepl(io: IOManager, client: NetworkManager) {
while (true) { while (true) {
val line = io.readLine(" ☭ ") ?: break val line = io.readLine(" ☭ ") ?: break
if (line.isBlank()) continue if (line.isBlank()) continue
@@ -55,9 +47,10 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
val commandName = parts[0].lowercase() val commandName = parts[0].lowercase()
val args = parts.drop(1) val args = parts.drop(1)
// Move to CommandValidate val (isValidCommand, message) = validator.validate(commandName, args)
if (commandName !in KNOWN_COMMANDS) {
io.print("Неизвестная команда: '$commandName'. Введите 'help' для справки.") if (!isValidCommand) {
if (message != null) io.print(message)
continue continue
} }
@@ -66,26 +59,12 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
break break
} }
// In the event of using fucking script (don't)
if (commandName == "execute_script") { if (commandName == "execute_script") {
if (args.isEmpty()) {
io.print("[Ошибка] Укажите путь к файлу. Пример: execute_script script.txt")
continue
}
io.addScriptScanner(args[0]) io.addScriptScanner(args[0])
continue continue
} }
// MOVE TO FUCKING VALIDATE val humanBeing = if (validator.isBuildsHumanBeing(commandName)) {
// 4. Для update — нужен UUID аргумент ДО ввода HumanBeing
if (commandName == "update" && args.isEmpty()) {
io.print("[Ошибка] Укажите id элемента. Пример: update <uuid>")
continue
}
// 5. Строим HumanBeing если нужно
val humanBeing = if (commandName in HUMAN_BEING_COMMANDS ||
commandName == "update") {
try { try {
HumanBeingBuilder(io).build() HumanBeingBuilder(io).build()
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
@@ -94,7 +73,7 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
} }
} else null } else null
// 6. Формируем запрос и отправляем // Формируем запрос и отправляем
val request = Request( val request = Request(
commandName = commandName, commandName = commandName,
args = args, args = args,
@@ -103,7 +82,7 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
val response = client.sendRequest(request) val response = client.sendRequest(request)
// 7. Обрабатываем ответ // Обрабатываем ответ
if (response == null) { if (response == null) {
io.print("[!] Сервер недоступен. Попробуйте позже.") io.print("[!] Сервер недоступен. Попробуйте позже.")
} else { } else {
@@ -3,47 +3,44 @@ package network
import kotlinx.serialization.SerializationException import kotlinx.serialization.SerializationException
import lab6.prog.network.AppJson import lab6.prog.network.AppJson
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.PortUnreachableException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.channels.DatagramChannel import java.nio.channels.DatagramChannel
/**
* UDP-клиент на основе DatagramChannel в неблокирующем режиме.
*
* При недоступности сервера уведомляет пользователя и повторяет
* попытку каждые [retryDelayMS] мс, не более [maxRetries] раз.
*
* @property host адрес сервера
* @property port порт сервера
*/
class NetworkManager( class NetworkManager(
private val host: String, private val host: String,
private val port: Int, private val port: Int,
private val bufferSize: Int = 65507, private val bufferSize: Int = 65507,
private val timeoutMS: Long = 3000, private val timeoutMS: Long = 3000,
private val retryDelayMS: Long = 5000, private val retryDelayMS: Long = 5000,
private val maxRetries: Int = 3 private val maxRetries: Int = 3,
) { ) {
private val channel: DatagramChannel = DatagramChannel.open().also { private var channel: DatagramChannel = openChannel()
private fun openChannel(): DatagramChannel =
DatagramChannel.open().also {
it.configureBlocking(false) it.configureBlocking(false)
it.connect(InetSocketAddress(host, port)) it.connect(InetSocketAddress(host, port))
} }
/**
* Отправляет [Request] и возвращает [Response].
* При недоступности сервера возвращает null.
*/
fun sendRequest(request: Request): Response? { fun sendRequest(request: Request): Response? {
val requestBytes = AppJson.encodeToString(Request.serializer(), request).toByteArray(Charsets.UTF_8) val requestBytes = AppJson.encodeToString(Request.serializer(), request)
.toByteArray(Charsets.UTF_8)
repeat(maxRetries) { attempt -> repeat(maxRetries) { attempt ->
try {
channel.write(ByteBuffer.wrap(requestBytes)) channel.write(ByteBuffer.wrap(requestBytes))
val response = waitForResponse() val response = waitForResponse()
if (response != null) return response if (response != null) return response
} catch (e: PortUnreachableException) {
reconnect()
} catch (e: Exception) {
System.err.println("[Client] Ошибка отправки: ${e.message}")
reconnect()
}
println( println(
"[Client] Сервер не отвечает " + "[Client] Сервер не отвечает " +
"(попытка ${attempt + 1}/$maxRetries). " + "(попытка ${attempt + 1}/$maxRetries). " +
@@ -56,29 +53,36 @@ class NetworkManager(
return null return null
} }
/**
* Опрашивает канал до [timeoutMS] мс, возвращает ответ или null.
*/
// catch java.net.PortUnreachableException if server is no more reachable
private fun waitForResponse(): Response? { private fun waitForResponse(): Response? {
val buf = ByteBuffer.allocate(bufferSize) val buf = ByteBuffer.allocate(bufferSize)
val deadline = System.currentTimeMillis() + timeoutMS val deadline = System.currentTimeMillis() + timeoutMS
while (System.currentTimeMillis() < deadline) { while (System.currentTimeMillis() < deadline) {
try {
buf.clear() buf.clear()
if (channel.read(buf) > 0) { if (channel.read(buf) > 0) {
buf.flip() buf.flip()
return try { return AppJson.decodeFromString<Response>(
AppJson.decodeFromString<Response>(Charsets.UTF_8.decode(buf).toString()) Charsets.UTF_8.decode(buf).toString()
)
}
} catch (e: PortUnreachableException) {
return null
} catch (e: SerializationException) { } catch (e: SerializationException) {
System.err.println("[Client] Некорректный ответ сервера: ${e.message}") System.err.println("[Client] Некорректный ответ сервера: ${e.message}")
null return null
}
} }
Thread.sleep(50) Thread.sleep(50)
} }
return null return null
} }
fun close() = channel.close() private fun reconnect() {
try { channel.close() } catch (_: Exception) {}
channel = openChannel()
}
fun close() {
try { channel.close() } catch (_: Exception) {}
}
} }
@@ -1,4 +1,40 @@
package runner package runner
class CommandValidator { class CommandValidator {
// Команды, которые требуют ввода HumanBeing
private val humanBeingCommands = setOf("add", "add_if_max", "add_if_min")
// Команды с аргументом
private val commandsWArgs = setOf("update", "remove_by_id", "execute_script")
// Все команды, доступные клиенту
private val knownCommands = setOf(
"help", "info", "show", "add", "update",
"remove_by_id", "clear", "execute_script",
"add_if_max", "add_if_min", "history",
"sum_of_minutes_of_waiting", "min_by_name",
"print_field_descending_minutes_of_waiting",
"exit"
)
fun validate(command: String, args: List<String>?): Pair<Boolean, String?> {
if (command !in knownCommands){
return Pair(false, "Неизвестная команда: '$command'. Введите 'help' для справки.")
}
if (command in commandsWArgs && args?.isEmpty() == true) {
return if (command == "execute_script") {
Pair(false, "[Ошибка] Укажите путь к файлу. Пример: execute_script script.txt")
} else {
// maybe create another set w commands w file args and another for other args
Pair(false, "[Ошибка] Укажите id элемента. Пример: $command <uuid>")
}
}
return Pair(true, null)
}
fun isBuildsHumanBeing(command: String): Boolean {
return command in humanBeingCommands
}
} }
+1
View File
@@ -0,0 +1 @@
id,name,coordX,coordY,creationDate,realHero,hasToothpick,impactSpeed,soundtrackName,minutesOfWaiting,weaponType,carCool
1 id name coordX coordY creationDate realHero hasToothpick impactSpeed soundtrackName minutesOfWaiting weaponType carCool
+1
View File
@@ -67,6 +67,7 @@ fun registerServerCommands(
AddIfMaxCommand(manager), AddIfMaxCommand(manager),
AddIfMinCommand(manager), AddIfMinCommand(manager),
HistoryCommand(invoker), HistoryCommand(invoker),
GetCommandsCommand(invoker),
SumOfMinutesCommand(manager), SumOfMinutesCommand(manager),
MinByNameCommand(manager), MinByNameCommand(manager),
PrintDescendingMinutesCommand(manager), PrintDescendingMinutesCommand(manager),
@@ -0,0 +1,15 @@
package commands
import models.HumanBeing
import runner.CommandInvoker
class GetCommandsCommand (
private val invoker: CommandInvoker
): Command {
override val name: String = "GetCommands"
override val description: String = "Получает все доступные с сервера команды"
override fun execute(args: List<String>, humanBeing: HumanBeing?): String {
return invoker.getCommands().toString()
}
}