この記事で共有する内容は、PHP でのシステム プログラミングのネットワーク ソケットと IO の多重化に関するものです。必要な方は参考にしてください。
長い間、PHP はほとんど使用されていませんでした。結局のところ、プログラミングはスクリプト言語であり、効率が大きなネックになります。ただし、PHP がソケット プログラミングに使用できないとは言えませんし、PHP のソケット プログラミングのパフォーマンスがそれほど低いとも言えません。たとえば、有名な PHP ソケット フレームワーク Workerman は純粋な PHP で開発されており、優れたパフォーマンスを誇るため、環境によっては PHP ソケット プログラミングのスキルを発揮できる場合もあります。
PHP は、C 言語ソケット ライブラリにあるものと同様の一連のメソッドを提供しており、これを呼び出すことができます:
socket_accept — Accepts a connection on a socket socket_bind — 给套接字绑定名字 socket_clear_error — 清除套接字或者最后的错误代码上的错误 socket_close — 关闭套接字资源 socket_cmsg_space — Calculate message buffer size socket_connect — 开启一个套接字连接 socket_create_listen — Opens a socket on port to accept connections socket_create_pair — Creates a pair of indistinguishable sockets and stores them in an array socket_create — 创建一个套接字(通讯节点) socket_get_option — Gets socket options for the socket socket_getopt — 别名 socket_get_option 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 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 socket_import_stream — Import a stream socket_last_error — Returns the last error on the socket socket_listen — Listens for a connection on a socket socket_read — Reads a maximum of length bytes from a socket socket_recv — 从已连接的socket接收数据 socket_recvfrom — Receives data from a socket whether or not it is connection-oriented socket_recvmsg — Read a message socket_select — Runs the select() system call on the given arrays of sockets with a specified timeout socket_send — Sends data to a connected socket socket_sendmsg — Send a message socket_sendto — Sends a message to a socket, whether it is connected or not socket_set_block — Sets blocking mode on a socket resource socket_set_nonblock — Sets nonblocking mode for file descriptor fd socket_set_option — Sets socket options for the socket socket_setopt — 别名 socket_set_option socket_shutdown — Shuts down a socket for receiving, sending, or both socket_strerror — Return a string describing a socket error socket_write — Write to a socket
詳細については、ソケットに関する PHP 公式マニュアルを確認してください: http ://php .net/manual/zh/book.sockets.php
単純な TCP サーバーの例 phptcpserver.php:
<?php $servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // 创建一个socket if (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
その後、サーバーはそこでブロックされ、クライアントが接続するのを待ちます:
[root@localhost ~]# telnet 127.0.0.1 8888 Trying 127.0.0.1... Connected to 127.0.0.1. Escape character is '^]'. ajdjajksdjkaasda AJDJAJKSDJKAASDA 小明哈哈哈哈笑 小明哈哈哈哈笑 小明efsfsdfsdf了哈哈哈 小明EFSFSDFSDF了哈哈哈
サーバーの出力:
[root@localhost php]# php phptcpserver.php client connect server: ip = 127.0.0.1, port = 50398 read from client:ajdjajksdjkaasda read from client:小明哈哈哈哈笑 read from client:小明efsfsdfsdf了哈哈哈
しかし、実際には、この TCP サーバーは一度に 1 つのクライアントの接続とデータ送信しか処理できないためです。接続すると、プロセスはクライアントの読み取りと書き込みを担当します。クライアントがデータを送信しない場合、TCP サーバーは読み取りブロック状態になり、他のクライアントからの接続要求を処理できなくなります。
この問題を解決する 1 つの方法は、クライアントが接続するたびに、サーバーはクライアントとのデータ送信を担当する子プロセスを開き、親プロセスは引き続きそのプロセスを監視します。しかし、このマルチプロセス メカニズムでは、明らかに高い同時実行性をサポートできません。
もう 1 つの解決策は、IO 多重化メカニズムを使用し、PHP が提供するsocket_select メソッドを使用することです。これは、複数のソケットのステータスが、書き込み不可から書き込み可能に、書き込み不可から読み取り不可になど変化した場合に監視できます。読み取り可能である場合、このメソッドは返されるため、ソケットの処理、クライアント接続の処理、読み取りおよび書き込み操作などが可能になります。 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 --- 指定されたソケット配列のセットに対してselect()システムコールを実行します。特定のタイムアウト。
socket_select() は、いくつかのソケット配列をパラメーターとして受け入れ、状態の変更をリッスンします
これらの BSD スコケットは、これらのソケット リソース配列が実際にはファイル記述子のコレクションであることを認識する機能に基づいています。
3 つの異なるソケット リソース配列が同時に監視されます。
これら 3 つのリソース配列は必須ではありません。関数が返された後、これらの配列の値が参照によって渡されることを忘れないでください。変化すること。
socket_select() は、これら 3 つの配列でステータスが変更されたソケットの合計数を正常に返します。タイムアウトが設定されており、タイムアウト内にステータスの変更がない場合、この関数は 0 を返します。エラーが発生した場合は、この関数は 0 を返します。 FALSE を返します。socket_last_error() を使用してエラー コードを取得します。 T phptcpserver.php コードの前に、socket_select() を使用して最適化します:
<?php $servsock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // 创建一个socket if (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 = 50404 read from client # 127.0.0.1:50404 # hello world client connect server: ip = 127.0.0.1, port = 50406 read from client # 127.0.0.1:50406 # hello PHP read 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 close client 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 ~]#
このような同時実行性の高い HTTP サーバーが開発されました。ストレス テスト ソフトウェアを使用して同時実行機能をテストします。
5,000 QPS を超えることに少し興奮していますか?^^。
PHP は世界で最高の言語です、それだけです!
以上がPHPはシステムプログラミングのネットワークソケットとIOの多重化を実現しますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。