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。
啟動主要執行下面步驟:
- 包含 自動載入器 Autoloader ,載入各 Application 下啟動檔案;
- 設定 _appInitPath 根目錄;
- 解析,初始化引數,執行相應命令。
下面是具體實現(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_server 和 stream_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 }
下面還是隻說該過程的關鍵點:
- 初始化環境變數,例如設定主程序名稱、日誌路徑,初始化定時器等等;
- 解析命令列引數,主要用到 $argc 和 $argc 用法同C語言;
- 生成守護程序,以脫離當前終端(兩年前大部分認為PHP無法做daemon,其實這是個誤區!其實PHP在linux的程序模型很穩定,現在wm在商業的應用已經非常成熟,國內某公司每天處理幾億的連線,用於訂單、支付呼叫,大家可以打消顧慮了);
- 初始化所有worker例項(注意,這裡是在主程序做的,只是生成了一堆 server 並沒有設定監聽,多程序模型是在子程序做的監聽,即IO複用);
- 為主程序註冊訊號處理函式;
- 儲存主程序PID,當系統執行後,我們在終端檢視系統狀態或者執行關閉、重啟命令,是通過主程序進行通訊,所以需要知道主程序PID,我們知道在終端下敲入一個可執行命令,實則是在當前終端下新建一個子程序來執行,所以我們需要得知主程序PID,以向WM主程序傳送SIGNAL,這時訊號處理函式捕獲該訊號,並通過回撥方式執行。
- 建立子程序,設定當前程序使用者(root)。在多程序模型中,兩給子程序,分別監聽不同的server地址,我們在主程序只是建立server並沒有設定監聽,也沒有生成指定數目的server,原因在於,我們在一個程序多次建立同一個 socket, woker數目其實就是 socket 數量,也就是該 socket 的子程序數目,否則會報錯;
- 在子程序中,將 server socket 註冊監聽事件,用到一個擴充套件 Event ,可以實現IO複用,並註冊資料讀取回調,同時也可註冊socket連線事件回撥;
- 輸入輸出重定向;
- 主程序監聽子程序狀態,在一個無限迴圈中呼叫 pcntl_signal_dispatch() 函式,用於捕獲子程序退出狀態,該函式會一直阻塞,直到有子程序退出時才觸發;
至此,一個完整的啟動過程大致處理完成,然後 server 會一直執行,一直等待 socket 連線事件,等待資料可讀可寫事件,通過事先註冊的處理函式,就能完整的處理整個網路過程。
結束語
其實網路程式設計過程大致都差不多,這些都有標準答案,每個語言實現的大致過程基本相同,當然類似 golang 的 goroutine 另說。。。需要了解應用層協議(如果可能,需要手動解包和封包),網路模型,TCP/UDP,程序間通訊,IO複用等等,當然最重要的是會 debug。。。自己動手嘗試寫一個簡單的 server 就會遇到很多無法遇見的坑,所以紙上得來終覺淺,絕知此事要躬行。