简体   繁体   中英

How can I share variables between threads or otherwise handle this logic?

I'm working on a server to run speed-dating-style Chat Sessions.

Here's how it works:

  1. Users request to join a Session
  2. When 20 Users are requesting to join a Session, a new Session is created
  3. While the Session is running, groups of 2 Users are paired up in Chats
  4. After a Chat ends, those 2 Users go back into the User Pool to be paired up again
  5. Eventually the Session finishes

I'm trying to figure out how to handle the Sessions and pairings

I can't figure out how I can pass the sockets around between the threads and keep track of them

I'm using JSON over Binary Sockets, and I'm connecting to a MySQL database with Slick.

I think my threading architecture is logical, but tell me if something doesn't make sense:

ChatServer (main app, 
|           starts 1 ServerHandler thread, 
|           starts 1 SessionWaiter thread, 
|           then loops waiting for server-side commands)
├──ServerHandler (loops waiting for new clients, 
|  |              starts a new ClientHandler thread for each client)
|  └──ClientHandler (each thread communicates with 1 client, 
|                    client can request to join a chat session, 
|                    then database is updated to show the request)
└──SessionWaiter (loops checking database every 5 seconds, 
   |              if 20 Users are requesting a session then it creates a new session in the database, 
   |              assigns those 20 Users to that SessionID, 
   |              and creates 1 SessionRunner thread to handle the session, 
   |              passing the 20 client sockets to the SessionRunner - BUT HOW?)
   └──SessionRunner (each thread handles 1 Session (20 Users), pairing Users in Chats, until the Session ends)

application.conf:

mydb = {
  driver = "com.mysql.cj.jdbc.Driver",
  url = "jdbc:mysql://localhost:3306/chatsession?serverTimezone=UTC&useSSL=false",
  user = "root",
  password = "password",
  connectionPool = disabled
}

built.sbt:

scalaVersion := "2.13.1"
scalacOptions += "-deprecation"
libraryDependencies ++= Seq(
  "com.typesafe.slick" %% "slick" % "3.3.2",
  "org.slf4j" % "slf4j-nop" % "1.7.26",
  "com.typesafe.slick" %% "slick-hikaricp" % "3.3.2",
  "mysql" % "mysql-connector-java" % "6.0.6",
  "com.typesafe.play" %% "play-json" % "2.8.0"
)

Main.scala:

import java.net.ServerSocket
import java.io.PrintStream
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.BufferedWriter
import java.io.OutputStreamWriter
import java.net.Socket
import slick.jdbc.MySQLProfile.api._
import scala.concurrent.Future
import scala.concurrent.blocking // needed if using "blocking { }"
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Await
import scala.concurrent.duration.Duration
import scala.collection.mutable.ArrayBuffer
import java.util.concurrent.ConcurrentHashMap
import scala.jdk.CollectionConverters._
import scala.util.{Failure, Success}
import java.util.concurrent.{Executors, ExecutorService} // for threads
import play.api.libs.json._

class ClientHandler(socket: Socket) extends Runnable {
  def run() : Unit = {
    val inputstream = new BufferedReader(new InputStreamReader(socket.getInputStream()))
    val outputstream = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))
    var break = false
    var startTime = System.currentTimeMillis()
    outputstream.write("welcome") // convert to json
    outputstream.newLine()
    outputstream.flush()
    println("client welcomed")
    while(!break){
      if(inputstream.ready()){
        val input = inputstream.readLine() // blocking call
        println("client: " + input)
        val json: JsValue = Json.parse(input)
        val command = (json \ "command").validate[String].getOrElse("unknown command")
        command match {
          case "connect" =>
            val id = (json \ "id").validate[String].getOrElse("unknown command")
            println(id + " connected")
          case "joinsession" =>
            // update user row in database (set WantsToJoin=1, LastActive=CurrentTime)
            // should I store their socket number in the db or can I pass it internally somehow?
            // respond to client and tell them they are in a queue
          case "ping" =>
            // client program sends a ping every 3 seconds
            // if ping or another command is not received for 5 seconds, 
            // then the socket will be closed below
          case _ => // any other input, break and close socket
            println("breaking and closing socket")
            break = true
        }
        startTime = System.currentTimeMillis()
      } else if (System.currentTimeMillis() - startTime >= 5000) {
        break = true
      }
    }
    socket.close()
  }
}

class ServerHandler(serversocket: ServerSocket) extends Runnable {
  def run() : Unit = {
    while(true) {
      val socket = serversocket.accept // blocking call
      println("client connected")
      (new Thread(new ClientHandler(socket))).start()
    }
  }
}

// each thread of this class will manage an individual session with 10 users
class SessionRunner() extends Runnable {
  def run() : Unit = {
    while(true) {
      // have an array of the 10 users (with each socket, userid from database, etc)
      // take over each user's socket connection 
      // how do I get the sockets?
    }
  }
}

// one thread of this class will be run in a loop
// every 5 seconds it will check how many users are requesting a session
// if there are 10 users requesting a session, a new SessionRunner thread will be created
// -and passed the 10 sockets? of those 10 users so it knows which clients to contact
// how do I keep track of those sockets and pass them?
class SessionWaiter() extends Runnable {
  def run() : Unit = {
    while(true) {
      // time out for 5 seconds
      // do a database read
      // if there are 20 users:
      // -who are requesting a session, and
      // -who have been online within the last 30 seconds
      // then create a new thread to handle that session
      // update those user rows to show:
      //  -they are no longer requesting a session, and
      //  -that they are in a session, and update sessionid
      //  -so they can rejoin the session if they lose and regain connection before the session ends
      (new Thread(new SessionRunner())).start()
      // -(how do I pass the 10 users' client sockets to that thread??)
    }
  }
}

// TDL: server prints to console every 10 seconds:
// # of active sessions, # of users in sessions, # of users waiting for a session
object ChatServer extends App {
  val server = new ServerSocket(10000)
  (new Thread(new ServerHandler(server))).start()

  var break = false
  while(!break) {
    print(">")
    val input = scala.io.StdIn.readLine() // blocking call
    input match {
      case "quit" =>
        println("\nQUITTING")
        server.close()
        break = true
      case _ =>
        println("\nUnrecognized command:"+input+"<")
    }
  }
}

Where I'm stuck: How can I manage the sockets of the 20 Users so that I can pair them up and relay Chats between the pairs?

When I have 20 Users waiting for a Session, I'd like to build an array of their sockets and other user data from the database (userid, username, etc), and pass the array to the SessionRunner thread that is created to handle that Session.

Then that thread should take control of those 20 sockets and deal with managing the session and communicating with the clients.

But when a User leaves the Session, the SessionHandler should hand their socket back to the ClientHandler again, and they should be able to request to join a new Session or otherwise communicate with the ClientHandler.

Disclaimer : Besides that, you are using very low-level concurrent primitives (Thread, Runnable) at the same time when you have a bunch of very powerful libraries and solutions for such tasks, especially in Scala (Futures, Akka, Akka-streams, cats effect, fs2, etc), I will try to help you out and I will use simplest approaches and primitives so it would be in the style of current code. I hope this is a studying task, not a part of a production solution.

You can do it in at least 2 ways that would be in the style of your current program:

  • share global state and reference it from a static context
  • share state and pass it in modules (objects) by constructors

For a simpler one (the global state from static context) it would be something like below.

In SessionWaiter you need a reference for some collection from which you can get 10 sockets and create a session. The important thing, that you must work with that collection in a thread-safe way. I suggest a simpler one - through synchronized blocks. We will assume that the collection would contain only requesting connections. For that we will write a static object that holds a reference for that collection:

type MyConnectionType = Socket
object RequestingConnectionsHolder {
  @volatile var requestingConnections: List[MyConnectionType] = List()
}

In SessionWaiter you will check that there is at least 10 connection, start a session with first 10 connections and remove them from the collection.

class SessionWaiter() extends Runnable {
  def run() : Unit = {
    while(true) {
      val collection = RequestingConnectionsHolder.requestingConnections
      val sessionSize = 10
      RequestingConnectionsHolder.synchronized {
        if(collection.size >= sessionSize) {
          val (firsts, lasts) = (collection.take(10), collection.drop(10))
          (new Thread(new SessionRunner(firsts))).start()
          RequestingConnectionsHolder.requestingConnections = lasts
        }
      }
    }
  }
}

In ClientHandler you will fill the requestingConnections when connection/client transfers in that case. As I understand it would be in case "joinsession" =>

case "joinsession" => RequestingConnectionsHolder.synchronized { RequestingConnectionsHolder.requestingConnections += socket }

And finally in SessionRunner after (when) you end a session and its connections became "requesting a new session", you must add it to requestingConnections

class SessionRunner(connections: List[MyConnectionType]) extends Runnable {
  def run() : Unit = {
    while(sessionIsAlive) { }
    RequestingConnectionsHolder.synchronized {
      RequestingConnectionsHolder.requestingConnections += connections
    }
  }
}

Also at some point, you should check if a connection is closed and you need to delete it from requesting connections.

And again, this is not the way tasks like that should be solved. The code above is very, very bad. But if you at a stage of studying when you need to practice with raw threads and the simplest concurrent primitives - it is Ok.

For a better solution, at the current level of your knowledge and skills, I suggest you explore Akka, because your task seems to be very suitable to be solved by an actor model of processing.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM