PHP SOCKET編程

1. 預備知識php

       一直以來不多看到有多少人使用PHP的socket模塊來作一些事情,大概你們都把它定位在腳本語言的範疇內吧,可是其實php的socket模塊能夠作不少事情,包括作ftplist,http post提交,smtp提交,組包並進行特殊報文的交互(如smpp協議),whois查詢。這些都是比較常見的查詢。node

特別是php的socket擴展庫能夠作的事情簡直不會比c差多少。
php的socket鏈接函數
一、集成於內核的socket
這個系列的函數僅僅只能作主動鏈接沒法實現端口監聽相關的功能。並且在4.3.0以前全部socket鏈接只能工做在阻塞模式下。
此係列函數包括
fsockopen,pfsockopen
這兩個函數的具體信息能夠查詢php.net的用戶手冊
他們均會返回一個資源編號對於這個資源可使用幾乎全部對文件操做的函數對其進行操做如fgets(),fwrite(), fclose()等單注意的是全部函數遵循這些函數面對網絡信息流時的規律,例如:
fread() 從文件指針 handle 讀取最多 length 個字節。 該函數在讀取完 length 個字節數,或到達 EOF 的時候,或(對於網絡流)當一個包可用時就會中止讀取文件,視乎先碰到哪一種狀況。 
能夠看出對於網絡流就必須注意取到的是一個完整的包就中止。
二、php擴展模塊帶有的socket功能。
php4.x 之後有這麼一個模塊extension=php_sockets.dll,Linux上是一個extension=php_sockets.so。
當打開這個此模塊之後就意味着php擁有了強大的socket功能,包括listen端口,阻塞及非阻塞模式的切換,multi-client 交互式處理等
這個系列的函數列表參看http://www.php.net/manual/en/ref.sockets.php
看過這個列表以爲是否是很是豐富呢?不過很是遺憾這個模塊還很是年輕還有不少地方不成熟,相關的參考文檔也很是少:(
我也正在研究中,所以暫時不具體討論它,僅給你們一個參考文章react

http://www.zend.com/pecl/tutorials/sockets.phplinux

 

2. 使用PHP socket擴展程序員

 

服務器端代碼:編程

 

[php] 
數組

  1. <?php  緩存

  2. /** 安全

  3.  * File name server.php 服務器

  4.  * 服務器端代碼 

  5.  *  

  6.  * @author guisu.huang 

  7.  * @since 2012-04-11 

  8.  *  

  9.  */  

  10.   

  11. //確保在鏈接客戶端時不會超時  

  12. set_time_limit(0);  

  13. //設置IP和端口號  

  14. $address = "127.0.0.1";  

  15. $port = 2046; //調試的時候,能夠多換端口來測試程序!  

  16. /** 

  17.  * 建立一個SOCKET  

  18.  * AF_INET=是ipv4 若是用ipv6,則參數爲 AF_INET6 

  19.  * SOCK_STREAM爲socket的tcp類型,若是是UDP則使用SOCK_DGRAM 

  20. */  

  21. $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() 失敗的緣由是:" . socket_strerror(socket_last_error()) . "/n");  

  22. //阻塞模式  

  23. socket_set_block($sock) or die("socket_set_block() 失敗的緣由是:" . socket_strerror(socket_last_error()) . "/n");  

  24. //綁定到socket端口  

  25. $result = socket_bind($sock, $address, $port) or die("socket_bind() 失敗的緣由是:" . socket_strerror(socket_last_error()) . "/n");  

  26. //開始監聽  

  27. $result = socket_listen($sock, 4) or die("socket_listen() 失敗的緣由是:" . socket_strerror(socket_last_error()) . "/n");  

  28. echo "OK\nBinding the socket on $address:$port ... ";  

  29. echo "OK\nNow ready to accept connections.\nListening on the socket ... \n";  

  30. do { // never stop the daemon  

  31.     //它接收鏈接請求並調用一個子鏈接Socket來處理客戶端和服務器間的信息  

  32.     $msgsock = socket_accept($sock) or  die("socket_accept() failed: reason: " . socket_strerror(socket_last_error()) . "/n");  

  33.       

  34.     //讀取客戶端數據  

  35.     echo "Read client data \n";  

  36.     //socket_read函數會一直讀取客戶端數據,直到碰見\n,\t或者\0字符.PHP腳本把這寫字符看作是輸入的結束符.  

  37.     $buf = socket_read($msgsock, 8192);  

  38.     echo "Received msg: $buf   \n";  

  39.       

  40.     //數據傳送 向客戶端寫入返回結果  

  41.     $msg = "welcome \n";  

  42.     socket_write($msgsock, $msg, strlen($msg)) or die("socket_write() failed: reason: " . socket_strerror(socket_last_error()) ."/n");  

  43.     //一旦輸出被返回到客戶端,父/子socket都應經過socket_close($msgsock)函數來終止  

  44.     socket_close($msgsock);  

  45. while (true);  

  46. socket_close($sock);  

 

客戶端代碼:

 

[php] 

  1. <?php  

  2. /** 

  3.  * File name:client.php 

  4.  * 客戶端代碼 

  5.  *  

  6.  * @author guisu.huang 

  7.  * @since 2012-04-11 

  8.  */  

  9. set_time_limit(0);  

  10.   

  11. $host = "127.0.0.1";  

  12. $port = 2046;  

  13. $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)or die("Could not create  socket\n"); // 建立一個Socket  

  14.    

  15. $connection = socket_connect($socket, $host, $port) or die("Could not connet server\n");    //  鏈接  

  16. socket_write($socket, "hello socket") or die("Write failed\n"); // 數據傳送 向服務器發送消息  

  17. while ($buff = socket_read($socket, 1024, PHP_NORMAL_READ)) {  

  18.     echo("Response was:" . $buff . "\n");  

  19. }  

  20. socket_close($socket);  

 

使用cli方式啓動server:

php server.php

 

這裏注意socket_read函數:

可選的類型參數是一個命名的常數:
PHP_BINARY_READ - 使用系統recv()函數。用於讀取二進制數據的安全。 (在PHP>「默認= 4.1.0)
PHP_NORMAL_READ - 讀停在\ n或\r(在PHP <= 4.0.6默認)  

針對參數PHP_NORMAL_READ ,若是服務器的響應結果沒有\ n。形成socket_read(): unable to read from socket

 

3.PHP的併發IO編程

原文:http://rango.swoole.com/archives/508

 

1) 多進程/多線程同步阻塞

 

最先的服務器端程序都是經過多進程、多線程來解決併發IO的問題。進程模型出現的最先,從Unix系統誕生就開始有了進程的概念。最先的服務器端程序通常都是Accept一個客戶端鏈接就建立一個進程,而後子進程進入循環同步阻塞地與客戶端鏈接進行交互,收發處理數據。

多線程模式出現要晚一些,線程與進程相比更輕量,並且線程之間是共享內存堆棧的,因此不一樣的線程之間交互很是容易實現。好比聊天室這樣的程序,客戶端鏈接之間能夠交互,比聊天室中的玩家能夠任意的其餘人發消息。用多線程模式實現很是簡單,線程中能夠直接讀寫某一個客戶端鏈接。而多進程模式就要用到管道、消息隊列、共享內存實現數據交互,統稱進程間通訊(IPC)複雜的技術才能實現。

代碼實例:

多進程/線程模型的流程是

  1. 建立一個 socket,綁定服務器端口(bind),監聽端口(listen),在PHP中用stream_socket_server一個函數就能完成上面3個步驟,固然也可使用php sockets擴展分別實現。

  2. 進入while循環,阻塞在accept操做上,等待客戶端鏈接進入。此時程序會進入隨眠狀態,直到有新的客戶端發起connect到服務器,操做系統會喚醒此進程。accept函數返回客戶端鏈接的socket

  3. 主進程在多進程模型下經過fork(php: pcntl_fork)建立子進程,多線程模型下使用pthread_create(php: new Thread)建立子線程。下文如無特殊聲明將使用進程同時表示進程/線程。

  4. 子進程建立成功後進入while循環,阻塞在recv(php: fread)調用上,等待客戶端向服務器發送數據。收到數據後服務器程序進行處理而後使用send(php: fwrite)向客戶端發送響應。長鏈接的服務會持續與客戶端交互,而短鏈接服務通常收到響應就會close。

  5. 當客戶端鏈接關閉時,子進程退出並銷燬全部資源。主進程會回收掉此子進程。

 

這種模式最大的問題是,進程/線程建立和銷燬的開銷很大。因此上面的模式沒辦法應用於很是繁忙的服務器程序。對應的改進版解決了此問題,這就是經典的Leader-Follower模型。

代碼實例:

它的特色是程序啓動後就會建立N個進程。每一個子進程進入Accept,等待新的鏈接進入。當客戶端鏈接到服務器時,其中一個子進程會被喚醒,開始處理客戶端請求,而且再也不接受新的TCP鏈接。當此鏈接關閉時,子進程會釋放,從新進入Accept,參與處理新的鏈接。

這個模型的優點是徹底能夠複用進程,沒有額外消耗,性能很是好。不少常見的服務器程序都是基於此模型的,好比Apache、PHP-FPM。

多進程模型也有一些缺點。

  1. 這種模型嚴重依賴進程的數量解決併發問題,一個客戶端鏈接就須要佔用一個進程,工做進程的數量有多少,併發處理能力就有多少。操做系統能夠建立的進程數量是有限的。

  2. 啓動大量進程會帶來額外的進程調度消耗。數百個進程時可能進程上下文切換調度消耗佔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個核心的操做:

  1. add添加socket監聽到reactor,能夠是listen socket也可使客戶端socket,也能夠是管道、eventfd、信號等

  2. set修改事件監聽,能夠設置監聽的類型,如可讀、可寫。可讀很好理解,對於listen socket就是有新客戶端鏈接到來了須要accept。對於客戶端鏈接就是收到數據,須要recv。可寫事件比較難理解一些。一個SOCKET是有緩存區的,若是要向客戶端鏈接發送2M的數據,一次性是發不出去的,操做系統默認TCP緩存區只有256K。一次性只能發256K,緩存區滿了以後send就會返回EAGAIN錯誤。這時候就要監聽可寫事件,在純異步的編程中,必須去監聽可寫才能保證send操做是徹底非阻塞的。

  3. del從reactor中移除,再也不監聽事件

  4. 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


 

4. PHP socket內部源碼

          從PHP內部源碼來看,PHP提供的socket編程是在socket,bind,listen等函數外添加了一個層,讓其更加簡單和方便調用。可是一些業務邏輯的程序仍是須要程序員本身去實現。
下面咱們以socket_create的源碼實現來講明PHP的內部實現。
前面咱們有說到php的socket是以擴展的方式實現的。在源碼的ext目錄,咱們找到sockets目錄。這個目錄存放了PHP對於socket的實現。直接搜索PHP_FUNCTION(socket_create),在sockets.c文件中找到了此函數的實現。以下所示代碼:

 

[cpp] view plain copy print?

  1. /* {{{ proto resource socket_create(int domain, int type, int protocol) U 

  2.    Creates an endpoint for communication in the domain specified by domain, of type specified by type */  

  3. PHP_FUNCTION(socket_create)  

  4. {  

  5.         long            arg1, arg2, arg3;  

  6.         php_socket      *php_sock = (php_socket*)emalloc(sizeof(php_socket));  

  7.    

  8.         if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lll", &arg1, &arg2, &arg3) == FAILURE) {  

  9.                 efree(php_sock);  

  10.                 return;  

  11.         }  

  12.    

  13.         if (arg1 != AF_UNIX  

  14. #if HAVE_IPV6  

  15.                 && arg1 != AF_INET6  

  16. #endif  

  17.                 && arg1 != AF_INET) {  

  18.                 php_error_docref(NULL TSRMLS_CC, E_WARNING, "invalid socket domain [%ld] specified for argument 1, assuming AF_INET", arg1);  

  19.                 arg1 = AF_INET;  

  20.         }  

  21.    

  22.         if (arg2 > 10) {  

  23.                 php_error_docref(NULL TSRMLS_CC, E_WARNING, "invalid socket type [%ld] specified for argument 2, assuming SOCK_STREAM", arg2);  

  24.                 arg2 = SOCK_STREAM;  

  25.         }  

  26.    

  27.         php_sock->bsd_socket = socket(arg1, arg2, arg3);  

  28.         php_sock->type = arg1;  

  29.    

  30.         if (IS_INVALID_SOCKET(php_sock)) {  

  31.                 SOCKETS_G(last_error) = errno;  

  32.                 php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to create socket [%d]: %s", errno, php_strerror(errno TSRMLS_CC));  

  33.                 efree(php_sock);  

  34.                 RETURN_FALSE;  

  35.         }  

  36.    

  37.         php_sock->error = 0;  

  38.         php_sock->blocking = 1;  

  39.                                                                                                                                            1257,1-8      61%  

  40.         ZEND_REGISTER_RESOURCE(return_value, php_sock, le_socket);  

  41. }  

  42. /* }}} */  

 

Zend API實際對c函數socket作了包裝,供PHP使用。 而在c的socket編程中,咱們使用以下方式初始化socket。

 

[cpp] view plain copy print?

  1. //初始化Socket    

  2.     if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){    

  3.          printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);    

  4.          exit(0);    

  5.     }    

5. socket函數

函數名 描述
socket_accept() 接受一個Socket鏈接
socket_bind() 把socket綁定在一個IP地址和端口上
socket_clear_error() 清除socket的錯誤或最後的錯誤代碼
socket_close() 關閉一個socket資源
socket_connect() 開始一個socket鏈接
socket_create_listen() 在指定端口打開一個socket監聽
socket_create_pair() 產生一對沒有差異的socket到一個數組裏
socket_create() 產生一個socket,至關於產生一個socket的數據結構
socket_get_option() 獲取socket選項
socket_getpeername() 獲取遠程相似主機的ip地址
socket_getsockname() 獲取本地socket的ip地址
socket_iovec_add() 添加一個新的向量到一個分散/聚合的數組
socket_iovec_alloc() 這個函數建立一個可以發送接收讀寫的iovec數據結構
socket_iovec_delete() 刪除一個已分配的iovec
socket_iovec_fetch() 返回指定的iovec資源的數據
socket_iovec_free() 釋放一個iovec資源
socket_iovec_set() 設置iovec的數據新值
socket_last_error() 獲取當前socket的最後錯誤代碼
socket_listen() 監聽由指定socket的全部鏈接
socket_read() 讀取指定長度的數據
socket_readv() 讀取從分散/聚合數組過來的數據
socket_recv() 從socket裏結束數據到緩存
socket_recvfrom() 接受數據從指定的socket,若是沒有指定則默認當前socket
socket_recvmsg() 從iovec裏接受消息
socket_select() 多路選擇
socket_send() 這個函數發送數據到已鏈接的socket
socket_sendmsg() 發送消息到socket
socket_sendto() 發送消息到指定地址的socket
socket_set_block() 在socket裏設置爲塊模式
socket_set_nonblock() socket裏設置爲非塊模式
socket_set_option() 設置socket選項
socket_shutdown() 這個函數容許你關閉讀、寫、或指定的socket
socket_strerror() 返回指定錯誤號的周詳錯誤
socket_write() 寫數據到socket緩存
socket_writev() 寫數據到分散/聚合數組

 

 

 

6. PHP Socket模擬請求

咱們使用stream_socket來模擬:

 

 

[php] view plain copy print?

  1. /** 

  2.  *  

  3.  * @param $data= array=array('key'=>value) 

  4.  */  

  5. function post_contents($data = array()) {  

  6.     $post = $data ? http_build_query($data) : '';  

  7.     $header = "POST /test/ HTTP/1.1" . "\n";  

  8.     $header .= "User-Agent: Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1)" . "\n";  

  9.     $header .= "Host: localhost" . "\n";  

  10.     $header .= "Accept: */*" . "\n";  

  11.     $header .= "Referer: http://localhost/test/" . "\n";  

  12.     $header .= "Content-Length: ". strlen($post) . "\n";  

  13.     $header .= "Content-Type: application/x-www-form-urlencoded" . "\n";  

  14.     $header .= "\r\n";  

  15.     $ddd = $header . $post;  

  16.     $fp = stream_socket_client("tcp://localhost:80", $errno, $errstr, 30);  

  17.     $response = '';  

  18.     if (!$fp) {  

  19.         echo "$errstr ($errno)<br />\n";  

  20.     } else {  

  21.         fwrite($fp, $ddd);  

  22.         $i = 1;  

  23.         while ( !feof($fp) ) {  

  24.             $r = fgets($fp, 1024);  

  25.             $response .= $r;  

  26.             //處理這一行  

  27.         }  

  28.     }  

  29.     fclose($fp);  

  30.     return $response;  

  31. }  

注意,以上程序可能會進入死循環;

 

這個PHP的feof($fp) 須要注意的地方了,咱們來分析爲何進入死循環。

 

[php] view plain copy print?

  1. while ( !feof($fp) ) {  

  2.     $r = fgets($fp, 1024);  

  3.     $response .= $r;  

  4. }  

 

實際上,feof是可靠的,可是結合fgets函數一塊使用的時候,必需要當心了。一個常見的作法是:

 

[php] view plain copy print?

  1. $fp = fopen("myfile.txt", "r");  

  2. while (!feof($fp)) {  

  3.    $current_line = fgets($fp);  

  4.    //對結果作進一步處理,防止進入死循環  

  5. }  

當處理純文本的時候,fgets獲取最後一行字符後,foef函數返回的結果並非TRUE。實際的運算過程以下:

 1) while()繼續循環。

 2) fgets 獲取倒數第二行的字符串

 3) feof返回false,進入下一次循環

 4)fgets獲取最後一行數據

 5)  一旦fegets函數被調用,feof函數仍然返回的是false。因此繼續執行循環

 6) fget試圖獲取另一行,但實際結果是空的。實際代碼沒有意識到這一點,試圖處理另外根本不存在的一行,但fgets被調用了,feof放回的結果仍然是false

 7)    .....

8) 進入死循環

相關文章
相關標籤/搜索