本篇文章給大家分享的內容是關於PHP實現系統編程之網路Socket及IO多工 ,有著一定的參考價值,有需要的朋友可以參考一下
一直以來,PHP很少用於socket編程,畢竟是一門指令碼語言,效率會成為很大的瓶頸,但是不能說PHP就無法用於socket編程,也不能說PHP的socket編程效能就有多麼的低,例如知名的一款PHP socket架構 workerman 就是用純PHP開發,並且號稱擁有優秀的效能,所以在某些環境下,PHP socket編程或許也可一展身手。
PHP提供了一系列類似C語言socket庫中的方法供我們調用:
socket_accept — Accepts a connection on a socketsocket_bind — 給通訊端綁定名字socket_clear_error — 清除通訊端或者最後的錯誤碼上的錯誤socket_close — 關閉通訊端資源socket_cmsg_space — Calculate message buffer sizesocket_connect — 開啟一個通訊端串連socket_create_listen — Opens a socket on port to accept connectionssocket_create_pair — Creates a pair of indistinguishable sockets and stores them in an arraysocket_create — 建立一個通訊端(通訊節點)socket_get_option — Gets socket options for the socketsocket_getopt — 別名 socket_get_optionsocket_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 typesocket_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 typesocket_import_stream — Import a streamsocket_last_error — Returns the last error on the socketsocket_listen — Listens for a connection on a socketsocket_read — Reads a maximum of length bytes from a socketsocket_recv — 從已串連的socket接收資料socket_recvfrom — Receives data from a socket whether or not it is connection-orientedsocket_recvmsg — Read a messagesocket_select — Runs the select() system call on the given arrays of sockets with a specified timeoutsocket_send — Sends data to a connected socketsocket_sendmsg — Send a messagesocket_sendto — Sends a message to a socket, whether it is connected or notsocket_set_block — Sets blocking mode on a socket resourcesocket_set_nonblock — Sets nonblocking mode for file descriptor fdsocket_set_option — Sets socket options for the socketsocket_setopt — 別名 socket_set_optionsocket_shutdown — Shuts down a socket for receiving, sending, or bothsocket_strerror — Return a string describing a socket errorsocket_write — Write to a socket
更多細節請查看PHP關於socket的官方手冊:http://php.net/manual/zh/book.sockets.php
一個簡單的TCP伺服器樣本 phptcpserver.php :
<?php$servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // 建立一個socketif (FALSE === $servsock){ $errcode = socket_last_error(); fwrite(STDERR, "socket create fail: " . socket_strerror($errcode)); exit(-1);}if (!socket_bind($servsock, '127.0.0.1', 8888)) // 綁定ip地址及連接埠{ $errcode = socket_last_error(); fwrite(STDERR, "socket bind fail: " . socket_strerror($errcode)); exit(-1);}if (!socket_listen($servsock, 128)) // 允許多少個用戶端來排隊串連{ $errcode = socket_last_error(); fwrite(STDERR, "socket listen fail: " . socket_strerror($errcode)); exit(-1);}while (1){ $connsock = socket_accept($servsock); //響應用戶端串連 if ($connsock) { socket_getpeername($connsock, $addr, $port); //擷取串連過來的用戶端ip地址和連接埠 echo "client connect server: ip = $addr, port = $port" . PHP_EOL; while (1) { $data = socket_read($connsock, 1024); //從用戶端讀取資料 if ($data === '') { //用戶端關閉 socket_close($connsock); echo "client close" . PHP_EOL; break; } else { echo 'read from client:' . $data; $data = strtoupper($data); //小寫轉大寫 socket_write($connsock, $data); //回寫給用戶端 } } }}socket_close($servsock);
啟動這個伺服器:
[root@localhost php]# php phptcpserver.php
之後這個伺服器就一直阻塞在那裡,等待用戶端串連,我們可以用telnet命令來串連這個伺服器:
[root@localhost ~]# telnet 127.0.0.1 8888Trying 127.0.0.1...Connected to 127.0.0.1.Escape character is '^]'.ajdjajksdjkaasdaAJDJAJKSDJKAASDA小明哈哈哈哈笑小明哈哈哈哈笑小明efsfsdfsdf了哈哈哈小明EFSFSDFSDF了哈哈哈
伺服器端輸出:
[root@localhost php]# php phptcpserver.php client connect server: ip = 127.0.0.1, port = 50398read from client:ajdjajksdjkaasdaread from client:小明哈哈哈哈笑read from client:小明efsfsdfsdf了哈哈哈
但其實這個TCP伺服器是有問題的,它一次只能處理一個用戶端的串連和資料轉送,這是因為一個用戶端串連過來後,進程就去負責讀寫用戶端資料,當用戶端沒有傳輸資料時,tcp伺服器處於阻塞讀狀態,無法再去處理其他用戶端的串連請求了。
解決這個問題的一種辦法就是採用多進程伺服器,每當一個用戶端串連過來,伺服器開一個子進程專門負責和該用戶端的資料轉送,而父進程仍然監聽用戶端的串連,但是起進程的代價是昂貴的,這種多進程的機制顯然支撐不了高並發。
另一個解決辦法是使用IO多工機制,使用php為我們提供的socket_select方法,它可以監聽多個socket,如果其中某個socket狀態發生了改變,比如從不可寫變為可寫,從不可讀變為可讀,這個方法就會返回,從而我們就可以去處理這個socket,處理用戶端的串連,讀寫操作等等。來看php文檔中對該socket_select的介紹
socket_select — Runs the select() system call on the given arrays of sockets with a specified timeout說明int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] )socket_select() accepts arrays of sockets and waits for them to change status. Those coming with BSD sockets background will recognize that those socket resource arrays are in fact the so-called file descriptor sets. Three independent arrays of socket resources are watched.You do not need to pass every array to socket_select(). You can leave it out and use an empty array or NULL instead. Also do not forget that those arrays are passed by reference and will be modified after socket_select() returns.傳回值On success socket_select() returns the number of socket resources contained in the modified arrays, which may be zero if the timeout expires before anything interesting happens. 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$servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // 建立一個socketif (FALSE === $servsock){ $errcode = socket_last_error(); fwrite(STDERR, "socket create fail: " . socket_strerror($errcode)); exit(-1);}if (!socket_bind($servsock, '127.0.0.1', 8888)) // 綁定ip地址及連接埠{ $errcode = socket_last_error(); fwrite(STDERR, "socket bind fail: " . socket_strerror($errcode)); exit(-1);}if (!socket_listen($servsock, 128)) // 允許多少個用戶端來排隊串連{ $errcode = socket_last_error(); fwrite(STDERR, "socket listen fail: " . socket_strerror($errcode)); exit(-1);}/* 要監聽的三個sockets數組 */$read_socks = array();$write_socks = array();$except_socks = NULL; // 注意 php 不支援直接將NULL作為引用傳參,所以這裡定義一個變數$read_socks[] = $servsock;while (1){ /* 這兩個數組會被改變,所以用兩個臨時變數 */ $tmp_reads = $read_socks; $tmp_writes = $write_socks; // int socket_select ( array &$read , array &$write , array &$except , int $tv_sec [, int $tv_usec = 0 ] ) $count = socket_select($tmp_reads, $tmp_writes, $except_socks, NULL); // timeout 傳 NULL 會一直阻塞直到有結果返回 foreach ($tmp_reads as $read) { if ($read == $servsock) { /* 有新的用戶端串連請求 */ $connsock = socket_accept($servsock); //響應用戶端串連, 此時不會造成阻塞 if ($connsock) { socket_getpeername($connsock, $addr, $port); //擷取遠程用戶端ip地址和連接埠 echo "client connect server: ip = $addr, port = $port" . PHP_EOL; // 把新的串連sokcet加入監聽 $read_socks[] = $connsock; $write_socks[] = $connsock; } } else { /* 用戶端傳輸資料 */ $data = socket_read($read, 1024); //從用戶端讀取資料, 此時一定會讀到數組而不會產生阻塞 if ($data === '') { //移除對該 socket 監聽 foreach ($read_socks as $key => $val) { if ($val == $read) unset($read_socks[$key]); } foreach ($write_socks as $key => $val) { if ($val == $read) unset($write_socks[$key]); } socket_close($read); echo "client close" . PHP_EOL; } else { socket_getpeername($read, $addr, $port); //擷取遠程用戶端ip地址和連接埠 echo "read from client # $addr:$port # " . $data; $data = strtoupper($data); //小寫轉大寫 if (in_array($read, $tmp_writes)) { //如果該用戶端可寫 把資料回寫給用戶端 socket_write($read, $data); } } } }}socket_close($servsock);
現在,這個TCP伺服器就可以支援多個用戶端同時串連了,測試下:
伺服器端:
[root@localhost php]# php phptcpserver.php client connect server: ip = 127.0.0.1, port = 50404read from client # 127.0.0.1:50404 # hello worldclient connect server: ip = 127.0.0.1, port = 50406read from client # 127.0.0.1:50406 # hello PHPread from client # 127.0.0.1:50404 # 少小離家老大回read from client # 127.0.0.1:50404 # 鄉音無改鬢毛衰read from client # 127.0.0.1:50406 # 老當益壯,read from client # 127.0.0.1:50406 # 寧移白首之心client closeclient connect server: ip = 127.0.0.1, port = 50408
稍微修改上面的伺服器返回,返回一個HTTP回應標頭和一個簡單的HTTP響應體,這樣就搖身一變成了一個最簡單的HTTP伺服器:
.... socket_getpeername($read, $addr, $port); //擷取遠程用戶端ip地址和連接埠 echo "read from client # $addr:$port # " . $data; $response = "HTTP/1.1 200 OK\r\n"; $response .= "Server: phphttpserver\r\n"; $response .= "Content-Type: text/html\r\n"; $response .= "Content-Length: 3\r\n\r\n"; $response .= "ok\n"; if (in_array($read, $tmp_writes)) { //如果該用戶端可寫 把資料回寫給用戶端 socket_write($read, $response); socket_close($read); // 主動關閉用戶端串連 //移除對該 socket 監聽 foreach ($read_socks as $key => $val) { if ($val == $read) unset($read_socks[$key]); } foreach ($write_socks as $key => $val) { if ($val == $read) unset($write_socks[$key]); } }.....
重新啟動該伺服器,用curl類比請求該http伺服器:
[root@localhost ~]# curl '127.0.0.1:8888'ok[root@localhost ~]# curl '127.0.0.1:8888'ok[root@localhost ~]# curl '127.0.0.1:8888'ok[root@localhost ~]# curl '127.0.0.1:8888'ok[root@localhost ~]# curl '127.0.0.1:8888'ok[root@localhost ~]#
伺服器端輸出:
client connect server: ip = 127.0.0.1, port = 50450read from client # 127.0.0.1:50450 # GET / HTTP/1.1User-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.2Host: 127.0.0.1:8888Accept: */*client closeclient connect server: ip = 127.0.0.1, port = 50452read from client # 127.0.0.1:50452 # GET / HTTP/1.1User-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.2Host: 127.0.0.1:8888Accept: */*client closeclient connect server: ip = 127.0.0.1, port = 50454read from client # 127.0.0.1:50454 # GET / HTTP/1.1User-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.2Host: 127.0.0.1:8888Accept: */*client closeclient connect server: ip = 127.0.0.1, port = 50456read from client # 127.0.0.1:50456 # GET / HTTP/1.1User-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.2Host: 127.0.0.1:8888Accept: */*client close
這樣一個高並發的HTTP伺服器就開發好了,用壓測軟體測試下並發能力:
看到高達5000多的QPS,有沒有小激動呢^^。
PHP是世界上最好的語言 that's all !