1. 程式人生 > >websocket基於php 記一次結合PHP多程序和socket.io解決問題的經歷

websocket基於php 記一次結合PHP多程序和socket.io解決問題的經歷

記一次結合PHP多程序和socket.io解決問題的經歷

 

  公司是做棋牌遊戲的。前段時間接到一個後臺人工鑑定並處理通牌作弊玩家的需求,其中需要根據幾個玩家的遊戲ID查詢並計算他們在某段時間內彼此之間玩牌輸贏次數和輸贏總額。

  牌局資料是儲存在日誌中心的,他們把牌局資料分成兩個表來儲存,一個表儲存牌局概況資料,例如牌局時間、牌局ID、桌子ID、使用者ID等資訊,另一個表則儲存每一個牌局的詳情資料,例如,牌局有多少玩家參與,荷官在哪一輪發了什麼牌,玩家每一輪都有什麼動作等等。要想計算出幾個玩家在某段時間之內玩牌輸贏次數和輸贏總額,就需要知道每一個牌局的詳情資料,所以需要針對每一個玩家的遊戲ID,先查詢第一個表,查出所有牌局概況資料列表,然後遍歷這個列表,根據每個牌局的牌局ID、桌子ID,從第二個表中查詢每個牌局的詳情資料,所有玩家的所有牌局詳情資料都查詢完成之後再進行統計。

  日誌中心的同學給出了查詢以上兩個表的介面,其中牌局詳情的查詢介面一次只能查詢一個牌局的資料(和他們使用的資料表設計有關)。剛開始我的做法是在js程式碼中遍歷所有給出的玩家ID,先查詢出每個玩家的牌局列表,然後使用第二層迴圈來呼叫介面請求每一個牌局的詳情資料,但這樣做的問題是,有些使用者在某段時間內的牌局數量是很大的,儘管控制了查詢時間段的最大範圍,但還是出現了一個使用者幾千個牌局的情況,這就意味著瀏覽器需要幾乎在同一時間內對同一個域名的伺服器發出幾千個請求,而瀏覽器是基於域名進行併發控制的,超過限制數量的請求會被阻塞,阻塞嚴重的時候經常導致頁面變成空白,好長時間才恢復,得到查詢結果。這樣的體驗顯然是不行的。

  那怎麼辦呢?在老大的指導下,幾經思慮,決定採用PHP多執行緒結合socket.io來完成這個任務。整體思路是這樣的:首先js向PHP發起資料查詢請求,PHP收到請求之後不是直接進行資料查詢,而是在後臺掛載一個程序去處理請求,然後返回一個確認狀態值給js,這時js請求暫時結束了。這樣做好處有二:其一,js請求的PHP介面是php-fpm執行的,使用php-fpm來fork多程序不太穩定,而使用php比較穩定;其二,可以避免資料查詢過程時間太長導致超時。

  掛載程序程式碼示例:

1 2 3 4 5 6 7 <?php $par  = [ 'startTime'  =>  '' 'endTime'  =>  '' 'mids'  =>  $mid , ...]; //牌局查詢引數 $pKey  'plog_proccess' ; //傳給命令列的引數,作為程序標識,便於查詢統計當前程序數量 $php  '/usr/local/php/bin/php' ; //php執行檔案路徑 $file  '/www/query.php' ; //牌局查詢指令碼檔案 $cmd  $php . ' ' . $file . ' ' . $pKey . ' ' . base64_encode (serialize( $par )). ' > /dev/null 2>&1 &' ; //命令 system( $cmd ); //執行命令,掛載後臺程序執行查詢

  接下來就要在程序執行的PHP指令碼/www/query.php中進行資料查詢了。首先遍歷每一個玩家ID,查出每個玩家的所有牌局列表,然後遍歷每個玩家的牌局列表,fork多個子程序進行每個牌局詳情資料的查詢了,一個子程序負責查詢一個牌局的詳情資料,並將資料寫入檔案中,程式碼示例如下:(注意:以下程式碼只是基本程式碼框架,無法直接執行)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <?php $pKey  $argv [1]; $par  = unserialize( base64_decode ( $argv [2])); $mids  $par [ 'mids' ]; $max_pnum  = 100; //最大子程序數量,避免搶佔了過多的資源   for ( $mids  as  $mid ) { //遍歷查詢各個使用者的牌局資料      $list  = ...; //這裡進行當前使用者牌局列表資料查詢      $num  count ( $list ); //牌局總數      $count  = 0; //已有多少個牌局在查詢            while (true) { //fork多個子程序查詢各個牌局的詳情資料          $s  "ps aux|awk '"  . '/query.php/ && / ' . $pKey . ' / && !/awk/ ' . "' |wc -l";          ob_start();          system( $s );          $pNum  = (int)ob_get_clean(); //當前查詢程序數量                    if ( $count  >=  $num ) { //當前牌局列表都已經交給各個子程序查詢了              if ( $pnum  > 1) { //有子程序沒有完成,稍等                  sleep(3);                  continue ;              else  { //所有子程序都已經完成,退出while迴圈,回到for迴圈中查詢下一個使用者的牌局資料                  break ;              }          else  if ( $pNum  $max_pnum ) { //子程序數量超出限制,稍等              sleep(3);              continue ;          }                    $rs  $list [ $count ]; //從牌局列表中取出一個牌局來進行牌局詳情資料查詢          pcntl_signal(SIGCHLD, SIG_IGN);          $pid  = pcntl_fork(); //fork一個子程序,子程序會從此位置開始執行          if ( $pid  < 0) { //子程序建立失敗              //這裡可以做一些日誌記錄                            exit (0);          }          if ( $pid ) { //子程序建立成功(主程序邏輯)              $count  ++;                        else  if ( $pid  == 0) { //進行牌局詳情資料查詢(子程序邏輯)              $pid  = posix_setsid(); //子程序ID              //這裡根據$rs中的牌局資料進行牌局詳情查詢,並將得到的資料寫入當前子程序專屬檔案(檔案路徑+檔名要唯一,可以使用時間戳、桌子ID和牌局ID組合表示)                            exit (0); //當前子程序任務完成,退出          }      } } exit (0); //查詢完成,主程序退出

  這個PHP後臺掛載程序執行完成之後,所有需要查詢的牌局資料就已經全部寫入檔案中了。現在問題來了,PHP應該怎麼把這些資料傳給前端頁面呢?我們知道http協議是單向協議,只能由前端向伺服器主動發起請求,而伺服器是無法主動把資料傳送給前端的,那怎麼辦呢?使用socket.io!可以在所有子程序執行完成之後,通過socket.io使用當前sock連線通知js,js收到訊息之後即傳送請求給一個PHP介面,這個PHP介面的任務便是讀取上述多程序在檔案中寫下的資料,返回給js進行頁面渲染。

  關於socket.io,沒有進行過多研究,使用的是公司框架封裝好的,當然也可以使用原生的,簡單教程地址:http://www.workerman.net/phpsocket_io,這裡只是簡單介紹一下思路。
  首先需要到上面這個地址中下載phpsocket,然後啟動一個服務端,注意,只能在命令列中啟動,同樣可以作為一個後臺掛載程序執行。

1 2 3 4 5 6 7 8 9 10 11 12 <?php require_once  __DIR__ .  '/socketio/vendor/autoload.php' ; use  Workerman\Worker; use  PHPSocketIO\SocketIO;   //建立socket.io伺服器,監聽2021埠 $io  new  SocketIO(2021);   //向客戶端傳送訊息,通知資料已查詢完成 $io ->emit( 'hello' , json_encode([1 =>  'hello' 'aaa'  =>  'ewfewr' ]));   Worker::runAll();

  然後在客戶端js中監聽這個訊息:

1 2 3 4 5 6 7 <script src= 'https://cdn.bootcss.com/socket.io/2.0.3/socket.io.js' ></script> <script> var  socket = io( 'http://127.0.0.1:2021' ); socket.on( 'hello' function (par){      //這裡便是傳送請求到PHP介面進行資料讀取了 }); </script>

  若是覺得使用原生socket.io麻煩,也可以使用封裝好的ElephantIO。

  當然,這裡有個問題,就是寫資料產生的檔案會越來越多,可以在每次掛載程序進行寫檔案之前先把之前寫的檔案(已經沒用了的)進行刪除:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function  rmDataDir( $dir ) {      if (! is_dir ( $dir ))  return ;            $handle  = opendir( $dir );      while ( $file  = readdir( $handle )) {          if (in_array( $file , [ '.' '..' ]))  continue ;                    $str  $dir  $file ;          if ( is_dir ( $str )) {              rmDataDir( $str  '/' );          else  {              unlink( $str );          }      }      closedir ( $handle );      $arr  = scandir( $dir ); //readdir()有時候沒有識別完所有檔案就返回false了。。。      if ( count ( $arr ) <= 2) { //只有.和..的時候可以刪除          rmdir ( $dir );      } }

  同時,由於在這個功能中,每次傳送查詢資料請求的代價都是比較昂貴的,可以考慮在js中對查詢過的資料進行快取,例如,相同查詢條件下相同使用者ID,已經查詢過的就不需要查詢了,直接從js快取中讀取資料進行頁面渲染就可以了。

  然而,儘管使用了PHP多程序,但是進行了很多的檔案讀寫操作,磁碟IO也是很耗時間的,所以速度上並沒有提升多少,只是不會再出現瀏覽器頁面卡死的情況了。這個功能中關於速度的提升不知還有什麼更好的方法呢???各位朋友,走過路過,別忘了給下建議哈~

  公司是做棋牌遊戲的。前段時間接到一個後臺人工鑑定並處理通牌作弊玩家的需求,其中需要根據幾個玩家的遊戲ID查詢並計算他們在某段時間內彼此之間玩牌輸贏次數和輸贏總額。

  牌局資料是儲存在日誌中心的,他們把牌局資料分成兩個表來儲存,一個表儲存牌局概況資料,例如牌局時間、牌局ID、桌子ID、使用者ID等資訊,另一個表則儲存每一個牌局的詳情資料,例如,牌局有多少玩家參與,荷官在哪一輪發了什麼牌,玩家每一輪都有什麼動作等等。要想計算出幾個玩家在某段時間之內玩牌輸贏次數和輸贏總額,就需要知道每一個牌局的詳情資料,所以需要針對每一個玩家的遊戲ID,先查詢第一個表,查出所有牌局概況資料列表,然後遍歷這個列表,根據每個牌局的牌局ID、桌子ID,從第二個表中查詢每個牌局的詳情資料,所有玩家的所有牌局詳情資料都查詢完成之後再進行統計。

  日誌中心的同學給出了查詢以上兩個表的介面,其中牌局詳情的查詢介面一次只能查詢一個牌局的資料(和他們使用的資料表設計有關)。剛開始我的做法是在js程式碼中遍歷所有給出的玩家ID,先查詢出每個玩家的牌局列表,然後使用第二層迴圈來呼叫介面請求每一個牌局的詳情資料,但這樣做的問題是,有些使用者在某段時間內的牌局數量是很大的,儘管控制了查詢時間段的最大範圍,但還是出現了一個使用者幾千個牌局的情況,這就意味著瀏覽器需要幾乎在同一時間內對同一個域名的伺服器發出幾千個請求,而瀏覽器是基於域名進行併發控制的,超過限制數量的請求會被阻塞,阻塞嚴重的時候經常導致頁面變成空白,好長時間才恢復,得到查詢結果。這樣的體驗顯然是不行的。

  那怎麼辦呢?在老大的指導下,幾經思慮,決定採用PHP多執行緒結合socket.io來完成這個任務。整體思路是這樣的:首先js向PHP發起資料查詢請求,PHP收到請求之後不是直接進行資料查詢,而是在後臺掛載一個程序去處理請求,然後返回一個確認狀態值給js,這時js請求暫時結束了。這樣做好處有二:其一,js請求的PHP介面是php-fpm執行的,使用php-fpm來fork多程序不太穩定,而使用php比較穩定;其二,可以避免資料查詢過程時間太長導致超時。

  掛載程序程式碼示例:

1 2 3 4 5 6 7 <?php $par  = [ 'startTime'  =>  '' 'endTime'  =>  '' 'mids'  =>  $mid , ...]; //牌局查詢引數 $pKey  'plog_proccess' ; //傳給命令列的引數,作為程序標識,便於查詢統計當前程序數量 $php  '/usr/local/php/bin/php' ; //php執行檔案路徑 $file  '/www/query.php' ; //牌局查詢指令碼檔案 $cmd  $php . ' ' . $file . ' ' . $pKey . ' ' . base64_encode (serialize( $par )). ' > /dev/null 2>&1 &' ; //命令 system( $cmd ); //執行命令,掛載後臺程序執行查詢

  接下來就要在程序執行的PHP指令碼/www/query.php中進行資料查詢了。首先遍歷每一個玩家ID,查出每個玩家的所有牌局列表,然後遍歷每個玩家的牌局列表,fork多個子程序進行每個牌局詳情資料的查詢了,一個子程序負責查詢一個牌局的詳情資料,並將資料寫入檔案中,程式碼示例如下:(注意:以下程式碼只是基本程式碼框架,無法直接執行)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <?php $pKey  $argv [1]; $par  = unserialize( base64_decode ( $argv [2])); $mids  $par [ 'mids' ]; $max_pnum  = 100; //最大子程序數量,避免搶佔了過多的資源   for ( $mids  as  $mid ) { //遍歷查詢各個使用者的牌局資料      $list  = ...; //這裡進行當前使用者牌局列表資料查詢      $num  count ( $list ); //牌局總數      $count  = 0; //已有多少個牌局在查詢            while (true) { //fork多個子程序查詢各個牌局的詳情資料          $s  "ps aux|awk '"  . '/query.php/ && / ' . $pKey . ' / && !/awk/ ' . "' |wc -l";          ob_start();          system( $s );          $pNum  = (int)ob_get_clean(); //當前查詢程序數量                    if ( $count  >=  $num ) { //當前牌局列表都已經交給各個子程序查詢了              if ( $pnum  > 1) { //有子程序沒有完成,稍等                  sleep(3);                  continue ;              else  { //所有子程序都已經完成,退出while迴圈,回到for迴圈中查詢下一個使用者的牌局資料                  break ;              }          else  if ( $pNum  $max_pnum ) { //子程序數量超出限制,稍等              sleep(3);              continue ;          }                    $rs