Начало работы с сетевым программированием на PHP

PHP
Начало работы с сетевым программированием на PHP
#network-programming

предисловие

С более длительным сроком службы чувство срочности в этой строке по-прежнему такое же, как и тогда, когда я начал работу, нет никаких сомнений в том, что как разработчик серверовсетевое программированиеЭто одно из мест, куда мне нужно проникнуть дальше:

идеи обучения

Вот мои идеи для простого обучения сетевому программированию, затем я буду следовать этому плану, чтобы постепенно изучать сетевое программирование.

step 1. 原生php实现TCP Server -> 原生php实现http协议 -> 掌握tcpdump的使用 -> 深刻理解tcp连接过程
step 2. 原生php实现多进程webserver
    2.1 引入I/O多路复用
    2.2 引入php协程(yield)
    2.3 对比 I/O多路复用版本 和 协程版本的性能差异

step 3. 实现简单的go web框架

step 4. php c扩展实现简单的webserver

Почему я выберу использование PHP для изучения сетевого программирования? Потому что для меня наиболее привычным является PHP, второй PHP относительно прост, а сам PHP имеет соответствующую поддержку функций.

Давайте начнем первую часть исследования сегодня.

шаг 1. Собственный php реализует TCP-сервер -> собственный php реализует протокол http -> осваивает использование tcpdump -> глубоко понимает процесс подключения tcp

текст

Кратко рассмотрим общий процесс взаимодействия php как внутреннего языка:

клиент –(протокол:http) –> nginx –(протокол:fastcgi) –> php-fpm –(интерфейс:sapi) –> php

Здесь nginx выступает в роли веб-сервера и обратного прокси-сервера, преобразуя протокол http в протокол fastcgi. Смотрите здесь, некоторые друзья могут сказать:«Если PHP обрабатывает HTTP-запрос напрямую, не может вам не нужен nginx & php-fpm?"К сожалению, родной PHP не реализует протокол HTTP (да, добро пожаловать на правильные ошибки).

Тогда другой друг может сказать:«Разве собственный php не поддерживает протокол tcp? nginx может проксировать http-запросы на протокол tcp, чтобы php-fpm не использовался»., эм, да, верно. Процесс взаимодействия, описанный этим маленьким другом, выглядит следующим образом:

клиент –(протокол:http) –> nginx –(протокол:tcp) –> php

Это вроде бы не проблема, очень хорошая идея, но по идее протокол http все еще не реализован, и полученный контент все равно должен быть кучей строк. Давайте попробуем это сейчас:

шаг 1: nginx из сервиса
шаг 2: php просто реализует TCP-сервер, простой код выглядит следующим образом
<?php

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

socket_bind($server, '127.0.0.1', '8889');
socket_listen($server);

while (true) {
    $client = socket_accept($server);
    if (! $client) {
        continue;
    }
    $request = socket_read($client, 1024);
    // 查看接收到的内容
    var_dump($request);
    socket_close($client);
}
Шаг 3: HTTP-запрос обратного прокси-сервера nginx к указанному выше серверу TCP, конфигурация выглядит следующим образом.
upstream tcp_server {
    ip_hash;
    server 127.0.0.1:8889 max_fails=3 fail_timeout=5;
}

server {
    listen       80;
    server_name  test.local;

    access_log  /tmp/logs/nginx/test.access.log  main;

    location / {
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header Host            $http_host;
        proxy_pass http://tcp_server;
    }

}

Наконец мы посещаемtest.local/?aaa=1/Посмотрите на распечатанный результат, и предыдущее предположение соответствует:

string(127) "GET /?aaa=1 HTTP/1.0
X-Forwarded-For: 127.0.0.1
Host: test.local
Connection: close
User-Agent: curl/7.54.0
Accept: */*

"

Поэтому нам нужно реализовать протокол http.Поскольку протокол http реализован, мы можем напрямую использовать http в качестве веб-сервера.

клиент – (протокол:http) –> php

правильно!之后nginx的角色就是负载均衡,其实过分点你自己也可以用php做负载均衡。

Собственный php реализует TCP-сервер

Тогда давайте посмотрим, как создать простой TCP-сервер с помощью php Процесс выглядит следующим образом:





Основные задействованные функции PHP следующие:

socket_create

socket_listen

socket_accept

socket_recv || socket_read

socket_write

socket_close

Код:

<?php

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

socket_bind($socket, '127.0.0.1', '8889');

socket_listen($socket);

while (true) {
    // accept
    $client = socket_accept($server);
    if (! $client) {
        continue;
    }
    $request = socket_read($client, 1024);
    socket_close($client);
    echo socket_strerror(socket_last_error($server)) . "\n";
}

Запустите приведенный выше код в командной строке, а затем используйте команду nc, чтобы проверить, успешно ли установлено маленькое TCP-соединение:

(tigerb) ➜  demo git:(master) ✗ nc -z -v 127.0.0.1 8889
found 0 associations
found 1 connections:
     1: flags=82<CONNECTED,PREFERRED>
        outif lo0
        src 127.0.0.1 port 60668
        dst 127.0.0.1 port 8889
        rank info not available
        TCP aux info available

Connection to 127.0.0.1 port 8889 [tcp/ddi-tcp-2] succeeded!

Нет проблем, TCP-сервер работает.

Собственный php реализует HTTP-протокол

Приведенный выше простой TCP-сервер в основном вышел.Нам нужно сделать php непосредственно веб-сервером.Учтите, что веб-сервер основан на протоколе HTTP, а протокол HTTP реализован на основе протокола TCP. То есть мы можем реализовать HTTP-протокол на основе вышеупомянутого TCP-сервера. Мы улучшили блок-схему, добавив часть HTTP (оранжевая), как показано ниже.





Процесс реализации HTTP-протокола на самом деле таков:

  1. Может читать информацию, отправленную в запрос
  2. Может возвращать информацию клиентам, таким как браузеры, которые они могут понять

Протокол представляет собой не что иное, как спецификацию, согласованную обеими сторонами, и в HTTP/1.1 формат запроса и ответа в основном выглядит следующим образом.

просить:

<HTTP Method> <url> <HTTP Version>
<KEY>:<VALUE>\r\n
...
\r\n

отклик:

<HTTP Version> <HTTP Status> <HTTP Status Description>
<KEY>:<VALUE>\r\n
...
\r\n

Так что просто поставить, наш PHP требуется только для следующих спецификацийРазобратьивозвращениеПросто выведите соответствующий контент.Простой пример кода выглядит следующим образом:

/**
 * php实现简单的http协议
 */
class HttpProtocol
{
    /**
     * 原始请求字符串
     *
     * @var string
     */
    public  $originRequestContentString = '';

    /**
     * 原始请求字符串拆得的列表
     *
     * @var array
     */
    private $originRequestContentList = [];

    /**
     * 原始请求字符串拆得的键值对
     *
     * @var array
     */
    private $originRequestContentMap = [];

    /**
     * 定义响应头信息
     *
     * @var array
     */
    private $responseHead = [
        'http'         => 'HTTP/1.1 200 OK',
        'content-type' => 'Content-Type: text/html',
        'server'       => 'Server: php/0.0.1',
    ];

    /**
     * 定义响应体信息
     *
     * @var string
     */
    private $responseBody = '';

    /**
     * 响应内容
     *
     * @var string
     */
    public  $responseData = '';

    /**
     * 解析请求信息
     *
     * @param string $content
     * @return void
     */
    public function request($content = '')
    {
        if (empty($content)) {
            // exception
        
        }
        $this->originRequestContentList = explode("\r\n", $this->originRequestContentString);
        if (empty($this->originRequestContentList)) {
            // exception

        }
        foreach ($this->originRequestContentList as $k => $v) {
            if ($v === '') {
                // 过滤空
                continue;
            }
            if ($k === 0) {
                // 解析http method/request_uri/version
                list($http_method, $http_request_uri, $http_version) = explode(' ', $v);
                $this->originRequestContentMap['Method'] = $http_method;
                $this->originRequestContentMap['Request-Uri'] = $http_request_uri;
                $this->originRequestContentMap['Version'] = $http_version;
                continue;
            }
            list($key, $val) = explode(': ', $v);
            $this->originRequestContentMap[$key] = $val;
        }
    }
    
    /**
     * 组装响应内容
     *
     * @param [type] $responseBody
     * @return void
     */
    public function response($responseBody)
    {
        $count = count($this->responseHead);
        $finalHead = '';
        foreach ($this->responseHead as $v) {
            $finalHead .= $v . "\r\n";
        }
        $this->responseData = $finalHead . "\r\n" . $responseBody;
    }
}

Мы можем вставить код после socket_read

while (true) {
    // accept
    $client = socket_accept($server);
    if (! $client) {
        continue;
    }
    $request = socket_read($client, 1024);

    /**
     * HTTP 
     */
    $http = new HttpProtocol;
    $http->originRequestContentString = $request;
    $http->request($request);
    $http->response("Hello World");
    socket_write($client, $http->responseData);
    
    socket_close($client);
    echo socket_strerror(socket_last_error($server)) . "\n";
}

последнее посещениеhttp://127.0.0.1:8889/Результат такой или браузер открывает страницу и выводит "Hello World"

(tigerb) ➜  demo git:(master) ✗ curl "http://127.0.0.1:8889/" -vv
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8889 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:8889
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: text/html
< Server: php/0.0.1
* no chunk, no close, no size. Assume close to signal end
<
* Closing connection 0
Hello World%

Эпилог

До сих пор мы просто построили веб-сервер с php, и на этой основе php может напрямую взаимодействовать с клиентом. Наконец, мы будем использовать этот простой веб-сервер для захвата пакетов через tcpdump для анализа процесса соединения tcp. подожди~