iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 4
1
Software Development

從讀遊戲原始碼學做連線遊戲系列 第 4

Day04 - Client 與 Server 的溝通 - 使用 EventMachine 管理連線

既然知道了 EventMachine 在 Unlight 專案中扮演了處理 TCP 連線的角色,不過實際上又是怎麼設計跟實作的呢?

我們先來看一下在 EventMachine 在 GitHub 提供的 EchoServer 範例

require 'eventmachine'

 module EchoServer
   def post_init
     puts "-- someone connected to the echo server!"
   end

   def receive_data data
     send_data ">>>you sent: #{data}"
     close_connection if data =~ /quit/i
   end

   def unbind
     puts "-- someone disconnected from the echo server!"
   end
end

# Note that this will block current thread.
EventMachine.run {
  EventMachine.start_server "127.0.0.1", 8081, EchoServer
}

看起來跟我們原本使用的原生 Ruby 版本似乎更容易使用多了,在 EventMachine 可以透過定義一個模組或者物件用來處理連線,只需要有對應的方法即可(像是 #receive_data#unbind 等等)
直接搜尋 Unlight 的原始碼看看,因此會發現有好幾個檔案都使用了 EventMachine.start_server

[elct9620] server ‹master› % ag 'start_server'
src/raid_chat.rb
40:    EM.start_server "0.0.0.0", SV_PORT, RaidChatServer

src/data_lobby.rb
39:    EM.start_server "0.0.0.0", SV_PORT, DataServer

src/watch.rb
45:    EM.start_server "0.0.0.0", SV_PORT, WatchServer

src/raid.rb
32:    EM.start_server "0.0.0.0", SV_PORT, RaidServer

src/raid_data.rb
41:    EM.start_server "0.0.0.0", SV_PORT, RaidDataServer

src/quest.rb
32:    EM.start_server "0.0.0.0", SV_PORT, QuestServer

src/lobby.rb
41:    EM.start_server "0.0.0.0", SV_PORT, LobbyServer

src/authentication.rb
42:    EM.start_server "0.0.0.0", $SV_PORT, AuthServer

src/chat.rb
41:    EM.start_server "0.0.0.0", SV_PORT, ChatServer

src/raid_rank.rb
41:    EM.start_server "0.0.0.0", SV_PORT, RaidRankServer

src/global_chat.rb
40:    EM.start_server "0.0.0.0", SV_PORT, GlobalChatServer

src/game.rb
45:    EM.start_server "0.0.0.0", SV_PORT, GameServer

src/matching.rb
48:    EM.start_server "0.0.0.0", SV_PORT, MatchServer

在 Ruby 裡面因為沒有 Interface (介面)的概念,取而代之的是 Duck Typing 的做法,也就是只要有對應的方法就可以被視為該種類型的物件或者介面。因此在 EventMachine 的範例中,只要是某個物件的實例(Instance)有相關的方法就可以被呼叫。而 Ruby 中所有東西都是物件,所以一個 Module 其實是 Module 物件的實例,也就符合了這個條件。

Unlight 會切割出這麼多伺服器的情境來看,以做 Web 服務的角度來看可能會有點疑惑,不過如果我們用最近幾年比較熱門的 MicroService (微服務) 來說明這個情境可能就會相對的有能接受這樣的做法。另一方面在遊戲伺服器的設計中,如果都集中在一個伺服器上處理,那麼某些特別消耗資源的步驟就會拖累其他人。因此拆分開來變成獨立的伺服器就能夠針對某個類型伺服器做拓展,這其實也是我們在線上遊戲常見的「分流」的概念。

如果從架構層面來看,其實就是 SOA 或者 MicroService 的方式去設計,只是切割的單位大或小的差異。雖然我們不可能直接將這樣的設計跟思考直接搬到其他類型的應用上,但是切割服務的方式以及如何在不同服務之間溝通的設計仍是我們值得參考跟學習的地方。

在 Unlight 原始碼中看到 EM 而不是 EventMachine 的原因是因為 EMEventMachine 的別名。

接著我們以 src/authentication.rb 這個檔案為基礎來追蹤,因為登入伺服器通常是遊戲的入口用來當作閱讀的起點是不錯的選項。下面的 AuthServer 位於 src/protocol/authserver.rb 這個位置。

module Unlight
  module Protocol
    class AuthServer < ULServer

      # クラスの初期化
      def self.setup
        super
        Player.auth_off_all
        # コマンドクラスをつくる
        @@receive_cmd=Command.new(self,:Auth)
        # 暗号化クラスを作る
        @@srp = SRP.new()
        @@invited_id_set = []
      end

我們先關注最開始的地方,這個檔案繼承自 ULServer 這個檔案,因此先往回看一下 src/protocol/ulserver.rb 來確認 AuthServer 繼承了哪些特性。

module Unlight
  module Protocol
    class ULServer < EventMachine::Connection

      # これ以上前に反応していなかった切る
      CONNECT_LIVE_SEC = 3600   # 1時間
      
      # ...
      
      # データの受信
      def receive_data data
        a = data2command(data)
        @command_list += a unless a.empty?
        do_command
      end

      # 切断時
      def unbind
        SERVER_LOG.info("#{@@class_name}: [Connection close] #{@ip}")
      end
      
      # ...
  end
end

前面有提到 EventMachine 提供了不少輔助方法來讓我們可以處理連線,在 ULServer 裡面就定義了文章最開頭所展示的 EventMachine 版本 EchoServer 所需的 #receive_data#unbind 等方法,由此可見透過 EM.start_serverAuthServer 是符合 EventMachine 的要求。

因為 EventMachine 已經幫我們解決了管理 TCP 連線的部分,所以我們只需要關注在怎麼「處理」這些連線。每當我們接受一個玩家連線時,EventMachine 就會產生一個 AuthServer 實例(一個玩家對應一個 Server Instance)並且在收到資料的時候呼叫 #receive_data 方法來解析玩家的操作,並進行後續的動作。

我的個人部落格是弦而時習之平常會把自己發現的一些新技巧紀錄在上面,也歡迎大家來逛逛。


上一篇
Day 03 - Client 與 Server 的溝通 - 同時處理多個連線
下一篇
Day 05 - 指令系統 - Unlight 指令結構分析
系列文
從讀遊戲原始碼學做連線遊戲33

尚未有邦友留言

立即登入留言