plain framework的實際應用和擴充套件

戀月發表於2022-07-01

首先在這裡慶祝香港迴歸祖國的懷抱25週年,想起那年還是一個小學生戴著紅領巾和胸章激動不已,實現祖國的統一是每個中華兒女從小的夢想!趁著這歡慶的日子,突然想要寫些什麼,其實最近也在做一些事,由於工作繁忙加上自身的懶惰,因此對於自己所維護的這個框架感覺有些疏遠。但我還是沒有忘記8年前,當初實現這個框架的激情和夢想,在這裡我用一個具體的例子來看看PF框架到底能做些什麼。

  例子

  這次的選擇是以一個實際運營的專案做測試,其實我在以前的文章中也提到過這個應用,也許大家都並不陌生,它就是現在還比較火爆的劍俠3,一款多人線上的MMO遊戲。網路上很久以前就流出了其部分的原始碼,很久以前我也對它經過一定的分析,不過感覺到並沒有修復和學習的必要,也就沒有再看。現在想來最大的原因就是那份程式碼缺少的東西太多,就連一些基本的標頭檔案都缺失,那麼參考的意義就不大,除非有極深的興趣和足夠的精力。

  這次選擇的是幾年前稍微完整的一份程式碼,當然也只是衝著研究和學習的目的,畢竟那麼一個大型的專案還是有一定的研究價值,不過想要實際執行並沒有那麼容易,雖然一份可以32編譯的版本,但我是索然無味的。其最大的原因,就是我想要將plain framework(PF)應用到這裡面。其實從殘缺的程式碼中可以看到其實確實比較重要的地方就是網路方面,而正好PF擁有這個能力,一切就自此開始了。

  PF的網路

  修復那流出並不太全的程式碼,最大的就是需要實現網路方面的介面,好在PF主要做這方面的,而在修改這部分程式碼之前,我還以為可能會費一番周折。可是經過一段時間的思考後,結果就如果下面的短短程式碼,一切都如同古語說的那樣:大道至簡!

// 這個執行緒池目前只用於重連                                                     
pf_sys::ThreadPool g_thread_pool(6);                                            
                                                                                
void reconnect(                                                                 
    pf_net::connection::Basic *conn,                                            
    const std::string &name,                                                    
    const std::string &ip,                                                      
    uint16_t port) {                                                            
  for (;;) {                                                                    
    std::this_thread::sleep_for(std::chrono::milliseconds(5000));               
    KGLogPrintf(KGLOG_DEBUG,                                                    
        "reconnect: %s(%s:%d)", name.c_str(), ip.c_str(), port);                
    auto r = conn->connect(ip.c_str(), port);                                   
    if (r) {                                                                    
      get_conn_mgr().add(conn); //執行緒不安全?                                     
      get_conn_mgr().set_connection_name(conn->get_id(), name);                    
      conn->set_name(name);                                                        
      break;                                                                       
    }                                                                              
  }                                                                                
};                                                                                 
                                                                                   
std::unique_ptr < pf_net::connection::manager::Connector > g_connector;            
// static bool g_connector_init{ false };                                          
pf_net::connection::manager::Connector & get_conn_mgr()                            
{                                                                                  
  if (is_null(g_connector)) {                                                      
    auto connector = new pf_net::connection::manager::Connector;                   
    unique_move(pf_net::connection::manager::Connector, connector, g_connector);
    g_connector->callback_disconnect([](pf_net::connection::Basic *conn){          
      auto flag = conn->get_param("no_reconnect"); // 如果設定了不自動重連...   
      if (flag == true) return;                                                    
      std::string ip = conn->socket()->host();                                     
      if ('\0' == ip[0]) return;                                                   
      auto port = conn->socket()->port();                                          
      std::string name = conn->name();                                             
      conn->set_empty(false);                                                      
      g_thread_pool.enqueue([conn, name, ip, port]{                                
        reconnect(conn, name, ip, port);                                           
      });                                                                          
    });                                                                            
  }                                                                             
  return *g_connector.get();                                                    
}

  上面的程式碼是用於客戶端連線的,也就是需要連線到伺服器使用,在這個專案的構架中游戲伺服器也需要連線到其他伺服器的,如中心服和閘道器。這裡在原來的基礎上,實現了斷線自動重連的功能,原本的伺服器是斷線後就必須重啟所有的程式才能正常工作的,這裡做了一點小小的改進,而且實現起來並不複雜。

  那麼面向伺服器的監聽的如何實現呢?其實比較簡單,直接使用PF自帶的服務進行建立並監聽即可,程式碼如下:

  listener = new pf_net::connection::manager::Listener;                             
  unique_move(pf_net::connection::manager::Listener, listener, listener_);          
  bRetCode = listener_->init(m_nMaxPlayer, nPort, szIP);                            
  KGLOG_PROCESS_ERROR(bRetCode);

  是不是感覺使用挺容易的?不過到這裡功能並沒有做完,如果你需要處理連線和斷開時的處理則需要註冊相應的處理函式(這個是在修復這段程式碼時,對PF做了一點小小的功能支援調整)。

  程式碼如下(因為原來的處理客戶端連線的程式碼比較多,這裡只擷取其中一部分,如果有興趣可以在殘碼中找到):

  

  listener_->callback_connect([this] (pf_net::connection::Basic * conn) {           
    std::pair < KPlayerTable::iterator, BOOL > InsRet;                              
    std::string ip;                                                                 
    KPlayerAgency * pPlayer = NULL;                                                 
    KG_PROCESS_ERROR(m_PlayerTable.size() < (size_t) m_nMaxPlayer);                 
    while (true) {                                                                  
      KPlayerTable::iterator it = m_PlayerTable.find(m_nNextPlayerIndex);           
      if (it == m_PlayerTable.end()) {                                              
        break;                                                                      
      }                                                                             
      m_nNextPlayerIndex++;                                                         
    }
   ...
};

 

  listener_->callback_disconnect([this] (pf_net::connection::Basic * conn) {    
    // 該設定在接受連線時進行處理                                               
    auto index = conn->get_param("player_index");                               
    std::string ip = conn->socket()? conn->socket()->host() : "";               
    KPlayerAgency * pPlayer = NULL;                                             
    KGLOG_PROCESS_ERROR((index.type != pf_basic::type::kVariableTypeInvalid));  
    pPlayer = GetPlayer(index.get < int32_t > ());                              
    KGLOG_PROCESS_ERROR(pPlayer);                                               
    OnDisconnect(pPlayer);                                                      
    KGLogPrintf(KGLOG_INFO,                                                     
        "Player disconnect from %s, index = %d\n",                              
        ip.c_str(), index.get<int32_t>());                                      
  Exit0:                                                                        
    return;                                                                     
  });

  由於要配置這個專案的心跳邏輯,因此無法直接使用PF的執行模式(實際上是可以的,不過為了儘可能的改動不多),於是在這裡PF又提供了一個基礎環境初始化的介面,方便註冊網路包的處理以及可以使用日誌等介面。

  實現如下:

  // PF basic enviroment.                                                          
  r = pf_engine::init_basic_env();                                                 
  KG_PROCESS_ERROR(r);

  使用上面的模式可以脫離pf::Application啟動,而且基本的介面都能正常使用,但為了正常的使用,現在需要在你的程式邏輯成功後加上如下程式碼:

  // 標記啟動應用                                                               
  GLOBALS["app.status"] = kAppStatusRunning;

  關於這份程式碼

  目前網路上已經有了這個遊戲的一鍵啟動,在這裡我也放一張進入遊戲的圖:

  有興趣的朋友們如果需要學習和研究這份殘碼,我這裡可以簡單總結一下,它是10年初的版本,因此大概就是1.5的70版本,網上由於流出的指令碼是80的,而且很多的指令碼殘缺不全,導致了許多的AI甚至物品以及技能無法正常使用。以前也玩過這個遊戲,是遊戲剛出來的時候(大約09年左右),我覺得這個遊戲的技術在當時還是很不錯的,無論和畫面還是其他各方面在當時國內算得上是前沿。如果作為學習和興趣,嘗試慢慢修復各種指令碼需要很長一段時間,就算是比較精通指令碼,要達到完整也不容易。

  如果喜歡本遊戲的還是建議大家到官網下載該遊戲,這款遊戲算是目前國內花錢不太多的遊戲之一了,而且經過十多年的更替,其畫質和各方面都表現的比較出色。

  

  總結

  這次選擇使用這段殘碼作為PF的一個實驗,證明了PF在實際應用中還是具有其特性的簡單、快速、高效的目的,當然為了完善在實際應用中做了略微的調整。當前PF還是有實際應用的不足,但是我相信在未來可以得到逐步的完善。

  PF2.0的版本或許在將來,使用全新的如C++23進行一次重大更新。

相關文章