PHP 原生 Socket 网络编程:通过fork实现高性能网络服务器

Socket 编程1424 字

你好,我是一笑。

在前面的两章中,我们已经实现了守护进程和基础的 TCP Server,在这个章节我们将实现高性能的网络服务,其实实现高性能网络服务有很多方法,在接下来的几个章节中,我将一一列举。

在我们上一节的代码中,如果你比较细心,就会发现当多个客户端连接服务其的时候是可以连接上,但是只有只有第一个连接到 tcp 的服务才可以进行通讯,其他的客户端必须等待第一个连接关闭后才可以发送请求。

真正的网络服务肯定不能这样实现,今天我们就要解决这个问题,怎么样才能够让 TCP 的服务器能够接收成百上千,甚至是上万的请求。

对于上一章的 TCP 服务器只有一个用户能够通讯,通过 fork 的改造就可以变成成百上千的用户来来连接服务器,每个连接过来我们都 fork 一个子进程来并行处理,就可以实现多人并发处理。

在对服务端进行修改之前,我们先对客户端进行一定的修改,我们先让客户端支持 Ctrl+C 退出程序,并且通知服务端要进行关闭 Scoket 了。

实现起来非常简单,我们只需要对 Ctrl+C 信号进行监听就可以了。

pcntl_signal(SIGINT, function () use ($socket) {
    echo "接收到退出信号,关闭 socket 并且退出进程" . PHP_EOL;
    $message = '.exit';
    socket_write($socket, $message, strlen($message));
    socket_close($socket);
    exit();
});

我们使用 pcntl_signal 来监听 SIGINT 信号,如果接收到信号就向服务端发送 .exit 告诉服务端,客户端要退出了,并且关闭 socket 连接,这么做的好处是,当我们客户端退出后可以主动通知服务端,服务端可以自由的做一些其他处理。

SIGINT:程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

同时,我们在循环发送的数据中增加 pcntl_signal_dispatch()函数检测是否有新的信号等待 dispatching。

while (true) {
      // 检测是否有新的信号等待 dispatching
    pcntl_signal_dispatch();
    // 向服务端写入字符串信息
    $ret = socket_write($socket, $message, strlen($message));
    if (!$ret) {
        echo 'fail to write' . socket_strerror(socket_last_error());
        exit();
    }
    // 读取服务端返回来的套接流信息
    $callback = socket_read($socket, 1024);
    echo 'server return message is:' . PHP_EOL . $callback;
    sleep(1);
}

接下来,我们来实现今天的主题,使用 fork 来实现高性能的网络服务器。它的基本原理非常简单,首先主进程进行监听连接,每收到一个连接就创建一个子进程进行数据包的收发。

    while ($string = socket_read($accept_resource, 1024)) {
        echo 'server receive is :' . $string . PHP_EOL;//PHP_EOL为php的换行预定义常量
        $return_client = 'server receive is : ' . $string . PHP_EOL;
        //Socket_write的作用是向socket_create的套接流写入信息,或者向socket_accept的套接流写入信息
        socket_write($accept_resource, $return_client, strlen($return_client));
    }

在上一章,我们使用 while 死循环来接收客户端传来的数据,主进程就会被阻塞在这里,其他的连接虽然可以连接但是无法传输数据,我们只需要将这段代码放到子进程中执行就可以了。

<?php
cli_set_process_title("tcp-master-process");
pcntl_async_signals(true);

//创建服务端的socket套接流,net协议为IPv4,protocol协议为TCP
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

if (!socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1)) {
    echo "Failed to set socket options!";
    exit();
}

// 绑定接收的套接流主机和端口,与客户端相对应
if (!socket_bind($socket, '0.0.0.0', 8888)) {
    echo 'server bind fail:' . socket_strerror(socket_last_error());
    exit();
}

// 监听套接流
if (!socket_listen($socket, 4)) {
    echo 'server listen fail:' . socket_strerror(socket_last_error());
    exit();
}

// 处理子进程退出
pcntl_signal(SIGCHLD, function ($id) {

    fprintf(STDOUT, "我接收到一个信号,编号为:%d \n", $id);

    $pid = pcntl_waitpid(-1, $status, WNOHANG);

    if ($pid > 0) {
        fprintf(STDOUT, "PID=%d 子进程退出了", $pid);
    }
});

//让服务器无限获取客户端传过来的信息
for (; ;) {
    // socket_accept的作用就是接受socket_bind()所绑定的主机发过来的套接流
    $accept_resource = socket_accept($socket);
    if (!$accept_resource) {
        echo "socket_accept error:!" . socket_strerror(socket_last_error());
        exit();
    }

    $pid = pcntl_fork();
    if ($pid == 0) {
        cli_set_process_title("tcp-worker-process");
        //读socket_read的作用就是读出socket_accept()的资源并把它转化为字符串

        while ($string = socket_read($accept_resource, 1024)) {
            if ($string == '.exit') {
                break;
            }
            echo 'server receive is :' . $string . PHP_EOL;//PHP_EOL为php的换行预定义常量
            $return_client = 'server receive is : ' . $string . PHP_EOL;
            //Socket_write的作用是向socket_create的套接流写入信息,或者向socket_accept的套接流写入信息
            socket_write($accept_resource, $return_client, strlen($return_client));
        }

        //关闭客户端连接
        socket_close($accept_resource);
        echo "没有数据了,关闭连接" . PHP_EOL;
        exit();
    } else {
        //关闭连接
        pcntl_signal_dispatch();
    }
}

if ($pid != 0) {
    //关闭连接
    socket_close($socket);
}

我们在接收到新连接后 fork 了子进程,在子进程中处理 tcp 的收发包,同时设置了进程名称方便我们在命令行中查看。

 cli_set_process_title("tcp-worker-process");

这样一来,我们就实现了一个比较高性能的服务了,通过 fork 的方式虽然可以实现高性能网络服务,但是同样也带来了很多问题。首先是资源被长期占用,只要客户端与服务端一直建立长连接,那么子进程一直被占用,进程在Linux 中是非常重要的资源,占用一个就少一个,他是有一定的限量的,几千几万被占用对 Linux 系统来说是灾难性的。

分配子进程花费时间长,因为创建子进程的成本是非常高的,如果很多连接突然过来,你会发现创建子进程的时间会很长,性能非常的满,这就是 fork 带来的问题,在下一节我们将利用 Select 异步I/O 解决这个问题。

同时在使用 fork 子进程这种方式的时候,我们还需要对子进程的状态进行维护,如果子进程先退出,而父进程没有处理的话就会变成僵尸进程,最终耗尽我们的进程号,导致无法启动新的进程,这是非常恐怖的事情。

处理僵尸进程的方法也很简单,我们只需要在父进程中接受子进程的退出信号即可。

// 处理子进程退出
pcntl_signal(SIGCHLD, function ($id) {
    fprintf(STDOUT, "我接收到一个信号,编号为:%d \n", $id);
    $pid = pcntl_waitpid(-1, $status, WNOHANG);
    if ($pid > 0) {
        fprintf(STDOUT, "PID=%d 子进程退出了", $pid);
    }
});

我们启动服务后,我们分别启动两个client,将其中的代码进行稍微的更改,第二个的编号设置为#2,就会发现都可以进行通信了。

server receive is :hello echo server #2 !
server receive is :hello echo server #1 !
server receive is :hello echo server #2 !
server receive is :hello echo server #1 !

同时我们使用 ps 命令来查看。

$ ps ajx | grep process
501 53843 98750   0  1:46下午 ttys004    0:00.03 tcp master process
501 53854 53843   0  1:46下午 ttys004    0:00.00 tcp tcp process
501 53907 53843   0  1:46下午 ttys004    0:00.00 tcp tcp process

我们一共是启动了两个 tcp client,在后台也就 fork 出了两个子进程,我们可以在多启动一个 #3 的 client 来试验一下。

maksim
Maksim(一笑,吡罗),PHPer,Goper
OωO
开启隐私评论,您的评论仅作者和评论双方可见