首页 > 编程语言 >PHP 这么拉?长连接都搞不了?说说 PHP 的 socket 编程

PHP 这么拉?长连接都搞不了?说说 PHP 的 socket 编程

时间:2024-08-15 21:17:57浏览次数:8  
标签:CGI socket 编程 server static 进程 PHP

对 PHP 的误解颇深

网络上似乎存在一种现象,一提到 PHP 人们的第一反应是简单且慢,这种简单甚至已经到了简陋的地步,比如不少人认为 PHP 无法独立创建一个服务,只能配合 Apache 或 Nginx 一起使用,而且 PHP 只能在处理完请求后销毁资源关闭进程,所以也无法处理长连接业务,这些都是对 PHP 的误解,我想这种误解的形成可能与 PHP 的发展历史有关,实际上 PHP 能做的有很多,下面就先从 PHP 的发展历史说起。

PHP 的发展简史

在我看来,PHP 的发展路线确实与其他主流编程语言不太相同。PHP 天生就是为了 Web 而生的,早期的 Web 网页都是静态的,例如在个人主页上展示一些固定的个人信息,为了能够让网页展示一些动态的统计数据和简单的交互,Rasmus Lerdorf 在 1995 年开发了 Personal Home Page 工具集合,简称为 PHP,PHP 通过 CGI 协议与 Web 服务器交互,通过实时计算生成动态的内容。这里可能是与其他主流编程语言差别最大的点,其他语言的运行环境要么是通过编译后直接执行,要么是在命令行中调用解释器执行的。
因为 PHP 最初的目标就是做一些简单的计算,所以并不具备主流编程语言中的一些高级特性,后来越来越多的网站开始使用 PHP 并希望能提供更多的功能,之后 Lerdorf 将 PHP 开源,在这之后 Zeev Suraski 和 Andi Gutmans 重写了 PHP 的解析器,并从此开始 PHP 改为 Hypertext Preprocessor,新版的解析器命名为 Zend Engine。Zend 的命名来自于两位作者的名字,至此 PHP 支持了面向对象、命名空间等特性,已经脱胎换骨成为了一门完善的编程语言。在 2015 年 PHP 7 发布,重构了 PHP 中很多重要且常用的数据结构,内存占用得到显著优化,性能也得到了大幅提升。

火爆的 LAMP 架构

虽然 PHP 经过几次版本迭代已经具备了现代编程语言的必要特性,但依旧有很多人对 PHP 有着类似前面提到的种种误解,造成这种误解的原因很大程度上是因为曾经 Web 领域中应用最广泛的架构 - LAMP 实在是太火了。在这套架构中 Linux 作为操作系统,MySQL 用于数据存储,Apache 负责处理网络连接和 HTTP 协议,而 PHP 放在其后面负责处理动态内容。由于这套架构简单有效且开源免费,可以低成本快速搭建起一个可用的服务,这对于初创团队业务试错来说十分具有吸引力,一度出现了很多一键安装的集成软件包,让这套架构的上手门槛进一步降低,但长此以往可能让不少人以为 PHP 只能配合 Apache 或 Nginx 使用,而 PHP 远不止于此。放在 Apache 或 Nginx 后面只是 PHP 运行模式的一种,也就是 CGI 模式,此外 PHP 支持其他模式,下面做一个对比。

PHP 运行的几种模式

按我的理解,PHP 运行模式严格来说就分两种,CGI 模式和 CLI 模式,CGI 后来衍生出了 Apache mod、FastCGI、FPM 等模式。

CGI 模式

CGI (Common Gateway Interface)通用网关接口是一种协议,是早期 Web 服务器与外部程序交互的一种方式,Web 服务器与外部程序之间通过环境变量、标准输入和标准输出交换数据。
CGI 的 logo 是一个三棱镜,其中一束光穿过三棱镜被分解成不同颜色,象征着 CGI 可以将网络请求分解并传递给不同应用程序处理,展现出了 CGI 的多样性和灵活性。

遵循 CGI 协议的 Web 服务器一般会有一个名为 cgi-bin 的目录,目录下面默认都是可执行 CGI 脚本文件,如果前端访问到了这些文件那么 Web 服务器并不会像处理普通文件那样直接将文件返回给前端,而是会 fork 出子进程并在子进程中运行指定的 CGI 脚本,脚本运行完成后通过标准输出将结果返回给 Web 服务器,并关闭子进程。
运行前 Web 服务器会将一些必要的请求信息设置在环境变量中,CGI 脚本运行后便可以通过读取环境变量得到这些请求信息,例如 uri、请求参数等。CGI 脚本的标准输出会重定向给 Web 服务器,服务器接到输出后返回给前端,这就是为什么早期的 CGI 模式下运行的 PHP 程序可以通过 echo 来返回结果的原因。
这种模式特点是比较简单,并且由于每次处理完成后都会销毁进程和资源,所以也不会出现内存泄漏等问题,但缺点是由于每次都需要重新创建新的进程并销毁,性能开销较大,也无法利用到长连接或池化技术,在处理大量并发请求时处理能力较低。

FastCGI 模式与 PHP-FPM

为了解决 CGI 模式下每次都要新建子进程并销毁子进程导致的性能低下问题,FastCGI 模式在 CGI 基础上做出了改进,这种模式下会预先创建出一些 CGI 进程常驻内存,当有请求到来时会分配一个空闲进程处理,完成后并不销毁而是作为空闲进程重新等待处理请求。
FastCGI 是协议,而 PHP-FPM 是 FastCGI 的实现,全称为 PHP FastCGI Process Manager。这种模式根本上还是基于 CGI 模式衍生出来的,主要优化的是引入常驻内存特性以及多个 FPM 进程的管理,减少了频繁开启关闭进程带来的性能损耗,但由于 Web 服务器与 FPM 进程之间还是短连接,所以这种模式不支持与客户端的长连接。

CLI 模式

CLI 模式则是直接使用 PHP 解释器来运行 PHP 代码,例如 php test.php,在我看来无论哪种编程语言,CLI 模式才应该是最为广大人民群众所喜闻乐见的模式,但由于 PHP 以 CGI 以及 FastCGI 模式运行实在太过深入人心,以至于 CLI 模式反而对很多人来说较为陌生。
在这种模式下 PHP 的运行方式与其他高级编程语言区别并不大,支持常见的系统调用,就算不支持还可以通过扩展的形式支持,自然可以实现 socket 网络编程以及常驻内存,实现长连接也是很自然的事。
CLI 模式下实现 socket 编程常见的方式有两种,一种是使用官方 sockets 扩展提供 socket 支持的方式,另一种是基于第三方扩展例如 swoole,本文主要介绍原生 PHP 的实现方式。

PHP CGI 与 CLI 示例

下面分别列出两个例子,介绍 CGI 和 CLI 两个典型模式是如何运行的。

CGI 模式示例

首先是一个 C 语言实现的服务器,监听 8080 端口,接到请求时如果请求的是指定 CGI 脚本则会通过 fp = popen(cgi_script, "r"); 以子进程的方式启动 CGI 脚本,由于使用了 setenv 来设置环境变量,所以在子进程中可以读取到这些环境变量并做出一些计算处理。
下面就是 CGI 协议中规定的环境变量,是否很眼熟,例如 QUERY_STRING 环境变量就是 CGI 协议中规定的经过 URL-encoded 的参数:

下面实现一个最基本的 CGI server,接到请求会启动一个 PHP 子进程处理,最后接到 PHP 的输出后返回客户端

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <netinet/in.h>  
  
#define PORT 8080  
#define BUFFER_SIZE 1024  
  
void handle_cgi(int client_fd, const char *cgi_script, const char *query_string) {  
    char buffer[BUFFER_SIZE];  
    FILE *fp;  
  
    // 设置 CGI 环境变量  
    setenv("REQUEST_METHOD", "GET", 1);  
    // QUERY_STRING 是用来设置请求参数的
    setenv("QUERY_STRING", query_string, 1);  
    setenv("SCRIPT_FILENAME", cgi_script, 1);  
    setenv("SERVER_PROTOCOL", "HTTP/1.1", 1);  
    setenv("GATEWAY_INTERFACE", "CGI/1.1", 1);  
    setenv("SERVER_SOFTWARE", "MyServer/1.0", 1);  
    setenv("REMOTE_ADDR", "127.0.0.1", 1);  
    setenv("REDIRECT_STATUS", "200", 1);  
  
    // 启动子进程启动 CGI 脚本  
    fp = popen(cgi_script, "r");  
    if (fp == NULL) {  
        perror("popen");  
        return;  
    }  
  
    // 发送 HTTP 头  
    write(client_fd, "HTTP/1.1 200 OK\r\n", 17);  
  
    // 读取 CGI 脚本的输出并发送给客户端  
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {  
        write(client_fd, buffer, strlen(buffer));  
    }  
  
    pclose(fp);  
}  
  
int main() {  
    int server_fd, client_fd;  
    struct sockaddr_in server_addr, client_addr;  
    socklen_t client_len = sizeof(client_addr);  
    char buffer[BUFFER_SIZE];  
  
    server_fd = socket(AF_INET, SOCK_STREAM, 0);  
    if (server_fd < 0) {  
        perror("socket");  
        exit(EXIT_FAILURE);  
    }  
  
    server_addr.sin_family = AF_INET;  
    server_addr.sin_addr.s_addr = INADDR_ANY;  
    server_addr.sin_port = htons(PORT);  
  
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {  
        perror("bind");  
        exit(EXIT_FAILURE);  
    }  
  
    if (listen(server_fd, 10) < 0) {  
        perror("listen");  
        exit(EXIT_FAILURE);  
    }  
    printf("Server is running on port %d\n", PORT);  
    
    while (1) {  
        client_fd = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);  
        if (client_fd < 0) {  
            perror("accept");  
            continue;  
        }  
        read(client_fd, buffer, sizeof(buffer) - 1);  
        
        char method[BUFFER_SIZE];  
        char path[BUFFER_SIZE];  
        char protocol[BUFFER_SIZE];  
        sscanf(buffer, "%s %s %s", method, path, protocol);  
  
        // 一个简单的路由处理
        if (strncmp(path, "/cgi-bin/script.php", 19) == 0) {  
            char *query_string = strchr(path, '?');  
            if (query_string != NULL) {  
                query_string++;
            } else {  
                query_string = "";  
            }
            handle_cgi(client_fd, "./script.php", query_string); // 确保脚本路径正确  
        } else {  
            write(client_fd, "HTTP/1.1 404 Not Found\r\n", 24);  
            write(client_fd, "Content-Type: text/html\r\n\r\n", 27);  
            write(client_fd, "<html><body>CGI script Not Found</body></html>", 38);  
        }  
  
        close(client_fd);  
    }  
  
    close(server_fd);  
    return 0;  
}

PHP 脚本,需要添加可执行权限,指定默认使用 #!/usr/local/bin/php-cgi 执行,$_GET$_SERVER 都是 PHP 根据 CGI 协议从环境变量中解析出来的,最终通过 echo 输出结果,传递给 Web 服务器。

#!/usr/local/bin/php-cgi  
  
<?php  
// 解析 GET 请求参数  
echo "<h2>GET 请求参数:</h2>";  
echo "<pre>";  
print_r($_GET);  
echo "</pre>";  
  
// 解析通用请求参数(GET 和 POST)  
echo "<h2>SERVER:</h2>";  
echo "<pre>";  
print_r($_SERVER);  
echo "</pre>";  
?>

通过编译并启动 server.c 就可以访问 8080 端口,看到输出结果。

> gcc -o server server.c
> ./server
Server is running on port 8080


> curl localhost:8080

<h2>GET 请求参数:</h2><pre>Array
(
)
</pre><h2>SERVER:</h2><pre>Array
(
    [GATEWAY_INTERFACE] => CGI/1.1
    [HOSTNAME] => a05d15a93523
    [PHP_INI_DIR] => /usr/local/etc/php
    [REMOTE_ADDR] => 127.0.0.1
    [HOME] => /root
    [QUERY_STRING] => 
    [PHP_LDFLAGS] => -Wl,-O1 -pie
    [PHP_CFLAGS] => -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
    [PHP_VERSION] => 8.2.21
    [SCRIPT_FILENAME] => ./script.php
    [GPG_KEYS] => 39B641343D8C104B2B146DC3F9C39DC0B9698544 E60913E4DF209907D8E30D96659A97C9CF2A795A 1198C0117593497A5EC5C199286AF1F9897469DC
    [PHP_CPPFLAGS] => -fstack-protector-strong -fpic -fpie -O2 -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64
    [PHP_ASC_URL] => https://www.php.net/distributions/php-8.2.21.tar.xz.asc
    [PHP_URL] => https://www.php.net/distributions/php-8.2.21.tar.xz
    [SERVER_SOFTWARE] => MyServer/1.0
    [TERM] => xterm
    [PATH] => /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    [SERVER_PROTOCOL] => HTTP/1.1
    [REDIRECT_STATUS] => 200
    [REQUEST_METHOD] => GET
    [PHPIZE_DEPS] => autoconf       dpkg-dev        file        g++         gcc         libc-dev        make        pkg-config      re2c
    [PWD] => /app
    [PHP_SHA256] => 8cc44d51bb2506399ec176f70fe110f0c9e1f7d852a5303a2cd1403402199707
    [PHP_SELF] => 
    [REQUEST_TIME_FLOAT] => 1722446968.3435
    [REQUEST_TIME] => 1722446968
    [argv] => Array
        (
        )

    [argc] => 0
)
</pre>

CLI 模式示例

PHP 通过 sockets 扩展提供了 socket 网络编程相关的系统调用封装,下面代码中使用的是 socket_createsocket_bindsocket_listensocket_acceptsocket_readsocket_writesocket_close等一系列 socket 函数实现的 TCP 长连接服务

<?php  
  
$address = '0.0.0.0';  
$port = 8080;  
  
$sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);  
if (!$sock) {  
    die("Could not create socket: " . socket_strerror(socket_last_error()) . "\n");  
}  
  
if (!socket_bind($sock, $address, $port)) {  
    die("Could not bind socket: " . socket_strerror(socket_last_error($sock)) . "\n");  
}  
  
if (!socket_listen($sock, 5)) {  
    die("Could not listen on socket: " . socket_strerror(socket_last_error($sock)) . "\n");  
}  
  
echo "Server listening on $address:$port\n";  
  
do {  
    $client = socket_accept($sock);  
    if ($client) {  
        $input = socket_read($client, 1024);  
        echo "client say:" . $input;  
        $output = "Hello, " . trim($input) . "\n";  
        socket_write($client, $output, strlen($output)); 
        socket_close($client);    
	}
} while (true);  
  
socket_close($sock);

服务端测试

> php server.php
Server listening on 0.0.0.0:8080
client say:123

客户端测试

> nc localhost 8080
123
Hello, 123

除此了直接使用 socket 相关函数之外,PHP 还提供了以 stream 方式处理 socket 的一系列函数,如 stream_socket_server 相当于整合了 socket_createsocket_bindsocket_listen 函数。

Workerman 的实现

Workerman 是一款高性能 PHP 应用容器,是一个典型的基于 PHP socket 的以 CLI 模式运行的应用容器,结合 IO 多路复用和多进程达到了相当不错的性能。下面就看看 Workerman 的核心部分是如何实现的。
以下代码来自 Workerman 4.1.0 版本,只展示了核心部分。
Workerman 入口函数是 runAll

public static function runAll()
{
	static::checkSapiEnv();
	static::init();
	static::parseCommand();
	static::daemonize();
	static::initWorkers();
	static::installSignal();
	static::saveMasterPid();
	static::displayUI();
	static::forkWorkers();
	static::resetStd();
	static::monitorWorkers();
}

initWorkers 函数中初始化 server 实例,其中会根据 reusePort 属性判断是否要在主进程中调用 listen 初始化 socket。

protected static function initWorkers()
{
	foreach (static::$_workers as $worker) {
	  // 如果没开启端口重用,则主进程会主动 listen
		if (!$worker->reusePort) {
			$worker->listen();
		}
	}
}

public function listen()
{
    if (!$this->_mainSocket) {
        // 创建 socket
        $this->_mainSocket = \stream_socket_server($local_socket, $errno, $errmsg, $flags, $this->_context);
        if (!$this->_mainSocket) {
            throw new Exception($errmsg);
        }

        if (\function_exists('socket_import_stream') && static::$_builtinTransports[$this->transport] === 'tcp') {
            \set_error_handler(function(){});
            $socket = \socket_import_stream($this->_mainSocket);
            \socket_set_option($socket, SOL_SOCKET, SO_KEEPALIVE, 1);
            \socket_set_option($socket, SOL_TCP, TCP_NODELAY, 1);
            \restore_error_handler();
        }
        // 设置非阻塞 socket
        \stream_set_blocking($this->_mainSocket, 0);
    }

    $this->resumeAccept();
}

reusePort 属性相当于 socket 的 SO_REUSEPORT 选项,表示是否开启端口重用,这个选项涉及到惊群问题。
默认没有开启 SO_REUSEPORT,那么主进程会在 initWorkers 函数中主动调用一次 listen 函数创建 socket,之后在 forkWorkers 函数中 fork 出子进程,子进程会继承这个 socket,并在其之上进行事件循环的阻塞等待。之后当客户端请求到来时,所有子进程都会被唤醒尝试去 accept 客户端连接,但最终只有一个子进程可以 accpet 成功,其他子进程只能重新阻塞挂起,这种现象就是惊群,频繁且大量的进程状态切换会浪费系统资源。
而如果开启 SO_REUSEPORT 那么主进程中不会调用 listen,而是在 forkOneWorkerForLinux 时由每个子进程各自创建 socket 并分别在自己的 socket 上进行事件循环,由于开启了端口重用,所以操作系统运行不同进程监听相同端口。当客户端请求到来时,操作系统会以负载均衡的方式唤醒其中一个子进程处理请求,这样就避免了惊群问题导致的性能损耗。

protected static function forkOneWorkerForLinux(self $worker)
{
	$pid = \pcntl_fork();
	// 主进程
	if ($pid > 0) {
		static::$_pidMap[$worker->workerId][$pid] = $pid;
		static::$_idMap[$worker->workerId][$id]   = $pid;
	} // 子进程
	elseif (0 === $pid) {
		// 这里决定是否应该调用 listen 函数创建自己的 socket
		if ($worker->reusePort) {
			$worker->listen();
		}
		$worker->run();
	} else {
		throw new Exception("forkOneWorker fail");
	}
}

最终在 run 方法中创建并启动事件循环

public function run()
{
	// 创建事件循环
	if (!static::$globalEvent) {
		$event_loop_class = static::getEventLoopName();
		static::$globalEvent = new $event_loop_class;
		$this->resumeAccept();
	}

	// 启动事件循环
	static::$globalEvent->loop();
}

workerman 在 CLI 模式下结合多路复用 IO 和事件循环,并采用多进程模式运行,可以较好的支持高并发长连接场景。

PHP 不适合干这个?

可能有的人会说 PHP 不适合干这种活,不过在我看来适不适合应该以成本为前提。Web 应用属于典型的 IO 密集型应用,这种场景下使用这种方案已经可以应对大部分业务规模,如果团队是 PHP 为主语言那么使用这个方案成本是最低的而且效果也相当不错,或者说在业务发展到瓶颈之前这个方案一般不会先遇到瓶颈,如果遇到了那么首先恭喜你的业务取得了长足进步,其次应该考虑的是通过架构的方式来解决更大规模问题,例如进行服务化和分层化等等。
总的来说 PHP 不仅仅停留在 FPM,也绝不是低性能的代名词,结合业务场景和团队实际情况,采用合适的 PHP 解决方案不仅能达到不错的效果,开发和维护成本方面也具有一定优势。

标签:CGI,socket,编程,server,static,进程,PHP
From: https://www.cnblogs.com/caipi/p/18361822

相关文章

  • 基于Php湖南旅游网站/旅游推荐系统/景区管理系统/开题报告/课设/源码
    摘 要使用湖南旅游网站管理用户预定景点门票,不仅实现了智能化管理,还提高了管理员的管理效率,用户查询的功能也需要湖南旅游网站来提供。设计湖南旅游网站是本文的目标,湖南旅游网站是一个不断创新的系统,创新是将系统框架进行结合,经过很长一段时间了解了前后端开源框架之后,最......
  • shell编程:集群多主机一键启停服务脚本
    本文任务一、Kafka服务批量启动函数封装二、Kafka服务批量停止函数封装三、Kafka服务状态批量检测函数封装四、Kafka服务一键启停脚本主函数体实现五、进一步抽象脚本,改进为通用的一键启停其他服务的脚本核心知识点知识点1:一键启停多主机集群服务实现知识点2:服务状态检......
  • websocket连接
    https://blog.csdn.net/qq_70071513/article/details/133171580//暴露自定义websocket对象exportconstsocket={//后台请求路径url:"",//websocket对象websocket:null,//websocket状态websocketState:false,//重新连接次数reconnectNum:0,//......
  • Spring Boot中的异步编程技巧
    SpringBoot中的异步编程技巧大家好,我是微赚淘客返利系统3.0的小编,是个冬天不穿秋裤,天冷也要风度的程序猿!在现代的软件开发中,异步编程已经成为提高应用性能和响应速度的关键技术之一。SpringBoot作为Java开发中一个流行的框架,提供了多种异步编程的方法。本文将探讨SpringBoot......
  • 编程常用计算机小知识
    1.在Windows中打开命令窗口:按Win+R,输入CMD,按Enter键即可打开命令窗口2.如果想在任意目录执行某个执行文件,则可以将执行文件所在目录加到系统变量path中去,如D:\Java\bin。系统变量是从上至下的路径查找执行文件的,上移和下移可以调整path变量的位置。如果想尽快找到运行文件执行,......
  • shell编程中的else exit -1是什么意思
    在编程中,`elseexit-1`是一种控制流语句,通常用于条件判断中。具体来说,这段代码的意思是:如果某个条件不满足(即`else`部分),则执行`exit1`,这表示程序将以状态码`-1`退出。 这里的`exit`是一个系统调用或函数,用于终止程序的执行。退出状态码是一个整数,通常用于表示程序......
  • shell编程中的管道符 ‘|‘
    在Shell编程中,管道符`|`是一个非常有用的工具,用于将一个命令的输出传递给另一个命令作为输入。这种操作叫做管道(piping)。 具体来说,当你在Shell中使用`|`时,它会将前一个命令的标准输出(stdout)作为后一个命令的标准输入(stdin)。这允许你将多个命令组合在一起,实现复杂的操作......
  • GPHP Vol.1 Math
    GPHPVol.1MathIntroduction:本计划将记录杂题,采用循序渐进的方法,先给出Hint,然后给出Solution,力求还原思维过程。记录什么题随缘。这是GPHP第一期。每期计划4-5个题目。TableofContent<luogu>P3312[SDOI2014]数表P3312[SDOI2014]数表P3312[SDOI2014]数......
  • PHP常用数学函数
    数学函数1、取整、取余函数 1、ceil():进一法取整 2、floor():舍去法取整 3、round():浮点数四舍五入 4、fmod():浮点数取余2、最大值、最小值 1、max():求最大值 2、min():求最小值 3、abs():求绝对值3、数值类型判断 1、is_finite():判断是否为有限值。 2、is_......
  • Java、python、php版的宠物美容预约服务系统的设计与实现 (源码、调试、LW、开题、PPT)
    ......