• 周年纪念勋章活动已圆满结束,如有已购买但仍未申请的用户,可以通过对应勋章的下载链接申请~

教程 从零解析PM内核运行原理EP1

云易墨轩

【Lv:4】

注册
2018/11/25
消息
16
金粒
4,391金粒
========= 前言 =========

时隔以及,我再次回来了。这次我在论坛里看见了关于 PM 为何没落的消息,看了之后的确感觉到唏嘘不已。无奈之前就已经有了解析内核的想法,所以此时此刻我只能重操旧业,用我那不渊博的知识来解析解析 PM 背后的运行逻辑。大家可以一边看一边学。以下便是大家如果要学习,所要准备的东西。

========= 所需工具 =========
一、PocketMine 源码(获取地址,点击前往
二、称心如意的编辑器(我使用的是 PhpStorm )
三、一台好电脑
四、PHP 基础

========= 目录 =========

第一章 开启服务器的瞬间
|
——第一节 关键的 server 方法
——第二节 关键的 server.lock
========= 第一章 开启服务器的瞬间 =========

========= 第一节 关键的 server 方法 =========

我们现在就来探究一下,当我们每次按下 Start Server(开服)按钮的时候,PM 内核脑海中想的第一件事情是什么呢?现在我们就进入到 PocketMine.php 文件里的内容,看看它是怎么运行的。不过我在主文件里看见了非常有意思的一段话,让大家看看。

PHP:
/*
     * Startup code. Do not look at it, it may harm you.
     * This is the only non-class based file on this project.
     * Enjoy it as much as I did writing it. I don't want to do it again.
     */

写这个文件的程序员指出了自己写的这段代码是不符合规范的,让大家不要模仿,让我在阅读源码的时候变得开心起来。废话少说,既然作者都说了这个不是类,那么在文件里必然有一个总启动的办法,在我仔细寻找的过程中,终于发现了这个决定性的代码,也就是这一句代码让我们的服务器开始运行起来。

PHP:
\pocketmine\server();

既然找到了是 server() 这个方法是主入口,那么我们就前往这个方法代码块一看究竟。

PHP:
function server(){
        if(count($messages = check_platform_dependencies()) > 0){
            echo PHP_EOL;
            $binary = version_compare(PHP_VERSION, "5.4") >= 0 ? PHP_BINARY : "unknown";
            critical_error("Selected PHP binary does not satisfy some requirements.");
            foreach($messages as $m){
                echo " - $m" . PHP_EOL;
            }
            critical_error("PHP binary used: " . $binary);
            critical_error("Loaded php.ini: " . (($file = php_ini_loaded_file()) !== false ? $file : "none"));
            critical_error("Please recompile PHP with the needed configuration, or refer to the installation instructions at http://pmmp.rtfd.io/en/rtfd/installation.html.");
            echo PHP_EOL;
            exit(1);
        }
        unset($messages);

        error_reporting(-1);
        set_ini_entries();

        $opts = getopt("", ["bootstrap:"]);
        if(isset($opts["bootstrap"])){
            $bootstrap = ($real = realpath($opts["bootstrap"])) !== false ? $real : $opts["bootstrap"];
        }else{
            $bootstrap = dirname(__FILE__, 3) . '/vendor/autoload.php';
        }

        if($bootstrap === false or !is_file($bootstrap)){
            critical_error("Composer autoloader not found at " . $bootstrap);
            critical_error("Please install/update Composer dependencies or use provided builds.");
            exit(1);
        }
        define('pocketmine\COMPOSER_AUTOLOADER_PATH', $bootstrap);
        require_once(\pocketmine\COMPOSER_AUTOLOADER_PATH);

        set_error_handler([Utils::class, 'errorExceptionHandler']);

        $version = new VersionString(\pocketmine\BASE_VERSION, \pocketmine\IS_DEVELOPMENT_BUILD, \pocketmine\BUILD_NUMBER);
        define('pocketmine\VERSION', $version->getFullVersion(true));

        $gitHash = str_repeat("00", 20);

        if(\Phar::running(true) === ""){
            $gitHash = Git::getRepositoryStatePretty(\pocketmine\PATH);
        }else{
            $phar = new \Phar(\Phar::running(false));
            $meta = $phar->getMetadata();
            if(isset($meta["git"])){
                $gitHash = $meta["git"];
            }
        }

        define('pocketmine\GIT_COMMIT', $gitHash);

        $composerGitHash = InstalledVersions::getReference('pocketmine/pocketmine-mp');
        if($composerGitHash !== null){
            $currentGitHash = explode("-", \pocketmine\GIT_COMMIT)[0];
            if($currentGitHash !== $composerGitHash){
                critical_error("Composer dependencies and/or autoloader are out of sync.");
                critical_error("- Current revision is $currentGitHash");
                critical_error("- Composer dependencies were last synchronized for revision $composerGitHash");
                critical_error("Out-of-sync Composer dependencies may result in crashes and classes not being found.");
                critical_error("Please synchronize Composer dependencies before running the server.");
                exit(1);
            }
        }

        $opts = getopt("", ["data:", "plugins:", "no-wizard", "enable-ansi", "disable-ansi"]);

        define('pocketmine\DATA', isset($opts["data"]) ? $opts["data"] . DIRECTORY_SEPARATOR : realpath(getcwd()) . DIRECTORY_SEPARATOR);
        define('pocketmine\PLUGIN_PATH', isset($opts["plugins"]) ? $opts["plugins"] . DIRECTORY_SEPARATOR : realpath(getcwd()) . DIRECTORY_SEPARATOR . "plugins" . DIRECTORY_SEPARATOR);

        if(!file_exists(\pocketmine\DATA)){
            mkdir(\pocketmine\DATA, 0777, true);
        }

        $lockFile = fopen(\pocketmine\DATA . 'server.lock', "a+b");
        if($lockFile === false){
            critical_error("Unable to open server.lock file. Please check that the current user has read/write permissions to it.");
            exit(1);
        }
        define('pocketmine\LOCK_FILE', $lockFile);
        if(!flock(\pocketmine\LOCK_FILE, LOCK_EX | LOCK_NB)){
            //wait for a shared lock to avoid race conditions if two servers started at the same time - this makes sure the
            //other server wrote its PID and released exclusive lock before we get our lock
            flock(\pocketmine\LOCK_FILE, LOCK_SH);
            $pid = stream_get_contents(\pocketmine\LOCK_FILE);
            critical_error("Another " . \pocketmine\NAME . " instance (PID $pid) is already using this folder (" . realpath(\pocketmine\DATA) . ").");
            critical_error("Please stop the other server first before running a new one.");
            exit(1);
        }
        ftruncate(\pocketmine\LOCK_FILE, 0);
        fwrite(\pocketmine\LOCK_FILE, (string) getmypid());
        fflush(\pocketmine\LOCK_FILE);
        flock(\pocketmine\LOCK_FILE, LOCK_SH); //prevent acquiring an exclusive lock from another process, but allow reading

        //Logger has a dependency on timezone
        $tzError = Timezone::init();

        if(isset($opts["enable-ansi"])){
            Terminal::init(true);
        }elseif(isset($opts["disable-ansi"])){
            Terminal::init(false);
        }else{
            Terminal::init();
        }

        $logger = new MainLogger(\pocketmine\DATA . "server.log");
        $logger->registerStatic();

        foreach($tzError as $e){
            $logger->warning($e);
        }
        unset($tzError);

        emit_performance_warnings($logger);

        $exitCode = 0;
        do{
            if(!file_exists(\pocketmine\DATA . "server.properties") and !isset($opts["no-wizard"])){
                $installer = new SetupWizard();
                if(!$installer->run()){
                    $exitCode = -1;
                    break;
                }
            }

            //TODO: move this to a Server field
            define('pocketmine\START_TIME', microtime(true));

            /*
             * We now use the Composer autoloader, but this autoloader is still for loading plugins.
             */
            $autoloader = new \BaseClassLoader();
            $autoloader->register(false);

            new Server($autoloader, $logger, \pocketmine\DATA, \pocketmine\PLUGIN_PATH);

            $logger->info("Stopping other threads");

            $killer = new ServerKiller(8);
            $killer->start(PTHREADS_INHERIT_NONE);
            usleep(10000); //Fixes ServerKiller not being able to start on single-core machines

            if(ThreadManager::getInstance()->stopAll() > 0){
                $logger->debug("Some threads could not be stopped, performing a force-kill");
                Process::kill(Process::pid());
            }
        }while(false);

        $logger->shutdown();
        $logger->join();

        echo Terminal::$FORMAT_RESET . PHP_EOL;

        if(!flock(\pocketmine\LOCK_FILE, LOCK_UN)){
            critical_error("Failed to release the server.lock file.");
        }

        if(!fclose(\pocketmine\LOCK_FILE)){
            critical_error("Could not close server.lock resource.");
        }

        exit($exitCode);
    }

这个代码块非常冗长,我们从现在开始慢慢的一步步解析下去。我在解析过程中贴的代码比较多,大家可以慢慢去消化。那么我们继续从这个方法的第一个代码区块开始研究。

PHP:
if(count($messages = check_platform_dependencies()) > 0){
            echo PHP_EOL;
            $binary = version_compare(PHP_VERSION, "5.4") >= 0 ? PHP_BINARY : "unknown";
            critical_error("Selected PHP binary does not satisfy some requirements.");
            foreach($messages as $m){
                echo " - $m" . PHP_EOL;
            }
            critical_error("PHP binary used: " . $binary);
            critical_error("Loaded php.ini: " . (($file = php_ini_loaded_file()) !== false ? $file : "none"));
            critical_error("Please recompile PHP with the needed configuration, or refer to the installation instructions at http://pmmp.rtfd.io/en/rtfd/installation.html.");
            echo PHP_EOL;
            exit(1);
        }
        unset($messages);

PHP:
function check_platform_dependencies(){
        if(version_compare(MIN_PHP_VERSION, PHP_VERSION) > 0){
            //If PHP version isn't high enough, anything below might break, so don't bother checking it.
            return [
                "PHP >= " . MIN_PHP_VERSION . " is required, but you have PHP " . PHP_VERSION . "."
            ];
        }

        $messages = [];

        if(PHP_INT_SIZE < 8){
            $messages[] = "32-bit systems/PHP are no longer supported. Please upgrade to a 64-bit system, or use a 64-bit PHP binary if this is a 64-bit system.";
        }

        if(php_sapi_name() !== "cli"){
            $messages[] = "Only PHP CLI is supported.";
        }

        $extensions = [
            "curl" => "cURL",
            "ctype" => "ctype",
            "date" => "Date",
            "hash" => "Hash",
            "json" => "JSON",
            "mbstring" => "Multibyte String",
            "openssl" => "OpenSSL",
            "pcre" => "PCRE",
            "phar" => "Phar",
            "pthreads" => "pthreads",
            "reflection" => "Reflection",
            "sockets" => "Sockets",
            "spl" => "SPL",
            "yaml" => "YAML",
            "zip" => "Zip",
            "zlib" => "Zlib"
        ];

        foreach($extensions as $ext => $name){
            if(!extension_loaded($ext)){
                $messages[] = "Unable to find the $name ($ext) extension.";
            }
        }

        if(extension_loaded("pthreads")){
            $pthreads_version = phpversion("pthreads");
            if(substr_count($pthreads_version, ".") < 2){
                $pthreads_version = "0.$pthreads_version";
            }
            if(version_compare($pthreads_version, "3.2.0") < 0){
                $messages[] = "pthreads >= 3.2.0 is required, while you have $pthreads_version.";
            }
        }

        if(extension_loaded("leveldb")){
            $leveldb_version = phpversion("leveldb");
            if(version_compare($leveldb_version, "0.2.1") < 0){
                $messages[] = "php-leveldb >= 0.2.1 is required, while you have $leveldb_version.";
            }
        }

        if(extension_loaded("pocketmine")){
            $messages[] = "The native PocketMine extension is no longer supported.";
        }

        return $messages;
    }

我们会看见一开始就又引入了另一个方法 check_platform_dependencies(), 这个方法是用来检测核心所需要的模块是否拥有,如果缺少就会把缺少模块的信息存进 $message 这个数组里,然后在返回这个数组。所以一旦这个数组消息数大于0,便会开始解析 php 的运行版本是否大于 5.4,一旦不符合同样也会报错退出。

我这边有点啰嗦,我觉得还是得要简洁意概,所以接下来我会挑重点的去讲,其他不太重要的大家自行翻阅源码。

========= 第二节 关键的 server.lock =========

在我们开启服务器的瞬间,PocketMine 为了避免同一个服务器重复开启,他们在 server 方法里生成了一个 server.lock 文件,然后每次开启服务器之时就会检测是否有 server.lock 这个文件,如果有就说明之前已经有一个服务器开启了,便会阻止这个服务器的启动,如果没有那就生成一个 server.lock 文件。具体实现代码我就贴在下方,大家可以去感受感受。

PHP:
$lockFile = fopen(\pocketmine\DATA . 'server.lock', "a+b");
if($lockFile === false){
   critical_error("Unable to open server.lock file. Please check that the current user has read/write permissions to it.");
   exit(1);
}
define('pocketmine\LOCK_FILE', $lockFile);
if(!flock(\pocketmine\LOCK_FILE, LOCK_EX | LOCK_NB)){
   //wait for a shared lock to avoid race conditions if two servers started at the same time - this makes sure the
   //other server wrote its PID and released exclusive lock before we get our lock
   flock(\pocketmine\LOCK_FILE, LOCK_SH);
   $pid = stream_get_contents(\pocketmine\LOCK_FILE);
   critical_error("Another " . \pocketmine\NAME . " instance (PID $pid) is already using this folder (" . realpath(\pocketmine\DATA) . ").");
   critical_error("Please stop the other server first before running a new one.");
   exit(1);
}
ftruncate(\pocketmine\LOCK_FILE, 0);
fwrite(\pocketmine\LOCK_FILE, (string) getmypid());
fflush(\pocketmine\LOCK_FILE);
flock(\pocketmine\LOCK_FILE, LOCK_SH); //prevent acquiring an exclusive lock from another process, but allow reading

我回来了,今天下午这段时间真是忙碌死了。那么我们继续接上上次的话题 server.lock 文件能够有限的防止服务器在开启的状态下被误开。判断完服务器是否开启之后,才是真正的重头戏。接下来的代码就直接开始服务器的配置。

PHP:
if(!file_exists(\pocketmine\DATA . "server.properties") and !isset($opts["no-wizard"])){
    $installer = new SetupWizard();
    if(!$installer->run()){
        $exitCode = -1;
        break;
    }
}

在这里就可以发现,当服务器的配置文件不存在和没启动配置都存在的时候,我们服务器的初始配置系统才会开始启动,所以大家能清楚的看见 SetupWizard 这个类便是 PocketMine 内核初始配置的类,以上内容就为第一章的内容了。我决定把 第二章 服务器的初始配置 放在明天更新。明天再见吧!
 
最后编辑:

在线会员

  • 王聪聪
  • ewsk
  • 千本樱
  • MineSunshineone
  • 大逍遥
  • 量子时代
  • 2801326928
  • eternal36900
  • baisuishan
  • 许执
  • Navy36393
  • 侘傺
  • 怡儿
  • 北纬一星
  • xinge54088
  • Maplef_snow
  • gdjd1412
  • 黄泉
  • 芜湖胡子
  • Sh1roCu
  • polang
...和 39 更多。
后退
顶部 底部