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.IOManager
import network.Request
// 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
import runner.CommandValidator
fun main(args: Array<String>) {
val host = if (args.isNotEmpty()) args[0] else "localhost"
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 validator = CommandValidator()
io.print("""
|╔══════════════════════════════════════╗
|║ Lab6 Client — подключение к $host:$port
|╚══════════════════════════════════════╝
|Введите 'help' для справки.
""".trimMargin())
// Shutdown hook — закрыть канал при выходе
// Shutdown hook
Runtime.getRuntime().addShutdownHook(Thread {
client.close()
})
runClientRepl(io, client)
io.print("""
|════════════════════════════════════════════
|Lab6 Client — подключение к $host:$port
|════════════════════════════════════════════
""".trimMargin())
runClientRepl(io, client, validator)
}
fun runClientRepl(io: IOManager, client: NetworkManager) {
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' для справки.")
}
while (true) {
val line = io.readLine(" ☭ ") ?: break
if (line.isBlank()) continue
@@ -55,9 +47,10 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
val commandName = parts[0].lowercase()
val args = parts.drop(1)
// Move to CommandValidate
if (commandName !in KNOWN_COMMANDS) {
io.print("Неизвестная команда: '$commandName'. Введите 'help' для справки.")
val (isValidCommand, message) = validator.validate(commandName, args)
if (!isValidCommand) {
if (message != null) io.print(message)
continue
}
@@ -66,26 +59,12 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
break
}
// In the event of using fucking script (don't)
if (commandName == "execute_script") {
if (args.isEmpty()) {
io.print("[Ошибка] Укажите путь к файлу. Пример: execute_script script.txt")
continue
}
io.addScriptScanner(args[0])
continue
}
// MOVE TO FUCKING VALIDATE
// 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") {
val humanBeing = if (validator.isBuildsHumanBeing(commandName)) {
try {
HumanBeingBuilder(io).build()
} catch (e: IllegalStateException) {
@@ -94,7 +73,7 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
}
} else null
// 6. Формируем запрос и отправляем
// Формируем запрос и отправляем
val request = Request(
commandName = commandName,
args = args,
@@ -103,7 +82,7 @@ fun runClientRepl(io: IOManager, client: NetworkManager) {
val response = client.sendRequest(request)
// 7. Обрабатываем ответ
// Обрабатываем ответ
if (response == null) {
io.print("[!] Сервер недоступен. Попробуйте позже.")
} else {
@@ -3,46 +3,43 @@ package network
import kotlinx.serialization.SerializationException
import lab6.prog.network.AppJson
import java.net.InetSocketAddress
import java.net.PortUnreachableException
import java.nio.ByteBuffer
import java.nio.channels.DatagramChannel
/**
* UDP-клиент на основе DatagramChannel в неблокирующем режиме.
*
* При недоступности сервера уведомляет пользователя и повторяет
* попытку каждые [retryDelayMS] мс, не более [maxRetries] раз.
*
* @property host адрес сервера
* @property port порт сервера
*/
class NetworkManager(
private val host: String,
private val port: Int,
private val bufferSize: Int = 65507,
private val timeoutMS: Long = 3000,
private val retryDelayMS: Long = 5000,
private val maxRetries: Int = 3
private val bufferSize: Int = 65507,
private val timeoutMS: Long = 3000,
private val retryDelayMS: Long = 5000,
private val maxRetries: Int = 3,
) {
private val channel: DatagramChannel = DatagramChannel.open().also {
it.configureBlocking(false)
it.connect(InetSocketAddress(host, port))
}
private var channel: DatagramChannel = openChannel()
private fun openChannel(): DatagramChannel =
DatagramChannel.open().also {
it.configureBlocking(false)
it.connect(InetSocketAddress(host, port))
}
/**
* Отправляет [Request] и возвращает [Response].
* При недоступности сервера возвращает null.
*/
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 ->
try {
channel.write(ByteBuffer.wrap(requestBytes))
val response = waitForResponse()
if (response != null) return response
channel.write(ByteBuffer.wrap(requestBytes))
val response = waitForResponse()
if (response != null) return response
} catch (e: PortUnreachableException) {
reconnect()
} catch (e: Exception) {
System.err.println("[Client] Ошибка отправки: ${e.message}")
reconnect()
}
println(
"[Client] Сервер не отвечает " +
@@ -56,29 +53,36 @@ class NetworkManager(
return null
}
/**
* Опрашивает канал до [timeoutMS] мс, возвращает ответ или null.
*/
// catch java.net.PortUnreachableException if server is no more reachable
private fun waitForResponse(): Response? {
val buf = ByteBuffer.allocate(bufferSize)
val deadline = System.currentTimeMillis() + timeoutMS
while (System.currentTimeMillis() < deadline) {
buf.clear()
if (channel.read(buf) > 0) {
buf.flip()
return try {
AppJson.decodeFromString<Response>(Charsets.UTF_8.decode(buf).toString())
} catch (e: SerializationException) {
System.err.println("[Client] Некорректный ответ сервера: ${e.message}")
null
try {
buf.clear()
if (channel.read(buf) > 0) {
buf.flip()
return AppJson.decodeFromString<Response>(
Charsets.UTF_8.decode(buf).toString()
)
}
} catch (e: PortUnreachableException) {
return null
} catch (e: SerializationException) {
System.err.println("[Client] Некорректный ответ сервера: ${e.message}")
return null
}
Thread.sleep(50)
}
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
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
}
}