PHP實現系統編程(一) --- 網絡Socket及IO多路複用【網摘】

一直以來,PHP不多用於socket編程,畢竟是一門腳本語言,效率會成爲很大的瓶頸,可是不能說PHP就沒法用於socket編程,也不能說PHP的socket編程性能就有多麼的低,例如知名的一款PHP socket框架 workerman 就是用純PHP開發,而且號稱擁有優秀的性能,因此在某些環境下,PHP socket編程或許也可一展身手。php

PHP提供了一系列相似C語言socket庫中的方法供咱們調用:html

 

[php]  view plain  copy
 
  1. socket_accept — Accepts a connection on a socket  
  2. socket_bind — 給套接字綁定名字  
  3. socket_clear_error — 清除套接字或者最後的錯誤代碼上的錯誤  
  4. socket_close — 關閉套接字資源  
  5. socket_cmsg_space — Calculate message buffer size  
  6. socket_connect — 開啓一個套接字鏈接  
  7. socket_create_listen — Opens a socket on port to accept connections  
  8. socket_create_pair — Creates a pair of indistinguishable sockets and stores them in an array  
  9. socket_create — 建立一個套接字(通信節點)  
  10. socket_get_option — Gets socket options for the socket  
  11. socket_getopt — 別名 socket_get_option  
  12. socket_getpeername — Queries the remote side of the given socket which may either result in host/port or in a Unix filesystem path, dependent on its type  
  13. socket_getsockname — Queries the local side of the given socket which may either result in host/port or in a Unix filesystem path, dependent on its type  
  14. socket_import_stream — Import a stream  
  15. socket_last_error — Returns the last error on the socket  
  16. socket_listen — Listens for a connection on a socket  
  17. socket_read — Reads a maximum of length bytes from a socket  
  18. socket_recv — 從已鏈接的socket接收數據  
  19. socket_recvfrom — Receives data from a socket whether or not it is connection-oriented  
  20. socket_recvmsg — Read a message  
  21. socket_select — Runs the select() system call on the given arrays of sockets with a specified timeout  
  22. socket_send — Sends data to a connected socket  
  23. socket_sendmsg — Send a message  
  24. socket_sendto — Sends a message to a socket, whether it is connected or not  
  25. socket_set_block — Sets blocking mode on a socket resource  
  26. socket_set_nonblock — Sets nonblocking mode for file descriptor fd  
  27. socket_set_option — Sets socket options for the socket  
  28. socket_setopt — 別名 socket_set_option  
  29. socket_shutdown — Shuts down a socket for receiving, sending, or both  
  30. socket_strerror — Return a string describing a socket error  
  31. socket_write — Write to a socket  

 

更多細節請查看PHP關於socket的官方手冊:http://php.net/manual/zh/book.sockets.phplinux

 

一個簡單的TCP服務器示例 phptcpserver.php :編程

 

[php]  view plain  copy
 
  1. <?php  
  2.   
  3. $servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);  // 建立一個socket  
  4.   
  5. if (FALSE === $servsock)  
  6. {  
  7.     $errcode = socket_last_error();  
  8.     fwrite(STDERR, "socket create fail: " . socket_strerror($errcode));  
  9.     exit(-1);  
  10. }  
  11.   
  12. if (!socket_bind($servsock, '127.0.0.1', 8888))    // 綁定ip地址及端口  
  13. {  
  14.     $errcode = socket_last_error();  
  15.     fwrite(STDERR, "socket bind fail: " . socket_strerror($errcode));  
  16.     exit(-1);  
  17. }  
  18.   
  19. if (!socket_listen($servsock, 128))      // 容許多少個客戶端來排隊鏈接  
  20. {  
  21.     $errcode = socket_last_error();  
  22.     fwrite(STDERR, "socket listen fail: " . socket_strerror($errcode));  
  23.     exit(-1);  
  24. }  
  25.   
  26. while (1)  
  27. {  
  28.     $connsock = socket_accept($servsock);  //響應客戶端鏈接  
  29.   
  30.     if ($connsock)  
  31.     {  
  32.         socket_getpeername($connsock, $addr, $port);  //獲取鏈接過來的客戶端ip地址和端口  
  33.         echo "client connect server: ip = $addr, port = $port" . PHP_EOL;  
  34.   
  35.         while (1)  
  36.         {  
  37.             $data = socket_read($connsock, 1024);  //從客戶端讀取數據  
  38.   
  39.             if ($data === '')  
  40.             {  
  41.                 //客戶端關閉  
  42.                 socket_close($connsock);  
  43.                 echo "client close" . PHP_EOL;  
  44.                 break;  
  45.             }  
  46.             else  
  47.             {  
  48.                 echo 'read from client:' . $data;  
  49.                 $data = strtoupper($data);  //小寫轉大寫  
  50.                 socket_write($connsock, $data);  //回寫給客戶端  
  51.             }  
  52.         }  
  53.     }  
  54. }  
  55.   
  56. socket_close($servsock);  


啓動這個服務器:數組

 

 

[plain]  view plain  copy
 
  1. [root@localhost php]# php phptcpserver.php   


以後這個服務器就一直阻塞在那裏,等待客戶端鏈接,咱們能夠用telnet命令來鏈接這個服務器:服務器

 

 

[plain]  view plain  copy
 
  1. [root@localhost ~]# telnet 127.0.0.1 8888  
  2. Trying 127.0.0.1...  
  3. Connected to 127.0.0.1.  
  4. Escape character is '^]'.  
  5. ajdjajksdjkaasda  
  6. AJDJAJKSDJKAASDA  
  7. 小明哈哈哈哈笑  
  8. 小明哈哈哈哈笑  
  9. 小明efsfsdfsdf了哈哈哈  
  10. 小明EFSFSDFSDF了哈哈哈  


服務器端輸出:併發

 

 

[plain]  view plain  copy
 
  1. [root@localhost php]# php phptcpserver.php   
  2. client connect server: ip = 127.0.0.1, port = 50398  
  3. read from client:ajdjajksdjkaasda  
  4. read from client:小明哈哈哈哈笑  
  5. read from client:小明efsfsdfsdf了哈哈哈  


但其實這個TCP服務器是有問題的,它一次只能處理一個客戶端的鏈接和數據傳輸,這是由於一個客戶端鏈接過來後,進程就去負責讀寫客戶端數據,當客戶端沒有傳輸數據時,tcp服務器處於阻塞讀狀態,沒法再去處理其餘客戶端的鏈接請求了。app

 

解決這個問題的一種辦法就是採用多進程服務器,每當一個客戶端鏈接過來,服務器開一個子進程專門負責和該客戶端的數據傳輸,而父進程仍然監聽客戶端的鏈接,可是起進程的代價是昂貴的,這種多進程的機制顯然支撐不了高併發。框架

另外一個解決辦法是使用IO多路複用機制,使用php爲咱們提供的socket_select方法,它能夠監聽多個socket,若是其中某個socket狀態發生了改變,好比從不可寫變爲可寫,從不可讀變爲可讀,這個方法就會返回,從而咱們就能夠去處理這個socket,處理客戶端的鏈接,讀寫操做等等。來看php文檔中對該socket_select的介紹ssh

 

[plain]  view plain  copy
 
  1. socket_select — Runs the select() system call on the given arrays of sockets with a specified timeout  
  2.   
  3. 說明  
  4.   
  5. int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )  
  6.   
  7. socket_select() accepts arrays of sockets and waits for them to change status.   
  8. Those coming with BSD sockets background will recognize that those socket resource arrays are in fact the so-called file descriptor sets.  
  9.  Three independent arrays of socket resources are watched.  
  10.   
  11. You do not need to pass every array to socket_select(). You can leave it out and use an empty array or NULL instead.  
  12.  Also do not forget that those arrays are passed by reference and will be modified after socket_select() returns.  
  13.   
  14. 返回值  
  15.   
  16. On success socket_select() returns the number of socket resources contained in the modified arrays,   
  17. which may be zero if the timeout expires before anything interesting happens.  
  18.  On error FALSE is returned. The error code can be retrieved with socket_last_error().  

 

大體翻譯下:

socket_select  ---  在給定的幾組sockets數組上執行 select() 系統調用,用一個特定的超時時間。

socket_select() 接受幾組sockets數組做爲參數,並監聽它們改變狀態

這些基於BSD scokets 可以識別這些socket資源數組實際上就是文件描述符集合。

三個不一樣的socket資源數組會被同時監聽。

這三個資源數組不是必傳的, 你能夠用一個空數組或者NULL做爲參數,不要忘記這三個數組是以引用的方式傳遞的,在函數返回後,這些數組的值會被改變。

socket_select() 調用成功返回這三個數組中狀態改變的socket總數,若是設置了timeout,而且在timeout以內都沒有狀態改變,這個函數將返回0,出錯時返回FALSE,能夠用socket_last_error() 獲取錯誤碼。

 

 

使用 socket_select() 優化以前 phptcpserver.php 代碼:

 

[php]  view plain  copy
 
  1. <?php  
  2.   
  3. $servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);  // 建立一個socket  
  4.   
  5. if (FALSE === $servsock)  
  6. {  
  7.     $errcode = socket_last_error();  
  8.     fwrite(STDERR, "socket create fail: " . socket_strerror($errcode));  
  9.     exit(-1);  
  10. }  
  11.   
  12. if (!socket_bind($servsock, '127.0.0.1', 8888))    // 綁定ip地址及端口  
  13. {  
  14.     $errcode = socket_last_error();  
  15.     fwrite(STDERR, "socket bind fail: " . socket_strerror($errcode));  
  16.     exit(-1);  
  17. }  
  18.   
  19. if (!socket_listen($servsock, 128))      // 容許多少個客戶端來排隊鏈接  
  20. {  
  21.     $errcode = socket_last_error();  
  22.     fwrite(STDERR, "socket listen fail: " . socket_strerror($errcode));  
  23.     exit(-1);  
  24. }  
  25.   
  26. /* 要監聽的三個sockets數組 */  
  27. $read_socks = array();  
  28. $write_socks = array();  
  29. $except_socks = NULL;  // 注意 php 不支持直接將NULL做爲引用傳參,因此這裏定義一個變量  
  30.   
  31. $read_socks[] = $servsock;  
  32.   
  33. while (1)  
  34. {  
  35.     /* 這兩個數組會被改變,因此用兩個臨時變量 */  
  36.     $tmp_reads = $read_socks;  
  37.     $tmp_writes = $write_socks;  
  38.   
  39.     // int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )  
  40.     $count = socket_select($tmp_reads, $tmp_writes, $except_socks, NULL);  // timeout 傳 NULL 會一直阻塞直到有結果返回  
  41.   
  42.     foreach ($tmp_reads as $read)  
  43.     {  
  44.   
  45.         if ($read == $servsock)  
  46.         {  
  47.             /* 有新的客戶端鏈接請求 */  
  48.             $connsock = socket_accept($servsock);  //響應客戶端鏈接, 此時不會形成阻塞  
  49.             if ($connsock)  
  50.             {  
  51.                 socket_getpeername($connsock, $addr, $port);  //獲取遠程客戶端ip地址和端口  
  52.                 echo "client connect server: ip = $addr, port = $port" . PHP_EOL;  
  53.   
  54.                 // 把新的鏈接sokcet加入監聽  
  55.                 $read_socks[] = $connsock;  
  56.                 $write_socks[] = $connsock;  
  57.             }  
  58.         }  
  59.         else  
  60.         {  
  61.             /* 客戶端傳輸數據 */  
  62.             $data = socket_read($read, 1024);  //從客戶端讀取數據, 此時必定會讀到數組而不會產生阻塞  
  63.   
  64.             if ($data === '')  
  65.             {  
  66.                 //移除對該 socket 監聽  
  67.                 foreach ($read_socks as $key => $val)  
  68.                 {  
  69.                     if ($val == $read) unset($read_socks[$key]);  
  70.                 }  
  71.   
  72.                 foreach ($write_socks as $key => $val)  
  73.                 {  
  74.                     if ($val == $read) unset($write_socks[$key]);  
  75.                 }  
  76.   
  77.   
  78.                 socket_close($read);  
  79.                 echo "client close" . PHP_EOL;  
  80.   
  81.             }  
  82.             else  
  83.             {  
  84.                 socket_getpeername($read, $addr, $port);  //獲取遠程客戶端ip地址和端口  
  85.   
  86.                 echo "read from client # $addr:$port # " . $data;  
  87.   
  88.                 $data = strtoupper($data);  //小寫轉大寫  
  89.   
  90.                 if (in_array($read, $tmp_writes))  
  91.                 {  
  92.                     //若是該客戶端可寫 把數據回寫給客戶端  
  93.                     socket_write($read, $data);  
  94.                 }  
  95.             }  
  96.         }  
  97.     }  
  98. }  
  99.   
  100. socket_close($servsock);  



如今,這個TCP服務器就能夠支持多個客戶端同時鏈接了,測試下:

 

服務器端:

 

[plain]  view plain  copy
 
  1. [root@localhost php]# php phptcpserver.php   
  2. client connect server: ip = 127.0.0.1, port = 50404  
  3. read from client # 127.0.0.1:50404 # hello world  
  4. client connect server: ip = 127.0.0.1, port = 50406  
  5. read from client # 127.0.0.1:50406 # hello PHP  
  6. read from client # 127.0.0.1:50404 # 少小離家老大回  
  7. read from client # 127.0.0.1:50404 # 鄉音無改鬢毛衰  
  8. read from client # 127.0.0.1:50406 # 老當益壯,  
  9. read from client # 127.0.0.1:50406 # 寧移白首之心  
  10. client close  
  11. client connect server: ip = 127.0.0.1, port = 50408  


稍微修改上面的服務器返回,返回一個HTTP響應頭和一個簡單的HTTP響應體,這樣就搖身一變成了一個最簡單的HTTP服務器:

 

 

 

[php]  view plain  copy
 
  1. ....  
  2.   
  3.                 socket_getpeername($read, $addr, $port);  //獲取遠程客戶端ip地址和端口  
  4.   
  5.                 echo "read from client # $addr:$port # " . $data;  
  6.   
  7.                 $response = "HTTP/1.1 200 OK\r\n";  
  8.                 $response .= "Server: phphttpserver\r\n";  
  9.                 $response .= "Content-Type: text/html\r\n";  
  10.                 $response .= "Content-Length: 3\r\n\r\n";  
  11.                 $response .= "ok\n";  
  12.   
  13.                 if (in_array($read, $tmp_writes))  
  14.                 {  
  15.                     //若是該客戶端可寫 把數據回寫給客戶端  
  16.                     socket_write($read, $response);  
  17.                     socket_close($read);  // 主動關閉客戶端鏈接  
  18.                     //移除對該 socket 監聽  
  19.                     foreach ($read_socks as $key => $val)  
  20.                     {  
  21.                         if ($val == $read) unset($read_socks[$key]);  
  22.                     }  
  23.   
  24.                     foreach ($write_socks as $key => $val)  
  25.                     {  
  26.                         if ($val == $read) unset($write_socks[$key]);  
  27.                     }  
  28.                 }  
  29. .....  



 


從新啓動該服務器,用curl模擬請求該http服務器:

 

 

[plain]  view plain  copy
 
  1. [root@localhost ~]# curl '127.0.0.1:8888'  
  2. ok  
  3. [root@localhost ~]# curl '127.0.0.1:8888'  
  4. ok  
  5. [root@localhost ~]# curl '127.0.0.1:8888'  
  6. ok  
  7. [root@localhost ~]# curl '127.0.0.1:8888'  
  8. ok  
  9. [root@localhost ~]# curl '127.0.0.1:8888'  
  10. ok  
  11. [root@localhost ~]#   


服務器端輸出:

 

 

[plain]  view plain  copy
 
  1. client connect server: ip = 127.0.0.1, port = 50450  
  2. read from client # 127.0.0.1:50450 # GET / HTTP/1.1  
  3. User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2  
  4. Host: 127.0.0.1:8888  
  5. Accept: */*  
  6.   
  7. client close  
  8. client connect server: ip = 127.0.0.1, port = 50452  
  9. read from client # 127.0.0.1:50452 # GET / HTTP/1.1  
  10. User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2  
  11. Host: 127.0.0.1:8888  
  12. Accept: */*  
  13.   
  14. client close  
  15. client connect server: ip = 127.0.0.1, port = 50454  
  16. read from client # 127.0.0.1:50454 # GET / HTTP/1.1  
  17. User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2  
  18. Host: 127.0.0.1:8888  
  19. Accept: */*  
  20.   
  21. client close  
  22. client connect server: ip = 127.0.0.1, port = 50456  
  23. read from client # 127.0.0.1:50456 # GET / HTTP/1.1  
  24. User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.27.1 zlib/1.2.3 libidn/1.18 libssh2/1.4.2  
  25. Host: 127.0.0.1:8888  
  26. Accept: */*  
  27.   
  28. client close  

 

 

這樣一個高併發的HTTP服務器就開發好了,用壓測軟件測試下併發能力:

 



看到高達5000多的QPS,有沒有小激動呢^^。

PHP是世界上最好的語言 that's all !

相關文章
相關標籤/搜索