Added third example.
							parent
							
								
									85af3e7611
								
							
						
					
					
						commit
						19295bf552
					
				@ -0,0 +1,11 @@
 | 
			
		||||
## Запуск сервера
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
python server.py 8080
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Запуск клиента
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
python main.py
 | 
			
		||||
```
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
*.iml
 | 
			
		||||
*.ipr
 | 
			
		||||
*.iws
 | 
			
		||||
.idea
 | 
			
		||||
out
 | 
			
		||||
target
 | 
			
		||||
*.class
 | 
			
		||||
@ -0,0 +1,12 @@
 | 
			
		||||
name := "ConsoleChat"
 | 
			
		||||
 | 
			
		||||
version := "0.1"
 | 
			
		||||
 | 
			
		||||
scalaVersion := "2.13.3"
 | 
			
		||||
 | 
			
		||||
// https://mvnrepository.com/artifact/com.github.java-json-tools/json-schema-validator
 | 
			
		||||
libraryDependencies += "com.github.java-json-tools" % "json-schema-validator" % "2.2.14"
 | 
			
		||||
libraryDependencies += "com.fasterxml.jackson.module" %% "jackson-module-scala" % "2.11.0"
 | 
			
		||||
libraryDependencies += "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2"
 | 
			
		||||
libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.2.3"
 | 
			
		||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % Test
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
sbt.version = 1.3.13
 | 
			
		||||
@ -0,0 +1,14 @@
 | 
			
		||||
{
 | 
			
		||||
  "$schema": "http://json-schema.org/draft/2019-09/schema#",
 | 
			
		||||
  "$id": "http://mychat.germanywestcentral.cloudapp.azure.com/schemas/v0.0.1/ClientStateSchema.json",
 | 
			
		||||
  "title": "ClientState",
 | 
			
		||||
  "type": "object",
 | 
			
		||||
  "properties": {
 | 
			
		||||
    "lastMessageTimestamp": {
 | 
			
		||||
      "type": "string"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "required": [
 | 
			
		||||
    "lastMessageTimestamp"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,173 @@
 | 
			
		||||
package client
 | 
			
		||||
 | 
			
		||||
import java.io.{BufferedReader, IOException, InputStreamReader}
 | 
			
		||||
import java.net.{InetSocketAddress, SocketAddress}
 | 
			
		||||
import java.nio.channels.{NonReadableChannelException, NotYetConnectedException, SelectionKey, Selector, SocketChannel}
 | 
			
		||||
 | 
			
		||||
import ch.qos.logback.classic.Level
 | 
			
		||||
import com.typesafe.scalalogging.{LazyLogging, Logger}
 | 
			
		||||
import datatypes.{ClientInfo, Message}
 | 
			
		||||
import org.slf4j.LoggerFactory
 | 
			
		||||
import ch.qos.logback.classic.Level
 | 
			
		||||
import com.fasterxml.jackson.core.JsonProcessingException
 | 
			
		||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException
 | 
			
		||||
import com.sun.mail.util.SocketConnectException
 | 
			
		||||
 | 
			
		||||
import scala.collection.immutable.ArraySeq
 | 
			
		||||
import scala.collection.mutable
 | 
			
		||||
import scala.jdk.CollectionConverters._
 | 
			
		||||
import scala.reflect.ClassTag
 | 
			
		||||
 | 
			
		||||
class Client(host:String,
 | 
			
		||||
             port:Int,
 | 
			
		||||
             recieveCallback: Message=>Unit,
 | 
			
		||||
             login:String
 | 
			
		||||
            ) extends Runnable with LazyLogging {
 | 
			
		||||
 | 
			
		||||
  @volatile var isRunning = false
 | 
			
		||||
  private val selector = Selector.open()
 | 
			
		||||
  private var clientChannel: SocketChannel = _
 | 
			
		||||
  private val inbox = mutable.ArrayDeque[Message]()
 | 
			
		||||
  private var isInitialized = false
 | 
			
		||||
 | 
			
		||||
  def start(): Unit = {
 | 
			
		||||
    isRunning = true
 | 
			
		||||
 | 
			
		||||
    Runtime.getRuntime.addShutdownHook(new Thread() {
 | 
			
		||||
      override def run(): Unit = {
 | 
			
		||||
        Thread.currentThread().setName("CLEANER")
 | 
			
		||||
        if (isRunning) {
 | 
			
		||||
          logger.info("Closing connection.")
 | 
			
		||||
          isRunning = false
 | 
			
		||||
          Thread.sleep(10)
 | 
			
		||||
          if (clientChannel != null) clientChannel.close()
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    val clientThread = new Thread(this, "CLIENT")
 | 
			
		||||
    clientThread.start()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def leaveMessage(receiverId:String, body: String): Selector = {
 | 
			
		||||
    val msg = Message(senderId = this.login, receiverId = receiverId, body = body)
 | 
			
		||||
    inbox.addOne(msg)
 | 
			
		||||
    clientChannel.register(selector, SelectionKey.OP_WRITE)
 | 
			
		||||
    selector.wakeup()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def run (): Unit = {
 | 
			
		||||
    clientChannel = SocketChannel.open()
 | 
			
		||||
    try {
 | 
			
		||||
      clientChannel.connect(new InetSocketAddress(host, port))
 | 
			
		||||
      logger.info(s"Connected to $host:$port")
 | 
			
		||||
    } catch {
 | 
			
		||||
      case e: SocketConnectException =>
 | 
			
		||||
        logger.info("Connection failed.")
 | 
			
		||||
        System.exit(0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    clientChannel.configureBlocking(false)
 | 
			
		||||
    clientChannel.register(selector, SelectionKey.OP_WRITE)
 | 
			
		||||
 | 
			
		||||
    sendRecieveLoop()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def sendRecieveLoop() = {
 | 
			
		||||
    while (isRunning) {
 | 
			
		||||
      selector.select()
 | 
			
		||||
      for (key <- selector.selectedKeys().asScala) {
 | 
			
		||||
        if (key.isValid && key.isReadable) {
 | 
			
		||||
          receiveFromServer()
 | 
			
		||||
        }
 | 
			
		||||
        if (key.isValid && key.isWritable) {
 | 
			
		||||
          sendToServer()
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def sendToServer() = {
 | 
			
		||||
    if (!isInitialized) {
 | 
			
		||||
      val clientInfo = ClientInfo(login = login)
 | 
			
		||||
      send[ClientInfo](clientInfo)
 | 
			
		||||
      isInitialized = true
 | 
			
		||||
    }
 | 
			
		||||
    if (isInitialized && inbox.nonEmpty) {
 | 
			
		||||
      try {
 | 
			
		||||
        val msg = inbox.removeHead()
 | 
			
		||||
        send(msg)
 | 
			
		||||
      } catch {
 | 
			
		||||
        case e@(_: MismatchedInputException |
 | 
			
		||||
                _: IOException) =>
 | 
			
		||||
          logger.info(s"Client disconnected.")
 | 
			
		||||
          System.exit(0)
 | 
			
		||||
        case e@(_: NotYetConnectedException |
 | 
			
		||||
                _: NonReadableChannelException |
 | 
			
		||||
                _: JsonProcessingException) =>
 | 
			
		||||
          logger.error(e.getMessage)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def send[T](msg: T)(implicit ct: ClassTag[T]) = {
 | 
			
		||||
    util.Util.send[T](msg, clientChannel)
 | 
			
		||||
    clientChannel.register(selector, SelectionKey.OP_READ)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def receiveFromServer() = {
 | 
			
		||||
    try {
 | 
			
		||||
      val msg = util.Util.recieve[Message](clientChannel)
 | 
			
		||||
      if (msg.nonEmpty) {
 | 
			
		||||
        recieveCallback(msg.get)
 | 
			
		||||
      }
 | 
			
		||||
    } catch {
 | 
			
		||||
      case e@(_: MismatchedInputException |
 | 
			
		||||
              _: IOException) =>
 | 
			
		||||
        logger.info(s"Client disconnected.")
 | 
			
		||||
        System.exit(0)
 | 
			
		||||
      case e@(_: NotYetConnectedException |
 | 
			
		||||
              _: NonReadableChannelException |
 | 
			
		||||
              _: JsonProcessingException) =>
 | 
			
		||||
        logger.error(e.getMessage)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
object Client extends LazyLogging {
 | 
			
		||||
  LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).asInstanceOf[ch.qos.logback.classic.Logger].setLevel(Level.INFO)
 | 
			
		||||
 | 
			
		||||
  val defaultArgs = Map(
 | 
			
		||||
    "host" -> "0.0.0.0",
 | 
			
		||||
    "port" -> "10000"
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  def uiLoop(client: Client): Unit = {
 | 
			
		||||
    Thread.currentThread().setName("UI")
 | 
			
		||||
    val consoleInput = new BufferedReader(new InputStreamReader(System.in))
 | 
			
		||||
 | 
			
		||||
    while(client.isRunning) {
 | 
			
		||||
      val line = consoleInput.readLine()
 | 
			
		||||
      if (line != null) {
 | 
			
		||||
        client.leaveMessage(util.Util.BROADCAST_RECIEVER_ID, line)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def main(args:Array[String]): Unit = {
 | 
			
		||||
    val argsMap = args.toSeq.sliding(2,2).map{ case ArraySeq(k,v) => (k.replace("--",""), v) }.toMap
 | 
			
		||||
      .withDefault(defaultArgs)
 | 
			
		||||
    val host = argsMap("host")
 | 
			
		||||
    val port = argsMap("port").toInt
 | 
			
		||||
    val login = argsMap.getOrElse("login", util.Util.genId(java.time.Instant.now()))
 | 
			
		||||
 | 
			
		||||
    val client = new Client(
 | 
			
		||||
      host,
 | 
			
		||||
      port,
 | 
			
		||||
      message => println(s"${message.senderId}: ${message.body}"),
 | 
			
		||||
      login = login)
 | 
			
		||||
    client.start()
 | 
			
		||||
 | 
			
		||||
    uiLoop(client)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
package datatypes
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonProperty
 | 
			
		||||
 | 
			
		||||
case class ClientInfo (
 | 
			
		||||
  @JsonProperty("timestamp") timestamp:String = java.time.Instant.now().toString,
 | 
			
		||||
  @JsonProperty("login") login:String
 | 
			
		||||
)
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
package datatypes
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonProperty
 | 
			
		||||
 | 
			
		||||
case class ClientState (
 | 
			
		||||
  @JsonProperty("lastMessageTimestamp") lastMessageTimestamp:String
 | 
			
		||||
)
 | 
			
		||||
@ -0,0 +1,10 @@
 | 
			
		||||
package datatypes
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.annotation.JsonProperty
 | 
			
		||||
 | 
			
		||||
case class Message(
 | 
			
		||||
    @JsonProperty("timestamp") timestamp:String = java.time.Instant.now().toString,
 | 
			
		||||
    @JsonProperty("senderId") senderId:String,
 | 
			
		||||
    @JsonProperty("receiverId") receiverId:String,
 | 
			
		||||
    @JsonProperty("body") body:String
 | 
			
		||||
)
 | 
			
		||||
@ -0,0 +1,36 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import java.nio.channels.SocketChannel
 | 
			
		||||
import java.nio.file.Path
 | 
			
		||||
 | 
			
		||||
import com.typesafe.scalalogging.LazyLogging
 | 
			
		||||
import datatypes.{ClientState, Message}
 | 
			
		||||
import util.SavableState
 | 
			
		||||
 | 
			
		||||
import scala.collection.mutable
 | 
			
		||||
 | 
			
		||||
class Client(
 | 
			
		||||
  var login:String = "",
 | 
			
		||||
  var lastMessageTimestamp:String = "",
 | 
			
		||||
  val inbox:mutable.ArrayDeque[Message] = new mutable.ArrayDeque[Message](),
 | 
			
		||||
  val socketChannel:SocketChannel,
 | 
			
		||||
  var isInitialized:Boolean = false) extends SavableState with LazyLogging {
 | 
			
		||||
 | 
			
		||||
  private def getStateFilePath(statesDir:Path): Path = statesDir.resolve(s"${login}.json")
 | 
			
		||||
 | 
			
		||||
  def saveState(statesDir:Path):Unit = {
 | 
			
		||||
    val filePath = getStateFilePath(statesDir)
 | 
			
		||||
    val clientState = ClientState(lastMessageTimestamp = lastMessageTimestamp)
 | 
			
		||||
    util.Util.writeJsonToFile(clientState, filePath)
 | 
			
		||||
    logger.debug(s"Client $login state saved.")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def loadState(statesDir:Path):Unit = {
 | 
			
		||||
    val filePath = getStateFilePath(statesDir)
 | 
			
		||||
    val clientState = util.Util.readJsonFromFile[ClientState](filePath)
 | 
			
		||||
    if (clientState.nonEmpty) {
 | 
			
		||||
      lastMessageTimestamp = clientState.get.lastMessageTimestamp
 | 
			
		||||
      logger.debug(s"Client $login state loaded.")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,220 @@
 | 
			
		||||
package server
 | 
			
		||||
 | 
			
		||||
import java.io.IOException
 | 
			
		||||
import java.net.{InetAddress, InetSocketAddress, ServerSocket, Socket, SocketAddress, SocketOption}
 | 
			
		||||
import java.nio.ByteBuffer
 | 
			
		||||
import java.nio.channels.{NonReadableChannelException, NotYetConnectedException, SelectionKey, Selector, ServerSocketChannel, SocketChannel}
 | 
			
		||||
import java.nio.file.{Files, Path}
 | 
			
		||||
import java.util.concurrent.{ConcurrentHashMap, Executor, Executors, TimeUnit}
 | 
			
		||||
 | 
			
		||||
import ch.qos.logback.classic.Level
 | 
			
		||||
import com.fasterxml.jackson.core.JsonProcessingException
 | 
			
		||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException
 | 
			
		||||
import com.typesafe.scalalogging.LazyLogging
 | 
			
		||||
import datatypes.{ClientInfo, Message}
 | 
			
		||||
import org.slf4j.LoggerFactory
 | 
			
		||||
 | 
			
		||||
import scala.collection.immutable.ArraySeq
 | 
			
		||||
import scala.collection.mutable
 | 
			
		||||
import scala.jdk.CollectionConverters._
 | 
			
		||||
 | 
			
		||||
class Server(host:String, port:Int, val savedStatesDir:Path) extends Runnable with LazyLogging {
 | 
			
		||||
  private val connectedClients = new mutable.HashMap[Int, Client]()
 | 
			
		||||
  private val loggedinClients = new mutable.HashMap[String, Client]()
 | 
			
		||||
  private val selector = Selector.open()
 | 
			
		||||
 | 
			
		||||
  @volatile var isRunning = false
 | 
			
		||||
 | 
			
		||||
  def run(): Unit = {
 | 
			
		||||
    logger.info(s"Server is running on $host:$port.")
 | 
			
		||||
    val serverSocketChannel = ServerSocketChannel.open()
 | 
			
		||||
    serverSocketChannel.bind(new InetSocketAddress(host, port))
 | 
			
		||||
    serverSocketChannel.configureBlocking(false)
 | 
			
		||||
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT)
 | 
			
		||||
 | 
			
		||||
    sendRecieveAcceptLoop(serverSocketChannel)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def sendRecieveAcceptLoop(serverSocketChannel: ServerSocketChannel) = {
 | 
			
		||||
    while (isRunning) {
 | 
			
		||||
      selector.select()
 | 
			
		||||
      for (key <- selector.selectedKeys().asScala) {
 | 
			
		||||
        if (key.isValid && key.isAcceptable)
 | 
			
		||||
          acceptNewClient(serverSocketChannel)
 | 
			
		||||
        if (key.isValid && key.isReadable)
 | 
			
		||||
          receiveClientData(key)
 | 
			
		||||
        if (key.isValid && key.isWritable)
 | 
			
		||||
          sendDataToClient(key)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def acceptNewClient(serverSocketChannel: ServerSocketChannel) = {
 | 
			
		||||
    val clientChannel = serverSocketChannel.accept()
 | 
			
		||||
    if (clientChannel != null) {
 | 
			
		||||
      clientChannel.configureBlocking(false)
 | 
			
		||||
      clientChannel.register(selector, SelectionKey.OP_READ)
 | 
			
		||||
 | 
			
		||||
      val clientId = clientChannel.hashCode()
 | 
			
		||||
      val client = new Client(socketChannel = clientChannel)
 | 
			
		||||
 | 
			
		||||
      connectedClients.put(clientId, client)
 | 
			
		||||
 | 
			
		||||
      logger.debug(s"Connection accepted.")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def receiveClientData(key: SelectionKey) = {
 | 
			
		||||
    val clientChannel = key.channel().asInstanceOf[SocketChannel]
 | 
			
		||||
    val clientId = clientChannel.hashCode()
 | 
			
		||||
    val client = connectedClients(clientId)
 | 
			
		||||
    try {
 | 
			
		||||
      if (!client.isInitialized) {
 | 
			
		||||
        val clientInfo = util.Util.recieve[ClientInfo](clientChannel)
 | 
			
		||||
        if (clientInfo.nonEmpty) {
 | 
			
		||||
          loginClient(client, clientInfo)
 | 
			
		||||
        } else {
 | 
			
		||||
          throw new java.io.IOException("Bad client info. Initialization aborted.")
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        val msg = util.Util.recieve[Message](clientChannel)
 | 
			
		||||
        if (msg.nonEmpty) {
 | 
			
		||||
          client.lastMessageTimestamp = msg.get.timestamp
 | 
			
		||||
          client.saveState(savedStatesDir)
 | 
			
		||||
          routeMessage(msg.get, clientId)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch {
 | 
			
		||||
      case e @ (_: com.fasterxml.jackson.databind.exc.MismatchedInputException |
 | 
			
		||||
                _: java.io.IOException ) =>
 | 
			
		||||
        logger.error(e.getMessage, e.getCause)
 | 
			
		||||
        handleDisconnection(clientChannel, clientId)
 | 
			
		||||
      case e @ (_ : java.nio.channels.NotYetConnectedException |
 | 
			
		||||
                _ : java.nio.channels.NonReadableChannelException |
 | 
			
		||||
                _ : com.fasterxml.jackson.core.JsonProcessingException) =>
 | 
			
		||||
        logger.error(e.getMessage, e.getCause)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def loginClient(client: Client, clientInfo: Option[ClientInfo]) = {
 | 
			
		||||
    val login = clientInfo.get.login
 | 
			
		||||
    if (loggedinClients.keySet.contains(login)) {
 | 
			
		||||
      val msg = Message(body=s"$login is already connected.", receiverId = "", senderId = "SERVER")
 | 
			
		||||
      leaveMessageForClient(msg, client)
 | 
			
		||||
    } else {
 | 
			
		||||
      loggedinClients.put(clientInfo.get.login, client)
 | 
			
		||||
      client.login = clientInfo.get.login
 | 
			
		||||
      client.loadState(savedStatesDir)
 | 
			
		||||
      client.isInitialized = true
 | 
			
		||||
      val msg = if ("".equals(client.lastMessageTimestamp)) {
 | 
			
		||||
        Message(body = s"Hello new client with login ${client.login} !", receiverId = "", senderId = "SERVER")
 | 
			
		||||
      } else {
 | 
			
		||||
        Message(body = s"Welcome back ${client.login} !", receiverId = "", senderId = "SERVER")
 | 
			
		||||
      }
 | 
			
		||||
      leaveMessageForClient(msg, client)
 | 
			
		||||
      logger.info(s"Client ${client.login} logged in.")
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def handleDisconnection(clientChannel: SocketChannel, clientId: Int) = {
 | 
			
		||||
    clientChannel.close()
 | 
			
		||||
    val disconnectedClient = connectedClients.remove(clientId)
 | 
			
		||||
    if (disconnectedClient.nonEmpty) {
 | 
			
		||||
      loggedinClients.remove(disconnectedClient.get.login)
 | 
			
		||||
      val msg = Message(body = s"Client ${disconnectedClient.get.login} left.", receiverId = "", senderId = "SERVER")
 | 
			
		||||
      leaveMessageForConnectedClients(msg)
 | 
			
		||||
    }
 | 
			
		||||
    logger.info(s"Client ${disconnectedClient.get.login} disconnected.")
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def sendDataToClient(key: SelectionKey) = {
 | 
			
		||||
    val clientChannel = key.channel().asInstanceOf[SocketChannel]
 | 
			
		||||
    val clientId = clientChannel.hashCode()
 | 
			
		||||
    val client = connectedClients(clientId)
 | 
			
		||||
    val clientInbox = client.inbox
 | 
			
		||||
    if (clientInbox.nonEmpty)
 | 
			
		||||
      try {
 | 
			
		||||
        val messageToSend = clientInbox.removeHead()
 | 
			
		||||
        client.lastMessageTimestamp = messageToSend.timestamp
 | 
			
		||||
        client.saveState(savedStatesDir)
 | 
			
		||||
        util.Util.send(messageToSend, clientChannel)
 | 
			
		||||
        clientChannel.register(selector, SelectionKey.OP_READ)
 | 
			
		||||
      } catch {
 | 
			
		||||
        case e@(_: MismatchedInputException |
 | 
			
		||||
                _: IOException) =>
 | 
			
		||||
          handleDisconnection(clientChannel, clientId)
 | 
			
		||||
        case e@(_: NotYetConnectedException |
 | 
			
		||||
                _: NonReadableChannelException |
 | 
			
		||||
                _: JsonProcessingException) =>
 | 
			
		||||
          logger.error(e.getMessage, e.getCause)
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def routeMessage(msg: Message, senderId:Int) = {
 | 
			
		||||
    msg.receiverId match {
 | 
			
		||||
      case _ =>
 | 
			
		||||
        for ((recieverId, reciever) <- connectedClients.iterator) {
 | 
			
		||||
          if (recieverId != senderId) {
 | 
			
		||||
            leaveMessageForClient(msg, reciever)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def leaveMessageForConnectedClients(msg:Message) = {
 | 
			
		||||
    for ((recieverId, reciever) <- connectedClients.iterator)
 | 
			
		||||
      leaveMessageForClient(msg, reciever)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private def leaveMessageForClient(msg: Message, reciever: Client) = {
 | 
			
		||||
    reciever.inbox.addOne(msg)
 | 
			
		||||
    reciever.socketChannel.register(selector, SelectionKey.OP_WRITE)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def start(): Unit = {
 | 
			
		||||
    isRunning = true
 | 
			
		||||
    Runtime.getRuntime.addShutdownHook(new Thread() {
 | 
			
		||||
      override def run(): Unit = {
 | 
			
		||||
        if (isRunning) {
 | 
			
		||||
          logger.info("Closing connections.")
 | 
			
		||||
          isRunning = false
 | 
			
		||||
          Thread.sleep(10)
 | 
			
		||||
          for (c <- connectedClients.values) {
 | 
			
		||||
            try c.socketChannel.close()
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    this.run()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
object Server extends LazyLogging {
 | 
			
		||||
  LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME).asInstanceOf[ch.qos.logback.classic.Logger].setLevel(Level.DEBUG)
 | 
			
		||||
 | 
			
		||||
  val defaultArgs = Map(
 | 
			
		||||
    "host" -> "0.0.0.0",
 | 
			
		||||
    "port" -> "10000",
 | 
			
		||||
    "states_path" -> "./clientStates/"
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  // how socket connection and key.isConnectable are related?
 | 
			
		||||
  // how socket connection and socket.isConnected are related?
 | 
			
		||||
  def main(args:Array[String]): Unit = {
 | 
			
		||||
    val argsMap = args.toSeq.sliding(2,2).map{ case ArraySeq(k,v) => (k.replace("--",""), v) }.toMap
 | 
			
		||||
      .withDefault(defaultArgs)
 | 
			
		||||
    val host = argsMap("host")
 | 
			
		||||
    val port = argsMap("port").toInt
 | 
			
		||||
    val statesPath = Path.of(argsMap("statesPath")).toAbsolutePath
 | 
			
		||||
 | 
			
		||||
    if (Files.notExists(statesPath)) {
 | 
			
		||||
      logger.info(s"Path statesPath = ${statesPath} does not exist. Aborting.")
 | 
			
		||||
      System.exit(0)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logger.info(s"Using ${statesPath} for client states.")
 | 
			
		||||
 | 
			
		||||
    val server = new Server(host, port, statesPath)
 | 
			
		||||
    server.start()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,8 @@
 | 
			
		||||
package util
 | 
			
		||||
 | 
			
		||||
import java.nio.file.Path
 | 
			
		||||
 | 
			
		||||
trait SavableState {
 | 
			
		||||
  def loadState(statesDir:Path):Unit
 | 
			
		||||
  def saveState(statesDir:Path):Unit
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,81 @@
 | 
			
		||||
package util
 | 
			
		||||
 | 
			
		||||
import java.nio.ByteBuffer
 | 
			
		||||
import java.nio.channels.SocketChannel
 | 
			
		||||
import java.nio.charset.{Charset, StandardCharsets}
 | 
			
		||||
import java.nio.file.{Files, Path, StandardOpenOption}
 | 
			
		||||
import java.security.MessageDigest
 | 
			
		||||
 | 
			
		||||
import com.fasterxml.jackson.databind.ObjectMapper
 | 
			
		||||
import com.fasterxml.jackson.module.scala.{DefaultScalaModule, ScalaObjectMapper}
 | 
			
		||||
import com.typesafe.scalalogging.LazyLogging
 | 
			
		||||
 | 
			
		||||
import scala.collection.mutable
 | 
			
		||||
import scala.reflect.ClassTag
 | 
			
		||||
 | 
			
		||||
object Util extends LazyLogging {
 | 
			
		||||
  val BUFFER_SIZE = 1024
 | 
			
		||||
  val ID_LIMIT = 2
 | 
			
		||||
  val BROADCAST_RECIEVER_ID = "-1"
 | 
			
		||||
 | 
			
		||||
  private val objectMapper = new ObjectMapper() with ScalaObjectMapper
 | 
			
		||||
  objectMapper.registerModule(DefaultScalaModule)
 | 
			
		||||
  private val charset = Charset.forName( "utf-8" )
 | 
			
		||||
  private val decoder = charset.newDecoder
 | 
			
		||||
 | 
			
		||||
  def recieveString(socketChannel:SocketChannel) : String = {
 | 
			
		||||
    val stringBuilder = new mutable.StringBuilder()
 | 
			
		||||
    val buffer = ByteBuffer.allocateDirect(BUFFER_SIZE)
 | 
			
		||||
    while (true) {
 | 
			
		||||
      val readBytes = socketChannel.read(buffer)
 | 
			
		||||
      if ((readBytes == 0) || (readBytes == -1))
 | 
			
		||||
        return stringBuilder.mkString
 | 
			
		||||
      buffer.flip()
 | 
			
		||||
      val charBuffer = decoder.decode( buffer )
 | 
			
		||||
      stringBuilder.appendAll(charBuffer.array())
 | 
			
		||||
    }
 | 
			
		||||
    ""
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def sendString(str: String, socketChannel:SocketChannel): Unit = {
 | 
			
		||||
    logger.debug(s"Send: $str")
 | 
			
		||||
    val buffer = ByteBuffer.wrap(str.getBytes())
 | 
			
		||||
    socketChannel.write(buffer)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def recieve[T](sc:SocketChannel)(implicit ct: ClassTag[T]): Option[T] = {
 | 
			
		||||
    val raw = recieveString(sc)
 | 
			
		||||
    if ("".equals(raw)) return None
 | 
			
		||||
    val v = objectMapper.readerFor(ct.runtimeClass).readValue[T](raw)
 | 
			
		||||
    logger.debug(s"Recieve: ${v.toString}")
 | 
			
		||||
    Some(v)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def send[T](x:T, sc:SocketChannel)(implicit ct: ClassTag[T]): Unit =
 | 
			
		||||
    sendString(objectMapper.writerFor(ct.runtimeClass).writeValueAsString(x), sc)
 | 
			
		||||
 | 
			
		||||
  def genId(s: Any): String = {
 | 
			
		||||
    val md5 = java.security.MessageDigest.getInstance("MD5").digest(s.toString.getBytes)
 | 
			
		||||
    md5.map("%02X" format _).take(ID_LIMIT).mkString
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def writeJsonToFile[T](x:T, file:Path)(implicit ct: ClassTag[T]) = {
 | 
			
		||||
    val json = objectMapper.writerFor(ct.runtimeClass).writeValueAsString(x)
 | 
			
		||||
    Files.writeString(
 | 
			
		||||
      file,
 | 
			
		||||
      json,
 | 
			
		||||
      StandardCharsets.UTF_8,
 | 
			
		||||
      StandardOpenOption.CREATE,
 | 
			
		||||
      StandardOpenOption.TRUNCATE_EXISTING)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  def readJsonFromFile[T](file:Path)(implicit ct: ClassTag[T]):Option[T] = {
 | 
			
		||||
    if (Files.exists(file)) {
 | 
			
		||||
      val json = Files.readString(file)
 | 
			
		||||
      val v = objectMapper.readerFor(ct.runtimeClass).readValue[T](json)
 | 
			
		||||
      Some(v)
 | 
			
		||||
    } else {
 | 
			
		||||
      None
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,26 @@
 | 
			
		||||
import java.nio.file.Path
 | 
			
		||||
 | 
			
		||||
import com.github.fge.jackson.JsonLoader
 | 
			
		||||
import com.github.fge.jsonschema.main.JsonSchemaFactory
 | 
			
		||||
import datatypes.ClientState
 | 
			
		||||
import org.scalatest.FunSuite
 | 
			
		||||
 | 
			
		||||
class SchemaCompliance extends FunSuite {
 | 
			
		||||
  test("checkClientStateSchemeCompliance") {
 | 
			
		||||
    val clientState = ClientState(lastMessageTimestamp = "2020-09-11T08:54:45.528807100Z")
 | 
			
		||||
    val clientStateFilePath = Path.of("./target/checkClientStateSchemeCompliance.json")
 | 
			
		||||
    val schemaPath = Path.of("./resources/schemas/v0.0.1/ClientStateSchema.json")
 | 
			
		||||
 | 
			
		||||
    util.Util.writeJsonToFile(clientState, clientStateFilePath)
 | 
			
		||||
    val jsonToValidate = JsonLoader.fromFile(clientStateFilePath.toFile)
 | 
			
		||||
 | 
			
		||||
    val sf = JsonSchemaFactory.byDefault()
 | 
			
		||||
    val schema = sf.getJsonSchema(JsonLoader.fromFile(schemaPath.toFile))
 | 
			
		||||
 | 
			
		||||
    val report = schema.validate(jsonToValidate)
 | 
			
		||||
    println(report)
 | 
			
		||||
 | 
			
		||||
    assert(report.isSuccess)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
					Loading…
					
					
				
		Reference in New Issue