PHP 原生 Socket 网络编程:通过fork实现高性能网络服务器
你好,我是一笑。
在前面的两章中,我们已经实现了守护进程和基础的 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 来试验一下。