您好,登錄后才能下訂單哦!
TCP協(xié)議的創(chuàng)建:
創(chuàng)建流程:1.客戶端主動(dòng)調(diào)用connect發(fā)送SYN分節(jié);2.服務(wù)器端必須回復(fù)一個(gè)ACK分節(jié)來確認(rèn)客戶端的SYN分節(jié),并發(fā)送一個(gè)SYN分節(jié)給客戶端;3.客戶端對(duì)服務(wù)器端發(fā)送SYN分節(jié)進(jìn)行ACK分節(jié)的確認(rèn)TCP協(xié)議的拆除(TCP為全雙工的傳輸協(xié)議,所以需要4次分節(jié)的交換):
拆除流程:1.首先申請(qǐng)拆除的一端調(diào)用close發(fā)送一個(gè)FIN分節(jié);2.另一端接收到FIN分節(jié)時(shí),發(fā)送一個(gè)ACK分節(jié)進(jìn)行確認(rèn);3.另一端要申請(qǐng)拆除連接時(shí),也要發(fā)送一個(gè)FIN分節(jié);4.接收端發(fā)送一個(gè)ACK分節(jié)進(jìn)行確認(rèn)TCP的狀態(tài)轉(zhuǎn)換圖
連接:[1.SYN_SENT主動(dòng)打開,SYN分節(jié)已發(fā)送;2.SYN_RCVD被動(dòng)打開,SYN分節(jié)已接收;3.ESTABLISHED已經(jīng)建立連接]關(guān)閉:[1.FIN_WAIT_1發(fā)起主動(dòng)關(guān)閉,F(xiàn)IN分節(jié)已發(fā)送;2.CLOSE_WAIT被動(dòng)關(guān)閉,F(xiàn)IN分節(jié)已接收,ACK分節(jié)已發(fā)送;3.FIN_WAIT_2成功實(shí)現(xiàn)半關(guān)閉,ACK分節(jié)已接收;4.LAST_ACK最終的ACK,FIN分節(jié)已發(fā)送;5.TIME_WAIT FIN分節(jié)已接收,ACK分節(jié)已發(fā)送;6.CLOSE ACK分節(jié)已接收,成功拆除連接]
我們可以簡單的把 Socket 理解為一個(gè)可以連通網(wǎng)絡(luò)上不同計(jì)算機(jī)應(yīng)用程序之間的管道,把一堆數(shù)據(jù)從管道的 A 端扔進(jìn)去,則會(huì)從管道的 B 端(同時(shí)還可以從C、D、E、F……端冒出來)(Socket 的官方解釋: 在網(wǎng)絡(luò)編程中最常用的方案便是Client/Server(客戶機(jī)/服務(wù)器)模型。在這種方案中客戶應(yīng)用程序向服務(wù)器程序請(qǐng)求服務(wù)。一個(gè)服務(wù)程序通常在一個(gè)眾所周知的地址監(jiān)聽對(duì)服務(wù)的請(qǐng)求,也就是說,服務(wù)進(jìn)程一 直處于休眠狀態(tài),直到一個(gè)客戶向這個(gè)服務(wù)的地址提出了連接請(qǐng)求。在這個(gè)時(shí)刻,服務(wù)程序被"驚醒"并且為客戶提供服務(wù)-對(duì)客戶的請(qǐng)求作出適當(dāng)?shù)姆磻?yīng)。)
Socket 通信依次會(huì)進(jìn)行 Socket 創(chuàng)建、Socket 監(jiān)聽、Socket 收發(fā)、Socket 關(guān)閉幾個(gè)階段。
常用函數(shù)1(創(chuàng)建的是socket資源):[socket_create() | socket_bind() | socket_listen() | socket_accept() | socket_write() | socket_read() | socket_close()]
常用函數(shù)2(創(chuàng)建的是stream資源):[stream_socket_server() | fwrite() | fread() | fclose()]
示例 server.php(并發(fā)量只有1);
<?php $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); socket_bind($sock, '127.0.0.1', 8080); socket_listen($sock); for(;;){ $conn = socket_accept($sock); $output_buffer = 'HTTP/1.0 200 OK\r\nServer: this is my server\r\nContent-Type:text/html;charset:utf-8\r\nthis is my frist socket program'; socket_write($conn, $output_buffer); socket_close($conn); }
或
<?php $sock = stream_socket_server('tcp://127.0.0.1:8080", $errno, $errstr); for(;;){ $conn = stream_socket_accept($sock); $output_buffer = 'HTTP/1.0 200 OK\r\nServer: this is my server\r\nContent-Type:text/html;charset:utf-8\r\nthis is my frist socket program'; fwrite($conn, $write_buffer); fclose($conn); }
控制臺(tái)運(yùn)行
sudo php-fpm7.2 start && php sertver.php
運(yùn)行成功之后,打開瀏覽器輸入 ‘127.0.0.1:8080’
多進(jìn)程簡介:就是多個(gè)進(jìn)程同時(shí)工作,這樣的進(jìn)程一般屬于親屬關(guān)系,通常由一個(gè)父進(jìn)程fork得到的. 注意這里所說的同時(shí)工作,是宏觀上的,同一時(shí)刻在單個(gè)單核CPU上
示例 multiProcess.php
<?php $pid = pcntl_fork(); if($pid){ echo "this is parent process\n"; pcntl_waitpid($pid, $status); } elseif($pid == 0){ echo "this is child process\n"; } else { die("fork faild\n"); }
運(yùn)行
php multiProcess.php
函數(shù)介紹:
int pcntl_fork(void);
執(zhí)行該函數(shù),會(huì)復(fù)制當(dāng)前進(jìn)程產(chǎn)生另一個(gè)進(jìn)程,稱之為當(dāng)前進(jìn)程的子進(jìn)程,該函在父進(jìn)程和子進(jìn)程的返回值不相同,在父進(jìn)程中返回的是fork出的子進(jìn)程的進(jìn)程ID,在子進(jìn)程中返回值為0。要注意的是在復(fù)制進(jìn)程時(shí),會(huì)復(fù)制該進(jìn)程的數(shù)據(jù)(堆數(shù)據(jù)、棧數(shù)據(jù)和靜態(tài)數(shù)據(jù)),包括在父進(jìn)程打開的文件描述符,在子進(jìn)程中也是打開的,這意味著當(dāng)你在父進(jìn)程使用了大量內(nèi)存時(shí),fork出來的子進(jìn)程必須擁有等量的內(nèi)存資源,否則可能會(huì)導(dǎo)致fork失敗.int pcntl_waitpid(int $pid, int &$status [,int $options=0]);
pid: 進(jìn)程ID;status: 子進(jìn)程的退出狀態(tài);option: 取決于操作系統(tǒng)是否提供wait3函數(shù),如果提供該函數(shù),則該選項(xiàng)參數(shù)才生效.為什么父進(jìn)程要調(diào)用 pcntl_waitpid() 函數(shù)呢?這是因?yàn)樽舆M(jìn)程在結(jié)束時(shí),不管是主動(dòng)結(jié)束(調(diào)用exit或main函數(shù)返回)還是被動(dòng)結(jié)束(被發(fā)出的信號(hào)打斷),都會(huì)保存退出狀態(tài)供父進(jìn)程調(diào)用,所以還會(huì)在操作系統(tǒng)的進(jìn)程表中占用一項(xiàng)。如果不調(diào)用pcntl_waitpid清除子進(jìn)程的退出狀態(tài),回收該表項(xiàng),那么子進(jìn)程雖然已經(jīng)死亡,但依然占用著寶貴的資源,就變成了“僵尸進(jìn)程”)
leader-follower模型
一個(gè)非常簡單的leader-follower模型,創(chuàng)建一個(gè)進(jìn)程池,隨機(jī)選出一個(gè)進(jìn)程作為leader進(jìn)程,該進(jìn)程監(jiān)聽是否有新連接,如果有則提升另一個(gè)follower為leader進(jìn)程來繼續(xù)監(jiān)聽,而原leader進(jìn)程則去處理新連接的請(qǐng)求,在/home/shiyanlou/目錄下創(chuàng)建文件leader.php:
$sock = stream_socket_server('tcp://127.0.0.1:80", $errno, $errstr); $pids = []; for($i=0;$i<10;$i++){ $pid = pcntl_fork(); $pids[] = $pid; if($pid == 0){ for(;;){ $conn = stream_socket_accept($sock); $out_buffer = "HTTP/1.0 200 OK\r\nServer: my_server\r\nContent-Type:text/html; charset=utf-8\r\n\r\n this is $i process"; fwrite($conn, $out_buffer); fclose($conn); } exit(0); } } foreach($pids as $pid){ $pcntl_waitpid($pid, $status); }
這樣,我們的WEB服務(wù)器的處理能力又上了一個(gè)臺(tái)階,可以同時(shí)處理10個(gè)并發(fā),當(dāng)然這個(gè)能力還會(huì)隨著你的進(jìn)程池中進(jìn)程的數(shù)量提升。那是不是意味著只要我們無限加大進(jìn)程的數(shù)量,就可以處理無限的并發(fā)呢?遺憾的是,事實(shí)并不是這樣。首先,系統(tǒng)創(chuàng)建進(jìn)程的開銷是大的,系統(tǒng)并不能無限地創(chuàng)建進(jìn)程,因?yàn)槊恳粋€(gè)進(jìn)程都占用一定的系統(tǒng)資源,而系統(tǒng)的資源是有限的,不可能無限地創(chuàng)建。 其次,大量進(jìn)程帶來的上下文切換,也會(huì)帶來巨大的資源消耗和性能浪費(fèi)。所以使用大量地創(chuàng)建進(jìn)程的方式來提升并發(fā),是不可行的。那么,沒有辦法了么?難道沒有一種技術(shù)在單進(jìn)程里就可以維持成千上萬的連接么?下一個(gè)實(shí)驗(yàn)我們將介紹IO復(fù)用技術(shù),使我們WEB服務(wù)器的并發(fā)處理量再次提升。
涉及知識(shí)點(diǎn):阻塞/非阻塞,同步/異步,I/O多路復(fù)用,輪詢,epoll
1.阻塞/非阻塞:這兩個(gè)概念是針對(duì) IO 過程中進(jìn)程的狀態(tài)來說的,阻塞 IO 是指調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起;相反,非阻塞指在不能立刻得到結(jié)果之前,該函數(shù)不會(huì)阻塞當(dāng)前線程,而會(huì)立刻返回;
2.同步/異步:這兩個(gè)概念是針對(duì)調(diào)用如果返回結(jié)果來說的,所謂同步,就是在發(fā)出一個(gè)功能調(diào)用時(shí),在沒有得到結(jié)果之前,該調(diào)用就不返回;相反,當(dāng)一個(gè)異步過程調(diào)用發(fā)出后,調(diào)用者不能立刻得到結(jié)果,實(shí)際處理這個(gè)調(diào)用的部件在完成后,通過狀態(tài)、通知和回調(diào)來通知調(diào)用者;
3.阻塞與非阻塞:在介紹IO復(fù)用技術(shù)之前,先介紹一下阻塞和非阻塞,在我們前幾節(jié)的WEB服務(wù)器中,調(diào)用socket_accept函數(shù)會(huì)使整個(gè)進(jìn)程阻塞,直到有新連接,操作系統(tǒng)才喚醒進(jìn)程繼續(xù)執(zhí)行。而非阻塞模式, stream_socket_accept的行為就不一樣了,如果沒有新連接,不會(huì)阻塞進(jìn)程,而是馬上返回false;
4.I/O 多路復(fù)用:多路復(fù)用(IO/Multiplexing):為了提高數(shù)據(jù)信息在網(wǎng)絡(luò)通信線路中傳輸?shù)男剩谝粭l物理通信線路上建立多條邏輯通信信道,同時(shí)傳輸若干路信號(hào)的技術(shù)就叫做多路復(fù)用技術(shù)。對(duì)于 Socket 來說,應(yīng)該說能同時(shí)處理多個(gè)連接的模型都應(yīng)該被稱為多路復(fù)用,目前比較常用的有 select/poll/epoll/kqueue 這些 IO 模型(目前也有像 Apache 這種每個(gè)連接用單獨(dú)的進(jìn)程/線程來處理的 IO 模型,但是效率相對(duì)比較差,也很容易出問題,所以暫時(shí)不做介紹了)。在這些多路復(fù)用的模式中,異步阻塞/非阻塞模式的擴(kuò)展性和性能最好;
5.select輪詢:使用select會(huì)輪詢連接池,當(dāng)有連接可讀或可寫時(shí),select函數(shù)返回可讀寫的連接數(shù),然后再輪詢一遍連接池,查找活動(dòng)連接進(jìn)行讀寫操作。比較尷尬的是,socket_select只支持socket類型的資源,而不支持stream類型的資源,所以這里需要使用socket_create創(chuàng)建socket資源;
創(chuàng)建文件select.php:
<?php $sock = socket_create(AF_IINIT, SOCK_STREAM,0); socket_bind($sock, '127.0.0.1', 80); socket_listen($sock); $reads = $clients = []; $writes = $exceptions = NULL; socket_set_nonblock($sock); $out_buffer = "HTTP/1.0 200 OK\r\nServer:server\r\nContent-Type:text/html;chartset=utf-8\r\n\r\nHello!world"; for(;;){ $reads = array_merge(array($sock), $clients); $activity_counts = @socket_select($reads, $writes, $exceptions, 0); if($activity_counts>0){ if(($conn=socket_accept($sock))!= false){ $clients[] = $conn; } $length = count($clients); for($i=0; $i<$length;$i++){ $client = $clients[$i]; if(($rad_buffer = @socket_read($client, 1024)) != false){ socket_write($client, $write_buffer); socket_close($client); break; } } } }
select雖然可以監(jiān)聽多個(gè)連接,但是它最多只能監(jiān)聽1024個(gè)連接。這雖然在poll中得到了改進(jìn),但是select和poll本質(zhì)上都是通過輪詢的方式進(jìn)行監(jiān)聽,這意味著當(dāng)監(jiān)聽了上萬連接時(shí),就算只有一個(gè)連接是活動(dòng)的,依然要把上萬連接都遍歷一次。顯然,這無疑是極大的性能浪費(fèi),而epoll的出現(xiàn)徹底地解決了這個(gè)問題
6.epoll:epoll并不是只有一個(gè)函數(shù)來實(shí)現(xiàn),而是多個(gè)函數(shù)。我們這里并不討論epoll相關(guān)的函數(shù),因?yàn)镻HP并不提供相關(guān)的函數(shù),但它提供了基于libevent庫的libevent擴(kuò)展,以及基于libevent庫的event擴(kuò)展。libevent庫實(shí)現(xiàn)了Reactor模型,關(guān)于Reactor模型,這里只作簡單的介紹(Reactor模型,包含了幾個(gè)組件:句柄,事件分發(fā)器,事件處理器。句柄:就是文件描述符,在Socket編程中,就是使用socket_create創(chuàng)建的socket資源.事件分發(fā)器:通過事件循環(huán),事件循環(huán)是通過諸如epoll
SelectPoll
等IO復(fù)用技術(shù)實(shí)現(xiàn)的,監(jiān)聽句柄期待的事件是否發(fā)生,發(fā)生了則將事件分發(fā)給事件處理器。事件處理器:當(dāng)事件發(fā)生時(shí),處理相關(guān)的邏輯)。而libevent庫已經(jīng)實(shí)現(xiàn)了Reactor模型,我們可以開箱即用。下面,我們將通過libevent對(duì)我們的WEB服務(wù)器再次改造,使它的處理并發(fā)的能力再次提高在此之前,我們需要安裝event擴(kuò)展,安裝php的event擴(kuò)展必須安裝libevent庫,
php -m|grep event
確保我們已經(jīng)安裝好了event庫;示例:epoll.php
<?php $fd = stream_socket_server("tcp://127.0.0.1:80", $errno, $errstr); stream_set_blocking($fd, 0); $event_base = new EventBase(); $event = new Event($event_base, $fd, Event::READ | Event::PERSIST, function($fd) use (&$event_base){ $conn = stream_socket_accept($fd); fwrite($conn, 'HTTP/1.0 200 OK\r\nContent-Length:2\r\r\r\rHi'); fclose($conn); }, $fd); $event->add(); $event_base->loop();
流程和創(chuàng)建Reactor模型一致:創(chuàng)建句柄->創(chuàng)建事件循環(huán)器->創(chuàng)建事件,并指定事件監(jiān)聽的事件類型及注冊事件處理器->向循環(huán)器中添加事件
這里我們主要看Event類,看看它的構(gòu)造函數(shù)原型:
public Event::__construct ( EventBase base , mixed base,mixedfd , int what , callable what,callablecb [, mixed $arg = NULL ] )
base: EventBase類的實(shí)例;fd: 要監(jiān)聽的句柄;what: 要監(jiān)聽的事件類型;cb: 事件處理器,在PHP中就是回調(diào)函數(shù);arg: 事件處理器的參數(shù)列表
通過我們進(jìn)一步的改造,我們的WEB服務(wù)器現(xiàn)在處理并發(fā)的能力已經(jīng)非常強(qiáng)勁,但是要用于生產(chǎn)環(huán)境,還有一些需要解決的問題,下一章我們將探討如何讓W(xué)EB服務(wù)器進(jìn)程脫離控制終端,變?yōu)槭刈o(hù)進(jìn)程
進(jìn)程的幾個(gè)ID[pid:進(jìn)程ID,ppid:父進(jìn)程ID,pgid:進(jìn)程組ID,sid:會(huì)話組ID],可以用命令去查看
ps -axj
,一般PPID為0的,都是內(nèi)核態(tài)進(jìn)程。一般PPID為1的,并且pid == pgid == sid的,都是守護(hù)進(jìn)程守護(hù)進(jìn)程創(chuàng)建的標(biāo)準(zhǔn)流程,讓W(xué)EB服務(wù)器進(jìn)程變?yōu)槭刈o(hù)進(jìn)程,成為守護(hù)進(jìn)程有幾個(gè)標(biāo)準(zhǔn)的步驟:
1.設(shè)置文件創(chuàng)建掩碼,一般設(shè)置為0,umask(0)
2.pcntl_fork一個(gè)子進(jìn)程,并馬上退出,這樣做的目的是讓子進(jìn)程繼承進(jìn)程組ID并獲取一個(gè)新的進(jìn)程ID,這樣就可以確保子進(jìn)程一定不是進(jìn)程組組長,因?yàn)檫M(jìn)程組組長不能創(chuàng)建新會(huì)話
3.posix_setsid創(chuàng)建新會(huì)話和新進(jìn)程組,并成為會(huì)話組長和進(jìn)程組組長,并和原來的控制終端脫離關(guān)系,這樣該進(jìn)程就不會(huì)被原來終端的控制信號(hào)中斷
4.pcntl_fork,再fork一次并不是必須的,只是在基于System-V的系統(tǒng)上,有人建議再fork一次,避免打開終端設(shè)備,使程序的通用性更強(qiáng)。示例:daemon.php:
<?php function daemon(){ umask(0); if(pcntl_fork()){ exit(0); } posix_setsid(); if(pcntl_fork()){ exit(0); } sleep(100); } daemon();
在終端運(yùn)行
php daemon.php && ps axj|grep daemon.php
,觀察一下ppid、pid、pgid、sid,結(jié)果顯示:ppid確實(shí)為1,這證明進(jìn)程已經(jīng)被init1號(hào)進(jìn)程收養(yǎng)。但是為什么pid、pgid、sid這三個(gè)值不一樣呢?是不是弄錯(cuò)了?我們再看看代碼,在調(diào)用posix_setsid之后,這三個(gè)值其實(shí)是一樣的,只是我們又fork了一次,所以pid變了。有興趣的同學(xué)把第二次fork的代碼注釋點(diǎn),再觀察一下,是不是一樣了?現(xiàn)在我我們對(duì)上節(jié)的server.php進(jìn)行改寫:
<?php unction daemon(){ umask(0); if(pcntl_fork()){ exit(0); } posix_setsid(); if(pcntl_fork()){ exit(0); } sleep(100); } daemon(); $fd = stream_socket_server('tcp://127.0.0.1:8080', $errno, $errstr); stream_set_blocking($fd, 0); $event_base = new EventBase(); $event = new Event($event_base, $fd, Event::READ | Event::PERSIST, function($fd) use(&$event_base){ $conn = stream_socket_accept($fd); fwrite($conn, 'HTTP/1.0 200 OK\r\nContent-Length:2\r\n\r\nHi'); fclose($conn); }, $fd) $event->add(); $event_base->loop();
運(yùn)行成功之后,關(guān)閉當(dāng)前終端,打開另一終端,輸入 ps axj | grep server.php觀察pid、pgid、sid、ppid,并打開瀏覽器輸入127.0.0.1:8080,看是否輸出結(jié)果到這兒,我們的WEB服務(wù)器才相對(duì)完善一些了,那有的同學(xué)就又要問了,變成了守護(hù)進(jìn)程,那我要怎么控制它重啟,暫停呢?接下來的一節(jié)我們將介紹如何使用信號(hào)與守護(hù)進(jìn)程進(jìn)行通信。
信號(hào): 我們在使用控制終端的時(shí)候,在上面鍵入各種各樣的子程序,比如sudo apt-get安裝程序,但有的時(shí)候子程序運(yùn)行時(shí)間過長,我們沒有耐心等下去時(shí),我們經(jīng)常會(huì)按Ctrl+c結(jié)束當(dāng)前進(jìn)程的運(yùn)行,Ctrl+c實(shí)質(zhì)上就是發(fā)送一個(gè)SIGINT信號(hào)給子程序,子程序的信號(hào)處理器接收到該信號(hào)之后,就會(huì)按預(yù)先編好的程序進(jìn)行處理,這樣的話即使我們脫離終端,無法進(jìn)行直接的手動(dòng)操作也可以利用信號(hào)控制我們編寫程序的狀態(tài),那在PHP中我們?nèi)绾握{(diào)用函數(shù)發(fā)送信號(hào)呢?
相關(guān)函數(shù)1 posix_kill
函數(shù)原型: bool posix_kill ( int pid , int pid,intsig )
pid: 進(jìn)程ID
sig: 系統(tǒng)預(yù)定義的信號(hào)常量相關(guān)函數(shù)2 pcntl_signal
函數(shù)原型: bool pcntl_signal ( int signo , callback signo,callbackhandler [, bool $restart_syscalls = true ] )
signo: 系統(tǒng)預(yù)定義的信號(hào)常量
handler: 信號(hào)處理器,一個(gè)回調(diào)函數(shù)
restart_syscalls: 當(dāng)進(jìn)程在進(jìn)行系統(tǒng)調(diào)用時(shí),被信號(hào)中斷時(shí),系統(tǒng)調(diào)用是否重新調(diào)用,一般默認(rèn)為true示例:signal.php:
<?php declare(ticks=1); pcntl_signal(SIGINT, function(){ file_put_content("signal.txt", "signal recevied\n") }) sleep(30);
編輯完成之后,我們在終端執(zhí)行
php signal.php
在進(jìn)程返回結(jié)果之前,我們按下Ctrl+c,此時(shí)系統(tǒng)會(huì)自動(dòng)調(diào)用kill發(fā)送信號(hào) SIGINT 我們編寫的信號(hào)處理器進(jìn)行信號(hào)的處理執(zhí)行回調(diào)函數(shù)。除了使用pcntl_signal安裝信號(hào)處理器,我們在上一章說過的Event類,也可以監(jiān)聽信號(hào)事件,將signal.php改寫為:<?php $event_base = new EventBase(); $event = new Event($event_base, SIGINT, Event::SIGNAL, function() use(&$event_base){ file_put_content("signal2.txt", "signal recevied\n") }) $event->add(); $event_base->loop();
使用守護(hù)進(jìn)程和信號(hào)再次重構(gòu)我們的WEB服務(wù)器,讓它更像一個(gè)真正的能用在生產(chǎn)環(huán)境的在此感謝實(shí)驗(yàn)樓提供的實(shí)驗(yàn)幫助
擴(kuò)展閱讀php手冊之socket
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。