摘要:下文如無特殊聲明將使用進程同時表示進程線程。收到數據后服務器程序進行處理然后使用向客戶端發送響應。現在各種高并發異步的服務器程序都是基于實現的,比如。
并發 IO 問題一直是服務器端編程中的技術難題,從最早的同步阻塞直接 Fork 進程,到 Worker 進程池/線程池,到現在的異步IO、協程。PHP 程序員因為有強大的 LAMP 框架,對這類底層方面的知識知之甚少,本文目的就是詳細介紹 PHP 進行并發 IO 編程的各種嘗試,最后再介紹 Swoole 的使用,深入淺出全面解析并發 IO 問題。
多進程/多線程同步阻塞
最早的服務器端程序都是通過多進程、多線程來解決并發IO的問題。進程模型出現的最早,從 Unix 系統誕生就開始有了進程的概念。最早的服務器端程序一般都是 Accept 一個客戶端連接就創建一個進程,然后子進程進入循環同步阻塞地與客戶端連接進行交互,收發處理數據。
多線程模式出現要晚一些,線程與進程相比更輕量,而且線程之間是共享內存堆棧的,所以不同的線程之間交互非常容易實現。比如聊天室這樣的程序,客戶端連接之間可以交互,比聊天室中的玩家可以任意的其他人發消息。用多線程模式實現非常簡單,線程中可以直接向某一個客戶端連接發送數據。而多進程模式就要用到管道、消息隊列、共享內存,統稱進程間通信(IPC)復雜的技術才能實現。
代碼實例:
多進程/線程模型的流程是
創建一個
socket,綁定服務器端口(bind),監聽端口(listen),在PHP中用stream_socket_server一個函數就能完成上面3個步驟,當然也可以使用更底層的sockets擴展分別實現。
進入while循環,阻塞在accept操作上,等待客戶端連接進入。此時程序會進入睡眠狀態,直到有新的客戶端發起connect到服務器,操作系統會喚醒此進程。accept函數返回客戶端連接的socket
主進程在多進程模型下通過fork(php: pcntl_fork)創建子進程,多線程模型下使用pthread_create(php:
new Thread)創建子線程。下文如無特殊聲明將使用進程同時表示進程/線程。
子進程創建成功后進入while循環,阻塞在recv(php:
fread)調用上,等待客戶端向服務器發送數據。收到數據后服務器程序進行處理然后使用send(php:
fwrite)向客戶端發送響應。長連接的服務會持續與客戶端交互,而短連接服務一般收到響應就會close。
當客戶端連接關閉時,子進程退出并銷毀所有資源。主進程會回收掉此子進程。
這種模式最大的問題是,進程/線程創建和銷毀的開銷很大。所以上面的模式沒辦法應用于非常繁忙的服務器程序。對應的改進版解決了此問題,這就是經典的 Leader-Follower 模型。
代碼實例:
它的特點是程序啟動后就會創建N個進程。每個子進程進入 Accept,等待新的連接進入。當客戶端連接到服務器時,其中一個子進程會被喚醒,開始處理客戶端請求,并且不再接受新的TCP連接。當此連接關閉時,子進程會釋放,重新進入 Accept ,參與處理新的連接。
這個模型的優勢是完全可以復用進程,沒有額外消耗,性能非常好。很多常見的服務器程序都是基于此模型的,比如 Apache 、PHP-FPM。
多進程模型也有一些缺點。
這種模型嚴重依賴進程的數量解決并發問題,一個客戶端連接就需要占用一個進程,工作進程的數量有多少,并發處理能力就有多少。操作系統可以創建的進程數量是有限的。
啟動大量進程會帶來額外的進程調度消耗。數百個進程時可能進程上下文切換調度消耗占CPU不到1%可以忽略不計,如果啟動數千甚至數萬個進程,消耗就會直線上升。調度消耗可能占到
CPU 的百分之幾十甚至 100%。
另外有一些場景多進程模型無法解決,比如即時聊天程序(IM),一臺服務器要同時維持上萬甚至幾十萬上百萬的連接(經典的C10K問題),多進程模型就力不從心了。
還有一種場景也是多進程模型的軟肋。通常Web服務器啟動100個進程,如果一個請求消耗100ms,100個進程可以提供1000qps,這樣的處理能力還是不錯的。但是如果請求內要調用外網Http接口,像QQ、微博登錄,耗時會很長,一個請求需要10s。那一個進程1秒只能處理0.1個請求,100個進程只能達到10qps,這樣的處理能力就太差了。
有沒有一種技術可以在一個進程內處理所有并發IO呢?答案是有,這就是IO復用技術。
IO復用/事件循環/異步非阻塞
其實IO復用的歷史和多進程一樣長,Linux很早就提供了 select 系統調用,可以在一個進程內維持1024個連接。后來又加入了poll系統調用,poll做了一些改進,解決了 1024 限制的問題,可以維持任意數量的連接。但select/poll還有一個問題就是,它需要循環檢測連接是否有事件。這樣問題就來了,如果服務器有100萬個連接,在某一時間只有一個連接向服務器發送了數據,select/poll需要做循環100萬次,其中只有1次是命中的,剩下的99萬9999次都是無效的,白白浪費了CPU資源。
直到Linux 2.6內核提供了新的epoll系統調用,可以維持無限數量的連接,而且無需輪詢,這才真正解決了 C10K 問題。現在各種高并發異步IO的服務器程序都是基于epoll實現的,比如Nginx、Node.js、Erlang、Golang。像 Node.js 這樣單進程單線程的程序,都可以維持超過1百萬TCP連接,全部歸功于epoll技術。
IO復用異步非阻塞程序使用經典的Reactor模型,Reactor顧名思義就是反應堆的意思,它本身不處理任何數據收發。只是可以監視一個socket句柄的事件變化。
Reactor有4個核心的操作:
add添加socket監聽到reactor,可以是listen
socket也可以使客戶端socket,也可以是管道、eventfd、信號等
set修改事件監聽,可以設置監聽的類型,如可讀、可寫。可讀很好理解,對于listen
socket就是有新客戶端連接到來了需要accept。對于客戶端連接就是收到數據,需要recv。可寫事件比較難理解一些。一個SOCKET是有緩存區的,如果要向客戶端連接發送2M的數據,一次性是發不出去的,操作系統默認TCP緩存區只有256K。一次性只能發256K,緩存區滿了之后send就會返回EAGAIN錯誤。這時候就要監聽可寫事件,在純異步的編程中,必須去監聽可寫才能保證send操作是完全非阻塞的。
del從reactor中移除,不再監聽事件
callback就是事件發生后對應的處理邏輯,一般在add/set時制定。C語言用函數指針實現,JS可以用匿名函數,PHP可以用匿名函數、對象方法數組、字符串函數名。
Reactor只是一個事件發生器,實際對socket句柄的操作,如connect/accept、send/recv、close是在callback中完成的。具體編碼可參考下面的偽代碼:
Reactor模型還可以與多進程、多線程結合起來用,既實現異步非阻塞IO,又利用到多核。目前流行的異步服務器程序都是這樣的方式:如
Nginx:多進程Reactor
Nginx+Lua:多進程Reactor+協程
Golang:單線程Reactor+多線程協程
Swoole:多線程Reactor+多進程Worker
協程是什么
協程從底層技術角度看實際上還是異步IO Reactor模型,應用層自行實現了任務調度,借助Reactor切換各個當前執行的用戶態線程,但用戶代碼中完全感知不到Reactor的存在。
PHP并發IO編程實踐
PHP相關擴展
Stream:PHP內核提供的socket封裝
Sockets:對底層Socket API的封裝
Libevent:對libevent庫的封裝
Event:基于Libevent更高級的封裝,提供了面向對象接口、定時器、信號處理的支持
Pcntl/Posix:多進程、信號、進程管理的支持
Pthread:多線程、線程管理、鎖的支持
PHP還有共享內存、信號量、消息隊列的相關擴展
PECL:PHP的擴展庫,包括系統底層、數據分析、算法、驅動、科學計算、圖形等都有。如果PHP標準庫中沒有找到,可以在PECL尋找想要的功能。
PHP語言的優劣勢
PHP的優點:
第一個是簡單,PHP比其他任何的語言都要簡單,入門的話PHP真的是可以一周就入門。C++有一本書叫做《21天深入學習C++》,其實21天根本不可能學會,甚至可以說C++沒有3-5年不可能深入掌握。但是PHP絕對可以7天入門。所以PHP程序員的數量非常多,招聘比其他語言更容易。
PHP的功能非常強大,因為PHP官方的標準庫和擴展庫里提供了做服務器編程能用到的99%的東西。PHP的PECL擴展庫里你想要的任何的功能。
另外PHP有超過20年的歷史,生態圈是非常大的,在Github可以找到很多代碼。
PHP的缺點:
性能比較差,因為畢竟是動態腳本,不適合做密集運算,如果同樣的 PHP 程序使用 C/C++ 來寫,PHP 版本要比它差一百倍。
函數命名規范差,這一點大家都是了解的,PHP更講究實用性,沒有一些規范。一些函數的命名是很混亂的,所以每次你必須去翻PHP的手冊。
提供的數據結構和函數的接口粒度比較粗。PHP只有一個Array數據結構,底層基于HashTable。PHP的Array集合了Map,Set,Vector,Queue,Stack,Heap等數據結構的功能。另外PHP有一個SPL提供了其他數據結構的類封裝。
所以PHP
PHP更適合偏實際應用層面的程序,業務開發、快速實現的利器
PHP不適合開發底層軟件
使用C/C++、JAVA、Golang等靜態編譯語言作為PHP的補充,動靜結合
借助IDE工具實現自動補全、語法提示
PHP的Swoole擴展
基于上面的擴展使用純PHP就可以完全實現異步網絡服務器和客戶端程序。但是想實現一個類似于多IO線程,還是有很多繁瑣的編程工作要做,包括如何來管理連接,如何來保證數據的收發原子性,網絡協議的處理。另外PHP代碼在協議處理部分性能是比較差的,所以我啟動了一個新的開源項目Swoole,使用C語言和PHP結合來完成了這項工作。靈活多變的業務模塊使用PHP開發效率高,基礎的底層和協議處理部分用C語言實現,保證了高性能。它以擴展的方式加載到了PHP中,提供了一個完整的網絡通信的框架,然后PHP的代碼去寫一些業務。它的模型是基于多線程Reactor+多進程Worker,既支持全異步,也支持半異步半同步。
Swoole的一些特點:
Accept線程,解決Accept性能瓶頸和驚群問題
多IO線程,可以更好地利用多核
提供了全異步和半同步半異步2種模式
處理高并發IO的部分用異步模式
復雜的業務邏輯部分用同步模式
底層支持了遍歷所有連接、互發數據、自動合并拆分數據包、數據發送原子性。
Swoole的進程/線程模型:
Swoole程序的執行流程:
使用PHP+Swoole擴展實現異步通信編程
實例代碼在https://github.com/swoole/swo... 主頁查看。
TCP服務器與客戶端
異步TCP服務器:
在這里new swoole_server對象,然后參數傳入監聽的HOST和PORT,然后設置了3個回調函數,分別是onConnect有新的連接進入、onReceive收到了某一個客戶端的數據、onClose某個客戶端關閉了連接。最后調用start啟動服務器程序。swoole底層會根據當前機器有多少CPU核數,啟動對應數量的Reactor線程和Worker進程。
異步客戶端:
客戶端的使用方法和服務器類似只是回調事件有4個,onConnect成功連接到服務器,這時可以去發送數據到服務器。onError連接服務器失敗。onReceive服務器向客戶端連接發送了數據。onClose連接關閉。
設置完事件回調后,發起connect到服務器,參數是服務器的IP,PORT和超時時間。
同步客戶端:
同步客戶端不需要設置任何事件回調,它沒有Reactor監聽,是阻塞串行的。等待IO完成才會進入下一步。
異步任務:
異步任務功能用于在一個純異步的Server程序中去執行一個耗時的或者阻塞的函數。底層實現使用進程池,任務完成后會觸發onFinish,程序中可以得到任務處理的結果。比如一個IM需要廣播,如果直接在異步代碼中廣播可能會影響其他事件的處理。另外文件讀寫也可以使用異步任務實現,因為文件句柄沒辦法像socket一樣使用Reactor監聽。因為文件句柄總是可讀的,直接讀取文件可能會使服務器程序阻塞,使用異步任務是非常好的選擇。
異步毫秒定時器
這2個接口實現了類似JS的setInterval、setTimeout函數功能,可以設置在n毫秒間隔實現一個函數或 n毫秒后執行一個函數。
異步MySQL客戶端
swoole還提供一個內置連接池的MySQL異步客戶端,可以設定最大使用MySQL連接數。并發SQL請求可以復用這些連接,而不是重復創建,這樣可以保護MySQL避免連接資源被耗盡。
異步Redis客戶端
異步的Web程序
程序的邏輯是從Redis中讀取一個數據,然后顯示HTML頁面。使用ab壓測性能如下:
同樣的邏輯在php-fpm下的性能測試結果如下:
WebSocket程序
swoole內置了websocket服務器,可以基于此實現Web頁面主動推送的功能,比如WebIM。有一個開源項目可以作為參考。https://github.com/matyhtf/ph...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/31178.html
摘要:我的是忙碌的一年,從年初備戰實習春招,年三十都在死磕源碼,三月份經歷了阿里五次面試,四月順利收到實習。因為我心理很清楚,我的目標是阿里。所以在收到阿里之后的那晚,我重新規劃了接下來的學習計劃,將我的短期目標更新成拿下阿里轉正。 我的2017是忙碌的一年,從年初備戰實習春招,年三十都在死磕JDK源碼,三月份經歷了阿里五次面試,四月順利收到實習offer。然后五月懷著忐忑的心情開始了螞蟻金...
摘要:網絡編程就是如何在程序中實現兩臺計算機的通信。而網絡編程最終要開發出來的應用大多數為支持各種協議的服務器,比如服務器服務器或者是基于自定義的協議實現的服務。在開始編碼之前,首先介紹一下協議棧上圖是我從網絡編程這本書拍下來的。 相信大部分的初中級PHP程序員平時寫的業務代碼占絕大多數,寫厭了平時的增刪改查,何不體驗體驗網絡編程的魅力呢。 學習網絡編程能夠很好的理解一些底層的網絡通信,比如...
摘要:唯一的知識點就是的基礎使用。可以簡單的理解下面的代碼就構建了一個服務器。握手完成之后的消息傳遞則在中處理。實際情況下,不可能那么多人同時說話廣播,而是說話的人少,接受廣播的人多。 硬廣一波 SF 官方首頁推薦《PHP進階之路》(你又多久沒有投資自己了?先看后買) 我們下面則將一些實際場景都添加進去,比如用戶身份的驗證,游客只能瀏覽不能發言,多房間(頻道)的聊天。該博客非常適合 Java...
摘要:一閱前熱身為了更加形象的說明同步異步阻塞非阻塞,我們以小明去買奶茶為例。等奶茶做好了,店員喊一聲小明,奶茶好了,然后小明去取奶茶。將響應結果發給相應的連接請求處理完成因為基于,所以每個可以處理無數個連接請求。如此,就輕松的處理了高并發。 一、閱前熱身 為了更加形象的說明同步異步、阻塞非阻塞,我們以小明去買奶茶為例。 1、同步與異步 ①同步與異步的理解 同步與異步的重點在消息通知的方式上...
閱讀 2475·2021-11-17 09:33
閱讀 762·2021-11-04 16:13
閱讀 1334·2021-10-14 09:50
閱讀 697·2019-08-30 15:53
閱讀 3664·2019-08-30 14:18
閱讀 3271·2019-08-30 14:14
閱讀 2100·2019-08-30 12:46
閱讀 3185·2019-08-26 14:05