首页
关于
Search
1
给你10个市场数据调研报告的免费下载网站!以后竞品数据就从这里找!
185 阅读
2
php接口优化 使用curl_multi_init批量请求
145 阅读
3
《从菜鸟到大师之路 ElasticSearch 篇》
107 阅读
4
2024年备考系统架构设计师
104 阅读
5
PHP 文件I/O
92 阅读
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
登录
Search
标签搜索
php函数
php语法
性能优化
安全
错误和异常处理
问题
vue
Composer
Session
缓存
框架
Swoole
api
并发
异步
正则表达式
php-fpm
mysql 索引
开发规范
协程
dafenqi
累计撰写
786
篇文章
累计收到
34
条评论
首页
栏目
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
副业
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
页面
关于
搜索到
560
篇与
的结果
2023-08-11
深入解析 composer 的自动加载原理
深入解析 composer 的自动加载原理前言PHP 自5.3的版本之后,已经重焕新生,命名空间、性状(trait)、闭包、接口、PSR 规范、以及 composer 的出现已经让 PHP 变成了一门现代化的脚本语言。PHP 的生态系统也一直在演进,而 composer 的出现更是彻底的改变了以往构建 PHP 应用的方式,我们可以根据 PHP 的应用需求混合搭配最合适的 PHP 组件。当然这也得益于 PSR 规范的提出。大纲PHP 自动加载功能PSR 规范comoposer 的自动加载过程composer 源码分析一、PHP 自动加载功能PHP 自动加载功能的由来在 PHP 开发过程中,如果希望从外部引入一个 Class ,通常会使用 include 和 require 方法,去把定义这个 Class 的文件包含进来。这个在小规模开发的时候,没什么大问题。但在大型的开发项目中,使用这种方式会带来一些隐含的问题:如果一个 PHP 文件需要使用很多其它类,那么就需要很多的 require/include 语句,这样有可能会 造成遗漏 或者 包含进不必要的类文件。如果大量的文件都需要使用其它的类,那么要保证每个文件都包含正确的类文件肯定是一个噩梦, 况且 require或 incloud 的性能代价很大。PHP5 为这个问题提供了一个解决方案,这就是 类的自动加载(autoload)机制。autoload机制 可以使得 PHP 程序有可能在使用类时才自动包含类文件,而不是一开始就将所有的类文件include进来,这种机制也称为 Lazy loading (惰性加载)。总结起来,自动加载功能带来了几处优点:使用类之前无需 include / require使用类的时候才会 include / require 文件,实现了 lazy loading ,避免了 include / require 多余文件。无需考虑引入 类的实际磁盘地址 ,实现了逻辑和实体文件的分离。PHP 自动加载函数 __autoload()从 PHP5 开始,当我们在使用一个类时,如果发现这个类没有加载,就会自动运行 __autoload() 函数,这个函数是我们在程序中自定义的,在这个函数中我们可以加载需要使用的类。下面是个简单的示例:<?phpfunction __autoload($classname) { require_once ($classname . ".class.php");}在我们这个简单的例子中,我们直接将类名加上扩展名 .class.php 构成了类文件名,然后使用 require_once 将其加载。从这个例子中,我们可以看出 __autoload 至少要做三件事情:根据类名确定类文件名;确定类文件所在的磁盘路径;将类从磁盘文件中加载到系统中。第三步最简单,只需要使用 include / require 即可。要实现第一步,第二步的功能,必须在开发时约定类名与磁盘文件的映射方法,只有这样我们才能根据类名找到它对应的磁盘文件。当有大量的类文件要包含的时候,我们只要确定相应的规则,然后在 __autoload() 函数中,将类名与实际的磁盘文件对应起来,就可以实现 lazy loading 的效果 。如果想详细的了解关于 autoload 自动加载的过程,可以查看手册资料:PHP autoload函数说明__autoload() 函数存在的问题如果在一个系统的实现中,如果需要使用很多其它的类库,这些类库可能是由不同的开发人员编写的, 其类名与实际的磁盘文件的映射规则不尽相同。这时如果要实现类库文件的自动加载,就必须 在 __autoload() 函数中将所有的映射规则全部实现,这样的话 __autoload() 函数有可能会非常复杂,甚至无法实现。最后可能会导致 __autoload()函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。那么问题出现在哪里呢?问题出现在 __autoload() 是全局函数只能定义一次 ,不够灵活,所以所有的类名与文件名对应的逻辑规则都要在一个函数里面实现,造成这个函数的臃肿。那么如何来解决这个问题呢?答案就是使用一个 __autoload调用堆栈 ,不同的映射关系写到不同的 __autoload函数 中去,然后统一注册统一管理,这个就是 PHP5 引入的 SPL Autoload 。SPL AutoloadSPL是 Standard PHP Library(标准PHP库)的缩写。它是 PHP5 引入的一个扩展标准库,包括 spl autoload 相关的函数以及各种数据结构和迭代器的接口或类。spl autoload 相关的函数具体可见 php中spl_autoload<?php// __autoload 函数//// function __autoload($class) {// include 'classes/' . $class . '.class.php';// }function my_autoloader($class) {include 'classes/' . $class . '.class.php';}spl_autoload_register('my_autoloader');// 定义的 autoload 函数在 class 里// 静态方法class MyClass { public static function autoload($className) {// ...}}spl_autoload_register(array('MyClass', 'autoload'));// 非静态方法class MyClass { public function autoload($className) {// ...}}$instance = new MyClass();spl_autoload_register(array($instance, 'autoload'));spl_autoload_register() 就是我们上面所说的__autoload调用堆栈,我们可以向这个函数注册多个我们自己的 autoload() 函数,当 PHP 找不到类名时,PHP就会调用这个堆栈,然后去调用自定义的 autoload() 函数,实现自动加载功能。如果我们不向这个函数输入任何参数,那么就会默认注册 spl_autoload() 函数。二、PSR 规范与自动加载相关的规范是 PSR4,在说 PSR4 之前先介绍一下 PSR 标准。PSR 标准的发明和推出组织是:PHP-FIG,它的网站是:www.php-fig.org。由几位开源框架的开发者成立于 2009 年,从那开始也选取了很多其他成员进来,虽然不是 “官方” 组织,但也代表了社区中不小的一块。组织的目的在于:以最低程度的限制,来统一各个项目的编码规范,避免各家自行发展的风格阻碍了程序员开发的困扰,于是大伙发明和总结了 PSR,PSR 是 PHP Standards Recommendation 的缩写,截止到目前为止,总共有 14 套 PSR 规范,其中有 7 套PSR规范已通过表决并推出使用,分别是:PSR-0 自动加载标准(已废弃,一些旧的第三方库还有在使用)PSR-1 基础编码标准PSR-2 编码风格向导PSR-3 日志接口PSR-4 自动加载的增强版,替换掉了 PSR-0PSR-6 缓存接口规范PSR-7 HTTP 消息接口规范具体详细的规范标准可以查看PHP 标准规范PSR4 标准2013 年底,PHP-FIG 推出了第 5 个规范——PSR-4。PSR-4 规范了如何指定文件路径从而自动加载类定义,同时规范了自动加载文件的位置。1)一个完整的类名需具有以下结构:\<命名空间>\<子命名空间>\<类名>完整的类名必须要有一个顶级命名空间,被称为 "vendor namespace";完整的类名可以有一个或多个子命名空间;完整的类名必须有一个最终的类名;完整的类名中任意一部分中的下滑线都是没有特殊含义的;完整的类名可以由任意大小写字母组成;所有类名都必须是大小写敏感的。2)根据完整的类名载入相应的文件完整的类名中,去掉最前面的命名空间分隔符,前面连续的一个或多个命名空间和子命名空间,作为「命名空间前缀」,其必须与至少一个「文件基目录」相对应;紧接命名空间前缀后的子命名空间 必须 与相应的「文件基目录」相匹配,其中的命名空间分隔符将作为目录分隔符。末尾的类名必须与对应的以 .php 为后缀的文件同名。自动加载器(autoloader)的实现一定不可抛出异常、一定不可触发任一级别的错误信息以及不应该有返回值。3) 例子PSR-4风格类名:ZendAbc命名空间前缀:Zend文件基目录:/usr/includes/Zend/文件路径:/usr/includes/Zend/Abc.php类名:SymfonyCoreRequest命名空间前缀:SymfonyCore文件基目录:./vendor/Symfony/Core/文件路径:./vendor/Symfony/Core/Request.php目录结构-vendor/| -vendor_name/| | -package_name/| | | -src/| | | | -ClassName.php # Vendor_Name\Package_Name\ClassName| | | -tests/| | | | -ClassNameTest.php # Vendor_Name\Package_Name\ClassNameTestComposer自动加载过程Composer 做了哪些事情你有一个项目依赖于若干个库。其中一些库依赖于其他库。你声明你所依赖的东西。Composer 会找出哪个版本的包需要安装,并安装它们(将它们下载到你的项目中)。例如,你正在创建一个项目,需要做一些单元测试。你决定使用 phpunit 。为了将它添加到你的项目中,你所需要做的就是在 composer.json 文件里描述项目的依赖关系。{ "require": { "phpunit/phpunit":"~6.0",} }然后在 composer require 之后我们只要在项目里面直接 use phpunit 的类即可使用。执行 composer require 时发生了什么composer 会找到符合 PR4 规范的第三方库的源将其加载到 vendor 目录下初始化顶级域名的映射并写入到指定的文件里(如:'PHPUnit\Framework\Assert' => DIR . '/..' . '/phpunit/phpunit/src/Framework/Assert.php')写好一个 autoload 函数,并且注册到 spl_autoload_register()里题外话:现在很多框架都已经帮我们写好了顶级域名映射了,我们只需要在框架里面新建文件,在新建的文件中写好命名空间,就可以在任何地方 use 我们的命名空间了。Composer 源码分析下面我们通过对源码的分析来看看 composer 是如何实现 PSR4标准 的自动加载功能。很多框架在初始化的时候都会引入 composer 来协助自动加载的,以 Laravel 为例,它入口文件 index.php 第一句就是利用 composer 来实现自动加载功能。启动<?php define('LARAVEL_START', microtime(true));require DIR . '/../vendor/autoload.php';去 vendor 目录下的 autoload.php :<?php require_once DIR . '/composer' . '/autoload_real.php';return ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29::getLoader();这里就是 Composer 真正开始的地方了Composer自动加载文件首先,我们先大致了解一下Composer自动加载所用到的源文件。autoload_real.php: 自动加载功能的引导类。composer 加载类的初始化(顶级命名空间与文件路径映射初始化)和注册(spl_autoload_register())。ClassLoader.php : composer 加载类。composer 自动加载功能的核心类。autoload_static.php : 顶级命名空间初始化类,用于给核心类初始化顶级命名空间。autoload_classmap.php : 自动加载的最简单形式,有完整的命名空间和文件目录的映射;autoload_files.php : 用于加载全局函数的文件,存放各个全局函数所在的文件路径名;autoload_namespaces.php : 符合 PSR0 标准的自动加载文件,存放着顶级命名空间与文件的映射;autoload_psr4.php : 符合 PSR4 标准的自动加载文件,存放着顶级命名空间与文件的映射;autoload_real 引导类在 vendor 目录下的 autoload.php 文件中我们可以看出,程序主要调用了引导类的静态方法 getLoader() ,我们接着看看这个函数。<?phppublic static function getLoader() { if (null !== self::$loader) { return self::$loader; } spl_autoload_register( array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'), true, true ); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister( array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader') ); $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION'); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func( \Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::getInitializer($loader) ); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } /***********************注册自动加载核心类对象********************/ $loader->register(true); /***********************自动加载全局函数********************/ if ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file); } return $loader; }我把自动加载引导类分为 5 个部分。第一部分——单例第一部分很简单,就是个最经典的单例模式,自动加载类只能有一个。<?php if (null !== self::$loader) { return self::$loader;}第二部分——构造ClassLoader核心类第二部分 new 一个自动加载的核心类对象。<?php /获得自动加载核心类对象*/ spl_autoload_register(array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'), true, true);self::$loader = $loader = new \Composer\Autoload\ClassLoader();spl_autoload_unregister(array('ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'));loadClassLoader()函数:<?phppublic static function loadClassLoader($class){if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; }}从程序里面我们可以看出,composer 先向 PHP 自动加载机制注册了一个函数,这个函数 require 了 ClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。第三部分 —— 初始化核心类对象<?php /初始化自动加载核心类对象*/ $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION'); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func( \Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::getInitializer($loader) );} else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } 这一部分就是对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。初始化的方法有两种:使用 autoload_static 进行静态初始化;调用核心类接口初始化。autoload_static 静态初始化 ( PHP >= 5.6 )静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机。我们深入 autoload_static.php 这个文件发现这个文件定义了一个用于静态初始化的类,名字叫 ComposerStaticInit7b790917ce8899df9af8ed53631a1c29,仍然为了避免冲突而加了 hash 值。这个类很简单:<?php class ComposerStaticInit7b790917ce8899df9af8ed53631a1c29{ public static $files = array(...); public static $prefixLengthsPsr4 = array(...); public static $prefixDirsPsr4 = array(...); public static $prefixesPsr0 = array(...); public static $classMap = array (...); public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { $loader->prefixLengthsPsr4 = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixLengthsPsr4; $loader->prefixDirsPsr4 = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixDirsPsr4; $loader->prefixesPsr0 = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$prefixesPsr0; $loader->classMap = ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$classMap; }, null, ClassLoader::class);}这个静态初始化类的核心就是 getInitializer() 函数,它将自己类中的顶级命名空间映射给了 ClassLoader 类。值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 ClassLoader类 中的 prefixLengthsPsr4 、prefixDirsPsr4等等变量都是 private的。利用匿名函数的绑定功能就可以将这些 private 变量赋给 ClassLoader 类 里的成员变量。关于匿名函数的绑定功能。接下来就是命名空间初始化的关键了。classMap(命名空间映射)<?php public static $classMap = array ( 'App\\Console\\Kernel' => __DIR__ . '/../..' . '/app/Console/Kernel.php', 'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php', 'App\\Http\\Controllers\\Auth\\ForgotPasswordController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php', 'App\\Http\\Controllers\\Auth\\LoginController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php', 'App\\Http\\Controllers\\Auth\\RegisterController' => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php',...)直接命名空间全名与目录的映射,简单粗暴,也导致这个数组相当的大。PSR4 标准顶级命名空间映射数组:<?php public static $prefixLengthsPsr4 = array( 'p' => array ( 'phpDocumentor\\Reflection\\' => 25, ), 'S' => array ( 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Component\\Yaml\\' => 23, 'Symfony\\Component\\VarDumper\\' => 28, ... ),...);public static $prefixDirsPsr4 = array ( 'phpDocumentor\\Reflection\\' => array ( 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src', 1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src', 2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src', ), 'Symfony\\Polyfill\\Mbstring\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', ), 'Symfony\\Component\\Yaml\\' => array ( 0 => __DIR__ . '/..' . '/symfony/yaml', ),...)PSR4 标准顶级命名空间映射用了两个数组,第一个是用命名空间第一个字母作为前缀索引,然后是 顶级命名空间,但是最终并不是文件路径,而是 顶级命名空间的长度。为什么呢?因为 PSR4 标准是用顶级命名空间目录替换顶级命名空间,所以获得顶级命名空间的长度很重要。具体说明这些数组的作用:假如我们找 Symfony\Polyfill\Mbstring\example 这个命名空间,通过前缀索引和字符串匹配我们得到了<?php'Symfony\\Polyfill\\Mbstring\\' => 26,这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4数组 获取它的映射目录数组:(注意映射目录可能不止一条)<?php 'Symfony\Polyfill\Mbstring\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', )然后我们就可以将命名空间 Symfony\Polyfill\Mbstring\example 前26个字符替换成目录 DIR . '/..' . '/symfony/polyfill-mbstring ,我们就得到了__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php,先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。ClassLoader 接口初始化( PHP < 5.6 )如果PHP版本低于 5.6 或者使用 HHVM 虚拟机环境,那么就要使用核心类的接口进行初始化。<?php// PSR0 标准 $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } // PSR4 标准 $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); }PSR4 标准的映射autoload_psr4.php 的顶级命名空间映射<?phpreturn array( 'XdgBaseDir\\' => array($vendorDir . '/dnoegel/php-xdg-base-dir/src'), 'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'), 'TijsVerkoyen\\CssToInlineStyles\\' => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'), 'Tests\\' => array($baseDir . '/tests'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), ... )PSR4 标准的初始化接口:<?phppublic function setPsr4($prefix, $paths) { if (!$prefix) { $this->fallbackDirsPsr4 = (array) $paths; } else { $length = strlen($prefix); if ('\\' !== $prefix[$length - 1]) { throw new \InvalidArgumentException( "A non-empty PSR-4 prefix must end with a namespace separator." ); } $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; $this->prefixDirsPsr4[$prefix] = (array) $paths; } }总结下上面的顶级命名空间映射过程:( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )( 顶级命名空间 -> 目录 )这两个映射数组。具体形式也可以查看下面的 autoload_static 的 $prefixLengthsPsr4 、 $prefixDirsPsr4 。命名空间映射autoload_classmap:<?phppublic static $classMap = array ('App\\Console\\Kernel' => __DIR__ . '/../..' . '/app/Console/Kernel.php', 'App\\Exceptions\\Handler' => __DIR__ . '/../..' . '/app/Exceptions/Handler.php', ...)addClassMap:<?phppublic function addClassMap(array $classMap) { if ($this->classMap) { $this->classMap = array_merge($this->classMap, $classMap); } else { $this->classMap = $classMap; } }自动加载核心类 ClassLoader 的静态初始化到这里就完成了!其实说是5部分,真正重要的就两部分——初始化与注册。初始化负责顶层命名空间的目录映射,注册负责实现顶层以下的命名空间映射规则。第四部分 —— 注册讲完了 Composer 自动加载功能的启动与初始化,经过启动与初始化,自动加载核心类对象已经获得了顶级命名空间与相应目录的映射,也就是说,如果有命名空间 'App\Console\Kernel,我们已经可以找到它对应的类文件所在位置。那么,它是什么时候被触发去找的呢?这就是 composer 自动加载的核心了,我们先回顾一下自动加载引导类:public static function getLoader() {/***************************经典单例模式********************/ if (null !== self::$loader) { return self::$loader; } /***********************获得自动加载核心类对象********************/ spl_autoload_register(array('ComposerAutoloaderInit 7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit 7b790917ce8899df9af8ed53631a1c29', 'loadClassLoader')); /***********************初始化自动加载核心类对象********************/ $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION'); if ($useStaticLoader) { require_once __DIR__ . '/autoload_static.php'; call_user_func(\Composer\Autoload\ComposerStaticInit 7b790917ce8899df9af8ed53631a1c29::getInitializer($loader)); } else { $map = require __DIR__ . '/autoload_namespaces.php'; foreach ($map as $namespace => $path) { $loader->set($namespace, $path); } $map = require __DIR__ . '/autoload_psr4.php'; foreach ($map as $namespace => $path) { $loader->setPsr4($namespace, $path); } $classMap = require __DIR__ . '/autoload_classmap.php'; if ($classMap) { $loader->addClassMap($classMap); } } /***********************注册自动加载核心类对象********************/ $loader->register(true); /***********************自动加载全局函数********************/ if ($useStaticLoader) { $includeFiles = Composer\Autoload\ComposerStaticInit 7b790917ce8899df9af8ed53631a1c29::$files; } else { $includeFiles = require __DIR__ . '/autoload_files.php'; } foreach ($includeFiles as $fileIdentifier => $file) { composerRequire 7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file); } return $loader;} 现在我们开始引导类的第四部分:注册自动加载核心类对象。我们来看看核心类的 register() 函数:public function register($prepend = false){spl_autoload_register(array($this, 'loadClass'), true, $prepend);}其实奥秘都在自动加载核心类 ClassLoader 的 loadClass() 函数上:public function loadClass($class){ if ($file = $this->findFile($class)) { includeFile($file); return true; } }这个函数负责按照 PSR 标准将顶层命名空间以下的内容转为对应的目录,也就是上面所说的将 'App\Console\Kernel 中' Console\Kernel 这一段转为目录,至于怎么转的在下面 “运行”的部分讲。核心类 ClassLoader 将 loadClass() 函数注册到PHP SPL中的 spl_autoload_register() 里面去。这样,每当PHP遇到一个不认识的命名空间的时候,PHP会自动调用注册到 spl_autoload_register 里面的 loadClass() 函数,然后找到命名空间对应的文件。全局函数的自动加载Composer 不止可以自动加载命名空间,还可以加载全局函数。怎么实现的呢?把全局函数写到特定的文件里面去,在程序运行前挨个 require就行了。这个就是 composer 自动加载的第五步,加载全局函数。if ($useStaticLoader) {$includeFiles = Composer\Autoload\ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files;} else {$includeFiles = require __DIR__ . '/autoload_files.php';}foreach ($includeFiles as $fileIdentifier => $file) {composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file);}跟核心类的初始化一样,全局函数自动加载也分为两种:静态初始化和普通初始化,静态加载只支持PHP5.6以上并且不支持HHVM。静态初始化:ComposerStaticInit7b790917ce8899df9af8ed53631a1c29::$files:public static $files = array ('0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => DIR . '/..' . '/symfony/polyfill-mbstring/bootstrap.php','667aeda72477189d0494fecd327c3641' => DIR . '/..' . '/symfony/var-dumper/Resources/functions/dump.php',...);普通初始化autoload_files:$vendorDir = dirname(dirname(__FILE__));$baseDir = dirname($vendorDir);return array('0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php','667aeda72477189d0494fecd327c3641' => $vendorDir . '/symfony/var-dumper/Resources/functions/dump.php', ....);其实跟静态初始化区别不大。加载全局函数class ComposerAutoloaderInit7b790917ce8899df9af8ed53631a1c29{ public static function getLoader(){ ... foreach ($includeFiles as $fileIdentifier => $file) { composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file); } ...}}function composerRequire7b790917ce8899df9af8ed53631a1c29($fileIdentifier, $file) {if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { require $file; $GLOBALS['__composer_autoload_files'][$fileIdentifier] = true; }}第五部分 —— 运行到这里,终于来到了核心的核心—— composer 自动加载的真相,命名空间如何通过 composer 转为对应目录文件的奥秘就在这一章。前面说过,ClassLoader 的 register() 函数将 loadClass() 函数注册到 PHP 的 SPL 函数堆栈中,每当 PHP 遇到不认识的命名空间时就会调用函数堆栈的每个函数,直到加载命名空间成功。所以 loadClass() 函数就是自动加载的关键了。看下 loadClass() 函数:public function loadClass($class){if ($file = $this->findFile($class)) { includeFile($file); return true; }}public function findFile($class){// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731 if ('\\' == $class[0]) { $class = substr($class, 1); } // class map lookup if (isset($this->classMap[$class])) { return $this->classMap[$class]; } if ($this->classMapAuthoritative) { return false; } $file = $this->findFileWithExtension($class, '.php'); // Search for Hack files if we are running on HHVM if ($file === null && defined('HHVM_VERSION')) { $file = $this->findFileWithExtension($class, '.hh'); } if ($file === null) { // Remember that this class does not exist. return $this->classMap[$class] = false; } return $file;}我们看到 loadClass() ,主要调用 findFile() 函数。findFile() 在解析命名空间的时候主要分为两部分:classMap 和 findFileWithExtension() 函数。classMap 很简单,直接看命名空间是否在映射数组中即可。麻烦的是 findFileWithExtension() 函数,这个函数包含了 PSR0 和 PSR4 标准的实现。还有个值得我们注意的是查找路径成功后 includeFile() 仍然是外面的函数,并不是 ClassLoader 的成员函数,原理跟上面一样,防止有用户写 $this 或 self。还有就是如果命名空间是以\开头的,要去掉\然后再匹配。看下 findFileWithExtension 函数:private function findFileWithExtension($class, $ext){// PSR-4 lookup $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; $first = $class[0]; if (isset($this->prefixLengthsPsr4[$first])) { foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) { if (0 === strpos($class, $prefix)) { foreach ($this->prefixDirsPsr4[$prefix] as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { return $file; } } } } } // PSR-4 fallback dirs foreach ($this->fallbackDirsPsr4 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { return $file; } } // PSR-0 lookup if (false !== $pos = strrpos($class, '\\')) { // namespaced class name $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); } else { // PEAR-like class name $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; } if (isset($this->prefixesPsr0[$first])) { foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { if (0 === strpos($class, $prefix)) { foreach ($dirs as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } } } } // PSR-0 fallback dirs foreach ($this->fallbackDirsPsr0 as $dir) { if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { return $file; } } // PSR-0 include paths. if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { return $file; }}最后小结我们通过举例来说下上面代码的流程:如果我们在代码中写下 new phpDocumentor\Reflection\Element(),PHP 会通过 SPL_autoload_register 调用 loadClass -> findFile -> findFileWithExtension。步骤如下:将 \ 转为文件分隔符/,加上后缀php,变成 $logicalPathPsr4, 即 phpDocumentor/Reflection//Element.php;利用命名空间第一个字母p作为前缀索引搜索 prefixLengthsPsr4 数组,查到下面这个数组: p' => array ( 'phpDocumentor\\Reflection\\' => 25, 'phpDocumentor\\Fake\\' => 19, )遍历这个数组,得到两个顶层命名空间 phpDocumentor\Reflection\ 和 phpDocumentor\Fake\在这个数组中查找 phpDocumentor\Reflection\Element,找出 phpDocumentor\Reflection\ 这个顶层命名空间并且长度为25。在prefixDirsPsr4 映射数组中得到phpDocumentor\Reflection\ 的目录映射为:'phpDocumentor\\Reflection\\' => array ( 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src', 1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src', 2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src', ),遍历这个映射数组,得到三个目录映射;查看 “目录+文件分隔符//+substr($logicalPathPsr4, $length)”文件是否存在,存在即返回。这里就是'__DIR__/../phpdocumentor/reflection-common/src + substr(phpDocumentor/Reflection/Element.php,25)'如果失败,则利用 fallbackDirsPsr4 数组里面的目录继续判断是否存在文件以上就是 composer 自动加载的原理解析!
2023年08月11日
14 阅读
0 评论
0 点赞
2023-08-11
PHP环境变量如何工作
PHP环境变量如何工作介绍Laravel,Symfony和其他现代PHP框架使用环境变量来存储从一台机器更改为下一台机器的安全凭证和配置。最新的Laravel版本对环境变量的加载方式做了一些小改动。这一变化最终破坏了第三方图书馆和Laravel本身。随后的讨论清楚地表明,许多开发人员(包括我自己)都没有意识到PHP中的复杂环境变量实际上是多少。有很多方法可以阅读它们,因为它们可以编写它们而且没有一个选项是万无一失的。让我们分解环境变量,它们如何工作以及如何在代码中正确使用它们。什么是环境变量?自20世纪80年代以来,每个流行的操作系统都支持变量,就像编程语言一样。当进程启动时,它继承父进程的变量。该过程使用这些变量来发现有关其运行环境的信息,例如保存临时文件的首选位置或用户主目录的位置。如果您使用的是Unix操作系统,如MacOS或Linux,您可以打开终端并查看$HOME环境变量的值,如下所示:» echo $HOME/Users/matt如果您使用的是Windows,则可以打开Powershell并输入以下内容:Write-Output $env:HOMEPATH通常,环境变量以大写形式写入,下划线分隔单词LIKE_THIS。对应用程序配置使用环境变量在十二因子应用方法推广使用环境变量来存储配置软件的想法。从那以后,它成为事实上的标准,拥有来自Web框架,云提供商以及您用于构建软件的任何其他内容的一流支持。有一些主要的缺点,所以如果你还没有,请在采用它们之前进行研究。如果您已经在使用它们,请继续阅读以了解如何安全地使用它们。设置环境变量我们将讨论如何设置环境变量,以便应用程序可以访问它。CLIshell中设置的任何环境变量都可用于您启动的任何进程。例如,您已经可以访问HOME我们在上面发现的变量:» php -r 'var_dump(getenv("HOME"));'string(11) "/Users/matt"但是,您可能希望添加自己的变量。有很多方法可以做到这一点。最简单的方法是在运行命令之前声明环境变量:» APP_ENV=local php -r 'var_dump(getenv("APP_ENV"));'string(5) "local"它不会持久存在,因此每次运行命令时都需要添加它。当您添加更多环境变量时,这很快就会变得烦人。你真的不想在生产中使用这种技术,但它对于快速测试某些东西很方便。Unix系统的另一个有用技巧是使用该export命令。导出环境变量后,它将在所有后续命令中可用,直到退出shell。» export APP_ENV=local» php -r 'var_dump(getenv("APP_ENV"));'string(5) "local"永久设置环境变量还有很多其他选项,但它们并不真正用于保密,通常需要以纯文本形式存储环境变量。卷筒纸当我们的Web服务器处理请求时,我们不会自己启动该过程。相反,PHP-FPM产生了这个过程。第一个选项是从PHP-FPM传递环境变量。默认情况下,PHP-FPM在启动PHP进程之前清除现有的环境变量。您可以使用clear_env配置指令禁用它。清理环境后,您可以使用以下env[name] = value语法添加自己的变量:; somewhere in the pool configuration file (www.conf by default); declare a new environment variableenv[APP_ENV] = production; reference an existing environment variableenv[DB_NAME] = $DB_NAME第二个选项是从Web服务器传递环境变量。你可以在Caddy中使用env参数,在NGINX中使用fastcgi_param,在apache中使用PassEnv或配置SetEnv。确认无法从互联网访问PHP-FPM!否则,任何人都可以使用Web服务器用于将环境变量传递给应用程序的相同机制来注入环境变量。检查listen.allowed_clients设置。.ENV因为设置环境变量很麻烦,所以Ruby社区提出了.env文件约定。您将环境变量声明在.env项目根目录中调用的文件中,并且库在引导时将所有环境变量加载到应用程序中。最初.env文件不适合生产。将所有秘密保留为纯文本并且解析文件很慢是不安全的。但是,它似乎相当普遍。Laravel只通过访问配置文件中的环境变量然后缓存配置来解决解析开销问题。Symfony 建议不要在生产中使用他们的DotEnv组件。云提供商云提供商通常会这么做,所以请先检查一下。Heroku有一个config:set命令。Laravel Forge允许从控制面板添加环境变量。Fortrabbit也是如此。如果您正在配置自己的服务器,则上面列出的选项可以使用,但它们不是很安全。Kubernetes支持定义环境变量并为环境变量使用秘密。如果您正在使用Hashicorp的Vault和Consul,您可以使用envconsul来启动填充了环境变量的流程。阅读环境变量在PHP中有3种不同的方法来读取环境变量。如果要在PHP中设置环境变量(.env库的工作方式),还有3种方法可以设置它们。了解差异非常重要,因为每种方法都可以根据服务器的配置方式返回不同的数据。$ _SERVER和$ _ENV该$_SERVER超全局包含除任何Web服务器一起传递环境变量。如果从variables_order指令中删除'S' $_SERVER将不会被填充。» APP_ENV=local php -d variables_order=EGPC -r 'var_dump($_SERVER["APP_ENV"] ?? false);'bool(false)还有一个$_ENV超全球。就像$_SERVER可以通过E从variables_order指令中删除它来禁用它。开发和生产的默认值是GPCS,意味着$_ENV您的服务器很可能是空的。那么$_ENV和之间的区别是$_SERVER什么?在CGI模式下,什么都没有。使用内置Web服务器时,仅$_ENV包含环境变量,并且仅$_SERVER包含服务器变量,例如标头,路径和脚本位置。当运行CLI脚本$_SERVER并且$_ENV包含环境变量时,$_SERVER还包含请求信息和CLI参数。最终,由SAPI来填充每个超全球。$_ENV并且$_SERVER是两个不同的变量 - 改变一个不会改变另一个。 $_ENV并$_SERVER在第一次访问时填充。如果auto_globals_jit禁用该指令,则会在脚本启动时填充它们。如果在填充变量(即通过调用putenv)后更改环境,则不会更新超全局。同样更新$_ENV或$_SERVER不会改变实际环境。如果您想改变您必须呼叫的实际环境putenv。GETENV该getenv功能与$_ENV超全球功能类似。但是,与superglobals不同,getenv不能用variables_order指令禁用。» APP_ENV=local php -d variables_order= -r 'var_dump(getenv("APP_ENV"));'string(5) "local"那么当你打电话时会发生什么getenv('APP_ENV')?让我们看看源代码,了解它是如何工作的。PHP_FUNCTION(getenv){ // ... if (!local_only) {ptr = sapi_getenv(str, str_len); if (ptr) { RETVAL_STRING(ptr); efree(ptr); return; }}// ...}首先,我们调用sapi_getenv如果local_only参数是假的。此函数是SAPI用于加载常规环境中不存在的变量的挂钩。这是getenv可以返回HTTP标头的原因。PHP_FUNCTION(getenv){ // .../ system method returns a const / ptr = getenv(str); if (ptr) {RETURN_STRING(ptr);}RETURN_FALSE;}接下来我们调用getenvc函数(在Unix上; Windows调用GetEnvironmentVariableW)。这非常重要。超级全局只会在首次初始化时读取系统环境变量。 getenv每次调用时都会读取系统环境变量。如果您使用线程,这将成为一个问题。线程安全c函数getenv不需要是线程安全的。如果getenv在另一个线程正在调用时调用putenv它,则可能导致分段错误。使用以下代码可以很容易地说明这一点。您将需要编译PHP zts并pthreads启用扩展程序来运行它。<?php$worker = new class() extends \Thread {function run() { while (true) { putenv('RAND' . rand() . '=value'); } }};$worker->start();while (true) {getenv('FOO');}如果从命令行运行此命令,则应在30秒内看到段错误。» php env_crash.phpSegmentation fault这篇优秀的文章深入解释了这个问题,并包含了一个示例c程序,如果您没有pthreads安装,可以运行该程序。我们如何避免分段错误?一些开发人员已经开始建议您使用$_SERVER或$_ENV代替getenv读取环境变量。这当然可以避免这个问题,但并不像你想象的那么容易。如上所述,如果您不控制服务器,则无法保证$_SERVER并将$_ENV启用。 $_ENV默认情况下禁用。它不太可能$_SERVER被禁用,但如果您使用$_SERVER您的应用程序将无法使用PHP的内置Web服务器。其次,很难保证你所依赖的所有C库都会避免getenv。例如, 如果您未指定,则finfo_open调用。偶数并在初始化时调用。您需要审核所有libc,PHP,每个PHP库,并保证不会调用每个扩展。getenv$magic_file$_SERVER$_ENVgetenv getenv第三,如果你正在使用pthreads,那么superglobals在工作线程中是空的。访问主线程的环境变量的唯一方法是调用getenv。一个更简单的解决方案是避免调用putenv工作线程。如果您使用putenv填充环境变量,则只需执行一次。每个工作线程都将继承父线程的环境变量,因此您无需再次填充它们。在创建线程之前获取引导程序,您将不会遇到任何问题。一些开发人员使用线程Web服务器,因此他们无法在工作线程之外实际执行其代码。PHP 无论如何都不应该与线程服务器一起使用,但是如果你坚持这样做,你可以通过只调用putenvif getenv返回false并将整个事物包装在互斥锁中来避免分段错误。由于环境变量在线程之间共享,因此只有第一个请求才会调用putenv。产卵过程环境变量不仅在线程之间共享,它们也与子进程共享。当你生成一个进程有exec,passthru,system,shell_exec,proc_open,或反引号操作符子进程继承父进程的环境。» APP_ENV=local php -r 'passthru("env");'TERM_PROGRAM=Apple_TerminalSHELL=/bin/zshTERM=xterm-256colorTMPDIR=/var/folders/9_/wn_qf7x97tl1l86lfxl1shg00000gn/T/USER=mattAPP_ENV=local如果您将环境变量用于机密并生成不受信任的子进程,则这可能是一个安全问题。如上所述,添加变量$_ENV或$_SERVER不将其添加到实际环境中。只返回的环境变量getenv将传递给子进程。添加的任何变量putenv都将传递给子进程,因为它putenv会修改环境。proc_open允许您指定应传递给子流程的环境变量。您可以proc_open在不希望将应用程序机密传递给子进程的情况下使用。该Symfony的进程组件 并 通过$_SERVER,并$_ENV在默认情况下的子进程。为了防止您可以显式设置环境变量。注意HTTP标头我在上面提到了这一点,但重要的是它应该属于自己的部分。 访问环境变量的每种方法都可以返回HTTP标头,包括getenv。当头文件包含在CGI应用程序中的环境中时,它的前缀是HTTP_。由于宣布了“httpoxy”漏洞,PHP不会让Proxy标题覆盖HTTP_PROXY,但是以HTTP_(ie HTTP_PROXIES)开头的任何其他环境变量仍然会受到影响。总之,永远不要使用以...开头的环境变量HTTP_。getenv允许你传递第二个参数,local_only。如果为true ,则不会检查SAPI。if是否local_only为真HTTP标头,fpm.conf中设置的变量以及Web服务器配置中设置的变量将被排除。local_only返回所有环境变量时无法使用- getenv(null, true)将返回false。保密秘密泄漏环境变量要比泄漏PHP变量容易得多。如果您将环境变量用于保密,那么了解泄漏环境变量的所有方法非常重要。来自$_ENV,$_SERVER和的环境变量getenv在phpinfo输出中可见。在命令行上传递的环境变量可以显示在shell历史记录中。您可以查看正在运行的进程的环境变量,但前提是该进程是您的进程或您是root用户。如果您使用的是.env文件并且它位于公共目录中,则Web服务器将以明文形式提供该文件。注意路径遍历攻击。如果你被欺骗要求或包含.env文件,秘密将在PHP脚本的输出中呈现。环境变量是全球性的。任何PHP或C代码都可以访问它们,而无需了解您的应用程序。环境变量传递给子进程,线程和分支。记录错误处理程序很常见$_SERVER。两个哨兵和减速板这样做。getenv并不总是有效在研究这篇文章时,我遇到了一些getenv我以前从未见过的错误。当使用getenv接收fastcgi_param时,其他任何人都会遇到这种非常奇怪的#php行为?我可以使用getenv访问它,但不能使用getenv($ key)。pic.twitter.com/98ah3yJl1n马特艾伦(@__mattallan),2019年3月22日另一个奇怪的getenv事情:如果你启用了auto_globals_jit,你必须访问脚本中的某个地方(甚至在!之后)才能运行getenv。 pic.twitter.com/Fvhkn3Zk8s马特艾伦(@__mattallan),2019年3月22日第一个问题只发生在Web服务器设置的变量上,即NGINX fastcgi_param。第二个问题发生在由Web服务器设置或由PHP-FPM设置的变量中。第二个问题非常惊人。当你考虑如何auto_globals_jit工作(“在编译期间检查SERVER,REQUEST和ENV变量的使用”)时,这是有道理的,但我认为这不是故意的。结论PHP中的环境变量令人困惑,不一致,有时甚至是危险的。CGI通过将用户输入合并到环境变量中使问题变得更糟。如果你不得不使用它们getenv,请避免调用putenv线程,永远不要信任一个以变量开头的变量HTTP_,并验证你没有泄露其他进程或服务的秘密。如果您正在编写将由没有经验的系统管理员配置的软件,那么最好完全避免使用环境变量。免责声明:我不是安全专家,本文未涵盖所有可能的安全风险。由您决定什么对您的情况安全。
2023年08月11日
6 阅读
0 评论
0 点赞
2023-08-11
通过PHP单例模式与长连接减少MySql连接数
通过PHP单例模式与长连接减少MySql连接数此单例代码有问题,请参考下面的 在Mysql驱动的PHP网站中,MySql连接的一般都是利用脚本的结束来进行释放,在一些分层写的php网站中,若一个页面含有多个数据访问类,由于每个数据访问类都会有数据库的连接,导致这一个页面在脚本结束前会有多个数据库连接,在一些大型的页面连接可能多至数十上百,为此需要进行必要的控制,对于解释性的PHP语言,脚本是顺序执行的,也就是说数据库连接的利用同时只有一个,根据这个特点,可以用单例模式来进行改造。<?php class ConnecToDB { private static $instance; private function _constuct() { } //私有构造函数,防止外界构造新对象, public static function GetConnec() { if (!self::$instance instanceof self) { self::$instance =new self;//若当前对象实例不存在 } $temp=self::$instance; //获取当前单例 return $temp::Con() ; //调用对象私有方法连接 数据库 } //连接到数据库 private static function Con() { try { $connec=mysqli_connect("127.0.0.1", "root", "root"); //数据库地址和密码等 mysqli_select_db($connec, "dbname");//选择数据库 } catch (Exception $e) { echo $e->getMessage().'<br/>'; } return $connec; } } $db=new ConnecToDB(); $db->GetConnec();这是正确的单例代码<?php class Config1 {} class Config { /* * 必须先声明一个静态私有属性:用来保存当前类的实例 * 1. 为什么必须是静态的?因为静态成员属于类,并被类所有实例所共享 * 2. 为什么必须是私有的?不允许外部直接访问,仅允许通过类方法控制方法 * 3. 为什么要有初始值null,因为类内部访问接口需要检测实例的状态,判断是否需要实例化 */ private static $instance = null; //保存用户的自定义配置参数 private $setting = []; //构造器私有化:禁止从类外部实例化 private function __construct(){} //克隆方法私有化:禁止从外部克隆对象 private function __clone(){} //因为用静态属性返回类实例,而只能在静态方法使用静态属性 //所以必须创建一个静态方法来生成当前类的唯一实例 public static function getInstance() { //检测当前类属性$instance是否已经保存了当前类的实例 if (self::$instance == null) { //如果没有,则创建当前类的实例 //self::$instance = new self(); self::$instance= new mysqli(DB_SERVER, DB_USERNAME, DB_PASSWORD, DB_NAME); self::$instance->set_charset('utf8'); } //如果已经有了当前类实例,就直接返回,不要重复创建类实例 return self::$instance; } //设置配置项 public function set($index, $value) { $this->setting[$index] = $value; } //读取配置项 public function get($index) { return $this->setting[$index]; } } //实例化Config类 $obj1 = Config::getInstance(); $obj2 = Config::getInstance(); var_dump($obj1,$obj2); $obj1->set('host','localhost'); echo $obj1->get('host');当然这个代码中,数据库的账号、密码等连接信息都是硬编码,可以通过改造GetConnec()函数注入相应的信息。通过这段脚本,就可以控制一个页面只有一次数据库连接。这就会减少mysql的连接数。但是虽然减少了单次请求的连接数,但是如果这个页面进行多次刷新的话,还是会生产大量mysql连接,降低性能,如图,页面进行多次刷新后,mysql连接数,出现很多:如何解决多次刷新情况下的大量连接呢,答案是用mysql长连接;我们把上面的代码的$connec=mysqli_connect("127.0.0.1", "root", "root"); //数据库地址和密码等 改为 $connec=mysqli_connect("p:127.0.0.1", "root", "root"); //数据库地址和密码等注意:上面的代码在IP地址前加了一个“p:”前缀,代表使用的是mysql长连接,修改完成后,我们再来多次刷新页面。如上图,只有2个连接了。解决成功。当然,除了用mysql的长连接外,还可以修改linux 内核参数的连接重用参数,也可以达到目的。用mysql长连接能大大提升性能,所以请务必使用mysql长连接。
2023年08月11日
16 阅读
0 评论
0 点赞
2023-08-10
Swoole进程结构
Swoole进程结构一、进程的基本知识什么是进程,所谓进程其实就是操作系统中一个正在运行的程序,我们在一个终端当中,通过php,运行一个php文件,这个时候就相当于我们创建了一个进程,这个进程会在系统中驻存,申请属于它自己的内存空间系统资源并且运行相应的程序对于一个进程来说,它的核心内容分为两个部分,一个是它的内存,这个内存是这进程创建之初从系统分配的,它所有创建的变量都会存储在这一片内存环境当中一个是它的上下文环境我们知道进程是运行在操作系统的,那么对于程序来说,它的运行依赖操作系统分配给它的资源,操作系统的一些状态。在操作系统中可以运行多个进程的,对于一个进程来说,它可以创建自己的子进程,那么当我们在一个进程中创建出若干个子进程的时候那么可以看到如图,子进程和父进程一样,拥有自己的内存空间和上下文环境clipboard.png二、Swoole进程结构Swoole的高效不仅仅于底层使用c编写,他的进程结构模型也使其可以高效的处理业务,我们想要深入学习,并且在实际的场景当中使用必须了解,下面我们先看一下结构图:clipboard.png首先先介绍下swoole的这几种进程分别是干什么的:从这些层级的名字,我们先大概说一下,下面这些层级分别是干什么的,做一个详细的说明。Master进程:主进程Manger进程:管理进程Worker进程:工作进程Task进程:异步任务工作进程1、Master进程第一层,Master进程,这个是swoole的主进程,这个进程是用于处理swoole的核心事件驱动的,那么在这个进程当中可以看到它拥有一个MainReactor[线程]以及若干个Reactor[线程],swoole所有对于事件的监听都会在这些线程中实现,比如来自客户端的连接,信号处理等。clipboard.png每一个线程都有自己的用途,下面多每个线程有一个了解MainReactor(主线程)主线程会负责监听server socket,如果有新的连接accept,主线程会评估每个Reactor线程的连接数量。将此连接分配给连接数最少的reactor线程,做一个负载均衡。Reactor线程组Reactor线程负责维护客户端机器的TCP连接、处理网络IO、收发数据完全是异步非阻塞的模式。swoole的主线程在Accept新的连接后,会将这个连接分配给一个固定的Reactor线程,在socket可读时读取数据,并进行协议解析,将请求投递到Worker进程。在socket可写时将数据发送给TCP客户端。心跳包检测线程(HeartbeatCheck)Swoole配置了心跳检测之后,心跳包线程会在固定时间内对所有之前在线的连接发送检测数据包UDP收包线程(UdpRecv)接收并且处理客户端udp数据包2、管理进程ManagerSwoole想要实现最好的性能必须创建出多个工作进程帮助处理任务,但Worker进程就必须fork操作,但是fork操作是不安全的,如果没有管理会出现很多的僵尸进程,进而影响服务器性能,同时worker进程被误杀或者由于程序的原因会异常退出,为了保证服务的稳定性,需要重新创建worker进程。Swoole在运行中会创建一个单独的管理进程,所有的worker进程和task进程都是从管理进程Fork出来的。管理进程会监视所有子进程的退出事件,当worker进程发生致命错误或者运行生命周期结束时,管理进程会回收此进程,并创建新的进程。换句话也就是说,对于worker、task进程的创建、回收等操作全权有“保姆”Manager进程进行管理。再来一张图梳理下Manager进程和Worker/Task进程的关系。clipboard.png3、Worker进程worker 进程属于swoole的主逻辑进程,用户处理客户端的一系列请求,接受由Reactor线程投递的请求数据包,并执行PHP回调函数处理数据生成响应数据并发给Reactor线程,由Reactor线程发送给TCP客户端可以是异步非阻塞模式,也可以是同步阻塞模式4、Task进程taskWorker进程这一进城是swoole提供的异步工作进程,这些进程主要用于处理一些耗时较长的同步任务,在worker进程当中投递过来。三、进程查看及流程梳理当启动一个Swoole应用时,一共会创建2 + n + m个进程,2为一个Master进程和一个Manager进程,其中n为Worker进程数。m为TaskWorker进程数。默认如果不设置,swoole底层会根据当前机器有多少CPU核数,启动对应数量的Reactor线程和Worker进程。我机器为1核的。Worker为1。所以现在默认我启动了1个Master进程,1个Manager进程,和1个worker进程,TaskWorker没有设置也就是为0,当前server会产生3个进程。在启动了server之后,在命令行查看当前产生的进程clipboard.png这三个进程中,所有进程的根进程,也就是例子中的2123进程,就是所谓的Master进程;而2212进程,则是Manager进程;最后的2321进程,是Worker进程。client跟server的交互1、client请求到达 Main Reactor,Client实际上是与Master进程中的某个Reactor线程发生了连接。2、Main Reactor根据Reactor的情况,将请求注册给对应的Reactor (每个Reactor都有epoll。用来监听客户端的变化) 3、客户端有变化时Reactor将数据交给worker来处理4、worker处理完毕,通过进程间通信(比如管道、共享内存、消息队列)发给对应的reactor。 5、reactor将响应结果发给相应的连接请求处理完成示意图:clipboard.png后续准备本文是在自己学习Swoole接触到的一些知识,在初步整理后发送出来,希望能与大家一起学习,文章不足等问题大家可以一起讨论学习,欢迎骚扰~~。后面准备从网络模型入手更好的理解swoole的实现原理,比较与传统PHP-FPM工作模式的问题,之前出过一篇关于(一)如何实现一个单进程阻塞的网络服务器大家可以先了解下,如何一步步演变为多进程master-worker模型。
2023年08月10日
17 阅读
0 评论
0 点赞
2023-08-10
PHP中的pack和unpack函数
PHP中的pack和unpack函数PHP有两个重要的冷门函数:pack和unpack。在网络编程,读写图像文件等场景,这两个函数几乎必不可少。鉴于文件读写/网络编程,或者说字节流处理的重要性,掌握这两个函数是迈向高级PHP编程的基础。本文先介绍字节和字符的区别,说明两个函数存在的必要性和重要性。然后介绍基本用法和使用场景,让读者对其有大体了解,为实际使用中奠定基础。字节和字符PHP的优势是简单易用,熟练运用 字符串 和 数组 相关函数就能抗住一般的需求。日常工作中多用到字符串,所以PHP开发对字符都比较熟悉,稍微资深点基本能也能弄清字符编码。但字符的伴生概念:字节,不少PHP开发并不知晓/熟悉。这不怪他们。PHP世界里极少出现“字节(流)”的概念:没有byte关键字(当然也没有char),官方文档也没提字节;没有原生的数组支持(常用的array其实是hashtable);当然字符串(string)能表达其他语言中的字节数组(Byte Array, byte[])。字节和字符有什么联系和区别呢?简单来说字节是计算机存储和操作的最小单位,字符是人们阅读的最小单位;字节是存储(物理)概念,字符是逻辑概念;字节代表数据(内涵和本质),字符代表其含义;字符由字节组成。举几个例子说明两者区别:“中国”包含2个字符,GBK编码表示需要4个字节,UTF-8编码需要6个字节;数字“1234567890”,包含10个字符,用int32类型表示只需4个字节;下面的图片占用42582个字节,用字符表示是“我老婆”,只占用3个字符:再举一个常用的例子说明字符和字节的区别。开发中我们常用md5算法获取数据的哈希值,算法返回一个128位(bit)的数据(16个字节)。为方便查看其值,人们约定成俗地用十六进制表示,结果就是我们熟知的32位长度的字符串(不区分大小写)。32长度字符串不是md5算法的必然结果,16字节数据才是其本质。如果你愿意,可以用一个小于2^128的数字表示哈希结果,也可以将16字节base64编码后作为其结果。所以常用的32位哈希值与md5返回的16字节关系为:一个是字符表示,另一个则是其本质(字符数组)(PHP的md5函数第二个参数值为true便可得到16字节数据,或hash函数第三个参数为true)。相关概念还有字节序、字符编码等,本文不做展开。感兴趣的读者可参考本人之前的博客“文件和字符编码”或相关材料。引言PHP中专门处理字符串的函数有几十个,加上正则、时间等函数,字符串处理的函数不下百个。相比之下字节处理门庭冷落,相关函数寥寥无几。除了常用的ord/chr,哈希加密函数返回的原始字节、openssl库的openssl_random_pseudo_bytes等函数真正处理或返回 字节外,最重要的两个字节处理函数是pack和unpack。本节从问题引出pack函数的使用。问题考虑一个简单的问题:宇宙的终极答案42在内存中是如何表示的(或者说怎么获取其字节数组)?因为42是一个整数,根据硬件不同,其占用字节大小可能为1, 2, 4, 8等。这里我们限定一个整数占用4个字节,于是问题的等价表述为:怎样将一个整数转换成字节数组(本机序,4个字节)?分析因为是多字节,所以要考虑字节序的问题。42不超过255,只占用一个字节,故而其他三个字节都是0。据此得到结论:如果是大端序(低位字节存放在地址高位),四个字节分别是:0 0 0 42;如果是小端序,结果则是:42 0 0 0。那怎么知道机器的字节序呢?PHP没有提供相关功能,也不能像C语言直接取地址访问字节数据。无所不能的PHP该怎么搞定字节序,或者说完成数据向字节的转换?方案PHP应用层面,数据向字节(数组)的转换是pack的专场,字节(数组)向数据的转换则是unpack的专场。除这两个函数,字节数组(或二进制数据)向数据的转换几无可能(如果有请不吝指教)。现在我们用pack函数获取42在内存中的字节数组。相关代码如下:function intToBytes(int $num) : string {return pack("l", $num);}function outputBytes(string $bytes) {echo "bytes: "; for ($i = 0; $i < strlen($bytes); ++ $i) { echo ord($bytes[$i]), " "; } echo PHP_EOL;}outputBytes(intToBytes(42));// 程序输出:bytes: 42 0 0 0本人计算机用的英特尔的CPU,x86架构是小端序,所以程序输出符合预期。延伸一下,怎么判断机器的字节序?有了pack函数,答案非常简单:function bigEndian() : bool {$data = 0x1200; $bytes = pack("s", $data); return ord($bytes[0]) === 0x12;}调用函数便返回本机是否大端序。上述是pack函数简单的使用场景,接下来分别介绍pack和unpack函数。pack和unpackpack函数pack是“打包/封包”的意思。如其名,pack函数的工作是将数据按照格式打包成字节数组。函数原型为:pack ( string $format [, mixed $… ] ) : string形式上与printf系列函数相同:第一个参数是格式字符串,其余参数是要格式化的参数。不同之处在于pack函数的格式中不能出现元字符和量词外的其他字符,所以不需要%符号。上文的例子中使用了”l”和”s”两个格式化元字符,pack函数的元字符主要分为三类:字符串:a、A等;将数据转成字符串,功能上与sprintf类似,例如整数32转换成字符串”32″;字节:h和H;对字节进行16进制编码,区别在于低位还是高位在前,功能上与dechex等函数类似;char/short/int/long/float/double六种基本类型:c/s/i/l等;将数据转换成对应类型的字节数组,除char类型外(暂)没有其他函数可替代;注意:char和a/A等的区别是a/A等输入为字符(串),而’s/S’的输入要求是小于256的整数,输入字符会得到0。量词比较简单:数字和”“两种。例如”i2″表示将两个参数按照整数转换,”c“表示后续都按照char类型转换。unpackunpack是pack的反向操作:将字节数组解析成有意义的数据。其函数原型为:unpack ( string $format , string $data [, int $offset = 0 ] ) : arrayunpack函数需要注意的是第一个参数和返回值。返回值好理解,pack函数相当于将除格式化参数外的参数数组(想象成call_user_func_array的参数)变成一个字节数组;unpack做相反的事情:释放数据,得到输入时的参数数组。返回一个数组,其键分别是什么呢?这便是格式化参数($format)在pack和unpack的不同之处:unpack应该对释放出来的数据命名,用”/”分隔各组数据。由于格式化参数允许有非元字符和量词外的字符,为了区分数据,不同数据间的”/”分隔符必不可少。一个例子:$bytes = pack("iaa*", 42, ":", "The answer to life, the universe and everything");outputBytes($bytes);$result = unpack("inumber/acolon/a*word", $bytes);print_r($result);// 程序输出:bytes: 42 0 0 0 58 84 104 101 32 97 110 115 119 101 114 32 116 111 32 108 105 102 101 44 32 116 104 101 32 117 110 105 118 101 114 115 101 32 97 110 100 32 101 118 101 114 121 116 104 105 110 103Array([num] => 42 [colon] => : [word] => The answer to life, the universe and everything)如果不对释放出来的数据命名会怎么样?例如上例中unpack的格式化参数为:“i/a/a*”,结果是什么呢?其结果为:Array([1] => The answer to life, the universe and everything)为何?官方文档上如是说:Caution If you do not name an element, numeric indices starting from 1 are used. Be aware that if you have more than one unnamed element, some data is overwritten because the numbering restarts from 1 for each element.翻译过来就是:如果你不对数据命名,默认的1, 2, 3…就用来当作键值。如果有多组数据,每组都用同样的下标,会导致数据覆盖。所以能理解 “i/a/a*” 为何只剩最后一组数据了吧?应用场景读取图像、word/excel文件,解析binlog、二进制ip数据库文件等场合,pack和unpack几乎必不可少。本文举例说一下pack和unpack在网络编程时协议解析的用途。假设我们的tcp包格式为:前四个字节表示包大小,其余字节为数据内容。于是客户(发送)端的send函数可以长这样:public function send($data) { // 这里假设$data已经做了序列化、加密等操作,是字节数组 // 计算报文长度,封装报文 $len = strlen($data); $header = pack("L", $len); // 转换成网络(大端)序 $header = xxx // 封包 $binary = $header . $data; // 调用fwrite/socket_send等将数据写入内核缓冲区 ...}服务(接收)端根据协议解析接收到的数据流:public function decodable($session, $buffer) { $dataLen = strlen($buffer); // 非法数据包 if ($dataLen < 4) {// 关闭连接、记录ip等 .... return NOT_OK;} // 获取前四个字节 $header = substr($buffer, 0, 4); // 转换成主机序 $header = xxx // 解析数据长度 $len = unpack("L", $header); // 单个报文不能超过8M,例如限制上传的图像大小 if ($len > 8 1024 1024) {// 关闭连接等 return NOT_OK;}// 检查数据包是否满足协议要求 if ($dataLen - 4 >= $len) {return OK;} // 数据未全部到达,继续等待 return NEED_DATA;}通过pack和unpack,我们顺利的处理报文协议和二进制字节流的发送和解析。如果你用\n作为报文分隔符,pack和unpack也许用不到。但在网络通讯中直接传递字符毕竟少数(相当于明文传送),大多数情况下的二进制数据流的解析还是要靠pack和unpack。总结除分配内存,最重要的系统调用莫过于文件读写和网络连接,而两者的本质操作对象都是字节流。pack和unpack为PHP提供了底层字节操作的能力,在二进制数据处理中十分有用。有志于跳出web编程的PHP开发应该都要掌握这两个函数。参考文件和字符编码PHP Manual: packPHP Manual: unpackHandling binary data in PHP with pack() and unpack()PHP: 深入pack/unpack
2023年08月10日
13 阅读
0 评论
0 点赞
1
...
54
55
56
...
112