Расширенное руководство по программированию сокетов PHP

PHP

подготовка к учебе

  • среда Linux или Mac;
  • Установлено расширение Sockets;
  • Узнайте о протоколе TCP/IP.

Функция сокета является лишь частью расширения PHP и должна быть добавлена ​​в конфигурацию при компиляции PHP.--enable-socketsэлемент конфигурации для включения.

Если встроенный PHP не компилирует расширение сокетов, можно скачать исходный код той же версии и ввестиext/socketsиспользоватьphpizeСкомпилируйте и установите.

функции серии розеток

Процесс сокет-сервера/клиента:
socket服务端/客户端流程

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

серверная часть

Далее мы пишем простой однопроцессный 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-сервер, а затем ждем, пока клиент подключится в цикле. После получения клиентского соединения выполните циклический анализ сообщений от клиента.

используется в примере\nВ качестве терминатора сообщения, если полное сообщение не получено за один раз, оно будет считываться в цикле до тех пор, пока не встретится терминатор; после прочтения полного сообщения полученное сообщение будет отправлено клиенту, а затем сообщение будет опорожнить для подготовки к следующему приему.

Запускаем сервер из командной строки:

$ 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!скопировать код

Далее мы пишем собственный tcp-клиент, используя socket.

сторона клиента

Следующий пример очень прост: создайте клиент, подключитесь к серверу, отправьте сообщение и закончите чтение.

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);скопировать код

Сначала мы входим на исходную страницу терминала telnetquitВыйдите из соединения, потому что в это время наш сервер может принять только одно клиентское соединение. Затем запустите клиент, который вы написали сами:

$ php socket_tcp_client.php 
from server: hello, I'm client!скопировать код

socket_select

В приведенном выше примере наш TCP-сервер может принимать только одно клиентское соединение. Как это можно сделать для поддержки нескольких клиентских подключений? Обычно используются:

  • мультипрогресс
  • Многопоточность
  • Мультиплексирование ввода-вывода с использованием технологий select, poll, epoll и других.
  • Мультипроцесс + мультиплексирование ввода/вывода

В этом разделе мы используем третий метод, мультиплексирование ввода/вывода. Уровень технической реализации реализуется с помощью системного вызова socket_select, предоставляемого PHP.

Мультиплексирование ввода/вывода позволяет программам прослушивать несколько файловых дескрипторов одновременно. Системные вызовы, которые реализуют мультиплексирование ввода/вывода, в основном включают 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-сервер, а затем запустите только что написанный сервер:

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скопировать код

На данный момент наш сервер не ограничен количеством клиентских подключений.

Примечание: 1. После использования socket_select место разбора сообщения больше не может быть бесконечным циклом, иначе это вызовет блокировку.

Файловые дескрипторы, отслеживаемые функцией select, делятся на три категории, а именно writefds, readfds, excludefds.После вызова функция select блокируется до тех пор, пока файловый дескриптор не будет готов (с данными, доступными для чтения, записи или исключения) или истечет время ожидания (тайм-аут). Укажите время ожидания, если он возвращается немедленно, установите его в null), функция возвращает значение; когда функция select возвращается, вы можете найти готовый дескриптор, пройдясь по fdset.

2. Системный вызов socket поддерживает максимальное число клиентских подключений — 1024. Если требуется большее количество клиентских подключений, необходимо использовать такие технологии, как poll и epoll. Эта статья не объясняет.

socket_set_option

Эта функция используется для установки параметров сокета, таких как настройка мультиплексирования портов. Прототип функции:

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() 写数据到分散/聚合数组скопировать код

Среди них розетка вwrite read,writev readv,recv `send,recvfrom sendto,recvmsg sendmsgМожно обратиться к пяти группам функций ввода-вывода: https://blog.csdn.net/yangbingzhou/article/details/45221649.

Функции серии stream_socket

Серия функций stream_socket эквивалентна дальнейшей инкапсуляции функции сокета. Использование этого семейства функций упрощает кодирование.

stream_socket_serverа такжеstream_socket_acceptВозвращаемый дескриптор может бытьfgets() , fgetss() , fwrite() , fclose()так же какfeof()вызов функции.

серверная часть

Давайте сначала посмотрим на прототип функции.

поток_сокет_сервер:

resource stream_socket_server ( string $local_socket [, int &$errno [, string &$errstr [, int $flags = STREAM_SERVER_BIND | STREAM_SERVER_LISTEN [, resource $context ]]]] )скопировать код

Если это служба udp, флаги указываются какSTREAM_SERVER_BIND. Кроме того,$contextЗависит отstream_context_createСоздайте, например:

$context_option['socket']['so_reuseport'] = 1;//端口复用
$context = stream_context_create($context_option);скопировать код

поток_сокет_акцепт:

resource stream_socket_accept ( resource $server_socket [, float $timeout = ini_get("default_socket_timeout") [, string &$peername ]] )скопировать код

Далее мы используемstream_socket_Ряд функций пишет TCP-сервер.

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 server

Сервер udp не нуждается в операции прослушивания.

<?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 очень просто написать клиенту:

<?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_select

streamФункции серии используютstream_selectРеализация мультиплексирования ввода-вывода — это, по сути, системный вызов select.

Далее мы пишем два примера, первый пример и вышеприведенное использованиеsocket_selectРеализация аналогична, а вторая заключается в том, чтобы отслеживать клиентские события чтения и записи, таким образом реализуя функцию, аналогичную telnet, я думаю, всем будет интересно.

Слушайте сокет и подключайте сокет одновременно

Использование stream_select может реализовать мультиплексирование ввода-вывода, так что программы с одним процессом могут одновременно поддерживать несколько клиентских подключений. Пример:

<?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. Изучив эту статью, вы узнали следующее:

  • Знаком с использованием функций серии сокетов
  • Знаком с использованием функций серии stream_socket
  • Знаком с мультиплексированием ввода/вывода
  • Как использовать функции серии сокетов для реализации TCP-сервера и клиента
  • Как реализовать мультиплексирование ввода-вывода с помощью socket_select
  • Как использовать серию функций stream_socket для реализации TCP-сервера и клиента
  • Как реализовать мультиплексирование ввода-вывода с помощью stream_select

Я также оставляю вас с вопросом:

Как реализовать TCP-сервер, поддерживающий мультиплексирование ввода-вывода на основе многопроцессорной модели PHP Master-Worker?

Совет: в моей официальной учетной записи (fhyblog) есть статьи, связанные с заметками о многопроцессорности PHP, студенты, которые не знакомы с многопроцессорностью, могут изучить ее.

(Конец полного текста)

Ссылаться на

1. Объясните простым языком: связь php socket
http://www.cnblogs.com/thinksasa/archive/2013/02/26/2934206.html
2, запись, чтение, запись, чтение, получение, отправка, получение, получение, отправка, recvmsg, sendmsg, пять групп функций ввода-вывода - Блог CSDN
https://blog.csdn.net/yangbingzhou/article/details/45221649
3. Разница между чтением, записью и получением, отправкой в ​​программировании сокетов — Блог CSDN
https://blog.csdn.net/xhu_eternalcc/article/details/18256561
4. php select socket - yuanlp_code - Blog Park
https://www.cnblogs.com/yuanlipu/p/6431834.html
5. Модель и реализация службы сокетов (3) — однопроцессное мультиплексирование ввода-вывода. | Здравствуйте, добро пожаловать в блог Лао Чжана, Чжан Суцзе.
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/
6. PHP Socket реализует веб-сокет (4) Функция выбора
https://www.cnblogs.com/yangxunwu1992/p/5564454.html .

 

Заявление об авторских правах: Перепечатка или специальное заявление не указываются как оригинальная работа автора. В этой статье используется следующее соглашение для авторизации,Бесплатная перепечатка — некоммерческая — не производная — сохранить авторство | Creative Commons BY-NC-ND 3.0, просьба указывать автора и источник при перепечатке.

Автор: Feihongying~

Источник:http://52fhy.cnblogs.com/


рекомендовать! Всего за 2,50 доллара в месяц вы можете получить VPS с SSD!