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