ホームページ >バックエンド開発 >PHPチュートリアル >PHP SOCKETプログラミングについて話しましょう(詳細な説明付き)

PHP SOCKETプログラミングについて話しましょう(詳細な説明付き)

慕斯
慕斯転載
2021-06-03 09:46:055307ブラウズ

在PHP中有太多我们需要学习和了解的东西,今天这篇文章就让我们一起聊聊PHP SOCKET编程(附详解)!我相信,当你们看完这篇文章后一定会收获很多东西,话不多说,一起看看吧!

PHP SOCKETプログラミングについて話しましょう(詳細な説明付き)

1. 预备知识

       一直以来很少看到有多少人使用php的socket模块来做一些事情,大概大家都把它定位在脚本语言的范畴内吧,但是其实php的socket模块可以做很多事情,包括做ftplist,http post提交,smtp提交,组包并进行特殊报文的交互(如smpp协议),whois查询。这些都是比较常见的查询。

特别是php的socket扩展库可以做的事情简直不会比c差多少。
php的socket连接函数
1、集成于内核的socket
这个系列的函数仅仅只能做主动连接无法实现端口监听相关的功能。而且在4.3.0之前所有socket连接只能工作在阻塞模式下。
此系列函数包括
fsockopen,pfsockopen
这两个函数的具体信息可以查询php.net的用户手册
他们均会返回一个资源编号对于这个资源可以使用几乎所有对文件操作的函数对其进行操作如fgets(),fwrite(), fclose()等单注意的是所有函数遵循这些函数面对网络信息流时的规律,例如:
fread() 从文件指针 handle 读取最多 length 个字节。 该函数在读取完 length 个字节数,或到达 EOF 的时候,或(对于网络流)当一个包可用时就会停止读取文件,视乎先碰到哪种情况。 
可以看出对于网络流就必须注意取到的是一个完整的包就停止。
2、php扩展模块带有的socket功能。
php4.x 以后有这么一个模块extension=php_sockets.dll,Linux上是一个extension=php_sockets.so。
当打开这个此模块以后就意味着php拥有了强大的socket功能,包括listen端口,阻塞及非阻塞模式的切换,multi-client 交互式处理等

2. 使用PHP socket扩展

服务器端代码:

<?php
/**
 * File name server.php
 * 服务器端代码
 * 
 * @author guisu.huang
 * @since 2012-04-11
 * 
 */

//确保在连接客户端时不会超时
set_time_limit(0);
//设置IP和端口号
$address = "127.0.0.1";
$port = 2046; //调试的时候,可以多换端口来测试程序!
/**
 * 创建一个SOCKET 
 * AF_INET=是ipv4 如果用ipv6,则参数为 AF_INET6
 * SOCK_STREAM为socket的tcp类型,如果是UDP则使用SOCK_DGRAM
*/
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP) or die("socket_create() 
失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//阻塞模式
socket_set_block($sock) or die("socket_set_block() 
失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//绑定到socket端口
$result = socket_bind($sock, $address, $port) or die("socket_bind() 
失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
//开始监听
$result = socket_listen($sock, 4) or die("socket_listen() 
失败的原因是:" . socket_strerror(socket_last_error()) . "/n");
echo "OK\nBinding the socket on $address:$port ... ";
echo "OK\nNow ready to accept connections.\nListening on the socket ... \n";
do { // never stop the daemon
	//它接收连接请求并调用一个子连接Socket来处理客户端和服务器间的信息
$msgsock = socket_accept($sock) or  die("socket_accept() failed: reason: " . socket_strerror(socket_last_error()) . "/n");
	
	//读取客户端数据
	echo "Read client data \n";
	//socket_read函数会一直读取客户端数据,直到遇见\n,\t或者\0字符.PHP脚本把这写字符看做是输入的结束符.
	$buf = socket_read($msgsock, 8192);
	echo "Received msg: $buf   \n";
	
	//数据传送 向客户端写入返回结果
	$msg = "welcome \n";
socket_write($msgsock, $msg, strlen($msg)) or die("socket_write() failed: reason: " . socket_strerror(socket_last_error()) ."/n");
	//一旦输出被返回到客户端,父/子socket都应通过socket_close($msgsock)函数来终止
    socket_close($msgsock);
} while (true);
socket_close($sock);

客户端代码:

<?php
/**
 * File name:client.php
 * 客户端代码
 * 
 * @author guisu.huang
 * @since 2012-04-11
 */
set_time_limit(0);

$host = "127.0.0.1";
$port = 2046;
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP)or die("Could not create	socket\n"); // 创建一个Socket
 
$connection = socket_connect($socket, $host, $port) or die("Could not connet server\n");    //  连接
socket_write($socket, "hello socket") or die("Write failed\n"); // 数据传送 向服务器发送消息
while ($buff = socket_read($socket, 1024, PHP_NORMAL_READ)) {
    echo("Response was:" . $buff . "\n");
}
socket_close($socket);

使用cli方式启动server:

<span style="font-family:Arial,Helvetica,Georgia,sans-serif; color:#525454"><span style="line-height:24px">php server.php</span></span><span style="font-family:Arial,Helvetica,Georgia,sans-serif; color:#525454"><span style="line-height:24px"></span></span><span style="font-family:Arial,Helvetica,Georgia,sans-serif; color:#525454"><span style="line-height:24px"></span></span>

这里注意socket_read函数:

オプションの型パラメータは名前付き定数です:
PHP_BINARY_READ - システムのrecv()関数を使用します。バイナリデータの読み取りに対するセキュリティ。 (PHP > = 4.1.0 のデフォルト)
PHP_NORMAL_READ - 読み取りは \n または \r で停止します (PHP

#パラメータ PHP_NORMAL_READ の場合、サーバーの応答結果が \ n ではない場合、socket_read(): ソケットから読み取ることができません


##

#3.PHP での同時 IO プログラミング

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


1) マルチプロセス/マルチスレッド同期のブロック


##初期のサーバー側プログラムは、マルチプロセスとマルチスレッドによって同時実行性の問題を解決しました。

IO

。プロセス モデルは最も早くに登場し、プロセスの概念は Unix システムの誕生以来存在しています。 最も初期のサーバー側プログラムは通常、クライアントが接続するとプロセスが作成され、子プロセスがループに入ります。同期ブロック領域: クライアント接続と対話し、処理データを送受信します。 マルチスレッド モードは後に登場しました。スレッドはプロセスよりも軽く、メモリ スタックはスレッド間で共有されるため、スレッド間での相互作用が可能になります。さまざまなスレッドの実装は非常に簡単です。たとえば、チャット ルームのようなプログラムでは、クライアント接続が相互に対話することができ、チャット ルーム内のプレーヤーは他の人にメッセージを送信できます。マルチスレッド モードでの実装は非常に簡単で、クライアント接続はスレッド内で直接読み書きできます。マルチプロセス モードでは、データ対話を実現するためにパイプライン、メッセージ キュー、共有メモリを使用する必要があります。これを総称してプロセス間通信と呼びます (IPC

) )および複雑なテクノロジーを実現できます。

コード例:


複数のプロセス/スレッドモデルのプロセスは#

  1. ##ソケット#を作成し、サーバー ポートをバインドします (bind#) ##)、リスニング ポート (listen)、PHPstream_socket_server を 1 つの関数で使用して、上記の 3 手順を完了します。もちろん、php ソケット 拡張機能を使用して、それらを個別に実装することもできます。
  2. while ループに入り、accept でブロックします。 操作に関しては、クライアント接続が受信されるまで待ちます。この時点で、プログラムは、新しいクライアントがサーバーへの connect を開始するまでスリープ状態に入り、オペレーティング システムがプロセスを起動します。 accept関数は ソケットを返します メインプロセスはマルチプロセス モデルで
  3. fork を渡します (#php: pcntl_fork ) 子プロセスを作成するには、マルチスレッド モデルで pthread_create を使用します (php: new Thread )子スレッドを作成します。以下に特に明記されていない限り、 process はプロセス / スレッドを表すためにも使用されます。 #子プロセスが正常に作成されると、#while
  4. ループに入り、# でブロックされます。 ## recv(php: fread) が呼び出され、クライアントがデータを送信するのを待ちます。サーバ。データを受信した後、サーバー プログラムはそれを処理し、send(php: fwrite##) を使用します。 #) クライアントに応答を送信します。接続時間が長いサービスはクライアントとの対話を継続しますが、接続時間が短いサービスは通常、応答を受信した後に #close になります。
  5. クライアント接続が閉じられると、子プロセスが終了し、すべてのリソースが破棄されます。メインプロセスはこの子プロセスをリサイクルします。
#このモデルの最大の問題は、プロセス内でスレッドの作成と破棄が行われることです。

/# # とても高価です。大きいです。したがって、上記のモデルは非常に負荷の高いサーバー プログラムには適用できません。対応する改良バージョンではこの問題が解決されており、これは古典的な Leader-Follower モデルです。

コード例:


プログラムの後に作成されるのが特徴です。

N プロセスが開始されます。各子プロセスは Accept に入り、新しい接続が受信されるのを待ちます。クライアントがサーバーに接続すると、子プロセスの 1 つが起動され、クライアント要求の処理が開始され、新しい TCP 接続は受け入れられなくなります。この接続が閉じられると、子プロセスは解放され、Accept を再入力して、新しい接続の処理に参加します。

このモデルの利点は、追加消費することなくプロセスを完全に再利用でき、パフォーマンスが非常に優れていることです。

Apache#、PHP-FPM## など、多くの一般的なサーバー プログラムはこのモデルに基づいています。 #マルチプロセス モデルにはいくつかの欠点もあります。

  1. このモデルは、同時実行の問題を解決するためにプロセスの数に大きく依存しています。1 つのクライアント接続には 1 つのプロセスが必要です。ワーカー プロセスの数は、同時処理能力によって異なります。オペレーティング システムが作成できるプロセスの数には制限があります。
  2. 多数のプロセスを開始すると、プロセス スケジューリングの消費量が増加します。プロセスが数百ある場合、プロセス コンテキスト切り替えスケジューリングの消費量は CPU1%## 未満になる可能性があります。 #無視して構いません。数千、さらには数万のプロセスを開始すると、消費量が急増します。スケジュールの消費量は CPU の数十パーセント、または 100%## を占める可能性があります # 。
  3. #インスタント チャット プログラム (

IM##) など、マルチプロセス モデルでは解決できないシナリオもいくつかあります。 #) の場合、サーバーは同時に数万、場合によっては数十万、あるいは数百万の接続を維持する必要があります (古典的な C10K 問題) 、マルチプロセスモデルはそのニーズを満たすことができません。 マルチプロセス モデルの弱点となるシナリオがもう 1 つあります。通常、

Web 1 つのリクエストが を消費すると、サーバーは 100 プロセスを開始します。 100ms100 プロセスは 1000qps# を提供できます##、この処理能力はかなり優れています。ただし、リクエストで外部ネットワーク Http インターフェイス (QQ など) を呼び出す必要がある場合は、 , Weibo のログインには時間がかかり、1 回のリクエストには 10s かかります。このプロセスは、0.1 リクエストを 1 でしか処理できません。 100 プロセスは 10qps までしか到達できません。この処理能力は貧弱すぎます。

すべての同時実行性を 1 つのプロセスで処理できるテクノロジはありますか?IO答えは「はい」です。これは IO 多重化テクノロジです。

IO/ イベントを再利用するループ/非同期ノンブロッキング

##実際にはIO 再利用の歴史は長く、マルチプロセス、Linux が長年にわたって提供されてきました。 select システム コールは、プロセス内の 1024 接続を維持できます。その後、poll システム コールが追加され、poll がいくつかの改善を加えました。 . 1024制限の問題を解決し、任意の数の接続を維持できるようになりました。ただし、select/poll もう 1 つの問題は、接続上にイベントがあるかどうかを検出するためにループする必要があることです。問題は、サーバーに #100 百万の接続があり、特定の時点でサーバーにデータを送信する接続が 1 つだけである場合に発生します。 select/poll100 百万回ループする必要があり、そのうち # のみ##1 回がヒットし、残り 9910,0009999 時間が無効です。#CPU リソース # を無駄にしています。

Linux 2.6 までは、カーネルは新しい epoll## を提供します#System call はポーリングなしで無制限の接続を維持できるため、C10K 問題は真に解決されます。現在、さまざまな同時実行性の高い非同期 IO サーバー プログラムは epoll に基づいています。 NginxNode.js# などの実装##アーランGolangNode.js のような単一プロセス、単一スレッドのプログラムは、1# を超えて存続する可能性があります。 ##数百万##TCP 接続、すべて epoll テクノロジーのおかげです。 ##IO

従来の Reactor # を使用して非同期ノンブロッキング プログラムを再利用する##Model, Reactor 名前のとおりリアクターを意味し、それ自体はデータの送受信を処理しません。 socket ハンドルのイベント変更のみを監視できます。 ##リアクター

4 コアオペレーション:

  1. add追加ソケットListen to reactor (listen ソケット または client ソケットにすることができます) 、またはパイプ、eventfd、シグナルなども可能です。
  2. setイベント監視を変更し、監視タイプ (読み取り可能、書き込み可能など) を設定します。 リッスン ソケット の場合、新しいクライアントが接続するときに が必要であることを意味します。受け入れる#### #####。クライアント接続でデータを受信するには、recv が必要です。書き込み可能なイベントは、理解するのが少し難しくなります。 SOCKET にはバッファ領域があります。クライアントに接続したい場合は、2M# を送信します。 # データは一度に送信できません。オペレーティング システムのデフォルトの TCP キャッシュ領域には 256K しかありません。一度に送信できるのは 256K のみです。バッファがいっぱいになると、send#EAGAIN エラーを返します。現時点では、書き込み可能なイベントをリッスンする必要があります。純粋な非同期プログラミングでは、send 操作が完全に無効であることを確認するために、書き込み可能なイベントをリッスンする必要があります。 -ブロッキング。 del
  3. reactor から削除されました、いいえ、それでは聞いてくださいイベント callback
  4. は、イベント発生後の対応する処理ロジックで、通常は add にあります。 / を設定すると定式化されます。Cこの言語は関数ポインターを使用して実装されており、JS匿名関数を使用できます。 , PHP匿名関数、オブジェクト メソッド配列、文字列関数名を使用できます。


Reactor は、実際には のための単なるイベント ジェネレーターです。 socket connect/accept send などの操作を処理します。 /recvclosecallback にあります 完了しましたで ###。特定のコーディングについては、次の疑似コードを参照してください。


##Reactor

モデルは次のようにすることもできます。マルチプロセスで使用されます。複数のスレッドを組み合わせることで、非同期ノンブロッキング IO が実現されるだけでなく、複数のコアも活用されます。現在人気のある非同期サーバー プログラムはすべてこのようになっている:

    Nginx
  • : マルチプロセス Reactor
  • Nginx Lua
  • : マルチプロセスReactor Coroutine
  • Golang
  • : シングルスレッドReactor マルチスレッド コルーチン
  • Swoole
  • :マルチスレッドReactor 複数のプロセスワーカー

#

4. PHP socket内部源码

          从PHP内部源码来看,PHP提供的socket编程是在socket,bind,listen等函数外添加了一个层,让其更加简单和方便调用。但是一些业务逻辑的程序还是需要程序员自己去实现。
下面我们以socket_create的源码实现来说明PHP的内部实现。
前面我们有说到php的socket是以扩展的方式实现的。在源码的ext目录,我们找到sockets目录。这个目录存放了PHP对于socket的实现。直接搜索PHP_FUNCTION(socket_create),在sockets.c文件中找到了此函数的实现。如下所示代码:

/* {{{ proto resource socket_create(int domain, int type, int protocol) U
   Creates an endpoint for communication in the domain specified by domain, of type specified by type */
PHP_FUNCTION(socket_create)
{
        long            arg1, arg2, arg3;
        php_socket      *php_sock = (php_socket*)emalloc(sizeof(php_socket));
 
        if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lll", &arg1, &arg2, &arg3) == FAILURE) {
                efree(php_sock);
                return;
        }
 
        if (arg1 != AF_UNIX
#if HAVE_IPV6
                && arg1 != AF_INET6
#endif
                && arg1 != AF_INET) {
                php_error_docref(NULL TSRMLS_CC, E_WARNING, "invalid socket domain [%ld] 
                specified for argument 1, assuming AF_INET", arg1);
                arg1 = AF_INET;
        }
 
        if (arg2 > 10) {
                php_error_docref(NULL TSRMLS_CC, E_WARNING, "invalid socket type [%ld] specified for argument 2, assuming SOCK_STREAM", arg2);
                arg2 = SOCK_STREAM;
        }
 
        php_sock->bsd_socket = socket(arg1, arg2, arg3);
        php_sock->type = arg1;
 
        if (IS_INVALID_SOCKET(php_sock)) {
                SOCKETS_G(last_error) = errno;
                php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unable to create socket [%d]: %s", errno, php_strerror(errno TSRMLS_CC));
                efree(php_sock);
                RETURN_FALSE;
        }
 
        php_sock->error = 0;
        php_sock->blocking = 1;
        
        ZEND_REGISTER_RESOURCE(return_value, php_sock, le_socket);
}
/* }}} */

Zend API实际对c函数socket做了包装,供PHP使用。 而在c的socket编程中,我们使用如下方式初始化socket。

//初始化Socket  
    if( (socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1 ){  
         printf("create socket error: %s(errno: %d)\n",strerror(errno),errno);  
         exit(0);  
    }

5. socket函数

関数名の説明
socket_accept() ソケット接続を受け入れる
socket_bind() ソケットを IP にバインドします。アドレスとポート
socket_clear_error() はソケット エラーまたは最後のエラー コードをクリアします。
socket_close() はソケット リソースを閉じます
socket_connect() ソケット接続を開始します
#socket_create_listen() 指定されたポートでリッスンするソケットを開きます#socket_create_pair() 未区別のソケットのペアを配列に生成します
socket_create() はソケットを生成します。これはソケット データ構造を生成するのと同じです。
socket_get_option() ソケット オプションを取得します
socket_getpeername () リモートの同様のホストの IP アドレスを取得します
socket_getsockname() ローカル ソケットの 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_listen( ) 指定されたソケットのすべての接続でリッスンします
socket_read() は指定された長さのデータを読み取ります
socket_readv() は分散/集約配列からデータを読み取ります
socket_recv() ソケットからキャッシュへのデータを終了します
socket_recvfrom() 指定されたソケットからデータを受け取ります。指定されていない場合は、デフォルトで現在のソケットが使用されます
socket_recvmsg() iovec からのメッセージの受信
socket_select() 複数選択
socket_send() この関数は、接続されたソケットにデータを送信します。
socket_sendmsg() ソケットにメッセージを送信します。
socket_sendto() 指定されたアドレスのソケットにメッセージを送信します。
socket_set_block()ソケットをブロック モードに設定します
socket_set_nonblock() ソケットで非ブロック モードに設定します
socket_set_option() ソケット オプションを設定します
socket_shutdown() この関数を使用すると、読み取り、書き込み、または指定されたソケットを閉じることができます。
socket_strerror() 指定されたエラー番号を持つ詳細なエラーを返します
socket_write()データをソケット キャッシュに書き込みます
socket_writev() データを分散/集約配列に書き込みます

## ##### ###################################

6. PHP Socket模拟请求

我们使用stream_socket来模拟:

/**
 * 
 * @param $data= array=array(&#39;key&#39;=>value)
 */
function post_contents($data = array()) {
    $post = $data ? http_build_query($data) : &#39;&#39;;
    $header = "POST /test/ HTTP/1.1" . "\n";
    $header .= "User-Agent: Mozilla/4.0+(compatible;+MSIE+6.0;+Windows+NT+5.1;+SV1)" . "\n";
    $header .= "Host: localhost" . "\n";
    $header .= "Accept: */*" . "\n";
    $header .= "Referer: http://localhost/test/" . "\n";
    $header .= "Content-Length: ". strlen($post) . "\n";
    $header .= "Content-Type: application/x-www-form-urlencoded" . "\n";
    $header .= "\r\n";
    $ddd = $header . $post;
    $fp = stream_socket_client("tcp://localhost:80", $errno, $errstr, 30);
    $response = &#39;&#39;;
    if (!$fp) {
        echo "$errstr ($errno)<br />\n";
    } else {
        fwrite($fp, $ddd);
        $i = 1;
        while ( !feof($fp) ) {
            $r = fgets($fp, 1024);
            $response .= $r;
            //处理这一行
        }
    }
    fclose($fp);
    return $response;
}

注意,以上程序可能会进入死循环;

这个PHP的feof($fp) 需要注意的地方了,我们来分析为什么进入死循环。

        while ( !feof($fp) ) {
            $r = fgets($fp, 1024);
            $response .= $r;
        }

实际上,feof是可靠的,但是结合fgets函数一块使用的时候,必须要小心了。一个常见的做法是:

$fp = fopen("myfile.txt", "r");
while (!feof($fp)) {
   $current_line = fgets($fp);
   //对结果做进一步处理,防止进入死循环
}

当处理纯文本的时候,fgets获取最后一行字符后,foef函数返回的结果并不是TRUE。实际的运算过程如下:

 1) while()继续循环。

 2) fgets 获取倒数第二行的字符串

 3) feof返回false,进入下一次循环

 4)fgets获取最后一行数据

 5)  一旦fegets函数被调用,feof函数仍然返回的是false。所以继续执行循环

 6) fget试图获取另外一行,但实际结果是空的。实际代码没有意识到这一点,试图处理另外根本不存在的一行,但fgets被调用了,feof放回的结果仍然是false

 7)    .....

8) 进入死循环

推荐学习:《PHP视频教程


以上がPHP SOCKETプログラミングについて話しましょう(詳細な説明付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcsdn.netで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。