[Übersetzung] [Entwicklung und Einbettung von PHP-Erweiterungen] Kapitel 1 – PHP-Lebenszyklus

黄舟
Freigeben: 2023-03-05 16:06:01
Original
1411 Leute haben es durchsucht

Der Lebenszyklus von PHP

In einer gewöhnlichen Webserverumgebung können Sie den PHP-Interpreter im Allgemeinen nicht direkt starten; Sie starten Apache oder andere Webserver, die die Skripte laden, die von PHP verarbeitet werden müssen (angefordert). .php-Dokumentation).

Alles beginnt mit sapi

Obwohl es anders aussieht, verhält sich die CLI tatsächlich genauso wie das Web, um den „Befehl“ zu starten Zeile „sapi“ ist eigentlich wie ein Mini-Webserver, der einzelne Anfragen bedienen soll. Wenn das Skript abgeschlossen ist, wird dieser Mini-PHP-Webserver beendet und gibt die Kontrolle an die Shell zurück.

Starten und beenden

Der Start- und Beendigungsprozess ist hier in zwei unabhängige Startphasen und zwei unabhängige Beendigungsphasen unterteilt. Ein Zyklus wird für die Initialisierungseinstellung der für die Gesamtausführung des PHP-Interpreters erforderlichen Strukturen und Werte verwendet, die sich im Sapi befinden Lebenszyklus Dauerhafte Existenz. Der andere bedient nur eine einzelne Seitenanforderung und der Lebenszyklus ist kürzer. Bevor alle Anforderungen auftreten, ruft PHP die MINIT-Methode (Modulinitialisierung) auf Die Erweiterung kann Konstanten definieren, Klassen definieren, Ressourcen registrieren, Streams, Filter-Handler und alle anderen Ressourcen, die vom Anforderungsskript verwendet werden. Alle diese Ressourcen haben eine Eigenschaft, das heißt, sie sind darauf ausgelegt, zu existieren über alle Anfragen hinweg, die auch als „persistent“ bezeichnet werden können.

Übliche MINIT-Methoden sind wie folgt:

Wenn eine Anfrage eintrifft, installiert PHP eine Betriebsumgebung, die die enthält Symboltabelle (Variablenspeicher) und synchronisiert dann die Konfigurationswerte jedes Verzeichnisses. PHP durchläuft dann alle Erweiterungen und ruft dieses Mal die RINIT-Methode (Anforderungsinitialisierung) auf. Hier kann die Erweiterung globale Variablen auf Standardwerte und voreingestellte Variablen neu laden Die Symboltabelle des Skripts oder das Ausführen anderer Aufgaben. Das Aufzeichnen des Seitenanforderungsprotokolls in einer Datei ähnelt der auto_prepend_file-Anweisung für alle Skriptanforderungen.
/* 初始化myextension模块 
 * 这在sapi启动后将立即发生 
 */  
PHP_MINIT_FUNCTION(myextension)  
{  
    /* 全局: 第12章 */  
  
#ifdef ZTS  
    ts_allocate_id(&myextension_globals_id,  
        sizeof(php_myextension_globals),  
        (ts_allocate_ctor) myextension_globals_ctor,  
        (ts_allocate_dtor) myextension_globals_dtor);  
#else  
    myextension_globals_ctor(&myextension_globals TSRMLS_CC);  
#endif  
  
  
    /* REGISTER_INI_ENTRIES() 指向一个全局的结构, 我们将在第13章"INI设置"中学习 */  
    REGISTER_INI_ENTRIES();  
  
  
    /* 等价于define('MYEXT_MEANING', 42); */  
    REGISTER_LONG_CONSTANT("MYEXT_MEANING", 42, CONST_CS | CONST_PERSISTENT);  
    /* 等价于define('MYEXT_FOO', 'bar'); */  
    REGISTER_STRING_CONSTANT("MYEXT_FOO", "bar", CONST_CS | CONST_PERSISTENT);  
  
  
    /* 资源: 第9章 */  
    le_myresource = zend_register_list_destructors_ex(  
                    php_myext_myresource_dtor, NULL,  
                    "My Resource Type", module_number);  
    le_myresource_persist = zend_register_list_destructors_ex(  
                    NULL, php_myext_myresource_dtor,  
                    "My Resource Type", module_number);  
  
  
    /* 流过滤器: 第16章 */  
    if (FAILURE == php_stream_filter_register_factory("myfilter",  
                   &php_myextension_filter_factory TSRMLS_CC)) {  
        return FAILURE;  
    }  
  
  
    /* 流包装器: 第15章 */  
    if (FAILURE == php_register_url_stream_wrapper ("myproto",  
                   &php_myextension_stream_wrapper TSRMLS_CC)) {  
        return FAILURE;  
    }  
  
  
    /* 自动全局变量: 第12章 */  
#ifdef ZEND_ENGINE_2  
    if (zend_register_auto_global("_MYEXTENSION", sizeof("_MYEXTENSION") - 1,  
                                                NULL TSRMLS_CC) == FAILURE) {  
        return FAILURE;  
    }  
    zend_auto_global_disable_jit ("_MYEXTENSION", sizeof("_MYEXTENSION") - 1  
                                                     TSRMLS_CC);  
#else  
    if (zend_register_auto_global("_MYEXTENSION", sizeof("_MYEXTENSION") - 1  
                                                     TSRMLS_CC) == FAILURE) {  
        return FAILURE;  
    }  
#endif  
    return SUCCESS;  
}
Nach dem Login kopieren

Die RINIT-Methode ist wie folgt geschrieben:

Nachdem die Verarbeitung einer Anfrage abgeschlossen ist (Erreichen des Skripts am Ende der Datei oder Aufruf der die()/exit()-Anweisung), startet PHP den Bereinigungsprozess durch Aufrufen von RSHUTDOWN (Anfragebeendigung) für jede Erweiterung So wie RINIT auto_prepend_file entspricht, kann RSHUTDOWN analog zur auto_append_file-Direktive sein. Zwischen RSHUTDOWN und auto_append_file besteht der wichtigste Unterschied: RSHUTDOWN wird immer ausgeführt, egal was passiert, und der die()/exit()-Aufruf des User-Space-Skripts überspringt alle auto_append_file.
/* 每个页面请求开始之前执行 
 */  
PHP_RINIT_FUNCTION(myextension)  
{  
    zval *myext_autoglobal;  
  
  
    /* 初始化MINIT函数中定义的自动全局变量为空数组. 这等价于$_MYEXTENSION = array(); */  
    ALLOC_INIT_ZVAL(myext_autoglobal);  
    array_init(myext_autoglobal);  
    zend_hash_add(&EG(symbol_table), "_MYEXTENSION", sizeof("_MYEXTENSION") - 1,  
                                (void**)&myext_autoglobal, sizeof(zval*), NULL);  
  
  
    return SUCCESS;  
}
Nach dem Login kopieren

bevor die Symboltabelle und andere Ressourcen freigegeben werden. Das letzte, was getan werden muss, ist RSHUTDOWN. Nachdem alle RSHUTDOWN-Methoden abgeschlossen sind, werden alle Variablen in der Symboltabelle sofort unset() ,

Während dieser Zeit werden alle nicht persistenten Ressourcen und Objekte aufgerufen. Die Destruktoren werden aufgerufen, um Ressourcen ordnungsgemäß freizugeben.

Wenn schließlich alle Anforderungen erfüllt sind (Verarbeitung abgeschlossen), werden die Destruktoren aufgerufen Der Webserver oder ein anderes Sapi beginnt mit der Vorbereitung auf die Beendigung und die Ausführung der PHP-Schleife. Die MSHUTDOWN-Methode (Modulbeendigung) jeder Erweiterung ist die letzte Gelegenheit für die Erweiterung, den Prozessor zu entladen und dauerhaft zugewiesenen Speicher während des MINIT-Zyklus freizugeben
/* 每个页面请求结束后调用 */  
PHP_RSHUTDOWN_FUNCTION(myextension)  
{  
    zval **myext_autoglobal;  
  
  
    if (zend_hash_find(&EG(symbol_table), "_MYEXTENSION", sizeof("_MYEXTENSION"),  
                                         (void**)&myext_autoglobal) == SUCCESS) {  
        /* 做一些对$_MYEXTENSION数组的值有意义的处理 */  
        php_myextension_handle_values(myext_autoglobal TSRMLS_CC);  
    }  
    return SUCCESS;  
}
Nach dem Login kopieren

Lebenszyklus

/* 这个模块正在被卸载, 常量和函数将被自动的卸载, 持久化资源, 类, 流处理器必须手动的卸载. */  
PHP_MSHUTDOWN_FUNCTION(myextension)  
{  
    UNREGISTER_INI_ENTRIES();  
    php_unregister_url_stream_wrapper ("myproto" TSRMLS_CC);  
    php_stream_filter_unregister_factory ("myfilter" TSRMLS_CC);  
    return SUCCESS;  
}
Nach dem Login kopieren
Auf jede PHP-Instanz, unabhängig davon, ob sie über das Init-Skript oder über die Befehlszeile gestartet wird, folgt eine Reihe von Anforderungs-/Modulinitialisierungs-/-beendigungsereignissen, die im vorherigen Abschnitt erwähnt wurden sowie die Ausführung des Skripts selbst. Wie oft wird es ausgeführt? In welcher Häufigkeit werden die vier häufigsten Sapis besprochen: cli/cgi, multi-process Modul, Multi-Thread-Modul, eingebettet.

cli-Lebenszyklus

cli (und cgi) Sapi ist in seinem Einzelanforderungslebenszyklus etwas ganz Besonderes, da zu diesem Zeitpunkt nur eine Anforderung vorhanden ist Obwohl der Spatz klein und gut ausgestattet ist, werden alle oben genannten Phasen ausgeführt. Die folgende Abbildung zeigt den Prozess des Aufrufs des PHP-Interpreters über die Befehlszeile, um den Test durchzuführen. PHP-Skript:

Die häufigste Verwendung von PHP besteht darin, PHP als Apache 1 zu erstellen oder das Apxs-Modul von Apache 2 von Pre-Fork MPM zu verwenden, um PHP in den Webserver einzubetten . Es gibt einige andere Webserver, die ebenfalls in diese Kategorie fallen. Sie werden später in diesem Buch zusammenfassend als „Multiprozessmodule“ bezeichnet.[Übersetzung] [Entwicklung und Einbettung von PHP-Erweiterungen] Kapitel 1 – PHP-Lebenszyklus

Sie werden Multiprozessmodule genannt, weil sie beim Start von Apache aktiviert werden wird sofort einige untergeordnete Prozesse austeilen, von denen jeder seinen eigenen unabhängigen Prozessraum hat und unabhängig voneinander ist. In einem untergeordneten Prozess sieht das Leben der PHP-Instanz wie im Bild unten aus. Die einzige Änderung besteht darin, dass mehrere Anforderungen eingeklemmt sind in einem einzelnen MINIT/MSHUTDOWN-Paar:

Dieses Muster erlaubt keine willkürlichen Der untergeordnete Prozess kennt die Daten, die anderen untergeordneten Prozessen gehören, aber es ermöglicht dem untergeordneten Prozess, zu sterben und ersetzt werden, ohne die Stabilität anderer untergeordneter Prozesse zu beeinträchtigen. Die folgende Abbildung zeigt mehrere Prozesse in einer Apache-Instanz und ihre Auswirkungen auf MINIT, RINIT, RSHUTDOWN und den Aufruf der MSHUTDOWN-Methode.[Übersetzung] [Entwicklung und Einbettung von PHP-Erweiterungen] Kapitel 1 – PHP-Lebenszyklus

Multi-Thread-Lebenszyklus[Übersetzung] [Entwicklung und Einbettung von PHP-Erweiterungen] Kapitel 1 – PHP-Lebenszyklus

随着发展, php逐渐的被一些webserver以多线程方式使用, 比如IIS的isapi接口, apapche 2的worker mpm. 在多线程webserver中永远都只有一个进程在运行, 但是在进程空间中有多个线程同时执行. 这样做能降低一些负载, 包括避免了MINIT/MSHUTDOWN的重复调用, 真正的全局数据只被分配和初始化一次, 潜在的打开了多个请求的信息共享之门. 下图展示了apache 2这样的多线程webserver上运行php时的进程状态:

[Übersetzung] [Entwicklung und Einbettung von PHP-Erweiterungen] Kapitel 1 – PHP-Lebenszyklus

嵌入式生命周期

回顾前面, 嵌入式sapi只是sapi的另外一种实现, 它还是遵循和cli, apxs, isapi接口一致的规则, 因此很容易猜到请求的生命周期遵循相同的基本路径: 模块初始化 => 请求初始化 => 请求 => 请求终止 => 模块终止. 实际上, 嵌入式sapi和它的同族一样遵循着这些步骤.

让嵌入式sapi变得特殊的是它可能被当做一个整个请求的一部分被潜入到多个脚本片段中. 多数情况下控制会在php和调用应用之间多次来回的传递.

虽然一个嵌入请求可能由一个或多个代码元素组成, 但嵌入式应用还是受和webserver一样的请求隔离影响. 为了处理两个或多个并行的嵌入环境, 你的应用要么像apache 1去fork, 要么旧像apache 2线程化. 尝试在单个非线程进程空间中处理两个独立的请求环境将产生不可预料的结果, 这肯定是你不期望的.

Zend线程安全

当php还在幼儿期的时候, 它作为一个单进程cgi运行, 并没有线程安全的概念, 因为没有比单个请求存活更久的进程空间. 内部变量可以在全局作用域中定义, 访问, 修改, 只要初始化没有问题就不会产生严重后果. 任何没有被正确清理的资源都会在cgi进程终止时被释放.

后来, php嵌入了多进程webserver, 比如apache. 给定的内部变量仍然可以定义在全局并且可以通过在每个请求启动时正确的初始化, 终止时去做适当的清理工作来做到安全访问, 因为在一个进程空间中同时只会有一个请求. 这个时候, 增加了每个请求的内存管理, 以放置资源泄露的增长失去控制.

单进程多线程webserver出现后, 就需要一种对全局数据处理的新的方法. 最后这作为新的一层TSRM(线程安全资源管理)

线程安全Vs.废线程安全定义

在一个简单的非线程应用中, 你可能很喜欢定义全局变量, 将它们放在你的源代码的顶部. 编译器会在你的程序的数据段分配内存块保存信息.

在多线程应用中, 每个线程需要它自己的数据元素, 需要为每个线程分配独立的内存块. 一个给定线程在它需要访问自己的数据时需要能够正确的访问到自己的这个内存块.

线程安全数据池

在一个扩展的MINIT阶段, 扩展可以调用ts_allocate_id()一次或多次告诉TSRM层它需要多少数据空间, TSRM接收到通知后, 将总的运行数据空间增大请求的字节数, 并返回一个新的唯一的标识, 标记线程数据池的数据段部分.

typedef struct {  
    int sampleint;  
    char *samplestring;  
} php_sample_globals;  
int sample_globals_id;  
PHP_MINIT_FUNCTION(sample)  
{  
    ts_allocate_id(&sample_globals_id,  
        sizeof(php_sample_globals),  
        (ts_allocate_ctor) php_sample_globals_ctor,  
        (ts_allocate_dtor) php_sample_globals_dtor);  
    return SUCCESS;  
}
Nach dem Login kopieren

当一个请求需要访问数据段的时候,扩展从TSRM层请求当前线程的资源池,以ts_allocate_id()返回的资源ID来获取偏移量。

换句话说,在代码流中,你可能会在前面所说的MINIT语句中碰到SAMPLE_G(sampleint) = 5;这样的语句。在线程安全的构建下,这个语句通过一些宏扩展如下:

(((php_sample_globals*)(*((void ***)tsrm_ls))[sample_globals_id-1])->sampleint = 5;
Nach dem Login kopieren

如果你看不懂上面的转换也不用沮丧,它已经很好的封装在PHPAPI中了,以至于许多开发者都不需要知道它怎样工作的。

当不在线程环境时

因为在PHP的线程安全构建中访问全局资源涉及到在线程数据池查找对应的偏移量,这是一些额外的负载,结果就是它比对应的非线程方式(直接从编译期已经计算好的真实的全局变量地址中取出数据)慢一些。

考虑上面的例子,这一次在非线程构建下:

typedef struct {  
    int sampleint;  
    char *samplestring;  
} php_sample_globals;  
php_sample_globals sample_globals;  
PHP_MINIT_FUNCTION(sample)  
{  
    php_sample_globals_ctor(&sample_globals TSRMLS_CC);  
    return SUCCESS;  
}
Nach dem Login kopieren

首先注意到的是这里并没有定义一个int型的标识去引用全局的结构定义,只是简单的在进程的全局空间定义了一个结构体。也就是说SAMPLE_G(sampleint) = 5;展开后就是sample_globals.sampleint = 5; 简单,快速,高效。

非线程构建还有进程隔离的优势,这样给定的请求碰到完全出乎意料的情况时,它也不会影响其他进程,即便是产生段错误也不会导致整个webserver瘫痪。实际上,Apache的MaxRequestsPerChild指令就是设计用来提升这个特性的,它经常性的有目的性的kill掉子进程并产生新的子进程,来避免某些可能由于进程长时间运行“累积”而来的问题(比如内存泄露)。

访问全局变量

在创建一个扩展时,你并不知道它最终的运行环境是否是线程安全的。幸运的是,你要使用的标准包含文件集合中已经包含了条件定义的ZTS预处理标记。当PHP因为SAPI需要或通过enable-maintainer-zts选项安装等原因以线程安全方式构建时,这个值会被自动的定义,并可以用一组#ifdef ZTS这样的指令集去测试它的值。

就像你前面看到的,只有在PHP以线程安全方式编译时,才会存在线程安全池,只有线程安全池存在时,才会真的在线程安全池中分配空间。这就是为什么前面的例子包裹在ZTS检查中的原因,非线程方式供非线程构建使用。

在本章前面PHP_MINIT_FUNCTION(myextension)的例子中,你可以看到#ifdef ZTS被用作条件调用正确的全局初始代码。对于ZTS模式它使用ts_allocate_id()弹出myextension_globals_id变量,而非ZTS模式只是直接调用myextension_globals的初始化方法。这两个变量已经在你的扩展源文件中使用Zend宏:DECLARE_MODULE_GLOBALS(myextension)声明,它将自动的处理对ZTS的测试并依赖构建的ZTS模式选择正确的方式声明。

在访问这些全局变量的时候,你需要使用前面给出的自定义宏SAMPLE_G()。在第12章,你将学习到怎样设计这个宏以使它可以依赖ZTS模式自动展开。

即便你不需要线程也要考虑线程

正常的PHP构建默认是关闭线程安全的,只有在被构建的sapi明确需要线程安全或线程安全在./configure阶段显式的打开时,才会以线程安全方式构建。

给出了全局查找的速度问题和进程隔离的缺点后,你可能会疑惑为什么明明不需要还有人故意打开它呢?这是因为,多数情况下,扩展和SAPI的开发者认为你是线程安全开关的操作者,这样做可以很大程度上确保新代码可以在所有环境中正常运行。

当线程安全启用时,一个名为tsrm_ls的特殊指针被增加到了很多的内部函数原型中。这个指针允许PHP区分不同线程的数据。回想一下本章前面ZTS模式下的SAMPLE_G()宏函数中就使用了它。没有它,正在执行的函数就不知道查找和设置哪个线程的符号表;不知道应该执行哪个脚本,引擎也完全无法跟踪它的内部寄存器。这个指针保留了线程处理的所有页面请求。

这个可选的指针参数通过下面一组定义包含到原型中。当ZTS禁用时,这些定义都被展开为空;当ZTS开启时,它们展开如下:

#define TSRMLS_D     void ***tsrm_ls  
#define TSRMLS_DC     , void ***tsrm_ls  
#define TSRMLS_C     tsrm_ls  
#define TSRMLS_CC     , tsrm_ls
Nach dem Login kopieren

非ZTS构建对下面的代码看到的是两个参数:int, char *。在ZTS构建下,原型则包含三个参数:int, char *, void ***。当你的程序调用这个函数时,只有在ZTS启用时才需要传递第三个参数。下面代码的第二行展示了宏的展开:

int php_myext_action(int action_id, char *message TSRMLS_DC);  
php_myext_action(42, "The meaning of life" TSRMLS_CC);
Nach dem Login kopieren

通过在函数调用中包含这个特殊的变量,php_myext_action就可以使用tsrm_ls的值和MYEXT_G()宏函数一起访问它的线程特有全局数据。在非ZTS构建上,tsrm_ls将不可用,但是这是ok的,因为此时MYEXT_G()宏函数以及其他类似的宏都不会使用它。

现在考虑,你在一个新的扩展上工作,并且有下面的函数,它可以在你本地使用CLI SAPI的构建上正常工作,并且即便使用apache 1的apxs SAPI编译也可以正常工作:

static int php_myext_isset(char *varname, int varname_len)  
{  
    zval **dummy;  
  
  
    if (zend_hash_find(EG(active_symbol_table),  
        varname, varname_len + 1,  
        (void**)&dummy) == SUCCESS) {  
        /* Variable exists */  
        return 1;  
    } else {  
        /* Undefined variable */  
        return 0;  
    }  
}
Nach dem Login kopieren

所有的一切看起来都工作正常,你打包这个扩展发送给他人构建并运行在生产服务器上。让你气馁的是,对方报告扩展编译失败。

事实上它们使用了Apache 2.0的线程模式,因此它们的php构建启用了ZTS。当编译期碰到你使用的EG()宏函数时,它尝试在本地空间查找tsrm_ls没有找到,因为你并没有定义它并且没有在你的函数中传递。

修复这个问题非常简单;只需要在php_myext_isset()的定义上增加TSRMLS_DC,并在每行调用它的地方增加TSRMLS_CC。不幸的是,现在对方已经有点不信任你的扩展质量了,这样就会推迟你的演示周期。这种问题越早解决越好。

现在有了enable-maintainer-zts指令。通过在./configure时增加该指令来构建php,你的构建将自动的包含ZTS,哪怕你当前的SAPI(比如CLI)不需要它。打开这个开关,你可以避免这些常见的不应该出现的错误。

注意:在PHP4中,enable-maintainer-zts标记等价的名字是enable-experimental-zts;请确认使用你的php版本对应的正确标记。

寻回丢失的tsrm_ls

有时,我们需要在一个函数中使用tsrm_ls指针,但却不能传递它。通常这是因为你的扩展作为某个使用回调的库的接口,它并没有提供返回抽象指针的地方。考虑下面的代码片段:

void php_myext_event_callback(int eventtype, char *message)  
{  
    zval *event;  
  
  
    /* $event = array('event'=>$eventtype, 
                    'message'=>$message) */  
    MAKE_STD_ZVAL(event);  
    array_init(event);  
    add_assoc_long(event, "type", eventtype);  
    add_assoc_string(event, "message", message, 1);  
  
  
    /* $eventlog[] = $event; */  
    add_next_index_zval(EXT_G(eventlog), event);  
}  
PHP_FUNCTION(myext_startloop)  
{  
    /* The eventlib_loopme() function, 
     * exported by an external library, 
     * waits for an event to happen, 
     * then dispatches it to the 
     * callback handler specified. 
     */  
    eventlib_loopme(php_myext_event_callback);  
}
Nach dem Login kopieren

虽然你可能不完全理解这段代码,但你应该注意到了回调函数中使用了EXT_G()宏函数,我们知道在线程安全构建下它需要tsrm_ls指针。修改函数原型并不好也不应该这样做,因为外部的库并不知道php的线程安全模型。那这种情况下怎样让tsrm_ls可用呢?

解决方案是前面提到的名为TSRMLS_FETCH()的Zend宏函数。将它放到代码片段的顶部,这个宏将执行给予当前线程上下文的查找,并定义本地的tsrm_ls指针拷贝。

这个宏可以在任何地方使用并且不用通过函数调用传递tsrm_ls,尽管这看起来很诱人,但是,要注意到这一点:TSRMLS_FETCH调用需要一定的处理时间。这在单次迭代中并不明显,但是随着你的线程数增多,随着你调用TSRMLS_FETCH()的点的增多,你的扩展就会显现出这个瓶颈。因此,请谨慎的使用它。

注意:为了和c++编译器兼容,请确保将TSRMLS_FETCH()和所有变量定义放在给定块作用域的顶部(任何其他语句之前)。因为TSRMLS_FETCH()宏自身有多种不同的解析方式,因此最好将它作为变量定义的最后一行。

小结

本章中主要是对后续章节将要解释的各种概念的一个概览. 你还应该对整件事建立了基础的认识, 它不只是要构建扩展, 还有幕后的Zend引擎和TSRM层, 它们将使你在将php嵌入到你的应用时获利.

以上就是 [翻译][php扩展开发和嵌入式]第1章-php的生命周期的内容,更多相关内容请关注PHP中文网(www.php.cn)!


Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage
Über uns Haftungsausschluss Sitemap
Chinesische PHP-Website:Online-PHP-Schulung für das Gemeinwohl,Helfen Sie PHP-Lernenden, sich schnell weiterzuentwickeln!