📜 ⬆️ ⬇️

The play "Developing a multiplayer online game." Part 3: Client-server interaction



Part 1: Architecture
Part 2: Protocol
Part 4: Moving to 3D

With the third part, I was a little late. But as they say better late than never ...
')
So, we continue the conversation.

In the third part of our production, we will implement the protocol, write the server and the client that will interact over the network. And (OMG!) Tanks will ride!
Under the cut that you have long wanted, but were afraid to ask ...



For the most snooty, let me remind you that all the code in the article does not claim to be the title “SuperPuperMegaFrigitelno FuckingAnable solution to all problems”. The code is intended to show the main points and only. It is ugly in places, is not optimal, but the nadis conveys the main essence.

Since the last article a lot has happened. One of them is that I switched to developing for Scala under IDEA. The reason is simple - the plug-in for NetBeans is completely lame ... Therefore, the project in bitbucket has been changed from NetBeans to IDEA, so don't be intimidated. And although the first impressions of IDEA are not very positive, I will try to chew this cactus.

Part Three Action one: What is there with the architector?



Recall that there with the architatura ...



She lays well on the actors in the rock. It turns out that there will be one process (GameServer) which accepts connections and, after the connection is established, passes the channel to the Actor (ClientHandler) for processing. Thus, an actor will be created for each client and he will be responsible for communication with the client. To send a message to the client, we simply send it to the actor and forget, the actor will send it to the client and accept the response. In general, the actors in the rock is a very interesting thing. They can be created by tens of thousands, practically for everybody. There is another implementation of the actors on the rock, the Akka project. He is much more sophisticated. And for real projects it makes sense to look at it.

Part Three Step two: The data transfer protocol.



To begin, create a class player Player. He will store the player id and his coordinates.
class Player(idd: Int, xx: Int, yy: Int) { var id = idd var x = xx var y = yy } 


We have the easiest protocol. To implement it, you need to create 2 classes. Packet class in which the message is stored.
 class Packet ( comm:Int, player: Player ) { val com = comm //  val id = player.id // id     val x = player.x val y = player.y } 


and Class encoding and decoding messages
 object Protocol { //  def encode( packet: Packet ): ByteBuffer = { val rez: ByteBuffer = ByteBuffer.allocate(16) rez.putInt(packet.com) rez.putInt(packet.id) rez.putInt(packet.x) rez.putInt(packet.y) rez } //  def decode( buffer: ByteBuffer ): Packet = { val com = buffer.getInt(0) val idd = buffer.getInt(4) val xx = buffer.getInt(8) val yy = buffer.getInt(12) val rez: Packet = new Packet( com, new Player(idd, xx, yy) ) rez } } 

The protocol implementation is ready. As you can see, in the simplest cases, nothing terrible. But in real projects for the invention of the bicycle there should be weight bases. It is better to use ready-made time-tested solutions.

Part Three Action three: Server, as much in this word ...



While we are creating a game frame. Therefore, the server will perform purely nominal work. Handle client connections and provide communication between clients. In the future, we will refine it.

Create a GameServer class
 object GameServer extends Runnable { var isActive = true var selector: Selector = null var numClients = 0 var port = 7778 //              var sessions = new HashMap[ SocketChannel, Actor ] var lock: AnyRef = new Object() //            def addPlayerMsg(player: Player) { lock.synchronized { ..... } } //   def init(portt: Int) { port = portt try { selector = Selector.open println( "Selector opened" ) } catch { case e => println( "Problems during Socket Selector init: " + e ) } } override def run() { //   bindSocket( "", port) //    while ( isActive ) { Loop() } } def Loop() { if ( selector.select > 0 ) { val it = selector.selectedKeys().iterator() while ( it.hasNext ) { val skey = it.next it.remove() if ( !skey.isValid ) { continue() } //  if ( skey.isAcceptable ) { val socket:ServerSocketChannel = skey.channel().asInstanceOf[ServerSocketChannel] try { numClients = numClients + 1 val channel = socket.accept channel.configureBlocking(false) channel.register(selector, SelectionKey.OP_READ) //      val player = new Player(numClients, numClients * 20, numClients * 20) val actor = new ClientHandler(player, channel) actor.start() //      val packet = new Packet( 0, player) actor.packets += packet //     sessions += channel -> actor println( "Accepted connection from:" + channel.socket().getInetAddress + ":" + channel.socket().getPort ) } catch { case e: Exception => println( "Game Loop Exception: " + e.getMessage ) } } //            else { val channel:SocketChannel = skey.channel.asInstanceOf[SocketChannel] val actor = sessions.get(channel).get.asInstanceOf[ClientHandler] if ( actor.packets.size > 0 ) { skey.interestOps(SelectionKey.OP_WRITE) } actor ! skey } } } } def close(remoteAddress:String, channel:SocketChannel) { channel.close(); println("Session close: " + remoteAddress); } //   def bindSocket(address: String, port: Int) { try { //   val hostAddr: InetAddress = null val isa = new InetSocketAddress(hostAddr, port) val serverChannel = ServerSocketChannel.open serverChannel.configureBlocking(false) serverChannel.socket.bind(isa) serverChannel.socket.setReuseAddress(true) serverChannel.register(selector, SelectionKey.OP_ACCEPT ) println( "Listening game on port: " + port ) } catch { case e: IOException => println("Could not listen on port: " + port + ".") System.exit(-1) case e => println("Unknown error " + e) System.exit(-1) } } } 

The server turned out simple as a perpendicular. It runs in a separate thread. Bread does not ask. Only the work with clients is shown here. There are no parts processing the correct connection / disconnection of clients. No session management. But this is already everyone can modify how he likes.

Part Three The fourth act: Actor is still Actor.



Now create an Actor that will handle the client connection.
 class ClientHandler(player: Player, chanel:SocketChannel) extends Actor { val player_id = player.id val channel = chanel val remoteAddress = channel.socket().getRemoteSocketAddress.toString var packets = new HashSet[ Packet ] def act() { loop { receive { //   case key: SelectionKey => { try { //   if (key.isReadable) { val buffer = ByteBuffer.allocate(16) channel.read(buffer) match { case -1 => close(remoteAddress, channel) case 0 => case x => processMessageRead(key, buffer) } } //   else if (key.isWritable) { packets.synchronized { for(packet <- packets) { processMessageWrite( Protocol.encode(packet) ) packets.remove(packet) } } if(packets.isEmpty) key.interestOps(SelectionKey.OP_READ) } } catch { case e: SocketException => println("ClientHandler SocketException error " + e.getMessage) case e: IOException => println("ClientHandler IOException error " + e.getMessage) case e => println("ClientHandler Unknown error " + e.getMessage) } } } } } //    def processMessageRead(key: SelectionKey, buffer: ByteBuffer) { if ( buffer.limit() == 0 ) { return } buffer.flip val protocol = Protocol.decode( buffer ) println( "Client : " + player.id + " - " + new Date + " - " + "com:" + protocol.com + " x:" + protocol.x + " y:" + protocol.y ) player.x = protocol.x player.y = protocol.y buffer.clear if (protocol.com == 0) { key.interestOps(SelectionKey.OP_WRITE) } else if (protocol.com == 1) { GameServer.addPlayerMsg(player) } } //    def processMessageWrite(buffer: ByteBuffer) { if ( buffer.limit() == 0 ) { return } buffer.flip channel.write(buffer) println( "Client write: " + player.id + " - " + new Date + " - " + buffer.array().mkString(":") ) buffer.clear } def close(remoteAddress: String, channel: SocketChannel) { channel.close() println("Session " + player.id + " close: " + remoteAddress) } } 

Actor turned out simple as a stick. He only receives messages and sends them.

For load testing, I launched the server on a rather weak laptop (1.3 GHz, AMD, WiFi 56Mbit). And as a client, I created a console java application that runs the specified number of threads, and in each continuously, without a pause, sends packets to the server. The client was launched on the desktop (3.6 GHz, 4 cores) in 100 threads.
As a result, the server digested about 6000 messages per second. Which is not bad in general. Depending on the computational load, on a real server hardware, it will be able to hold several thousand clients.

Part Three Step Five: The client ... and who else?



The client from the last part has not changed. Only the player’s graphic display in the form of a tank and protocol implementation was added.

Add a class describing the player
  public class Player extends MovieClip { public var Name:String = "Player"; public var id:int = 0; [Embed(source = '../../../../lib/tank.png')] public var _tank: Class; public var tank:Bitmap; public function Player() { width = 30; height = 30; tank = new _tank(); tank.width = 30; tank.height = 30; addChild(tank); } } 

And also we will add the methods implementing the protocol.
  //  public function sendMessage(val:int):void { if (socket.connected) { var bytes:ByteArray = new ByteArray(); bytes.writeInt(val); bytes.writeInt(player.id); bytes.writeInt(player.x); bytes.writeInt(player.y); socket.writeBytes( bytes, 0, 16); socket.flush(); } } //    private function dataHandler(e:ProgressEvent):void { var bytes:ByteArray = new ByteArray(); socket.readBytes(bytes, 0, 16); var com:int = bytes.readInt(); var id:int = bytes.readInt(); var x:int = bytes.readInt(); var y:int = bytes.readInt(); switch (com) { //   case 0: … //  case 1: ... } } 


Here is the basic interaction between the client and the server.

The issues of correct connection / disconnection of clients are not resolved, clients are synchronized (because of which the tanks are twitching while moving). All this awaits us in the following parts ...

PS Too much code ... can remove a part and leave only a description of the methods?

As always, all the sources can be viewed on Github

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


All Articles