📜 ⬆️ ⬇️

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 those times a lot of water has flowed. Scala and Akka are developing, but articles on them are not added. And the topic is very interesting. In general, I want to continue the cycle of articles about the server on Scala. This article will describe the overall solution architecture. And also that gives the use of Scala and Akka. Code examples.

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

Diskleimer.


Tech who fumbles in the subject, may find inaccuracies and simplifications in the description. So it was intended. I wanted to show the general points for those who do not know what it is and how it can be used. But nevertheless, welcome to everyone. I hope for a constructive discussion and hot holivars) For often, in holivars, there are moments about which 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 on the actors comes from the philosophy that the whole circle is actors. Just as the PLO comes from the philosophy that all circle objects. The principal 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 must be done. As well as the actors interact with each other not through method calls to 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 lightweight threads (green threads). Creating such an actor costs almost nothing, you can create millions. The creators declare that on 1Gb of memory, you can create about 2.5 million actors. And on one machine, you can reach an exchange rate of about 50 million msg / sec.

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

A profit in general is obvious. The code turns out to be loosely coupled, the actor does not need a direct link another actor to send him a message. In the actor model, there is no shared state. Messages coming to the 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 a single-threaded code is much easier than multi-threaded. But since our actors are running in parallel, in the end, the entire system runs in parallel, uniformly utilizing all available hardware. In general, the reliability of the system is higher.

There is an opinion (and I share it) that the actors are the most correct implementation of the PLO. For in life, for example, if I need to take a hammer, and I cannot reach it, I do not directly control the hand of a neighbor who gives me a hammer. I tell him (in fact, I am sending an oral message) “give the hammer”. He accepts it, processes it and delivers the hammer.

Of course, this is a very simple description of a complex system. Akka has a lot of possibilities. The same queues in the actor can be implemented in different ways. For example, to have a size limit, or to be stored in a database, or to have a certain sort. And no one actually interferes with the realization of their turn with chess and poetess. What else is special? Actors can be executed on different JVM mechanisms. For example, on the thread pool or on the Fork Join pool (by default it is used). You can manage threads by allocating a separate stream or even a pool of threads for an actor. Actors can work both within one 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 id12345 player session.
You can send a message to him:

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. At once I will say, the example is sucked from the finger, just to show the options. Suppose the players need to send periodic email messages. What could be easier? We do some sort of class, in it a method accepts an address and a message. All in all trite. But then the game became popular and the number of letters began to grow. Sign for how this may look like in Akka.
You create an actor that accepts a message in the form of a class with 2 fields (in Scala this will be a simple case class):

 Email(String address, String msg) 

In the handler, the message is sent.
 def receive = { case evt: Email(address, msg) ⇒ sendEmail(address, msg) } 


All in all. Now this actor will receive its piece of iron resources and send mail.
Here comes a crowd of people, and the system began to slow down with sending 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. Go to the config and select the actor thread pool.
The crowd is still growing. We transfer the actor to a separate computer, it begins to consume all the hardware of this computer.
The crowd is still growing. We select several machines, create a cluster and now we have a whole cluster involved in sending mail.
With all this, the code of our actor does not change, we all set it up through the config.
The entire application is not even aware that the actor has moved somewhere and is in fact already a cluster, and messages are also sent to the address “/ mailSender”.
Everyone can imagine how many gestures will have to be done in order to implement such a system in the classic version on OOP and streams. It is clear that the example is pulled by the ears and cracks at the seams. But if you don’t get bored, then it’s quite possible to imagine some kind of personal experience from this angle.

But where is the server?

Approximately dealt with actors. Let's try to design a game server using this model. Since we now have everything as actors, we first of all describe the main elements of the server through the actors, without taking into account the specific features of the particular game.
The diagram shows how the server might look in this case.
image

Front Actor - Actor responsible for communication with customers. Their connection, disconnection and control session. Is a supervisor for actor S
S - User sessions. Actually in this case, these are open socket connections. The actor is directly responsible for sending and receiving messages from the client. And is a child of FrontActor .
Location Actor - Actor responsible for handling some area in the game world. For example, part of the map or room.
You can also create an actor to work with the database, but we will not consider it yet. Working with the database is normal 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 highly efficient network core out of the box that supports TCP and UDP. Therefore, to create a front, you need to do very little gestures. Our actor accepts a connection from the client, creates a session for it and in the future, all sending and receiving messages goes through it.

The front actor looks 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) } } 

Session looks 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 do not need to think how many threads to allocate for receiving sending 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, since it will process the commands to calculate the game situation. In the simplest case, it can be done inside it. But it is better to single out a separate actor for calculating game mechanics. If this is a turn-based game, then you don’t need to do anything extra, just get a command and do the calculation. If this is some kind of real-time game, then it will already have to implement the game cycle, which the incoming teams will collect every N ms, make calculations and prepare replicas of the results for sending them to players from this location. Each such actor can be distinguished by its own flow.
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 them as unnecessary. Depending on the complexity of the game mechanics, you can complicate the calculation mechanism itself, for example, by selecting 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 spells.
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 a working version. Or about Akka in more detail, with examples.

Source: https://habr.com/ru/post/229045/


All Articles