您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關Swoole server的案例分析,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
一. 基礎知識
1.1 Swoole
Swoole是面向生產環境的php異步網絡通信引擎, php開發人員可以利用Swoole開發出高性能的server服務。Swoole的server部分, 內容很多, 也涉及很多的知識點, 本文僅對其server進行簡單的概述, 具體的實現細節在后續的文章中再進行詳細介紹。
推薦(免費):swoole
1.2 網絡編程
1. 網絡通信是指在一臺(或者多臺)機器上啟動一個(或者多個)進程, 監聽一個(或者多個)端口, 按照某種協議(可以是標準協議http, dns; 也可以是自行定義的協議)與客戶端交換信息。
2. 目前的網絡編程多是在tcp, udp或者更上層的協議之上進行編程。Swoole的server部分是基于tcp以及udp協議的。
3. 利用udp進行編程較為簡單, 本文主要介紹tcp協議之上的網絡編程
4. TCP網絡編程主要涉及4種事件
● 連接建立: 主要是指客戶端發起連接(connect)以及服務端接受連接(accept)
● 消息到達: 服務端接受到客戶端發送的數據,該事件是TCP網絡編程最重要的事件,服務端對于該類事件進行處理時, 可以采用阻塞式或者非阻塞式,除此之外, 服務端還需要考慮分包, 應用層緩沖區等問題
● 消息發送成功: 發送成功是指應用層將數據成功發送到內核的套接字發送緩沖區中,并不是指客戶端成功接受數據。對于低流量的服務而言,數據通常一次性即可發送完,并不需要關心此類事件。如果一次性不能將全部數據發送到內核緩沖區,則需要關心消息是否成功發送(阻塞式編程在系統調用(write, writev, send等)返回后即是發送成功, 非阻塞式編程則需要考慮實際寫入的數據是否與預期一致)
● 連接斷開: 需要考慮客戶端斷開連接(read返回0)以及服務端斷開連接(close, shutdown)
5. tcp建立連接的過程如下圖
● 圖中, ACK、SYN表示標志位, seq、ack為tcp包的序號以及確認序號
6. tcp斷開連接的過程如下圖
● 上圖考慮的是客戶端主動斷開連接的情況, 服務端主動斷開連接也類似
● 圖中, FIN、ACK表示標志位, seq、ack為tcp包的序號以及確認序號
1.3 進程間通信
1. 進程之間的通信有無名管道(pipe), 有名管道(fifo), 信號(signal), 信號量(semaphore), 套接字(socket), 共享內存(shared memory)等方式
2. Swoole中采用unix域套接字(套接字的一種)用于多進程之間的通信(指Swoole內部進程之間)
1.4 socketpair
1. socketpair用于創建一個套接字對, 類似于pipe, 不同的是pipe是單向通信, 雙向通信需要創建兩次, socketpair調用一次即可實現雙向通信, 除此之外, 由于使用的是套接字, 還可以定義數據交換的方式
2. socketpair系統調用
調用成功后sv[0], sv[1]分別存儲一個文件描述符
向sv[0]中寫入, 可以從sv[1]中讀取
向sv[1]中寫入, 可以從sv[0]中讀取
進程調用socketpair后, fork子進程, 子進程會默認繼承sv[0], sv[1]這兩個文件描述符, 進而可以實現父子進程間通信。例如, 父進程向sv[0]中寫入, 子進程從sv[1]中讀取; 子進程向sv[1]中寫入, 父進程從sv[0]中讀取
1.5 守護進程(daemon)
1. 守護進程是一種特殊的后臺進程, 它脫離于終端, 用于周期性的執行某種任務
2. 進程組
每個進程都屬于一個進程組
每個進程組都有一個進程組號, 也就是該組組長的進程號(PID)
一個進程只能為自己或者其子進程設置進程組號
3. 會話
一個會話可以包含多個進程組, 這些進程組中最多只能有一個前臺進程組(也可以沒有), 其余為后臺進程組
一個會話最多只能有一個控制終端
用戶通過終端登錄或者網絡登錄, 會創建一個新的會話
進程調用系統調用setsid可以創建一個新的會話, 調用setsid的進程不能是某個進程組的組長。setsid調用完成后, 該進程成為這個會話的首進程(領頭進程), 同時變成一個新的進程組的組長, 如果該進程之前有控制終端, 該進程與終端的聯系也被斷開
4. 創建守護進程的方式
fork子進程后, 父進程退出, 子進程執行setsid, 子進程即可成為守護進程。這種方式下, 子進程是會話的領頭進程, 可以重新打開終端, 此時可以再次fork, fork產生的子進程無法再打開終端(只有會話的領頭進程才能打開終端)。第二次fork并不是必須的, 只是為了防止子進程再次打開終端
linux提供了daemon函數(該函數并不是系統調用, 為庫函數)用于創建守護進程
1.6 Swoole tcp server示例
上述代碼在cli模式下執行時, 經過詞法分析, 語法分析生成opcode, 進而交由zend虛擬機執行
zend虛擬機在執行到$serv->start()時, 啟動Swoole server
上述代碼中設置的事件回調是在worker進程中執行, 后文會詳細介紹Swoole server模型
二. Swoole server
2.1 base模式
1. 說明
base模式采用多進程模型, 這種模型與nginx一致, 每個進程只有一個線程, 主進程負責管理工作進程, 工作進程負責監聽端口, 接受連接, 處理請求以及關閉連接
多個進程同時監聽端口, 會有驚群問題, linux 3.9之前版本的內核, Swoole沒有解決驚群問題
linux 內核3.9及其后續版本提供了新的套接字參數SO_REUSEPORT, 該參數允許多個進程綁定到同一個端口, 內核在接受到新的連接請求時, 會喚醒其中一個進行處理, 內核層面也會做負載均衡, 可以解決上述的驚群問題, Swoole也已經加入了這個參數
base模式下, reactor_number參數并沒有實際作用
如果worker進程數設置為1, 則不會fork出worker進程, 主進程直接處理請求, 這種模式適合調試
2. 啟動過程
php代碼執行到$serv->start()時,主進程進入int swServer_start(swServer *serv)函數, 該函數負責啟動server
在函數swServer_start中會調用swReactorProcess_start, 這個函數會fork出多個worker進程
主進程和worker進程各自進入自己的事件循環, 處理各類事件
2.2 process模式
1. 說明
這種模式為多進程多線程, 有主進程, manager進程, worker進程, task_worker進程
主進程下有多個線程, 主線程負責接受連接, 之后交給react線程處理請求。 react線程負責接收數據包, 并將數據轉發給worker進程進行處理, 之后處理worker進程返回的數據
manager進程, 該進程為單線程, 主要負責管理worker進程, 類似于nginx中的主進程, 當worker進程異常退出時, manager進程負責重新fork出一個worker進程
worker進程, 該進程為單線程, 負責具體處理請求
task_worker進程, 用于處理比較耗時的任務, 默認不開啟
worker進程與主進程中的react線程使用域套接字進行通信, worker進程之間不進行通信
2. 啟動過程
Swoole server啟動入口: swServer_start函數
如果設置了daemon模式, 在必要的參數檢查完后, 先將自己變為守護進程再fork manager進程, 進而創建reactor線程
主進程先fork出manager進程, manager進程負責fork出worker進程以及task_worker進程。worker進程之后進入int swWorker_loop
(swServer *serv, int worker_id), 也就是進入自己的事件循環, task_worker也是一樣, 進入自己的事件循環
主進程pthread_create出react線程, 主線程和react線程各自進入自己的事件循環, reactor線程執行static int swRea-torThread_loop (swThreadParam *param), 等待處理事件
3. 結構圖
Swoole process模式結構如下圖所示,
上圖并沒有考慮task_worker進程, 在默認情況下, task_worker進程數為0
三. 請求處理流程(process模式)
3.1 reactor線程與worker進程之間的通信
1. Swoole master進程與worker進程之間的通信如下圖所示
Swoole使用SOCK_DGRAM, 而不是SOCK_STREAM, 這里是因為每個reactor線程負責處理多個請求, reactor接收到請求后會將信息轉發給worker進程, 由worker進程負責處理,如果使用SOCK_STREAM, worker進程無法對tcp進行分包, 進而處理請求
swFactoryProcess_start函數中會根據worker進程數創建對應個數的套接字對, 用于reactor線程與worker進程通信(詳見swPipeUnsock_create函數)
2. 假設reactor線程有2個, worker進程有3個, 則reactor與worker之間的通信如下圖所示
每個reactor線程負責監聽幾個worker進程, 每個worker進程只有一個reactor線程監聽(reactor_num <= worker_num)。Swoole默認使用worker_process_id % reactor_num對worker進程進行分配, 交給對應的reactor線程進行監聽
reactor線程收到某個worker進程的數據后會進行處理, 值得注意的是, 這個reactor線程可能并不是發送請求的那個reactor線程。
3. reactor線程與worker進程通信的數據包
3.2 請求處理
1. master進程中的主線程負責監聽端口(listen
), 接受連接(accept
, 產生一個fd), 接受連接后將請求分配給reactor線程, 默認通過fd % reactor_number進行分配, 之后通過epoll_ctl
將fd加入到對應reactor線程中, 剛加入時監聽寫事件, 因為新接受連接創建的套接字寫緩沖區為空, 故而一定可寫, 會被立刻觸發, 進而reactor線程進行一些初始化操作
存在多個線程同時操作一個epollfd(通過系統調用epoll_create
創建)的情況
多個線程同時調用epoll_ctl
是線程安全的(對應一個epollfd), 一個線程正在執行, 其他線程會被阻塞(因為需要同時操作epoll底層的紅黑樹)
多個線程同時調用epoll_wait
也是線程安全的, 但是一個事件可能會被多個線程同時接收到, 實際中不建議多個線程同時epoll_wait
一個epollfd。Swoole中也是不存在這種情況的, Swoole中每個reactor線程都有自己的epollfd
一個線程調用epoll_wait
, 一個線程調用epoll_ctl
, 根據man手冊, 如果epoll_ctl
新加入的fd已經準備好, 會使得執行epoll_wait
的線程變成非阻塞狀態(可以通過man epoll_wait
查看相關內容)
2. reactor線程中fd的寫事件被觸發, reactor線程負責處理, 發現是首次加入, 沒有數據可寫, 則會開啟讀事件監聽, 準備接受客戶端發送的數據
3. reactor線程讀取到用戶的請求數據, 一個請求的數據接收完后, 將數據轉發給worker進程, 默認是通過fd % worker_number進行分配
reactor發送給worker進程的數據包, 會包含一個頭部, 頭部中記錄了reactor的信息
如果發送的數據過大, 則需要將數據進行分片, 限于篇幅, 數據分片, 后續再進行詳細講述
可能存在多個reactor線程同時向同一個worker進程發送數據的情況, 故而Swoole采用SOCK_DGRAM模式與worker進程進行通信, 通過每個數據包的包頭, worker進程可以區分出是由哪個reactor線程發送的數據, 也可以知道是哪個請求
4. worker進程收到reactor發送的數據包后, 進行處理, 處理完成后, 將請求結果發送給主進程
worker進程發送給主進程的數據包, 也會包含一個頭部, 當reactor線程收到數據包后, 能夠知道對應的reactor線程, 請求的fd等信息
5. 主進程收到worker進程發送的數據包, 這個會觸發某個reactor線程進行處理
這個reactor線程并不一定是之前發送請求給worker進程的那個reactor線程
主進程的每個reactor線程都負責監聽worker進程發送的數據包, 每個worker發送的數據包只會由一個reactor線程進行監聽, 故而只會觸發一個reactor線程
6. reactor線程處理worker進程發送的請求處理結果, 如果是直接發送數據給客戶端, 則可以直接發送, 如果需要改變這個這個連接的監聽狀態(例如close
), 則需要先找到監聽這個連接的reactor線程, 進而改變這個連接的監聽狀態(通過調用epoll_ctl
)
reactor處理線程與reactor監聽線程可能并不是同一個線程
reactor監聽線程負責監聽客戶端發送的數據, 進而轉發給worker進程
reactor處理線程負責監聽worker進程發送給主進程的數據, 進而將數據發送給客戶端
四. gdb調試
4.1 process模式啟動
4.2 base模式啟動
五. 總結及思考
1. 本文主要介紹了Swoole server的兩種模式: base模式、process模式, 詳細講解了兩種模式的網絡編程模型, 并重點介紹了process模式下, 進程間通信的方式、請求的處理流程等
2. process模式下, 為什么不直接在主進程中創建多個線程, 由線程直接進行處理請求(可以避免進程間通信的開銷), 而是創建出manager進程, 再由manager進程創建出worker進程, 由worker進程處理請求?
個人覺得可能是php對多線程的支持不是很友好, phper大都也只是進行單線程編程
ZendVM 提供的 TSRM 雖然也是支持多線程環境,但實際上這是一個 按線程隔離內存的方案, 多線程并沒有意義
3. process模式下, 主進程中的每個reactor線程都可以同時處理多個請求, 多個請求是并發處理的, 我們從2個維度看
從主進程的角度看, 主進程同時處理多個請求, 當一個請求包全部接收完后, 轉發給worker進程進行處理
從某個worker進程的角度看, 這個worker進程收到的請求是串行的, 默認情況下, worker進程也是串行處理請求, 如果單個請求阻塞(Swoole的worker進程會回調phper寫的事件處理函數, 該函數可能阻塞), 后續的請求也無法處理, 這個就是排頭阻塞問題, 這種情況下可以使用Swoole的協程, 通過協程的調度, 單個請求阻塞時, worker進程可以繼續處理其他請求
4. 使用Swoole創建tcp server時, 由于tcp是字節流的協議, 需要分包, 而Swoole在不清楚客戶端與服務端通信協議的情況下, 無法進行分包, process模式下, reactor交給worker進程的數據也只能是字節流的, 需要用戶自行處理。當然, 一般情況也不需要自行構建協議, 使用tcp server, Swoole已經支持Http, Https等協議
關于“Swoole server的案例分析”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。