PHP Socket 網絡編程

前言

在作PHP開發的過程當中,大部分咱們都在和http協議打交道,在ISO模型裏面,http屬於應用層協議,它底層會用到TCP協議。http協議很是簡單,它是一個文本協議,一個請求對應一個響應,客戶端發起一個請求,服務端響應這個請求。http是一個一問一答的對話,每次請求都得從新創建對話(這裏暫不討論Keep-Alive),若是你想經過一個請求進行屢次對話,那就是長鏈接通訊,必須使用TCP或者UDP協議。php

互聯網運行的基石是創建在一些協議上的,目前而言主要是TCP/IP協議族,大部分協議都是公開開放的,計算機遵循這些協議咱們才能通訊,固然也有一些私有協議,私有協議只有本身知道如何去解析,至關來講更安全,好比QQ所用的協議就是本身定義的。在ISO模型裏面,我們經常使用的有http、ftp、ssh、dns等,可是不經常使用的數不勝數,發明一個協議不難,難的是如何設計的更好用,並且你們都喜歡用。html

Socket

Socket並非一個協議,本質上說Socket是對 TCP/IP 協議的封裝,它是一組接口,在設計模式中,Socket 其實就是一個門面(facade)模式,它把複雜的 TCP/IP 協議族隱藏在 Socket 接口後面,對用戶來講,一組簡單的接口就是所有,讓 Socket 去組織數據,以符合指定的協議。編程

下圖展現了Socket在ISO模型裏面大概位置:json

PHP Socket

雖然PHP的強項是處理文本,通常用來寫網頁和http接口,可是官方依然提供了Socket擴展,編譯PHP時在配置中添加--enable-sockets 配置項來啓用,若是使用apt或yum安裝,默認狀況下是已啓用。設計模式

官方文檔裏面列出了大概40個函數,可是經常使用的也就那幾個,跟着文檔,我們一塊兒來學學如何使用,首先聲明一下,本人對Socket編程並不熟悉,若有錯誤的地方,但願你們指出來。數組

我們先看一幅圖,關於TCP客戶端和服務端之間的通訊過程,我們平時寫http接口的時候並未作這麼多工做,那是客戶端給封裝好了:瀏覽器

1.服務端代碼

<?php
set_time_limit(0);

$ip = '127.0.0.1';
$port = 8888;

$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

socket_bind($sock, $ip, $port);
socket_listen($sock, 4);

echo "Server Started, Listen On $ip:$port\n";

$accept = socket_accept($sock);

socket_write($accept, "Hello World!\n", 8192);

$buf = socket_read($accept, 8192);

echo "Receive Msg: " . $buf . "\n";

socket_close($sock);
複製代碼

簡單說一下,爲便於演示,因此省略了全部的錯誤處理代碼,能夠看到分爲create、bind、listen、accept、write\read、close這幾步,看上去很是簡單!具體參數你們能夠看一下文檔!在服務端啓動以後,當收到一個請求以後,咱們首先返回了一個Hello World\n,而後又讀取了8192個字節的數據,打印出來!最後關閉鏈接。安全

因爲這裏,咱尚未寫客戶端,因此暫時使用curl訪問一下,運行效果以下:swoole

===>服務端:網絡

===>客戶端:

從這個例子裏面咱們能夠看出來,curl發出是一個標準的http請求,實際上它的每一行後面是有\n的,在http協議裏面,這幾行文本實際上是頭(header),可是在這個例子裏面,對於咱們來講,它就是一段文本而已,服務端只是把它的內容打印出來了,並無去按照http協議去解析。雖然咱們返回了Hello World!\n,可是這也並無按照http協議的格式去作,缺乏響應頭。我只能說curl比較強大,若是使用瀏覽器訪問的話會失敗,提示127.0.0.1 sent an invalid response

可是稍加改造,咱們就能夠返回一個標準的http響應:

$response = "HTTP/1.1 200 OK\r\n";
$response .= "Server: Socket-Http\r\n";
$response .= "Content-Type: text/html\r\n";
$response .= "Content-Length: 13\r\n\r\n";
$response .= "Hello World!\n";

socket_write($accept, $response, 8192);
複製代碼

這時候若是再用瀏覽器訪問,就能夠看到 Hello World!了,可是這個服務端目前是一次性的,就是說它只能處理一次請求,而後就結束了,正常的服務端是能夠處理屢次請求的,很簡單,加一個死循環就好了!

只貼一下改動的部分,代碼以下:

while (true) {
    $accept = socket_accept($sock);

    $buf = socket_read($accept, 8192);

    echo "Receive Msg: " . $buf . "\n";

    $response = "HTTP/1.1 200 OK\r\n";
    $response .= "Server: Socket-Http\r\n";
    $response .= "Content-Type: text/html\r\n";
    $response .= "Content-Length: 13\r\n\r\n";
    $response .= "Hello World!\n";

    socket_write($accept, $response, 8192);

    socket_close($accept);
}
複製代碼

搖身一變,就是一個http服務了,使用ab測了一下,併發上萬,是否是有點小激動?

然而,之因此這麼快是由於邏輯簡單,假如你在while裏面任何位置加一個 sleep(1) 你就會發現,原來這特麼是串行的,一個個執行的,並非並行,這段腳本一次只能處理一個請求!

解決這個問題方法有不少種,具體能夠參考 PHP併發IO編程之路, 看看前半段就好了,後半段是廣告!該文章總結了3種方法:最先是採用多進程多線程方式,因爲進程線程開銷大,這種方式效率最低。後來演進出master-worker模型,也就是相似如今fpm採用的方式。目前最早進的方式就是異步io多路複用,基於epoll實現的。理論上講C能實現的,PHP都能經過擴展去實現,並且PHP確實提供了相關擴展,其思想和C寫的都差很少,然而今天咱不是說高併發編程的,仍是接着說Socket吧!

2.客戶端代碼

以前的例子裏面咱們使用的是curl訪問的,也可使用瀏覽器或者telnet,這些工具均可以算做是客戶端,客戶端也能夠本身實現。

set_time_limit(0);

$port = 8888;
$ip = '127.0.0.1';

$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

echo "Connecting $ip:$port\n";

socket_connect($sock, $ip, $port);

$input = "Hello World Socket";

socket_write($sock, $input, strlen($input));

$out = socket_read($sock, 8192);

echo "Receive Msg: $out\n";

socket_close($sock);
複製代碼

這段代碼一樣省略了錯誤處理代碼,能夠看到第一步都是create,可是第二步變成connect,而後是read\write、最後close。

具體運行效果這裏再也不展現,和curl訪問沒多大區別,可是這個客戶端也是一次性的,執行完了就結束!

實例

接下來,咱們來寫一個基於TCP通訊的應用,這個應用很是簡單,就是加減乘除!

(1)服務端代碼:

<?php
set_time_limit(0);

$ip = '127.0.0.1';
$port = 8888;

$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

socket_bind($sock, $ip, $port);

socket_listen($sock, 4);

echo "Server Started, Listen On $ip:$port\n";

while (true) {
    $accept = socket_accept($sock);

    $buf = socket_read($accept, 8192);

    echo "Receive Msg: " . $buf . "\n";

    $params = json_decode($buf, true);
    $m = $params['m'];
    $a = $params['a'];
    $b = $params['b'];

    switch ($m) {
        case '+';
            $response = $a + $b;
            break;
        case '-';
            $response = $a - $b;
            break;
        case '*';
            $response = $a * $b;
            break;
        case '/';
            $response = $a / $b;
            break;
        default:
            $response = $a + $b;
    }

    socket_write($accept, $response."\n", 8192);

    socket_close($accept);
}
複製代碼

(2)客戶端代碼:

<?php
set_time_limit(0);

$port = 8888;
$ip = '127.0.0.1';

$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

echo "Connecting $ip:$port\n";

socket_connect($sock, $ip, $port);

$input = json_encode([
    'a' => 15,
    'b' => 10,
    'm' => '+'
]);

socket_write($sock, $input, strlen($input));

$out = socket_read($sock, 8192);

echo "Receive Msg: $out\n";

socket_close($sock);
複製代碼

在這些代碼裏面,我按照本身的需求定義了一個「協議」,我把須要運算的數和方式經過一個json數組傳輸,約定了一個格式,這個協議只有我本身清楚,因此只有我才知道怎麼調用。服務端在接受到參數以後,經過運算得出結果,而後把結果返回給客戶端。

可是這個例子還有問題,客戶端依然是一次性的,參數都被硬編碼在代碼裏面,不夠靈活,最關鍵是沒有用到TCP長鏈接的特性,咱們每次計算都得從新發起請求、從新創建鏈接,實際上,我須要的是一次鏈接,屢次對話,也就是進行屢次計算!

目前爲止,這些演示代碼都沒有複用鏈接,由於在服務端最後我close了這個鏈接,這意味着每次都是一個新的請求,若是是http服務的話尚且能夠用一下,如何去實現一個TCP長鏈接呢?

IO多路複用之select

select系統調用的目的是在一段指定時間內,監聽用戶感興趣的文件描述符上的可讀、可寫和異常事件,雖然這個方式也比較低效,可是不妨瞭解一下,經過這種方式咱們能夠複用鏈接,完整的代碼以下:

<?php
set_time_limit(0);

$ip = '127.0.0.1';
$port = 8888;

$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

socket_bind($sock, $ip, $port);

socket_listen($sock, 4);

echo "Server Started, Listen On $ip:$port\n";

socket_set_nonblock($sock);

$clients = [];

while (true) {
    $rs = array_merge([$sock], $clients);
    $ws = [];
    $es = [];

    //監聽文件描述符變更
    $ready = socket_select($rs, $ws, $es, 3);
    if (!$ready) {
        continue;
    }

    if (in_array($sock, $rs)) {
        $clients[] = socket_accept($sock);
        $key = array_search($sock, $rs);
        unset($rs[$key]);
    }

    foreach ($rs as $client) {
        $input = socket_read($client, 8096);
        if ($input == null) {
            $key = array_search($client, $clients);
            unset($clients[$key]);
            continue;
        }
        echo "input: " . $input;

        //解析參數,計算結果
        preg_match("/(\d+)(\W)(\d+)/", $input, $params);
        if (count($params) === 4) {
            $a = intval($params[1]);
            $b = intval($params[3]);
            $m = $params[2];
        } else {
            continue;
        }

        switch ($m) {
            case '+';
                $result = $a + $b;
                break;
            case '-';
                $result = $a - $b;
                break;
            case '*';
                $result = $a * $b;
                break;
            case '/';
                $result = $a / $b;
                break;
            default:
                $result = $a + $b;
        }

        $output = "output: $result\n";
        echo $output;
        socket_write($client, $output, strlen($output));
    }
}
複製代碼

而後我使用了telnet鏈接服務端進行操做,運行效果以下,一個基於TCP長鏈接的網絡版簡易計算器:

在這個例子,傳參的「協議」稍微有點變化,只是爲了更方便在telnet裏面交互,可是很容易理解。這裏面最關鍵是定義了一個全局變量用來存儲鏈接資源描述符,而後經過select去監聽變化,最後遍歷整個數組,讀取\寫入數據!

總結

經過上面的簡單介紹,但願你們都對PHP Socket編程有一些瞭解和認識,其實做爲Web開發來講,不多會用到裸TCP去鏈接,大部分時候都是使用基於TCP的http協議,只有涉及到一些對響應速度要求很是高的應用,好比說遊戲、實時通訊、物聯網纔會用到,若是真的用到,不妨嘗試一下Workman、Swoole這些成熟的框架!

相關文章
相關標籤/搜索