php socket 編程

php socket 編程

1.實驗預習:tcp協議

TCP協議的建立:
建立流程:1.客戶端主動調用connect發送SYN分節;2.服務器端必須回覆一個ACK分節來確認客戶端的SYN分節,併發送一個SYN分節給客戶端;3.客戶端對服務器端發送SYN分節進行ACK分節的確認
php socket 編程php

TCP協議的拆除(TCP爲全雙工的傳輸協議,因此須要4次分節的交換):
拆除流程:1.首先申請拆除的一端調用close發送一個FIN分節;2.另外一端接收到FIN分節時,發送一個ACK分節進行確認;3.另外一端要申請拆除鏈接時,也要發送一個FIN分節;4.接收端發送一個ACK分節進行確認
php socket 編程html

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分節已接收,成功拆除鏈接]
php socket 編程瀏覽器

2.SOCKET 編程

咱們能夠簡單的把 Socket 理解爲一個能夠連通網絡上不一樣計算機應用程序之間的管道,把一堆數據從管道的 A 端扔進去,則會從管道的 B 端(同時還能夠從C、D、E、F……端冒出來)(Socket 的官方解釋: 在網絡編程中最經常使用的方案即是Client/Server(客戶機/服務器)模型。在這種方案中客戶應用程序向服務器程序請求服務。一個服務程序一般在一個衆所周知的地址監聽對服務的請求,也就是說,服務進程一 直處於休眠狀態,直到一個客戶向這個服務的地址提出了鏈接請求。在這個時刻,服務程序被"驚醒"而且爲客戶提供服務-對客戶的請求做出適當的反應。)
php socket 編程服務器

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);socket

<?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);
}

tcp

<?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’

3.多進程編程

多進程簡介:就是多個進程同時工做,這樣的進程通常屬於親屬關係,一般由一個父進程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服務器的併發處理量再次提高。

4 I/O複用

涉及知識點:阻塞/非阻塞,同步/異步,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資源.事件分發器:經過事件循環,事件循環是經過諸如epollSelectPoll等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服務器進程脫離控制終端,變爲守護進程

7信號通訊以及守護進程

進程的幾個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

相關文章
相關標籤/搜索