Skip to content

Commit

Permalink
New protocol in Kotlin server
Browse files Browse the repository at this point in the history
  • Loading branch information
rares45 committed Jul 15, 2022
1 parent 5860ceb commit eb3f932
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 33 deletions.
11 changes: 11 additions & 0 deletions server_kotlin/.run/Run.run.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run" type="JetRunConfigurationType">
<option name="MAIN_CLASS_NAME" value="com.perceivers25.betalk.MainKt" />
<module name="server_kotlin.main" />
<option name="PROGRAM_PARAMETERS" value="run" />
<shortenClasspath name="NONE" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
</component>
7 changes: 6 additions & 1 deletion server_kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
kotlin("jvm") version "1.5.10"
kotlin("plugin.serialization") version "1.6.21"
application
}

group = "com.perceivers25.betalk"
version = "0.9-BETA"
version = "0.9.1-BETA"

repositories {
mavenCentral()
}

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3")
}

tasks.jar {
duplicatesStrategy = DuplicatesStrategy.INCLUDE

Expand Down
53 changes: 53 additions & 0 deletions server_kotlin/src/main/kotlin/classes/DataPackage.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.perceivers25.betalk.classes

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.util.*

/**
* Data package is a class that defines a data package in the Betalk Protocol able to represent a login or a message.
* At anytime message or login name should be specified.
*
* @param id The universally unique identifier of the data package
* @param message The message itself if type is message
* @param loginName The user login if type is login
* @see Server
*/
@Serializable
class DataPackage(val id: String, val type: DataPackageType, val message: Message?, val loginName: String?) {

init {
assert(message != null || loginName != null)
assert(!(type == DataPackageType.Message && message == null))
assert(!(type == DataPackageType.Login && loginName == null))
}

fun toJson(): String {
val format = Json { encodeDefaults = true }
return format.encodeToString(this);
}

companion object {
fun fromJson(jsonString: String): DataPackage {
return Json.decodeFromString<DataPackage>(jsonString)
}

fun newMessageDataPackage(message: Message): DataPackage {
return DataPackage(
UUID.randomUUID().toString(), DataPackageType.Message, message, null,
)
}
}

@Serializable
enum class DataPackageType {
@SerialName("login")
Login,

@SerialName("message")
Message,
}
}
42 changes: 42 additions & 0 deletions server_kotlin/src/main/kotlin/classes/Message.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.perceivers25.betalk.classes

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*

@Serializable
class Message(
val messageID: String,
val messageAuthor: String?,
val messageTextContent: String?,
val messageFileContent: ByteArray?,
val time: String,
val type: MessageType,
) {
init {
assert(messageTextContent != null || messageFileContent != null)
}

@Serializable
enum class MessageType {
@SerialName("user-message")
UserMessage,

@SerialName("system-message")
SystemMessage,
}

companion object {
fun newSystemMessage(messageTextContent: String): Message {
return Message(
UUID.randomUUID().toString(), null, messageTextContent, null, DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss.SSSSSS")
.withZone(ZoneOffset.UTC)
.format(Instant.now()), MessageType.SystemMessage
)
}
}
}
115 changes: 83 additions & 32 deletions server_kotlin/src/main/kotlin/classes/Server.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

package com.perceivers25.betalk.classes

import kotlinx.serialization.SerializationException
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStream
import java.net.InetAddress
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketException
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.util.*
import kotlin.concurrent.thread
import com.perceivers25.betalk.utils.BetalkLogger as Log

Expand All @@ -20,7 +20,8 @@ class Server(ip: String? = null) {

private lateinit var server: ServerSocket

private var clients: MutableList<Socket> = mutableListOf()
private var clients: MutableList<Client> = mutableListOf()
private var messages: MutableList<Message> = mutableListOf()

fun startServer() {
Log.logStatus("Server starting...")
Expand All @@ -30,74 +31,124 @@ class Server(ip: String? = null) {

while (true) {
try {
val client: Socket = server.accept()
val client = Client(server.accept())
thread { handleClient(client) }
} catch (s: SocketException) {

Log.logError(s.message ?: "Something happened")
}
}
}

private fun handleClient(client: Socket) {
client.receiveBufferSize = 1024

val reader = BufferedReader(InputStreamReader(client.getInputStream()))
private fun handleClient(client: Client) {
client.socket.receiveBufferSize = 16384

val nickname: String = recv(reader)
client.reader = BufferedReader(InputStreamReader(client.socket.inputStream))
var dp: DataPackage = receive(client.reader)

if (dp.type == DataPackage.DataPackageType.Login) {
client.clientName = dp.loginName!!
} else {
client.close()
return
}
clients.add(client)
client.messageQueue.addAll(messages)

Log.logClient("Connected with ${client.inetAddress.hostAddress}")
Log.logClient("Nickname of client is $nickname")
Log.logClient("Connected with ${client.socket.inetAddress.hostAddress}")
Log.logClient("Nickname of client is ${client.clientName}")
Log.logInfo("Total clients: ${clients.size}")

broadcast("$nickname joined the chat\n")
broadcast(Message.newSystemMessage("${client.clientName} joined the chat"))

thread {
while (!client.socket.isClosed and client.socket.isConnected) {
val m: Message? = client.messageQueue.poll()
if (m != null)
send(client.socket.outputStream, DataPackage.newMessageDataPackage(m))
}
}

while (!client.isClosed and client.isConnected) {
while (!client.socket.isClosed and client.socket.isConnected) {
try {
val m = recv(reader)
if (m.isBlank())
try {
dp = receive(client.reader)
} catch (_: Throwable) {
break
Log.logClient(m.replace("@", " says: "))
broadcast(m)
}
if (dp.type == DataPackage.DataPackageType.Message) {
var details: String = dp.message!!.messageAuthor!!
if (dp.message!!.messageTextContent != null) {
details += " says: " + dp.message!!.messageTextContent
}
if (dp.message!!.messageFileContent != null) {
if (dp.message!!.messageTextContent != null) {
details += " and"
}
details += " sent a file"
}
Log.logClient(details)
}
broadcast(dp.message!!)
} catch (ex: Exception) {
break
}
}
reader.close()
client.close()
clients.remove(client)
broadcast("$nickname left\n")
Log.logClient("${client.inetAddress.hostAddress} closed the connection")
broadcast(Message.newSystemMessage("${client.clientName} left"))
Log.logClient("${client.socket.inetAddress.hostAddress} closed the connection. Remaining clients: ${clients.size}")
}

private fun broadcast(message: String) {
private fun broadcast(message: Message) {
messages.add(message)
for (client in clients) {
write(client.getOutputStream(), message)
client.messageQueue.add(message)
}
}

private fun write(writer: OutputStream, message: String) {
writer.write(message.encodeToByteArray())
/**
* Sends a data package to a client using its OutputStream
*/
private fun send(writer: OutputStream, dataPackage: DataPackage) {
writer.write("${dataPackage.toJson()} \u001a".encodeToByteArray())
}

private fun recv(reader: BufferedReader): String {
/**
* Receives a data package.
*
* @throws SerializationException if value is empty
*/
private fun receive(reader: BufferedReader): DataPackage {
var response = ""
val chars = CharArray(1024)
val chars = CharArray(16384)
try {
val input = reader.read(chars, 0, 1024)
val input = reader.read(chars, 0, 16384)
if (input > 0) {
response = String(chars, 0, input)
}
} catch (ex: java.lang.Exception) {
} catch (_: java.lang.Exception) {
}
try {
return DataPackage.fromJson(response)
} catch (e: SerializationException) {
throw e
}
return response
}

fun stop() {
for (client in clients)
client.close()
for (client in clients) client.close()
server.close()
Log.logStatus("Server has closed")
}

class Client(var socket: Socket) {
var clientName: String = ""
var messageQueue: LinkedList<Message> = LinkedList<Message>()
lateinit var reader: BufferedReader
fun close() {
socket.close()
reader.close()
}
}

}

0 comments on commit eb3f932

Please sign in to comment.