对于PHP码农来说,在进阶为高级程序员的路上,socket编程绝对算是一个需要专研的点,不说多精通,至少要能熟悉socket编程。于是作为菜鸡的我,决定来学习下在PHP中,socket编程是咋回事。首先用基础知识来写个http服务器吧!

Socket server 基本流程

1.创建http server。
2.绑定IP 端口。
3.监听客户端请求。
4.接收客户端请求数据。
5.处理客户端请求数据。
6.回应客户端。
7.关闭http server。

以上是一个简单的完整流程,接下来用代码来实现一哈。本人是在symfony框架中用command 来写的。

单进程阻塞式server

Server代码

<?php
/**
 * Created by PhpStorm.
 * User: skymei
 * Date: 2018/12/20
 * Time: 11:17
 */

namespace App\Command;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;

class httpServer extends Command
{
    public function configure()
    {
        $this
            // the name of the command (the part after "bin/console")
            ->setName('server:start')
            // the short description shown while running "php bin/console list"
            ->setDescription('start a simple http server.')
            // the full command description shown when running the command with
            // the "--help" option
            ->setHelp('start a simple http server');
    }

    public function execute(InputInterface $input, OutputInterface $output)
    {  
        set_time_limit(0);
        // create socket server
        $socketServer = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if ($socketServer === false) {
            $output->writeln('create socket server fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        //!!!!! important  the timeout setting
        socket_set_option($socketServer,SOL_SOCKET,SO_RCVTIMEO,array("sec"=>1, "usec"=>0 ) );
        socket_set_option($socketServer,SOL_SOCKET,SO_SNDTIMEO,array("sec"=>3, "usec"=>0 ) );

        // bind ip and port for server
        if (!socket_bind($socketServer, '127.0.0.1', 8081)) {
            $output->writeln('bind socket server fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        // how many clients the server can listen
        if (!socket_listen($socketServer, 32)) {
            $output->writeln('listen socket client fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        // continuously to handle the client's request
        while (true) {
            $clientConnect = socket_accept($socketServer);  // connect the client

            if ($clientConnect) {
                socket_getpeername($clientConnect, $addr, $port);
                $output->writeln("client $addr connect with port $port..");
            }
            while (true) {
                $data = socket_read($clientConnect, 1024);  // read data from client(1024 bytes one time)

                if (!empty($data)) {
                    $output->writeln('receive data from client : ' . $data);
                    $data = strtoupper($data);  // handle the request data
                    socket_write($clientConnect, $data);  // respond the client
                } else {
                    $output->writeln("client $addr:$port disconnect !");
                    socket_close($clientConnect);   // close the connection
                    break;
                }
            }
        }
        socket_close($socketServer);  // stop socket server
    }
}

代码解释

一个非常简单的http server 就实现了。通过bin/console server:start 启动server进程,server便会常驻来监听客户端的请求并处理。客户端通过telnet 127.0.0.1 8081 来连接。

server 端:
>php bin/console server:start
client 127.0.0.1 connect with port 57114..
receive data from client : asdsd

client 端:
>telnet 127.0.0.1 8081
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
asdsd
ASDSD

Client代码

当然也可以用客户端来连接

<?php
/**
 * Created by PhpStorm.
 * User: skymei
 * Date: 2018/12/20
 * Time: 15:09
 */

namespace App\Command;

use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;

class httpClient extends Command
{
    public function configure()
    {
        $this
            // the name of the command (the part after "bin/console")
            ->setName('client:start')
            // the short description shown while running "php bin/console list"
            ->setDescription('start a simple http client.')
            // the full command description shown when running the command with
            // the "--help" option
            ->setHelp('start a simple http client');
    }

    public function execute(InputInterface $input, OutputInterface $output)
    {
        set_time_limit(0);
        $serverHost = '127.0.0.1';
        $serverPort = 8081;
        // create socket client
        $socketClient = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
        if ($socketClient === false) {
            $output->writeln('create socket client fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        //!!!!! important  the timeout setting
        socket_set_option($socketClient,SOL_SOCKET,SO_RCVTIMEO,array("sec"=>1, "usec"=>0 ) );
        socket_set_option($socketClient,SOL_SOCKET,SO_SNDTIMEO,array("sec"=>3, "usec"=>0 ) );

        // create socket server connection
        $connection = socket_connect($socketClient, $serverHost, $serverPort);
        if ($connection === false) {
            $output->writeln('connect socket server fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        // send data to socket server
        $res = socket_write($socketClient , 'Hello World!');
        if ($res === false) {
            $output->writeln('connect socket server fail : ' . socket_strerror(socket_last_error()));
            die();
        }
        // receive data from socket server
        while (true){
            $data = socket_read($socketClient, 1024);
            if(!empty($data)){
                $output->writeln("responce is $data");
            }else{
                break;
            }
        }
        $output->writeln("disconnect from server");
        socket_close($socketClient);
    }
}

这其中有比较重要的一个配置,没有这个超时配置的话,客户端socket_read会陷入死循环。

socket_set_option($socketClient,SOL_SOCKET,SO_RCVTIMEO,array("sec"=>1, "usec"=>0 ) );
socket_set_option($socketClient,SOL_SOCKET,SO_SNDTIMEO,array("sec"=>3, "usec"=>0 ) );

方案优化

这个socket server可以处理单个client的请求,当有新的客户端接入的时候,需要阻塞等候。我们实际使用的服务中都是可以处理多客户端的。那怎么实现捏?有两个办法:

1. server 多进程,每接入一个client,fork一个子进程来处理,主进程保持等待。
2. IO多路复用,redis为什么单线程还能有这么高的效率?就跟这个离不开关系!通过使用系统级接口select来轮询进程状态,一个进程休息的时候,把资源让出来,保存状态,其他进程接入运行。由于系统调用是非常快的,最终的效果就是数个进程几乎在同时运行,互相之间没有打扰。

参考文章

PHP实现系统编程(一) --- 网络Socket及IO多路复用