Game server on Scala + Akka

image

Once upon a time, I already raised the topic of using Scala in a game server. Then it was a very simple example using only Scala. Since then, a lot of water has flowed. Scala and Akka are developing, but something is not adding articles on them. And the topic is very interesting. In general, I want to continue the series of articles about the server on Scala. This article will describe the overall architecture of the solution. And also that gives the use of Scala and Akka. Code examples.

So what's the point? What is so special about using this bundle?

Disclaimer.


Those who rummage in the subject may find inaccuracies and simplifications in the description. So it was conceived. I wanted to show general points for those who do not know what it is and how it can be used. Nevertheless, everyone is welcome to kamenty. I hope for a constructive discussion and hot holivars) For often, in holivars, moments come up that you usually don’t think about, but which play a significant role.

What is Akka and why is it good


If you do not go into details, then the development of the actors comes from the philosophy that all the circle of actors. Like OOP, it proceeds from the philosophy that the whole circle of objects. The fundamental differences are that the actors are executed in parallel. While OOP code is executed sequentially, and for parallel execution, additional and far from always simple actions need to be done. As well, actors interact with each other not through method calls on objects, as in OOP, but through sending messages. The actor has a queue of these messages (mailbox). Messages are processed strictly in turn.

Actors in Akka are described as light threads. Creating such an actor costs almost nothing; millions can be created. The creators declare that on 1Gb of memory you can create about 2.5 million actors. And on one machine you can achieve an exchange rate of about 50 million messages / sec.

Well, so what? You ask. What is the profit from all this?

And the profit is generally obvious. The code is loosely coupled; an actor does not need a direct link to another actor to send him a message. In the actor model, there is no shared state. Messages arriving in actors are processed sequentially. It turns out that the actor does not depend on anyone. The data in it does not need to be synchronized with other actors, and the code, in a single actor, is executed “in one thread”. Well, as you know, writing single-threaded code is much easier than multi-threaded. But since our actors are executed in parallel, in the end the whole system works in parallel, uniformly utilizing all available iron. In general, the reliability of the system is higher.

There is an opinion (and I share it) that just the actors are the most correct implementation of OOP. For in life, for example, if I need to take a hammer, but I can’t reach it, I don’t directly control the hand of the neighbor who gives me the hammer. I tell him (in fact I am sending an oral message) "give me the hammer." He takes it, processes it and delivers a hammer.

Of course, this is a very simple description of a complex system. Akka has so many features. The same lines in the actor can be implemented in different ways. For example, have a size limit, or be stored in the database, or have a certain sorting. Yes, and no one actually bothers to realize a turn with chess and poetesses. What else is special? Actors can be executed on different JVM mechanisms. For example, on a thread pool or on a Fork Join pool (by default it is used). You can control threads by allocating a separate thread or even a pool of threads for an actor. Actors can work both within the same machine and over the network. There is a cluster out of the box.

To send a message to an actor, you need to know his address or have a link in the form of ActorRef. The address system has a tree system. For example, “akka: // sessions / id12345”. This is the address of the actor responsible for processing messages in the session of the player id12345.
You can send him a message:

context.actorSelection(«akka://sessions/id12345») ! Msg

Or send a message to all connected players
context.actorSelection(«akka://sessions/*») ! Msg


In general, to make it clearer, I will give a simple example. I must say right away, the example is sucked from the finger, just to show the options. Suppose players need to periodically send messages to e-mail. What could be easier? We make some kind of class, in it the method accepts the address and message. Everything is generally trite. But then the game became popular and the number of letters began to grow. I will write how it can look in Akka.
You create an actor that accepts the message as a class with 2 fields (in Scala it will be a simple case class):

Email(String address, String msg)

In the handler, you describe sending a message
def receive = {
    case evt: Email(address, msg) ⇒ sendEmail(address, msg)
}


All in general. Now this actor will receive his piece of iron resources and send mail.
Then a crowd of people came, and the system began to slow down with the sending of mail. We go into the config and select a separate thread for this actor so that it is less dependent on another load.
The crowd is still growing. We go into the config and allocate the pool of threads to the actor.
The crowd is still growing. We transfer the actor to a separate computer, he begins to consume all the iron of this computer.
The crowd is still growing. We select several machines, create a cluster, and now a whole cluster is engaged in sending mail to us.
With all this, the code of our actor does not change, we configure all this through the config.
The whole application is not even aware that the actor has moved somewhere and, in fact, is already a cluster; he is also sent messages to the address “/ mailSender”.
Everyone can imagine how many gestures will need to be done to implement such a system in the classic version on OOP and threads. It is clear that the example is pulled over the ears and pops at the seams. But if you don’t get boring, then it’s quite possible to imagine some kind of personal experience in this perspective.

But where is the server?

With the actors roughly figured out. Let's try to design a game server using this model. Since everything is now actors in us, we primarily describe the main elements of the server through actors, without taking into account the specifics of a particular game.
The diagram shows how the server might look in this case.
image

Front Actor - Actor responsible for communication with clients. Their connection, disconnection and controls the session. Supervisor for actor S
S - User Sessions. Actually in this case, it is an open socket connection. The actor is directly responsible for the transmission and reception of messages from the client. And is a child of FrontActor .
Location actor- The actor is responsible for processing some area in the game world. For example, part of a map or room.
You can still create an actor to work with the database, but we will not consider it yet. Work with the database is usual and there is nothing special to describe there.

That's the whole server. What did we get?
We have an actor who is responsible for networking. Akka has a built-in high-performance network core that out of the box supports TCP and UDP. Therefore, to create a front, it is necessary to make very few gestures. Our actor accepts a connection from a client, creates a session for him, and in the future, all sending and receiving messages goes through him.

The front actor looks something like this:

class AkkaTCP( address: String, port: Int) extends Actor
{
  val log = Logging(context.system, this)
  override def preStart() {
    log.info( "Starting tcp net server" )
    import context.system
    val opts = List(SO.KeepAlive(on = true),SO.TcpNoDelay(on = true))
    IO(Tcp) ! Bind(self, new InetSocketAddress(address, port), options = opts )
  }
  def receive = {
    case b @ Bound(localAddress) ⇒
    // do some logging or setup ...
    case CommandFailed(_: Bind) ⇒ context stop self
    case c @ Connected(remote, local) ⇒
      log.info( "New incoming tcp connection on server" )
      val framer = new LengthFieldFrame( 8192, ByteOrder.BIG_ENDIAN, 4, false )
      val init = TcpPipelineHandler.withLogger(log, framer >> new TcpReadWriteAdapter )
      val connection = sender
      val sessact = Props( new Session( idCounter, connection, init, remote, local ) )
      val sess = context.actorOf(  sessact , remote.toString )
      val pipeline = context.actorOf(TcpPipelineHandler.props( init, connection, sess))
      connection ! Register(pipeline)
  }
}

The session looks something like this:
// ----- Класс-сообщение реализующий команду отправки на клиент
case class Send( data: Array[Byte] )
// -----
class Session( val id: Long, connect: ActorRef,
               init: Init[WithinActorContext, ByteString, ByteString],
               remote: InetSocketAddress,
               local: InetSocketAddress ) extends Actor
{
  val log = Logging(context.system, this)
  // ----- actor -----
  override def preStart() {
    // initialization code
    log.info( "Session start: {}", toString )
  }
  override def receive = {
    case init.Event(data) ⇒ receiveData(data)  // Обрабатываем получение сообщения
    case Send(cmd, data) ⇒ sendData(cmd, data) // Обрабатываем отправку сообщения
    case _: Tcp.ConnectionClosed ⇒ Closed()
    case _ => log.info( "unknown message" )
  }
  override def postStop() {
    // clean up resources
    log.info( "Session stop: {}", toString )
  }
  // ----- actions -----
  def receiveData( data: ByteString ) {
	...
	// Распаковываем сообщение, отправляем по назначению
  }
  def sendData( cmd: Int, data: Array[Byte] ) {
    val msg: ByteString = ByteString( ... ) // Упаковываем сообщение
    connect ! Write( msg ) // отправляем
  }
  def Closed(){
    context stop self
  }
  // ----- override -----
  override def toString =
    "{ Id: %d, Type:TCP, Connected: %s, IP: %s:%s }".format ( id, connected, clientIpAddress, clientPort )
}


As a result, we don’t have to think about how many threads to allocate to receive messages, how they will be synchronized, etc. The code is very simple.

We also have an actor responsible for locations (or rooms).
It is more complex, as it will process commands to calculate the game situation. In the simplest case, this can be done inside it. But it is better to select a separate actor for calculating the game mechanics. If this is a turn-based game, then you don’t need to do anything else, just get a team and do the calculation. If this is some kind of real-time game, then it will already need to implement a game cycle, which every N ms will collect the incoming teams, make calculations and prepare replicas of the results for sending them to players from this location. For each such actor, you can select your own stream.
In a real project, of course, it will be necessary to complicate the scheme. Add an actor supervisor who will steer the rooms. Create them when necessary, and delete as unnecessary. Depending on the complexity of the game mechanics, it is possible to complicate the calculation mechanism itself, for example, by allocating a separate server for it.

Here's what an actor might look like:

class Location( name: String )  extends Actor
{
  val log = Logging(context.system, this)
  // ----- actor -----
  override def preStart() {
    log.info( "Room start: {}", name )
  }
  override def receive = {
    case evt: Event ⇒ handleEvent( evt )
    case cmd: Command ⇒ handleCommand( cmd )
    case _ => log.info( "unknown message" )
  }
  override def postStop() {
    // clean up resources
    log.info( "Room stop: {}", name )
  }
  // ----- handles -----
  def handleEvent( evt: Event ) = {
  }
  def handleCommand( cmd: Command ) = {
	  // Пример отправки реплики отправителю команды
      cmd.sender ! Send( "gamedata".getBytes )
  }
}


Command - This is a message from the client, some kind of command. For example Shot, movement, activation of a spell.
Event is an internal server event. For example, creating a mob or switching the time of day.

If there is interest, you can continue and parse the code of some working option. Or about Akka in more detail, with examples.

Only registered users can participate in the survey. Please come in.

Where to go

  • 49.7% More about Akka with examples 259
  • 50.2% Parse a working server example 262

Also popular now: