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