diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 70b871fb..00000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,18 +0,0 @@ -# Для всех: - -Указать Фамилию Имя, № группы (можно не в теле PR, а в имени) - -Поставить label соответствующий сдаваемой лабе - -# Для тех, кто в тело PR вставляет отчет: - -Отчет должен содержать следующие элементы: - -### Инструкция по использованию - -### Инструкция по сборке/установке - -### Описание используемого протокола. - -Данные пункты не указывают строгий формат отчета, а напоминают, что -он должен содержать. diff --git a/README.md b/README.md index b69f039a..4939e3f0 100644 --- a/README.md +++ b/README.md @@ -1,91 +1,49 @@ -# Лабораторные работы за осенний семестр 2020 - -В `master` ветке данного репозитория присутствуют актуальные задания, включая -распределение протоколов по студентам. - -# Прогресс - -Легенда: - -| Символ | Значение | -| -- | -- | -| | Работа не сдана или находится на рассмотрении | -| + | Работа принята | -| ! | Работа рассмотрена, требуется демо | -| ? | Работа рассмотрена, но требует доработки | -| - | Работу сдавать не требуется | -| -------- | -------- | -| SSH ключ | [Отправить SSH ключ преподавателю](https://insysnw.github.io/labs/900-ssh-keygen/) | -| Л.1a | [TCP чат](https://insysnw.github.io/labs/01-tcp-chat/) | -| Л.1б | [TCP чат](https://insysnw.github.io/labs/01-tcp-chat/) (неблокирующие сокеты) | -| Л.2c | [UDP сервер существующего протокола](https://insysnw.github.io/labs/02-udp-real-protocol/) | -| Л.2к | [UDP клиент существующего протокола](https://insysnw.github.io/labs/02-udp-real-protocol/) | -| Л.3с | Сервер для задания из методички (пункт 1.2.`n`) | -| Л.3к | Клиент для задания из методички (пункт 1.2.`n`) | - -## Группа 201 - -| ФИО | SSH ключ | Л.1a | Л.1б | Л.2c | Л.2к | Л.3 | -| -- | -- | -- | -- | -- | -- | -- | -| Антропова А.А. | + | | | | | | -| Белов Е.А. | + | [?](../../pull/10) | | tftp | dhcp | | -| Буй К.Д. | + | | | tftp | dns | | -| Гладкова Е.Д. | + | | | | | | -| Голзицкий Н.С. | + | | | | | | -| Гуляев Д.В. | | | | | | | -| Данилов А.И. | + | [?](../../pull/8) | | dhcp | ntp | | -| Казанджи М.А. | + | [?](../../pull/7) | [?](../../pull/7) | ntp | snmp | | -| Киселев Н.Д. | + | | | dns | tftp | | -| Лялин А.С. | + | | | | | | -| Натура А.А. | + | | | snmp | dns | | -| Никитин И.Н. | + | | | | | | -| Романов А.Л. | + | | | dns | dhcp | | -| Свечников Р.А. | + | [?](../../pull/6) | | dhcp | dns | | -| Сибагатулин А.Ф. | | | | | | | -| Товпеко К.А. | + | [?](../../pull/2) | [?](../../pull/2) | dns | ntp | | -| Черноног С.А. | + | | | | | | -| Шаляпин Г.А. | + | | | | | | - -## Группа 203 - -| ФИО | Ssh ключ | Л.1a | Л.1б | Л.2с | Л.2к | Л.3с | Л.3к | -| -- | -- | -- | -- | -- | -- | -- | -- | -| Ворошилов А.А. | + | | | | | | | -| Гусев Н.С. | | | | | | | | -| Зарецкая Е.С. | | | | ntp | tftp | 17 | 18 | -| Иванов И.Д. | + | | [+](../../pull/13) | dns | ntp | 8 | 5 | -| Калашников Р.А. | | | | dns | snmp | | | -| Костарев В.И. | + | | | dhcp | snmp | | | -| Любченкова А.А. | + | | | tftp | dns | 18 | 17 | -| Меньшов П.А. | + | | | dns | tftp | | | -| Морозов Е.С. | + | | | dns | dhcp | | | -| Никитина Д.С. | + | | | | | | | -| Овсянников Е.А. | + | [?](../../pull/11) | | tftp | ntp | | | -| Орлова П.А. | + | | | tftp | dns | | | -| Пентегов А.О. | + | | | | | | | -| Семёнов Д.С. | + | | | dhcp | dns | 19 | 7 | -| Середин К.В. | + | [?](../../pull/5) | | dhcp | ntp | 7 | 19 | -| Трушин И.А. | + | | | tftp | dhcp | | | -| Черникова А.С. | + | | | tftp | ntp | 5 | 8 | -| Шелепов В.А. | + | | | dhcp | tftp | | | - -# Требования к отчету: - -* Инструкция по использованию; -* Инструкция по сборке/установке; -* Описание используемого протокола; - * Своего для первой и третьей лабораторной; - * Используемого подмножества для второй; -* ??? -* PROFIT - -Отчет можно писать как в сообщении к PR-у, так и присылать в иных -форматах (`.pdf`, `.docx`, `.txt` и т.п.). - -# Порядок сдачи - -* Fork от данного репозитория -* Push каждой лабораторной в отдельную ветку -* Создание отдельного PR на каждую лабораторную - -При создании PR, в качестве напоминалки, сделан шаблон. +# Лабораторная работа № 1 + +Работа выполнена на языке Kotlin (версия Kotlin 1.4.20, версия Java 15.0.1). +Сборка производится с помощью Gradle (версия 6.5.1) + +# Описание +При создании клиента необходимо ввести имя. Оно должно быть уникальным и не содержать символов `[` и `]`. +Чтобы завершить работу, необходимо ввести `!q`. +Помимо обычных сообщений передаются также служебные уведомления о подключении или отключении пользователей. + +# Использование +Названия jar-файлов (здесь и в дальнейшем без расширения): + +| Тип сокетов | Сервер | Клиент | +| ------------- |------------- | ------------- | +| блокирующие | `bserver` | `bclient` | +| неблокирующие | `nbserver` | `nbclient` | + +## Сборка +Сборка всех jar-файлов: +``` +gradle all +``` +Сборка конкретного файла: +``` +gradle (name) +``` + +## Запуск +``` +java -jar (name).jar [ip [port]] +``` + +# Протокол +Каждое сообщение наследует класс `Message`. Возможные типы сообщений: + +| Тип | Описание | Поля +| --- | --- | --- +| `ConnectionRequest` | клиент подключается к серверу | `username` +| `ConnectionResponse` | сервер рассылает всем уведомления о новом клиенте | `username`, `date` +| `ConnectionDenied` | отказ клиенту в подключении из-за неуникального ника | `username` +| `DisconnectionRequest` | клиент отключается от сервера | `username` +| `DisconnectionResponse` | сервер рассылает всем уведомления об отключении клиента |`username`, `date` +| `UserMessage` | сообщение клиента |`username`, `message`, `date` + +Поля: +* `username`: ник пользователя +* `date`: время прихода сообщения на сервер +* `message`: сообщение \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..090e954f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,38 @@ +val jarTasks = mapOf( + "bclient" to "blocking.Client", + "bserver" to "blocking.Server", + "nbclient" to "non_blocking.Client", + "nbserver" to "non_blocking.Server" +) + +plugins { + id("org.jetbrains.kotlin.jvm") version "1.4.20" +} + +repositories { + jcenter() +} + +sourceSets["main"].java { srcDir("src") } + +tasks.withType { + destinationDirectory.set(rootDir) +} + +for (taskMainClass in jarTasks) { + tasks.register(taskMainClass.key) { + from(sourceSets["main"].output) { + manifest { + attributes["Main-Class"] = taskMainClass.value + } + archiveFileName.set("${taskMainClass.key}.jar") + from(configurations.compileClasspath.map { config -> + config.map { if (it.isDirectory) it else zipTree(it) } + }) + } + } +} + +tasks.register("all") { + dependsOn(jarTasks.keys) +} \ No newline at end of file diff --git a/src/blocking/Client.kt b/src/blocking/Client.kt new file mode 100644 index 00000000..89a24a07 --- /dev/null +++ b/src/blocking/Client.kt @@ -0,0 +1,118 @@ +package blocking + +import common.* +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.net.Socket +import kotlin.system.exitProcess + +class Client(addr: String, port: Int) { + private var socket: Socket + private lateinit var socketInput: ObjectInputStream + private lateinit var socketOutput: ObjectOutputStream + private var username: String? = null + + init { + try { + socket = Socket(addr, port) + } catch (e: IOException) { + System.err.println(Strings.SOCKET_NOT_CREATED) + exitProcess(-1) + } + try { + socketInput = ObjectInputStream(socket.getInputStream()) + socketOutput = ObjectOutputStream(socket.getOutputStream()) + + enterUsername() + readThread().start() + writeThread().start() + } catch (e: IOException) { + shutdown(Status.EXCEPTION) + } + } + + private fun ObjectOutputStream.writeAndFlush(obj: Any) { + writeObject(obj) + flush() + } + + private fun enterUsername() { + print(Strings.ENTER_USERNAME) + while (username == null) { + try { + var userInput = readLine() + while (userInput != null && userInput.contains(Regex("""[\[\]]"""))) { + System.err.print(Strings.BAD_USERNAME) + userInput = readLine() + } + + socketOutput.writeAndFlush(ConnectionRequest(userInput!!)) + + when (socketInput.readObject()) { + is ConnectionResponse -> { + username = userInput + println(Strings.HELLO(username)) + } + is ConnectionDenied -> { + System.err.print(Strings.TAKEN_USERNAME) + } + } + } catch (e: IOException) { + shutdown(Status.EXCEPTION) + } + } + } + + private fun shutdown(status: Status) { + try { + if (!socket.isClosed) { + socketInput.close() + socketOutput.close() + socket.close() + } + } catch (e: IOException) { + } + println(status.message) + exitProcess(status.code) + } + + private fun readThread() = Thread { + try { + while (!socket.isClosed) { + println(socketInput.readObject()) + } + } catch (e: IOException) { + shutdown(Status.EXCEPTION) + } + } + + private fun writeThread() = Thread { + try { + while (!socket.isClosed) { + val userInput = readLine() + if (userInput == STOP_WORD || userInput == null) { + socketOutput.writeAndFlush(DisconnectionRequest(username!!)) + shutdown(Status.OK) + break + } else { + socketOutput.writeAndFlush(UserMessage(username!!, userInput)) + } + } + } catch (e: IOException) { + shutdown(Status.EXCEPTION) + } + } + + companion object { + @JvmStatic + fun main(args: Array) { + when { + args.size >= 2 -> Client(args[0], args[1].toIntOrNull() ?: DEFAULT_PORT) + args.size == 1 -> Client(args[0], DEFAULT_PORT) + else -> Client(DEFAULT_ADDRESS, DEFAULT_PORT) + } + } + } +} + diff --git a/src/blocking/Server.kt b/src/blocking/Server.kt new file mode 100644 index 00000000..0db47704 --- /dev/null +++ b/src/blocking/Server.kt @@ -0,0 +1,112 @@ +package blocking + +import common.* +import java.io.IOException +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.net.InetAddress +import java.net.ServerSocket +import java.net.Socket +import kotlin.system.exitProcess + +class Server(addr: String, port: Int) { + val connections = mutableMapOf() + + init { + val server = try { + ServerSocket(port, -1, InetAddress.getByName(addr)) + } catch (e: Exception) { + System.err.println(Strings.SERVER_NOT_STARTED) + exitProcess(-1) + } + println(Strings.SERVER_STARTED) + server.use { + while (true) { + val socket = it.accept() + try { + ServerConnection(socket) + } catch (e: IOException) { + socket.close() + } + } + } + } + + inner class ServerConnection(private val socket: Socket) : Thread() { + private val output = ObjectOutputStream(socket.getOutputStream()) + private val input = ObjectInputStream(socket.getInputStream()) + + init { + start() + } + + private fun ObjectOutputStream.writeAndFlush(obj: Any) { + writeObject(obj) + flush() + } + + override fun run() { + try { + listener@ while (!socket.isClosed) { + when (val received = try { + input.readObject() + } catch (e: IOException) { + DisconnectionRequest(connections[this] ?: return) + }) { + is ConnectionRequest -> { + val username = received.username + if (username in connections.values) + output.writeAndFlush(ConnectionDenied(username)) + else { + connections[this] = username + println(ConnectionResponse(username)) + connections.forEach { + it.key.output.writeAndFlush(ConnectionResponse(username)) + } + } + } + is DisconnectionRequest -> { + println(DisconnectionResponse(received.username)) + connections.remove(this) + connections.forEach { + it.key.output.writeAndFlush(DisconnectionResponse(received.username)) + } + shutdown() + break@listener + } + is UserMessage -> { + println(received) + connections.forEach { + it.key.output.writeAndFlush(received) + } + } + } + + } + } catch (e: IOException) { + connections.remove(this) + shutdown() + } + } + + private fun shutdown() { + if (!socket.isClosed) { + input.close() + output.close() + socket.close() + this.interrupt() + } + } + } + + companion object { + @JvmStatic + fun main(args: Array) { + when { + args.size >= 2 -> Server(args[0], args[1].toIntOrNull() ?: DEFAULT_PORT) + args.size == 1 -> Server(args[0], DEFAULT_PORT) + else -> Server(DEFAULT_ADDRESS, DEFAULT_PORT) + } + } + } +} diff --git a/src/common/Config.kt b/src/common/Config.kt new file mode 100644 index 00000000..36ba79d6 --- /dev/null +++ b/src/common/Config.kt @@ -0,0 +1,5 @@ +package common + +const val DEFAULT_ADDRESS = "localhost" +const val DEFAULT_PORT = 8080 +const val STOP_WORD = "!q" \ No newline at end of file diff --git a/src/common/Message.kt b/src/common/Message.kt new file mode 100644 index 00000000..854d2416 --- /dev/null +++ b/src/common/Message.kt @@ -0,0 +1,27 @@ +package common + +import java.io.Serializable +import java.text.SimpleDateFormat +import java.util.* + +sealed class Message : Serializable { + abstract var username: String +} + +class ConnectionRequest(override var username: String) : Message() +class ConnectionResponse(override var username: String, var date: Date? = null) : Message() { + override fun toString() = "${formatDate(date)} $username присоединился к чату" +} + +class ConnectionDenied(override var username: String) : Message() + +class DisconnectionRequest(override var username: String) : Message() +class DisconnectionResponse(override var username: String, var date: Date? = null) : Message() { + override fun toString() = "${formatDate(date)} $username вышел из чата" +} + +class UserMessage(override var username: String, var message: String, var date: Date? = null) : Message() { + override fun toString() = "${formatDate(date)} [$username] $message" +} + +fun formatDate(date: Date?) = "<${SimpleDateFormat("HH:mm:ss").format(date ?: Date())}>" diff --git a/src/common/Status.kt b/src/common/Status.kt new file mode 100644 index 00000000..26c78f44 --- /dev/null +++ b/src/common/Status.kt @@ -0,0 +1,5 @@ +package common + +enum class Status(val code: Int, val message: String) { + OK(0, Strings.STATUS_OK), EXCEPTION(-1, Strings.STATUS_EXCEPTION) +} \ No newline at end of file diff --git a/src/common/Strings.kt b/src/common/Strings.kt new file mode 100644 index 00000000..c5be4026 --- /dev/null +++ b/src/common/Strings.kt @@ -0,0 +1,13 @@ +package common + +object Strings { + const val SERVER_STARTED = "Сервер запущен" + const val SERVER_NOT_STARTED = "Сервер не запущен" + const val SOCKET_NOT_CREATED = "Не удалось создать сокет" + const val ENTER_USERNAME = "Введите имя: " + const val BAD_USERNAME = "Это имя содержит недопустимые символы \"[\" или \"]\". Введите другое имя: " + const val TAKEN_USERNAME = "Это имя занято. Введите другое имя: " + const val STATUS_OK = "Ok" + const val STATUS_EXCEPTION = "Соединение разорвано" + val HELLO = { username: String? -> "Привет, $username! Соединение установлено" } +} \ No newline at end of file diff --git a/src/non_blocking/Client.kt b/src/non_blocking/Client.kt new file mode 100644 index 00000000..416a5793 --- /dev/null +++ b/src/non_blocking/Client.kt @@ -0,0 +1,103 @@ +package non_blocking + +import common.* +import java.io.IOException +import java.net.InetSocketAddress +import java.nio.channels.SocketChannel +import kotlin.system.exitProcess + +class Client(addr: String, port: Int) { + private var socketChannel: SocketChannel + private var username: String? = null + + init { + try { + socketChannel = SocketChannel.open(InetSocketAddress(addr, port)) + } catch (e: IOException) { + System.err.println(Strings.SOCKET_NOT_CREATED) + exitProcess(-1) + } + + try { + enterUsername() + readThread().start() + writeThread().start() + } catch (e: IOException) { + shutdown(Status.EXCEPTION) + } + } + + private fun enterUsername() { + print(Strings.ENTER_USERNAME) + while (username == null) { + try { + var userInput = readLine() + while (userInput != null && userInput.contains(Regex("""[\[\]]"""))) { + System.err.print(Strings.BAD_USERNAME) + userInput = readLine() + } + + socketChannel.writeMessage(ConnectionRequest(userInput!!)) + when (socketChannel.readMessage()) { + is ConnectionResponse -> { + username = userInput + println(Strings.HELLO(username)) + } + is ConnectionDenied -> { + System.err.print(Strings.TAKEN_USERNAME) + } + } + } catch (e: IOException) { + shutdown(Status.EXCEPTION) + } + } + } + + private fun shutdown(status: Status) { + try { + socketChannel.close() + } + catch (e: IOException) { + } + println(status.message) + exitProcess(status.code) + } + + private fun readThread() = Thread { + try { + while (true) { + println(socketChannel.readMessage()) + } + } catch (e: IOException) { + shutdown(Status.EXCEPTION) + } + } + + private fun writeThread() = Thread { + try { + while (true) { + val userInput = readLine() + if (userInput == STOP_WORD || userInput == null) { + socketChannel.writeMessage(DisconnectionRequest(username!!)) + shutdown(Status.OK) + break + } else { + socketChannel.writeMessage(UserMessage(username!!, userInput)) + } + } + } catch (e: IOException) { + shutdown(Status.EXCEPTION) + } + } + + companion object { + @JvmStatic + fun main(args: Array) { + when { + args.size >= 2 -> Client(args[0], args[1].toIntOrNull() ?: DEFAULT_PORT) + args.size == 1 -> Client(args[0], DEFAULT_PORT) + else -> Client(DEFAULT_ADDRESS, DEFAULT_PORT) + } + } + } +} diff --git a/src/non_blocking/Server.kt b/src/non_blocking/Server.kt new file mode 100644 index 00000000..6924fbd9 --- /dev/null +++ b/src/non_blocking/Server.kt @@ -0,0 +1,101 @@ +package non_blocking + +import common.* +import java.io.IOException +import java.net.InetSocketAddress +import java.nio.channels.SelectionKey +import java.nio.channels.Selector +import java.nio.channels.ServerSocketChannel +import java.nio.channels.SocketChannel +import kotlin.system.exitProcess + +class Server(addr: String, port: Int) { + private val sockets = mutableMapOf() + + init { + val selector = Selector.open() + val serverSocket = try { + ServerSocketChannel.open().apply { + bind(InetSocketAddress(addr, port)) + configureBlocking(false) + register(selector, SelectionKey.OP_ACCEPT) + } + } + catch (e: IOException) { + System.err.println(Strings.SERVER_NOT_STARTED) + exitProcess(-1) + } + println(Strings.SERVER_STARTED) + + while (true) { + selector.select() + val selectedKeys = selector.selectedKeys() + val iter = selectedKeys.iterator() + while (iter.hasNext()) { + val key = iter.next() + if (key.isAcceptable) { + register(selector, serverSocket) + } + if (key.isReadable) { + readAndSendResponse(key) + } + iter.remove() + } + } + } + + @Throws(IOException::class) + private fun readAndSendResponse(key: SelectionKey) { + val socket: SocketChannel = key.channel() as SocketChannel + + when (val received = try { + socket.readMessage() + } catch (e: IOException) { + DisconnectionRequest(sockets[socket] ?: return) + }) { + is ConnectionRequest -> { + if (received.username in sockets.values) { + socket.writeMessage(ConnectionDenied(received.username)) + } else { + val username = received.username + sockets[socket] = username + println(ConnectionResponse(username)) + sockets.forEach { + it.key.writeMessage(ConnectionResponse(username)) + } + } + } + is DisconnectionRequest -> { + println(DisconnectionResponse(received.username)) + sockets.remove(socket) + sockets.forEach { + it.key.writeMessage(DisconnectionResponse(received.username)) + } + } + is UserMessage -> { + println(received) + sockets.forEach { + it.key.writeMessage(received) + } + } + } + } + + @Throws(IOException::class) + private fun register(selector: Selector, serverSocket: ServerSocketChannel) { + val socket = serverSocket.accept() + socket.configureBlocking(false) + socket.register(selector, SelectionKey.OP_READ) + } + + companion object { + @JvmStatic + fun main(args: Array) { + when { + args.size >= 2 -> Server(args[0], args[1].toIntOrNull() ?: DEFAULT_PORT) + args.size == 1 -> Server(args[0], DEFAULT_PORT) + else -> Server(DEFAULT_ADDRESS, DEFAULT_PORT) + } + } + } +} diff --git a/src/non_blocking/SocketChannelUtils.kt b/src/non_blocking/SocketChannelUtils.kt new file mode 100644 index 00000000..087d5636 --- /dev/null +++ b/src/non_blocking/SocketChannelUtils.kt @@ -0,0 +1,35 @@ +package non_blocking + +import common.Message +import java.io.* +import java.nio.ByteBuffer +import java.nio.channels.SocketChannel + +fun SocketChannel.writeMessage(message: Message) { + val bos = ByteArrayOutputStream() + ObjectOutputStream(bos).apply { + writeObject(message) + flush() + } + write(ByteBuffer.wrap(bos.toByteArray())) +} + +fun SocketChannel.readMessage(): Message { + val bos = ByteArrayOutputStream() + var result: Message + + while (true) { + val buffer = ByteBuffer.allocate(256) + read(buffer) + bos.write(buffer.array()) + + val bis = ByteArrayInputStream(bos.toByteArray()) + try { + result = ObjectInputStream(bis).readObject() as Message + break + } catch (e: EOFException) { + } + } + + return result +} \ No newline at end of file