socket函數只是PHP擴展的一部分,編譯PHP時必須在配置中添加
--enable-sockets
配置項來啓用。php
若是自帶的PHP沒有編譯scokets擴展,能夠下載相同版本的源碼,進入ext/sockets
使用phpize
編譯安裝。html
socket服務端/客戶端流程:
web
圖中所示流程在任何編程語言裏都是通用的。編程
接下來咱們寫一個簡單的單進程TCP服務器:數組
socket_tcp_server.php緩存
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ //參數domain: AF_INET,AF_INET6,AF_UNIX //參數type: SOCK_STREAM,SOCK_DGRAM //參數protocol: SOL_TCP,SOL_UDP $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if(!$socket) die("create server fail:".socket_strerror(socket_last_error())."\n"); //綁定 $ret = socket_bind($socket, "0.0.0.0", 9201); if(!$ret) die("bind server fail:".socket_strerror(socket_last_error())."\n"); //監聽 $ret = socket_listen($socket, 2); if(!$ret) die("listen server fail:".socket_strerror(socket_last_error())."\n"); echo "waiting client...\n"; while(1){ //阻塞等待客戶端鏈接 $conn = socket_accept($socket); if(!$conn){ echo "accept server fail:".socket_strerror(socket_last_error())."\n"; break; } echo "client connect succ.\n"; parseRecv($conn); } /** * 解析客戶端消息 * 協議:換行符(\n) */ function parseRecv($conn) { //循環讀取消息 $recv = ''; //實際接收到的消息 while(1){ $buffer = socket_read($conn, 100); //每次讀取100byte if($buffer === false || $buffer === ''){ echo "client closed\n"; socket_close($conn); //關閉本次鏈接 break; } //解析單次消息,協議:換行符 $pos = strpos($buffer, "\n"); if($pos === false){ //消息未讀取完畢,繼續讀取 $recv .= $buffer; }else{ //消息讀取完畢 $recv .= trim(substr($buffer, 0, $pos+1)); //去除換行符及空格 //客戶端主動端口鏈接 if($recv == 'quit'){ echo "client closed\n"; socket_close($conn); //關閉本次鏈接 break; } echo "recv: $recv \n"; socket_write($conn, "$recv \n"); //發送消息 $recv = ''; //清空消息,準備下一次接收 } } } socket_close($socket);
說明:例子裏咱們先建立了一個TCP server,而後循環等待客戶端鏈接。收到客戶端鏈接後,循環解析來自客戶端的消息。服務器
例子裏使用\n
做爲消息結束符,若是一次沒有接收到完整消息,就循環讀取,直到遇到結束符;讀取完一條完整消息後,向客戶端發送收到的消息,而後清空消息,準備下一次接收。websocket
咱們在命令行裏運行服務端:網絡
$ php socket_tcp_server.php waiting client...
新開終端使用telnet鏈接:數據結構
$ telnet 127.0.0.1 9201 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. hello Server!
咱們發送了一條消息,服務端這邊會收到:
client connect succ. recv: hello Server!
接下來,咱們使用socket寫一個本身的tcp客戶端。
下面的例子很簡單,建立客戶端,鏈接服務端,發送消息,讀取完後就結束了。
socket_tcp_client.php
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ //建立鏈接 $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if(!$socket) die("create server fail:".socket_strerror(socket_last_error())."\n"); //鏈接server $ret = socket_connect($socket, "127.0.0.1", 9201); if(!$ret) die("client connect fail:".socket_strerror(socket_last_error())."\n"); //發送消息 socket_write($socket, "hello, I'm client!\n"); //讀取消息 $buffer = socket_read($socket, 1024); echo "from server: $buffer\n"; //關閉鏈接 socket_close($socket);
咱們先在原來的telnet終端頁面輸入quit
退出鏈接,由於此時咱們的服務端還只能接受一個客戶端鏈接。而後運行本身寫的客戶端:
$ php socket_tcp_client.php from server: hello, I'm client!
上面的例子裏,咱們的tcp服務端僅能接受一個客戶端鏈接。怎麼能作到支持多個客戶端鏈接呢?經常使用的有:
本節裏咱們使用第三種方法,即I/O複用。技術實現層面則是使用PHP提供的socket_select系統調用來實現。
I/O複用使得程序能同時監聽多個文件描述符。實現I/O複用的系統調用主要的有select、poll、epoll。
接下來看實例:
socket_select.php
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if(!$socket) die("create server fail:".socket_strerror(socket_last_error())."\n"); //綁定 $ret = socket_bind($socket, "0.0.0.0", 9201); if(!$ret) die("bind server fail:".socket_strerror(socket_last_error())."\n"); //監聽 $ret = socket_listen($socket, 2); if(!$ret) die("listen server fail:".socket_strerror(socket_last_error())."\n"); echo "waiting client...\n"; $clients = [$socket]; $recvs = []; while(1){ $read = $clients; //拷貝一份,socket_select會修改$read $ret = @socket_select($read, $write = NULL, $except = NULL,0); if($ret === false){ break; } foreach ($read as $k=>$client) { //新鏈接 if($client === $socket){ //阻塞等待客戶端鏈接 $conn = socket_accept($socket); if(!$conn){ echo "accept server fail:".socket_strerror(socket_last_error())."\n"; break; } $clients[] = $conn; echo "client connect succ. fd: ".$conn."\n"; //獲取客戶端IP地址 socket_getpeername($conn, $addr, $port); echo "client addr: $addr:$port\n"; //獲取服務端IP地址 socket_getsockname($conn, $addr, $port); echo "server addr: $addr:$port\n"; // print_r($clients); echo "total: ".(count($clients)-1)." client\n"; }else{ //注意:後續使用$client而不是$conn if (!isset($recvs[$k]) ) $recvs[$k] = ''; //兼容可能沒有值的狀況 $buffer = socket_read($client, 100); //每次讀取100byte if($buffer === false || $buffer === ''){ echo "client closed\n"; unset($clients[array_search($client, $clients)]); //unset socket_close($client); //關閉本次鏈接 break; } //解析單次消息,協議:換行符 $pos = strpos($buffer, "\n"); if($pos === false){ //消息未讀取完畢,繼續讀取 $recvs[$k] .= $buffer; }else{ //消息讀取完畢 $recvs[$k] .= trim(substr($buffer, 0, $pos+1)); //去除換行符及空格 //客戶端主動端口鏈接 if($recvs[$k] == 'quit'){ echo "client closed\n"; unset($clients[array_search($client, $clients)]); //unset socket_close($client); //關閉本次鏈接 break; } echo "recv:".$recvs[$k]."\n"; socket_write($client, $recvs[$k]."\n"); //發送消息 $recvs[$k] = ''; } } } } socket_close($socket);
咱們先使用Crtl+C
關閉上一次運行的TCP server,而後運行新寫的server:
php socket_select.php waiting client...
新開終端telnet客戶端:
telnet 127.0.0.1 9201 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. hello world hello world
再打開終端新開一個telnet客戶端,咱們來看服務端的輸出:
client connect succ. fd: Resource id #5 client addr: 127.0.0.1:60065 server addr: 127.0.0.1:9201 total: 1 client recv:hello server! client connect succ. fd: Resource id #6 client addr: 127.0.0.1:60069 server addr: 127.0.0.1:9201 total: 2 client recv:hello world
此時咱們的服務端就不受客戶端鏈接數限制了。
注意點:
一、使用了socket_select後,解析消息的地方不能再是死循環,不然形成阻塞。
select 函數監視的文件描述符分爲3類,分別是 writefds, readfds, exceptfds,調用以後select函數就會阻塞,直到有文件描述符就緒(有數據可讀,可寫或者except),或者超時(timeout指定等待時間,若是當即返回設爲null便可),函數返回;當select函數返回以後,能夠經過遍歷 fdset來找到就緒的描述符。
二、socket系統調用最大支持1024個客戶端鏈接,若是須要更大的客戶端連連,則須要使用poll、epoll等技術。本文不作講解。
該函數用來設置socket選項,好比設置端口複用。函數原型:
bool socket_set_option ( resource $socket , int $level , int $optname , mixed $optval )
示例:
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1); //複用端口
該小節不是本文重點,該函數你們瞭解便可,須要設置的時候能知道怎麼調用。順便提一下,端口複用技術是用來解決"驚羣"問題的,你們感興趣能夠看看博文:Linux網絡編程「驚羣」問題總結 -
https://www.cnblogs.com/Anker/p/7071849.html 。
這些PHP官方手冊裏都有,貼出來供你們快速查閱。
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() 寫數據到分散/聚合數組
其中socket裏的write
read
、writev
readv
、recv
`send
、recvfrom
sendto
、recvmsg
sendmsg
五組 I/O 函數能夠參考:https://blog.csdn.net/yangbingzhou/article/details/45221649
stream_socket系列函數至關因而socket函數的進一步封裝。使用該系類函數能簡化咱們的編碼。
stream_socket_server
和stream_socket_accept
返回的句柄能夠由fgets()
, fgetss()
, fwrite()
, fclose()
以及feof()
函數調用。
咱們先看一下函數原型。
stream_socket_server:
resource stream_socket_server ( string $local_socket [, int &$errno [, string &$errstr [, int $flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN [, resource $context ]]]] )
若是是udp服務,flags指定爲STREAM_SERVER_BIND
。 另外,$context
由stream_context_create
建立,例如:
$context_option['socket']['so_reuseport'] = 1;//端口複用 $context = stream_context_create($context_option);
stream_socket_accept:
resource stream_socket_accept ( resource $server_socket [, float $timeout = ini_get("default_socket_timeout") [, string &$peername ]] )
接下來咱們使用stream_socket_
系列函數寫一個tcp server。
示例:
stream_socket_server.php
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $socket = stream_socket_server ("tcp://0.0.0.0:9201", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); if (false === $socket ) { echo "$errstr($errno)\n"; exit(); } while(1){ echo "waiting client...\n"; $conn = stream_socket_accept($socket, -1); if (false === $socket ) { exit("accept error\n"); } echo "new Client! fd:".intval($conn)."\n"; while(1){ $buffer = fread($conn, 1024); //非正常關閉 if(false === $buffer){ echo "fread fail\n"; break; } $msg = trim($buffer, "\n\r"); //強制關閉 if($msg == "quit"){ echo "client close\n"; fclose($conn); break; } echo "recv: $msg\n"; fwrite($conn, "recv: $msg\n"); } } fclose($socket);
代碼相比使用純socket
函數少了不少。
運行:
$ php stream_socket_server.php waiting client... new Client! fd:6 recv: hello
客戶端使用telnet:
$ telnet 127.0.0.1 9201 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. hello recv: hello
udp服務端不須要listen操做。
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $socket = stream_socket_server ("udp://0.0.0.0:9201", $errno, $errstr, STREAM_SERVER_BIND); if (false === $socket ) { echo "$errstr($errno)\n"; exit(); } while(1){ // $buffer = fread($socket, 1024); $buffer = stream_socket_recvfrom($socket, 1024, 0, $addr); echo $addr; //非正常關閉 if(false === $buffer){ echo "fread fail\n"; break; } $msg = trim($buffer, "\n\r"); //強制關閉 if($msg == "quit"){ echo "client close\n"; fclose($socket); break; } echo "recv: $msg\n"; // fwrite($socket, "recv: $msg\n"); stream_socket_sendto($socket, "recv: $msg\n", 0, $addr); }
運行:
$ php stream_socket_server_udp.php 127.0.0.1:43172recv: hello
客戶端使用 netcat:
netcat -u 127.0.0.1 9201 hello recv: hello quit
若是沒有netcat須要安裝:
sudo apt-get install netcat
上面咱們都是用的telnet
和netcat
來鏈接服務端,接下來咱們使用stream_socket_
系列函數編寫tcp/udp客戶端。
stream_socket系列函數寫client很是簡單:
<?php $client = stream_socket_client("tcp://127.0.0.1:9201", $errno, $erstr); if(!$client) die("err"); fwrite($client, "a"); while(1){ $rec = fread($client, 1024); echo $rec."\n"; }
udp客戶端僅須要修改tcp爲udp。
stream
系列函數使用stream_select
實現I/O複用,本質都是select系統調用。
接下來咱們寫兩個示例,第一個示例和上面使用socket_select
實現的相似,第二個則是監聽了客戶端讀寫事件,從而實現了相似telnet的功能,相信你們會感興趣的。
使用stream_select能夠實現IO複用,使得單進程程序也能支持同時處理多個客戶端鏈接。示例:
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $socket = stream_socket_server ("tcp://0.0.0.0:9201", $errno, $errstr, STREAM_SERVER_BIND | STREAM_SERVER_LISTEN); if (false === $socket ) { echo "$errstr($errno)\n"; exit(); } $clients = [$socket]; echo "waiting client...\n"; while(1){ $read = $clients; $ret = stream_select($read, $w, $e, 0); if(false === $ret){ break; } foreach($read as $client){ if($client == $socket){ //新客戶端 $conn = stream_socket_accept($socket, -1); if (false === $socket ) { exit("accept error\n"); } echo "new Client! fd:".intval($conn)."\n"; $clients[] = $conn; }else{ $buffer = fread($client, 1024);//注意,使用$client而不是$conn //非正常關閉 if(false === $buffer){ echo "fread fail\n"; $key = array_search($client, $clients); unset($clients[$key]); break; } $msg = trim($buffer, "\n\r"); //強制關閉 if($msg == "quit"){ echo "client close\n"; $key = array_search($client, $clients); unset($clients[$key]); fclose($client); break; } echo "recv: $msg\n"; fwrite($conn, "recv: $msg\n"); } } } fclose($socket);
運行服務端並隨後運行telnet客戶端:
$ php stream_select.php waiting client... new Client! fd:6 recv: ww new Client! fd:7 recv: kkk
能夠同時支持多個客戶端。從例子能夠看出來,stream_select
和socket_select
用法相同。
下面的例子使用stream_select實現了客戶端程序運行後,支持命令行界面手動實時輸入與服務端進程交互:
<?php /** * Created by PhpStorm. * User: 公衆號: 飛鴻影的博客(fhyblog) * Date: 2018/6/23 */ $socket = stream_socket_client("tcp://127.0.0.1:9201", $errno, $erstr); if(!$socket) die("err"); $clients = [$socket, STDIN]; fwrite(STDOUT, "ENTER MSG:"); while(1){ $read = $clients; $ret = stream_select($read, $w, $e, 0); if(false === $ret){ exit("stream_select err\n"); } foreach($read as $client){ if($client == $socket){ $msg = stream_socket_recvfrom($socket, 1024); echo "\nRecv: {$msg}\n"; fwrite(STDOUT, "ENTER MSG:"); }elseif($client == STDIN){ $msg = trim(fgets(STDIN)); if($msg == 'quit'){ //必須trim此處纔會相等 exit("quit\n"); } fwrite($socket, $msg); fwrite(STDOUT, "ENTER MSG:"); } } }
例子裏,咱們把$socket
和STDIN
使用stream_select監聽文件描述符的變化狀況,當有文件描述符就緒,函數會返回,從而執行咱們邏輯代碼。
先運行tcp服務端程序stream_select.php,而後運行該客戶端程序:
$ php tcp_client_select.php ENTER MSG:hello! ENTER MSG: Recv: recv: hello! ENTER MSG:
程序一直會等待咱們的輸入,除非輸入quit退出。
stream_socket_server() - 建立server stream_socket_accept() - 接受由 stream_socket_server建立的socket鏈接 stream_socket_get_name() - 獲取本地或者遠程的套接字名稱 stream_set_blocking() - 爲資源流設置阻塞或者阻塞模式 stream_set_timeout() - 爲資源流設置超時 stream_socket_client() - 建立client stream_select() - select系統調用,實現IO多路選擇 stream_socket_shutdown() - 這個函數容許關閉讀、寫、或者指定的socket stream_socket_recvfrom() - stream_socket_sendto() -
本文主要和你們講解了 PHP Socket 編程相關知識。經過學習本文,你們學到了以下內容:
也給你們留一個問題:
如何基於PHP多進程Master-Worker模型實現支持I/O複用的TCP server?
提示:我公衆號(fhyblog)裏有PHP多進程系列筆記相關文章,多進程不熟悉的同窗能夠學習一下。
(全文完)
一、深刻淺出講解:php的socket通訊 - 灑灑 - 博客園 http://www.cnblogs.com/thinksasa/archive/2013/02/26/2934206.html 二、write read;writev readv;recv send;recvfrom sendto;recvmsg sendmsg五組I/O函數彙總 - CSDN博客 https://blog.csdn.net/yangbingzhou/article/details/45221649 三、socket編程中的read、write與recv、send的區別 - CSDN博客 https://blog.csdn.net/xhu_eternalcc/article/details/18256561 四、php select socket - yuanlp_code - 博客園 https://www.cnblogs.com/yuanlipu/p/6431834.html 五、socket服務的模型以及實現(3)–單進程IO複用select | 你好,歡迎來到老張的博客,張素傑 http://www.xtgxiso.com/socket%e6%9c%8d%e5%8a%a1%e7%9a%84%e6%a8%a1%e5%9e%8b%e4%bb%a5%e5%8f%8a%e5%ae%9e%e7%8e%b03-%e5%8d%95%e8%bf%9b%e7%a8%8bio%e5%a4%8d%e7%94%a8select/ 六、PHP Socket實現websocket(四)Select函數 - 海上小綿羊 - 博客園 https://www.cnblogs.com/yangxunwu1992/p/5564454.html .