workerman源碼分析之啟動過程


  PHP一直以來以草根示人,它簡單,易學,被大量應用於web開發,非常可惜的是大部分開發都在簡單的增刪改查,或者加上pdo,redis等客戶端甚至分布式,以及規避語言本身的缺陷。然而這實在太委屈PHP了。記得有一次問walker,PHP能做什么?他說:什么都能做啊!當時我就震驚了,這怎么可能。。。直到后來一直看workerman源碼,發現PHP原來有很多不為大家所知的諸多用法,包括多進程(還有線程)、信號處理、namespace等等一大堆特點。而workerman正是這些很少被使用特性(或者說擴展)的集大成者,如果非要說它的缺點,那就是PHP的缺點了,當然PHP的優點它全占了~而且PHP7發布在即,workerman必將得到更多的優化,搭配HHVM更是叼的不行。

workerman

  版本:3.1.8(linux)

  模型:GatewayWorker(Worker模型可與之類比)

  注:只貼出講解部分代碼,出處以文件名形式給出,大家可自行查看

  workerman最初只開發了Linux版本,win是后來增加的,基於命令行模式運行(cli)

多進程模型

  工作進程,Master、Gateway和Worker,Gateway主要用於處理IO事件,保存客戶端鏈接狀態,將數據處理請求發送給Worker等工作,Worker則是完全的業務邏輯處理,前者為IO密集型,后者為計算密集型,它們之間通過網絡通信,Gateway和Worker兩兩間注冊通信地址,所以非常方便的進行分布式部署,如果業務處理量大可以單純的增加Worker服務。

  

  它們有一個負責監聽的父進程(Master),監聽子進程狀態,發送 signal 給子進程,接受來自終端的命令、信號等工作。父進程可以說是整個系統啟動后的入口。

啟動命令解析

  既然以命令模式(cli)運行(注意與 fpm 的區別,后者處理來自網頁端的請求),就必然有一個啟動腳本解析命令,譬如說3.x版本(之前默認為daemon)新增一個 -d 參數,以表示守護進程運行,解析到該參數設置 self::$daemon = true, 隨后fork子進程以脫離當前進程組,設置進程組組長等工作。這里有兩個非常重要的參數 $argc 和 $argc,前者表示參數個數,后者為一個數組,保存有命令的所有參數,比如:sudo php start.php start -d,$argv就是 array( [0]=>start.php, [1]=>start, [2]=>-d ),而解析主要用到$argv。

  啟動主要執行下面步驟:

  1. 包含自動加載器 Autoloader ,加載各 Application 下啟動文件;
  2. 設置 _appInitPath 根目錄;
  3. 解析,初始化參數,執行相應命令。

下面是具體實現(workerman/worker.php):

 1     public static function parseCommand()
 2     {
 3         // 檢查運行命令的參數
 4         global $argv;
 5         $start_file = $argv[0]; 
 6 
 7         // 命令
 8         $command = trim($argv[1]);
 9         
10         // 子命令,目前只支持-d
11         $command2 = isset($argv[2]) ? $argv[2] : '';
12         
13         // 檢查主進程是否在運行
14         $master_pid = @file_get_contents(self::$pidFile);
15         $master_is_alive = $master_pid && @posix_kill($master_pid, 0);
16         if($master_is_alive)
17         {
18             if($command === 'start')
19             {
20                 self::log("Workerman[$start_file] is running");
21             }
22         }
23         elseif($command !== 'start' && $command !== 'restart')
24         {
25             self::log("Workerman[$start_file] not run");
26         }
27         
28         // 根據命令做相應處理
29         switch($command)
30         {
31             // 啟動 workerman
32             case 'start':
33                 if($command2 === '-d')
34                 {
35                     Worker::$daemonize = true;
36                 }
37                 break;
38             // 顯示 workerman 運行狀態
39             case 'status':
40                 exit(0);
41             // 重啟 workerman
42             case 'restart':
43             // 停止 workeran
44             case 'stop':
45                 // 想主進程發送SIGINT信號,主進程會向所有子進程發送SIGINT信號
46                 $master_pid && posix_kill($master_pid, SIGINT);
47                 // 如果 $timeout 秒后主進程沒有退出則展示失敗界面
48                 $timeout = 5;
49                 $start_time = time();
50                 while(1)
51                 {
52                     // 檢查主進程是否存活
53                     $master_is_alive = $master_pid && posix_kill($master_pid, 0);
54                     if($master_is_alive)
55                     {
56                         // 檢查是否超過$timeout時間
57                         if(time() - $start_time >= $timeout)
58                         {
59                             self::log("Workerman[$start_file] stop fail");
60                             exit;
61                         }
62                         usleep(10000);
63                         continue;
64                     }
65                     self::log("Workerman[$start_file] stop success");
66                     // 是restart命令
67                     if($command === 'stop')
68                     {
69                         exit(0);
70                     }
71                     // -d 說明是以守護進程的方式啟動
72                     if($command2 === '-d')
73                     {
74                         Worker::$daemonize = true;
75                     }
76                     break;
77                 }
78                 break;
79             // 平滑重啟 workerman
80             case 'reload':
81                 exit;
82         }
83     }

walker代碼注釋已經非常詳盡,下面有幾點細節處:

  • 檢查主進程是否存活:17行的邏輯與操作,如果主進程PID存在情況下,向該進程發送信號0,實際上並沒有發送任何信息,只是檢測該進程(或進程組)是否存活,同時也檢測當前用戶是否有權限發送系統信號;
  • 為什么主進程PID會保存?系統啟動后脫離當前terminal運行,如果要執行關閉或者其他命令,此時是以另外的一個進程執行該命令,如果我們連進程PID都不知道,那該向誰發信號呢?!所以主進程PID必須保存起來,而且主進程負責監聽其他子進程,所以它是我們繼續操作的入口。

Worker::runAll()

  php的socket編程其實和C差不多,后者對socket進行了再包裹,並提供接口給php,在php下網絡編程步驟大大減少。譬如:stream_socket_serverstream_socket_client 直接創建了server/client socke(php有兩套socket操作函數)。wm則大量使用了前者,啟動過程如下(注釋已經非常詳盡):

 1     public static function runAll()
 2     {
 3         // 初始化環境變量
 4         self::init();
 5         // 解析命令
 6         self::parseCommand();
 7         // 嘗試以守護進程模式運行
 8         self::daemonize();
 9         // 初始化所有worker實例,主要是監聽端口
10         self::initWorkers();
11         //  初始化所有信號處理函數
12         self::installSignal();
13         // 保存主進程pid
14         self::saveMasterPid();
15         // 創建子進程(worker進程)並運行
16         self::forkWorkers();
17         // 展示啟動界面
18         self::displayUI();
19         // 嘗試重定向標准輸入輸出
20         self::resetStd();
21         // 監控所有子進程(worker進程)
22         self::monitorWorkers();
23     }

  下面還是只說該過程的關鍵點:

  1. 初始化環境變量,例如設置主進程名稱、日志路徑,初始化定時器等等;
  2. 解析命令行參數,主要用到 $argc 和 $argc 用法同C語言;
  3. 生成守護進程,以脫離當前終端(兩年前大部分認為PHP無法做daemon,其實這是個誤區!其實PHP在linux的進程模型很穩定,現在wm在商業的應用已經非常成熟,國內某公司每天處理幾億的連接,用於訂單、支付調用,大家可以打消顧慮了);
  4. 初始化所有worker實例(注意,這里是在主進程做的,只是生成了一堆 server 並沒有設置監聽,多進程模型是在子進程做的監聽,即IO復用);
  5. 為主進程注冊信號處理函數;
  6. 保存主進程PID,當系統運行后,我們在終端查看系統狀態或者執行關閉、重啟命令,是通過主進程進行通信,所以需要知道主進程PID,我們知道在終端下敲入一個可執行命令,實則是在當前終端下新建一個子進程來執行,所以我們需要得知主進程PID,以向WM主進程發送SIGNAL,這時信號處理函數捕獲該信號,並通過回調方式執行。
  7. 創建子進程,設置當前進程用戶(root)。在多進程模型中,兩類子進程,分別監聽不同的server地址,我們在主進程只是創建server並沒有設置監聽,也沒有生成指定數目的server,原因在於,我們在一個進程多次創建同一個 socket,會報錯, worker數目其實就是 socket 數量,也就是該 socket 的子進程數目,子進程繼承了父進程上下文,但是只監聽特定的 socket 事件;
  8. 在子進程中,將 server socket 注冊監聽事件,用到一個擴展Event,可以實現IO復用,並注冊數據讀取回調,同時也可注冊socket連接事件回調;
  9. 輸入輸出重定向;
  10. 主進程監聽子進程狀態,在一個無限循環中調用 pcntl_signal_dispatch() 函數,用於捕獲子進程退出狀態,該函數會一直阻塞,直到有子進程退出時才觸發;

  至此,一個完整的啟動過程大致處理完成,然后 server 會一直運行,一直等待 socket 連接事件,等待數據可讀可寫事件,通過事先注冊的處理函數,就能完整的處理整個網絡過程。

 

結束語

  其實網絡編程過程大致都差不多,這些都有標准答案,每個語言實現的大致過程基本相同,當然類似 golang 的 goroutine 另說。。。需要了解應用層協議(如果可能,需要手動解包和封包),網絡模型,TCP/UDP,進程間通信,IO復用等等,當然最重要的是會 debug。。。自己動手嘗試寫一個簡單的 server 就會遇到很多無法遇見的坑,所以紙上得來終覺淺,絕知此事要躬行。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM