首页
iYoRoy DN42 Network
关于
友情链接
Language
简体中文
English
Search
1
Docker下中心化部署EasyTier
1,940 阅读
2
给Android 4.9内核添加KernelSU支持
1,201 阅读
3
在TrueNAS上使用Docker安装1Panel
332 阅读
4
记一次为Android 4.9内核的ROM启用erofs支持
328 阅读
5
2025羊城杯初赛WriteUp
319 阅读
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
登录
Search
标签搜索
网络技术
BGP
Linux
BIRD
DN42
C&C++
Android
AOSP
MSVC
Docker
OSPF
Windows
iBGP
Clearnet
内部路由协议
ASN
服务
DNS
STL
Kernel
神楽悠笙
累计撰写
24
篇文章
累计收到
11
条评论
首页
栏目
Android
运维
NAS
开发
网络技术
专题向研究
DN42
个人ISP
CTF
页面
iYoRoy DN42 Network
关于
友情链接
Language
简体中文
English
搜索到
7
篇与
的结果
为Typecho配置简易多语言方案
想着给博客做一下国际化支持,为每篇文章和每个页面单独配置一个英文版本,但是上网查了一圈发现Typecho对国际化的支持并不好,最终自己设计了一套方案,写这篇文章记录下来 本文默认你对PHP、Nginx、Typecho的基础逻辑有部分了解 分析 需求 需要为每个文章和页面都配置中英双语 需要配置一个语言选择器,使得前端可以快速切换语言 需要搜索引擎可以正确识别并收录多语言版本的文章 大致方案 中英文文章的区分大致有这两种方案: 作为一个单独的参数,如访问文章时用/?lang=zh-CN和/?lang=en-US来区分,但是这种方案实现起来比较困难,同时对搜索引擎收录也不太好; 通过URL路径区分,如访问https://<host>/article是中文页面,访问https://<host>/en/article是英文页面,这种配置起来比较简单,直接分成两个Typecho实例即可,并且对搜索引擎友好。问题是评论和浏览量统计需要手动同步。 总结下来,我选择了第二种方案,并且打算直接通过在/en/新建一个Typecho实例实现多语言支持。 具体实现 首先将博客实例复制两份,一份中文一份英文,然后分别翻译成英文; 修改前端代码,实现语言选择器; 要保证两边文章URL只有一个/en之差,就得保证中文站点和英文站点文章的cid相同,而这个cid是根据创建文章和附件的顺序来的,因此打算编写一个同步插件,中文端发文章的时候自动在英文数据库中插入一条对应cid的文章; 改写SiteMap插件,因为sitemap不能既包含页面链接又包含其他sitemap的引用,因此主站需要创建两个sitemap,一个是收录中文站页面的主sitemap,另一个是索引,负责索引中文和英文的sitemap; 在<head></head>中添加hreflang属性来告知搜索引擎如何处理多语言; 将英文端的访问量和点赞量链接到中文的数据库; 将英文端的评论链接到中文数据库。 开整 创建英文实例 将整个网站复制一份,放在原先的网站根目录的/en/文件夹下,同时将数据库也复制一份,我这里命名成typecho_en。 接着,分别为两个实例配置伪静态: location /en/ { if (!-e $request_filename) { rewrite ^(.*)$ /en/index.php$1 last; } } location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php$1 last; } } 关于为什么要将中文的主实例也用location括起来,是因为实际测试的时候发现如果不括起来就会将英文实例也当作中文实例的一部分解析,造成404。 同时,修改<webroot>/en/config.inc.php里的数据库配置,指向英文实例的数据库。 这个时候,访问<host>/en/应该能看到一个和中文主站一模一样的站点。 修改Typecho语言 这一步其实感觉可以不要,因为基本上前端页面的语言都是由主题决定的,将Typecho本身语言设置为英文没什么大用,但是为了统一,还是选择改了一下(这样可以一眼看出来是哪个实例的后台(笑)) 直接参考GitHub上typecho官方的多语言支持即可,从Release里下载语言包,然后解压到<webroot>/en/usr/langs/下,然后进入https://<host>/en/admin/options-general.php,就能看见语言设置选项,改为English即可。 修改主题语言 最繁琐的一步,我使用的是Joe,进入<webroot>/en/usr/themes/Joe,将所有你能看到的显示相关的中文都翻译成英文,这里没什么非常方便的方法,用翻译工具的话翻出来会很别扭,我还是选择了手动翻译。 需要注意的是,有一部分的前端配置是在js中的,不止是PHP源文件,这些都需要翻译。 翻译文章 这一步没啥好说的,把/en/下的文章一篇篇翻译成英文,保存 配置文章同步发布 这一步是为了保持两边文章的cid同步,因为cid和访问url有关,保持cid同步可以使下文配置语言选择器一步更加简单,只要在host后加个/en或者去掉就行了。 cid是数据库中typecho_contents表的一个自增字段,同时也是主键,他的分配和typecho的文章、附件等有关。因为上传的文件也会占用cid,而我打算将所有附件都上传到中文站,因此如果不做特殊处理两边的cid容易匹配不上,增加后续工作量。 因此我选择的方案是用AI编写一个插件,当中文站发布文章的时候自动触发,读取中文站为其分配的cid并写入到英文站的数据库中。 创建文件<webroot>/usr/plugins/SyncToEnglish/Plugin.php并填入如下内容: <?php if (!defined('__TYPECHO_ROOT_DIR__')) exit; /** * 中文文章同步到英文数据库 * * @package SyncToEnglish * @author ChatGPT, iYoRoy * @version 1.0.0 * @link https://example.com */ class SyncToEnglish_Plugin implements Typecho_Plugin_Interface { public static function activate() { Typecho_Plugin::factory('Widget_Contents_Post_Edit')->finishPublish = [__CLASS__, 'push']; return 'SyncToEnglish 插件已启用:发布中文文章时会自动在英文库创建对应空文章'; error_log("[SyncToEnglish] 插件激活成功"); } public static function deactivate() { return 'SyncToEnglish 插件已禁用'; } public static function config(Typecho_Widget_Helper_Form $form) { $host = new Typecho_Widget_Helper_Form_Element_Text('host', NULL, 'localhost', _t('英文数据库主机')); $user = new Typecho_Widget_Helper_Form_Element_Text('user', NULL, 'root', _t('英文数据库用户名')); $password = new Typecho_Widget_Helper_Form_Element_Password('password', NULL, NULL, _t('英文数据库密码')); $database = new Typecho_Widget_Helper_Form_Element_Text('database', NULL, 'typecho_en', _t('英文数据库名称')); $port = new Typecho_Widget_Helper_Form_Element_Text('port', NULL, '3306', _t('英文数据库端口')); $charset = new Typecho_Widget_Helper_Form_Element_Text('charset', NULL, 'utf8mb4', _t('字符集')); $prefix = new Typecho_Widget_Helper_Form_Element_Text('prefix', NULL, 'typecho_', _t('表前缀')); $form->addInput($host); $form->addInput($user); $form->addInput($password); $form->addInput($database); $form->addInput($port); $form->addInput($charset); $form->addInput($prefix); } public static function personalConfig(Typecho_Widget_Helper_Form $form) {} public static function push($contents, $widget) { $options = Helper::options(); $config = $options->plugin('SyncToEnglish'); // 获取中文库文章信息 $cnDb = Typecho_Db::get(); if (is_array($contents) && isset($contents['cid'])) { $cid = $contents['cid']; $title = $contents['title']; } elseif (is_object($contents) && isset($contents->cid)) { $cid = $contents->cid; $title = $contents->title; } else { $db = Typecho_Db::get(); $row = $db->fetchRow($db->select()->from('table.contents')->order('cid', Typecho_Db::SORT_DESC)->limit(1)); $cid = $row['cid']; $title = $row['title']; error_log("[SyncToEnglish DEBUG] CID not found in param, fallback to latest cid={$cid}\n", 3, __DIR__ . '/debug.log'); } $article = $cnDb->fetchRow($cnDb->select()->from('table.contents')->where('cid = ?', $cid)); if (!$article) return; $enDb = new Typecho_Db('Mysql', $config->prefix); $enDb->addServer([ 'host' => $config->host, 'user' => $config->user, 'password' => $config->password, 'charset' => $config->charset, 'port' => (int)$config->port, 'database' => $config->database ], Typecho_Db::READ | Typecho_Db::WRITE); try { $exists = $enDb->fetchRow($enDb->select()->from('table.contents')->where('cid = ?', $article['cid'])); if ($exists) { $enDb->query($enDb->update('table.contents') ->rows([ // 'title' => $article['title'], 'slug' => $article['slug'], 'modified' => $article['modified'] ]) ->where('cid = ?', $article['cid']) ); } else { $enDb->query($enDb->insert('table.contents')->rows([ 'cid' => $article['cid'], 'title' => $article['title'], 'slug' => $article['slug'], 'created' => $article['created'], 'modified' => $article['modified'], 'type' => $article['type'], 'status' => $article['status'], 'authorId' => $article['authorId'], 'views' => 0, 'text' => $article['text'], 'allowComment' => $article['allowComment'], 'allowFeed' => $article['allowFeed'], 'allowPing' => $article['allowPing'] ])); } } catch (Exception $e) { error_log('[SyncToEnglish] 同步失败: ' . $e->getMessage()); } } } 随后去后台启用插件并配置上英文数据库的数据库信息即可。 完成后,在中文站发布文章,英文站应该能同步发布一篇相同cid的文章。 配置语言选择器 既然我们已经完成了同步文章cid,接下来我们修改语言的时候只需要修改url,在前面加上或者去掉/en/就行了。通过PHP编写一个选择器,放在主题的顶栏中: <!-- Language Selector --> <div class="joe_dropdown" trigger="hover" placement="60px"> <div class="joe_dropdown__link"> <a href="#" rel="nofollow">Language</a> <svg class="joe_dropdown__link-icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="14" height="14"> <path d="M561.873 725.165c-11.262 11.262-26.545 21.72-41.025 18.502-14.479 2.413-28.154-8.849-39.415-18.502L133.129 375.252c-17.697-17.696-17.697-46.655 0-64.352s46.655-17.696 64.351 0l324.173 333.021 324.977-333.02c17.696-17.697 46.655-17.697 64.351 0s17.697 46.655 0 64.351L561.873 725.165z" fill="var(--main)" /> </svg> </div> <nav class="joe_dropdown__menu"> <?php // 获取当前完整的 URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // 判断是否有英文前缀 "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // 生成两个超链接 echo '<a href="' . $new_url_cn . '">简体中文</a>'; echo '<a href="' . $new_url_en . '">English</a>'; ?> </nav> </div> 需要在中文和英文两个实例中都添加。 之后,全局都能通过这个选择器选择语言。对于我用的Joe主题,移动端和PC端需要单独编写两个语言选择器。 改写SiteMap插件 为了让搜索引擎能更快的收录到英文的页面,我打算修改SiteMap插件,使之包含英文站点的页面。SiteMap有两种,一种是sitemapindex,用于索引子SiteMap,一种是urlset,用于包含页面。我使用的是joyqi/typecho-plugin-sitemap插件,在这个基础上,将默认的/sitemap.xml改为sitemapindex,新建一个路由/sitemap_cn.xml来存放中文站的SiteMap,英文站点的插件不变,再有默认的/sitemap.xml引用/sitemap_cn.xml和/en/sitemap.xml。 修改SiteMap的Plugin.php: /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( - 'sitemap', + 'sitemap_index', '/sitemap.xml', Generator::class, - 'generate', + 'generate_index', 'index' ); + Helper::addRoute( + 'sitemap_cn', + '/sitemap_cn.xml', + Generator::class, + 'generate_cn', + 'index' + ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { - Helper::removeRoute('sitemap'); + Helper::removeRoute('sitemap_index'); + Helper::removeRoute('sitemap_cn'); } {collapse} {collapse-item label="完整代码"} <?php namespace TypechoPlugin\Sitemap; use Typecho\Plugin\PluginInterface; use Typecho\Widget\Helper\Form; use Utils\Helper; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * 自动生成 Typecho 站点地图的插件。 * 站点地图 Sitemap 的地址是:http(s)://yourdomain.com/sitemap.xml * * @package 站点地图插件 * @author joyqi * @version 1.0.0 * @since 1.2.1 * @link https://github.com/joyqi/typecho-plugin-sitemap */ class Plugin implements PluginInterface { /** * Activate plugin method, if activated failed, throw exception will disable this plugin. */ public static function activate() { Helper::addRoute( 'sitemap_index', '/sitemap.xml', Generator::class, 'generate_index', 'index' ); Helper::addRoute( 'sitemap_cn', '/sitemap_cn.xml', Generator::class, 'generate_cn', 'index' ); } /** * Deactivate plugin method, if deactivated failed, throw exception will enable this plugin. */ public static function deactivate() { Helper::removeRoute('sitemap_index'); Helper::removeRoute('sitemap_cn'); } /** * Plugin config panel render method. * * @param Form $form */ public static function config(Form $form) { $sitemapBlock = new Form\Element\Checkbox( 'sitemapBlock', [ 'posts' => _t('生成文章链接'), 'pages' => _t('生成独立页面链接'), 'categories' => _t('生成分类链接'), 'tags' => _t('生成标签链接'), ], ['posts', 'pages', 'categories', 'tags'], _t('站点地图显示') ); $updateFreq = new Form\Element\Select( 'updateFreq', [ 'daily' => _t('每天'), 'weekly' => _t('每周'), 'monthly' => _t('每月或更久'), ], 'daily', _t('更新频率') ); // $externalSitemap = new Typecho_Widget_Helper_Form_Element_Text('externalSitemap', NULL, '', _t('附加SiteMap')); $form->addInput($sitemapBlock->multiMode()); $form->addInput($updateFreq); // $form->addInput($externalSitemap); } /** * Plugin personal config panel render method. * * @param Form $form */ public static function personalConfig(Form $form) { // TODO: Implement personalConfig() method. } } {/collapse-item} {/collapse} 修改SiteMap的Generator.php: class Generator extends Contents { + public function generate_index(){ + $sitemap = '<?xml version="1.0" encoding="UTF-8"?> +<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> + <sitemap> + <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> + </sitemap> + <sitemap> + <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> + </sitemap> +</sitemapindex>'; + $this->response->throwContent($sitemap, 'text/xml'); + } + /** * @return void */ - public function generate() + public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; ... {collapse} {collapse-item label="完整代码"} <?php namespace TypechoPlugin\Sitemap; use Widget\Base\Contents; use Widget\Contents\Page\Rows; use Widget\Contents\Post\Recent; use Widget\Metas\Category\Rows as CategoryRows; use Widget\Metas\Tag\Cloud; if (!defined('__TYPECHO_ROOT_DIR__')) { exit; } /** * Sitemap Generator */ class Generator extends Contents { public function generate_index(){ $sitemap = '<?xml version="1.0" encoding="UTF-8"?> <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex>'; $this->response->throwContent($sitemap, 'text/xml'); } /** * @return void */ public function generate_cn() { $sitemap = '<?xml version="1.0" encoding="' . $this->options->charset . '"?>' . PHP_EOL; $sitemap .= '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' . ' xmlns:news="http://www.google.com/schemas/sitemap-news/0.9"' . ' xmlns:xhtml="http://www.w3.org/1999/xhtml"' . ' xmlns:image="http://www.google.com/schemas/sitemap-image/1.1"' . ' xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">' . PHP_EOL; // add homepage $sitemap .= <<<EOF <url> <loc>{$this->options->siteUrl}</loc> <changefreq>daily</changefreq> <priority>1.0</priority> </url> EOF; // add posts if (in_array('posts', $this->options->plugin('Sitemap')->sitemapBlock)) { $postsCount = $this->size($this->select() ->where('table.contents.status = ?', 'publish') ->where('table.contents.created < ?', $this->options->time) ->where('table.contents.type = ?', 'post')); $posts = Recent::alloc(['pageSize' => $postsCount]); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'monthly' : 'weekly'; while ($posts->next()) { $sitemap .= <<<EOF <url> <loc>{$posts->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$posts->date->format('c')}</lastmod> <priority>0.8</priority> </url> EOF; } } // add pages if (in_array('pages', $this->options->plugin('Sitemap')->sitemapBlock)) { $pages = Rows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq ==='monthly' ? 'yearly' : 'monthly'; while ($pages->next()) { $sitemap .= <<<EOF <url> <loc>{$pages->permalink}</loc> <changefreq>{$freq}</changefreq> <lastmod>{$pages->date->format('c')}</lastmod> <priority>0.5</priority> </url> EOF; } } // add categories if (in_array('categories', $this->options->plugin('Sitemap')->sitemapBlock)) { $categories = CategoryRows::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($categories->next()) { $sitemap .= <<<EOF <url> <loc>{$categories->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.6</priority> </url> EOF; } } // add tags if (in_array('tags', $this->options->plugin('Sitemap')->sitemapBlock)) { $tags = Cloud::alloc(); $freq = $this->options->plugin('Sitemap')->updateFreq; while ($tags->next()) { $sitemap .= <<<EOF <url> <loc>{$tags->permalink}</loc> <changefreq>{$freq}</changefreq> <priority>0.4</priority> </url> EOF; } } $sitemap .= '</urlset>'; $this->response->throwContent($sitemap, 'text/xml'); } } {/collapse-item} {/collapse} 请将代码中的博客地址改成你自己的 (最近太忙了实在是没空给单独写个配置页面了就直接把SiteMap URL写死在插件里了) 禁用再启用插件,访问https://<host>/sitemap.xml应该就能看到SiteMap的索引了: <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> <sitemap> <loc>https://www.iyoroy.cn/sitemap_cn.xml</loc> </sitemap> <sitemap> <loc>https://www.iyoroy.cn/en/sitemap.xml</loc> </sitemap> </sitemapindex> 通过Bing Search等其他搜索引擎的SEO管理页应该也能看到扫描到了英文站的SiteMap: 添加hreflang 这一步的作用是让搜索引擎知道当前页面存在多语言版本,使其可以根据用户的语言偏好或地理位置展示合适的页面。我们需要在页面的<head></head>中插入类似如下格式的link标签: <link rel="alternate" hreflang="en-us" href="https://example.com/us"> <link rel="alternate" hreflang="fr" href="https://example.com/fr"> <link rel="alternate" hreflang="x-default" href="https://example.com/default"> 其中,hreflang="x-default"意为本文的默认语言。hreflang的值是由ISO 639-1的语言代码和ISO 3166-1 Alpha-2的区域代码构成的,其中区域代码可以省略,只标识语言(类似于中文和简体中文(大陆—)、繁体中文(香港),繁体中文(台湾)的区别)。 在主题的<head></head>相关配置中加入如下内容: <?php // 获取当前完整的 URL $current_url = $_SERVER['REQUEST_URI']; $host = $_SERVER['HTTP_HOST']; // 判断是否有英文前缀 "/en/" if (strpos($current_url, '/en/') === 0) { $current_url = substr_replace($current_url, '', 0, 3); } $new_url_cn = 'https://' . $host . $current_url; $new_url_en = 'https://' . $host . '/en' . $current_url; // 生成两个超链接 echo '<link rel="alternate" hreflang="zh-cn" href="'.$new_url_cn.'" />'; echo '<link rel="alternate" hreflang="en-us" href="'.$new_url_en.'" />'; echo '<link rel="alternate" hreflang="x-default" href="'.$new_url_cn.'" />'; ?> 同时需要在中文和英文站点都添加一遍。 之后,访问我们的网站,应该就能在<head>中找到相应的hreflang配置。 同步点赞量和浏览量 这一步和主题关联性比较大,可能并不适用于所有主题。我用的Joe主题,由主题本身连接数据库并读取写入点赞量、浏览量。我直接通过修改英文实例的主题代码,使其直接读写中文实例的点赞量和浏览量即可。 修改<webroot>/en/usr/themes/Joe/core/function.php中获取浏览量的函数: /* 查询文章浏览量 */ function _getViews($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $item->cid))['views']; if ($type) echo number_format($result); else return number_format($result); } 修改<webroot>/en/usr/themes/Joe/core/function.php中获取点赞量的函数: /* 查询文章点赞量 */ function _getAgree($item, $type = true) { - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $result = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $item->cid))['agree']; if ($type) echo number_format($result); else return number_format($result); } 修改<webroot>/en/usr/themes/Joe/core/route.php中主页显示浏览量部分的代码: $result[] = array( "mode" => $item->fields->mode ? $item->fields->mode : 'default', "image" => _getThumbnails($item), "time" => date('Y-m-d', $item->created), "created" => date('d/m/Y', $item->created), "title" => $item->title, "abstract" => _getAbstract($item, false), "category" => $item->categories, - "views" => number_format($item->views), + // "views" => number_format($item->views), + "views" => _getViews($item, false), "commentsNum" => number_format($item->commentsNum), - "agree" => number_format($item->agree), + // "agree" => number_format($item->agree), + "agree" => _getAgree($item, false), "permalink" => $item->permalink, "lazyload" => _getLazyload(false), "type" => "normal" ); 文章页面中显示浏览量部分的代码本身就是调用的_getViews,因此不需要修改。 修改增加浏览量部分的代码: /* 增加浏览量 已测试 √ */ function _handleViews($self) { $self->response->setStatus(200); $cid = $self->request->cid; /* sql注入校验 */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('views')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { 修改点赞和取消点赞部分代码: /* 点赞和取消点赞 已测试 √ */ function _handleAgree($self) { $self->response->setStatus(200); $cid = $self->request->cid; $type = $self->request->type; /* sql注入校验 */ if (!preg_match('/^\d+$/', $cid)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } /* sql注入校验 */ if (!preg_match('/^[agree|disagree]+$/', $type)) { return $self->response->throwJson(array("code" => 0, "data" => "Illegal request! Blocked!")); } - $db = Typecho_Db::get(); + // $db = Typecho_Db::get(); + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho' + ], Typecho_Db::READ | Typecho_Db::WRITE); $row = $db->fetchRow($db->select('agree')->from('table.contents')->where('cid = ?', $cid)); if (sizeof($row) > 0) { 完成上述修改后保存,接着访问英文站可发现和中文站的浏览量同步了。 同步评论 本来想弄个插件,hook一下发布评论的函数,在发布的时候同时在另一个实例的数据库中同步插入评论信息,但是发现我的Joe主体已经hook了,我再hook会冲突,因此直接编辑Joe主题的代码: 编辑<webroot>/index/usr/themes/Joe/core/factory.php: <?php require_once("phpmailer.php"); require_once("smtp.php"); /* 加强评论拦截功能 */ Typecho_Plugin::factory('Widget_Feedback')->comment = array('Intercept', 'message'); class Intercept { public static function message($comment) { ... Typecho_Cookie::delete('__typecho_remember_text'); + + $db = new Typecho_Db('Mysql', 'typecho_' /* Prefix */); + $db->addServer([ + 'host' => 'mysql', + 'user' => 'typecho_en', + 'password' => '[数据删除]', + 'charset' => 'utf8mb4', + 'port' => 3306, + 'database' => 'typecho_en' + ], Typecho_Db::READ | Typecho_Db::WRITE); + + $row = [ + 'coid' => $comment['coid'], // 必须包含新生成的评论ID + 'cid' => $comment['cid'], + 'created' => $comment['created'], + 'author' => $comment['author'], + 'authorId' => $comment['authorId'], + 'ownerId' => $comment['ownerId'], + 'mail' => $comment['mail'], + 'url' => $comment['url'], + 'ip' => $comment['ip'], + 'agent' => $comment['agent'], + 'text' => $comment['text'], + 'type' => $comment['type'], + 'status' => $comment['status'], + 'parent' => $comment['parent'] + ]; + + // 插入数据到目标数据库的 `comments` 表 + $db->query($db->insert('typecho_comments')->rows($row)); return $comment; } } ... 同理也在英文实例如此操作一番,插入到中文的数据库中 这个方案存在的一个问题就是如果有垃圾评论需要删除,需要分别去两个实例各删除一次,后面再修吧(x 参考文章: typecho/languages - GitHub
2025年11月19日
21 阅读
0 评论
0 点赞
跨平台服务编写日记 Ep.2 进程间通讯(IPC)
前情提要 上一篇文章实现了统一的日志管理,这篇文章来实现进程间消息通讯,即IPC。 分析 Windows 在Windows下,主要通过管道(Pipe)实现进程间通讯。管道又分为命名管道(Named Pipe)和匿名管道(Anonymous Pipe)。其中,匿名管道是单向的,通常在父进程和子进程之间通讯[2]。而命名管道则可以是单向管道或双工管道,并且支持一对多通讯[3]。顾名思义,识别命名管道的唯一方式是它的名称,因此两个进程只要都连接到同一个名字的命名管道即可实现通信。 我们需要实现进程间的双向通讯,因此采用命名管道。大致思路就是:进程作为伺服模式,也就是接收端启动时创建一个线程,创建一个命名管道并监听管道内消息。当管道被连接时从中读取管道内数据;当进程作为发送端启动时尝试连接到同一个名称的管道,并写入消息内容。 Linux 在Linux下通常使用socket进行进程间通讯。不过不同于监听端口,进程间通讯一般会选择监听一个sock文件[5],常见的服务类应用如docker daemon、mysql都是通过这种方式。 因此,大致思路如下:作为伺服模式启动的进程创建一个socket监听,并等待从中接收消息;发送端连接到socket套接字并发送消息。和上文命名管道的名称类似,socket套接字会映射一个唯一的.sock文件,发送方只要打开这个文件即可发送消息。(实际上打开方式不是常规的打开文件,而是用socket专用的打开方式[5]) 代码实现 初始化 为了实现共用一套主代码,我使用了和上一篇文章中一样的通过宏定义区分系统类型的方案,将Windows和Linux的代码分别写在service-windows.h和service-linux.h两个头文件中: #ifdef _WIN32 #include "service-windows.h" #elif defined(__linux__) #include "service-linux.h" #endif 当接收端进程启动时,创建一个线程处理收信息(使用std::thread作为多线程库): thread_bind = std::thread(bind_thread_main); 监听部分 Windows 在Windows下,只要尝试从指定名称的命名管道读取数据即可。其中,因为设置了管道为等待模式(即下文中CreateNamedPipe的第三个参数DWORD dwPipeMode中设置了PIPE_WAIT),ConnectNamedPipe会是阻塞模式,因此不用担心不断循环造成的性能损失。 void bind_thread_main() { while (!exit_requested.load()) { HANDLE hPipe = CreateNamedPipe( PIPE_NAME, PIPE_ACCESS_DUPLEX, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, 1024, // Output buffer size 1024, // Input buffer size 0, // Default timeout NULL); if (hPipe == INVALID_HANDLE_VALUE) { service_log.push(LEVEL_WARN, "Failed to create pipe: %d", GetLastError()); continue; } if (ConnectNamedPipe(hPipe, NULL) || GetLastError() == ERROR_PIPE_CONNECTED) { char buffer[1024]; DWORD bytesRead; if (ReadFile(hPipe, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) { buffer[bytesRead] = '\0'; m_queueMsg.push(buffer); service_log.push(LEVEL_VERBOSE, "Message received: %s", buffer); } FlushFileBuffers(hPipe); DisconnectNamedPipe(hPipe); CloseHandle(hPipe); } else { CloseHandle(hPipe); } } } Linux 为了防止创建失败,在创建前会先尝试删除没有清理干净的sock文件,即代码中unlink(SOCKET_PATH)。SOCKET_PATH为全局变量,定义了套接字文件的路径。创建套接字时,指定family为AF_UNIX代表创建UNIX套接字,即.sock文件的这种类型(如果是网络套接字就是AF_INET)。timeval这段代码设置了一个超时限制,当accept函数等待时间超过设置的SOCKET_TIMEOUT超时时间(单位秒)后会自动结束阻塞并返回错误信息。创建完套接字后按照正常流程设置绑定和监听即可。 void bind_thread_main() { unlink(SOCKET_PATH); int server_fd = socket(AF_UNIX, SOCK_STREAM, 0); if (server_fd == -1) { service_log.push(LEVEL_FATAL, "Failed to create socket"); exit_requested.store(true); return; } struct timeval tv; tv.tv_sec = SOCKET_TIMEOUT; tv.tv_usec = 0; setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (bind(server_fd, (sockaddr*)&addr, sizeof(addr)) == -1) { service_log.push(LEVEL_FATAL, "Bind failed"); close(server_fd); exit_requested.store(true); return; } if (listen(server_fd, 5) == -1) { service_log.push(LEVEL_FATAL, "Listen failed"); close(server_fd); exit_requested.store(true); return; } while (!exit_requested.load()) { int client_fd = accept(server_fd, nullptr, nullptr); if (client_fd != -1) { char buffer[1024]; int bytes_read = read(client_fd, buffer, sizeof(buffer) - 1); if (bytes_read > 0) { buffer[bytes_read] = '\0'; m_queueMsg.push(buffer); service_log.push(LEVEL_VERBOSE, "Message received: %s", buffer); } close(client_fd); } else { if (errno == EWOULDBLOCK || errno == EAGAIN) { continue; } service_log.push(LEVEL_WARN, "Failed to accept socket connection"); } } } 当读取到消息后,两份代码都会将消息保存至阻塞队列m_queueMsg中。 发送部分 Windows 打开指定管道并写入消息内容即可: bool send_message(const std::string& msg) { if (!WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER)) { service_log.push(LEVEL_ERROR, "Failed to find valid pipe: %d", GetLastError()); return false; } HANDLE hPipe = CreateFile( PIPE_NAME, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if (hPipe == INVALID_HANDLE_VALUE) { service_log.push(LEVEL_ERROR, "Failed to connect: %d", GetLastError()); return false; } DWORD bytesWritten; if (WriteFile(hPipe, msg.c_str(), (DWORD)msg.size(), &bytesWritten, NULL)) { service_log.push(LEVEL_VERBOSE, "Message sent: %s", msg.c_str()); CloseHandle(hPipe); return true; } else { service_log.push(LEVEL_ERROR, "Message (%s) send failed: %d", msg.c_str(),GetLastError()); CloseHandle(hPipe); return false; } } Linux 同理,连接套接字,发送数据: bool send_message(const std::string& msg) { int sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock == -1) { service_log.push(LEVEL_ERROR, "Failed to create socket"); return false; } sockaddr_un addr{}; addr.sun_family = AF_UNIX; strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); if (connect(sock, (sockaddr*)&addr, sizeof(addr)) == -1) { service_log.push(LEVEL_ERROR, "Connect failed"); close(sock); return false; } if (write(sock, msg.c_str(), msg.size()) == -1) { service_log.push(LEVEL_ERROR, "Message send failed: %s", msg.c_str()); close(sock); return false; } else { service_log.push(LEVEL_VERBOSE, "Message sent success: %s", msg.c_str()); close(sock); return true; } } 清理 Windows下没什么需要清理的,Linux下删除套接字文件即可: unlink(SOCKET_PATH); 效果示意图 Windows Linux 样例代码下载: IPCTest.zip 参考文章: https://learn.microsoft.com/zh-cn/windows/win32/ipc/pipes https://learn.microsoft.com/zh-cn/windows/win32/ipc/anonymous-pipes https://learn.microsoft.com/zh-cn/windows/win32/ipc/named-pipes https://www.cnblogs.com/alantu2018/p/8493809.html https://blog.csdn.net/dog250/article/details/100998838
2025年05月19日
120 阅读
0 评论
2 点赞
跨平台服务编写日记 Ep.1 统一的日志管理
前阵子心血来潮,想为在使用的一个跨平台的控制台服务类应用程序编写一个自己的管理程序,加一些功能。因而设计了一套简单的服务运行流程。 {alert type="warning"} 本系列文章中的观点和方案为我根据自己已有的知识储备结合DeepSeek的帮助所归纳设计,并未经过严格测试,并不保证在生产环境中使用的可行性和稳定性 {/alert} 大致思路 大致分为个线程,分别用作: 日志记录 目标应用实例管理,可能不止一个线程 监听IPC消息 处理IPC收到的消息(主进程) 本文着重讨论的是日志记录部分。 编写思路 为什么要给日志记录单开一个线程,个人考虑是因为本身就是多线程的架构,需要编写一个统一的日志记录模块。如果每个线程单独打印,则很有可能出现两个线程同时写入文件或者同时输出到控制台,造成日志混乱。 因此,日志记录大致思路就是: 定义一个队列,存储日志内容和等级 创建一个线程,不断地从线程中取出元素,根据设定的日志等级决定是否打印到控制台或者输出到文件 外部push日志内容到队列中 一些细节上的内容 保证可移植性,尽量使用STL库编写,如使用std::thread而不是pthread 保证线程安全,需要使用互斥锁之类的保护相应变量 让日志队列为空时线程等待,想到编写一个类似于Java下BlockingQueue的阻塞队列 指定一个日志等级,超过这个等级的日志才会被保存或者打印 通过va_list实现不定参数,使日志记录有sprintf的的使用体验 开始编写 有了上述思路,整体编写就很简单了。 BlockingQueue 偷懒了,这部分直接让DeepSeek写的 为了实现一个多线程安全的阻塞队列,当队列为空时调用front()会阻塞直到其他线程添加元素,我们可以结合互斥锁(std::mutex)和条件变量(std::condition_variable)来同步线程操作。 代码实现 互斥锁(std::mutex) 所有对队列的操作(push、front、pop、empty)都需要先获取锁,确保同一时间只有一个线程能修改队列,避免数据竞争。 条件变量(std::condition_variable) 当调用front()时,如果队列为空,线程会通过cv_.wait()释放锁并阻塞,直到其他线程调用push()添加元素后,通过cv_.notify_one()唤醒一个等待线程。 cv_.wait()需配合std::unique_lock,并在等待时自动释放锁,避免死锁。 使用谓词检查([this] { return !queue_.empty(); })防止虚假唤醒。 元素获取与移除 front()返回队首元素的拷贝(而非引用),确保调用者获得数据时队列的锁已释放,避免悬空引用。 pop()需显式调用以移除元素,确保队列状态可控。 #include <queue> // 队列 #include <mutex> // 互斥锁 #include <condition_variable> // 条件变量 template<typename T> class BlockingQueue { public: // 向队列中添加元素 void push(const T& item) { std::lock_guard<std::mutex> lock(mtx_); queue_.push(item); cv_.notify_one(); // 通知一个等待的线程 } // 获取队首元素(阻塞直到队列非空) T front() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); // 阻塞直到队列非空 return queue_.front(); } // 获取队首元素并移除 T take() { std::unique_lock<std::mutex> lock(mtx_); cv_.wait(lock, [this] { return !queue_.empty(); }); T item = std::move(queue_.front()); // 移动语义避免拷贝 queue_.pop(); return item; } // 移除队首元素(需外部调用,非阻塞) void pop() { std::lock_guard<std::mutex> lock(mtx_); if (!queue_.empty()) { queue_.pop(); } } // 检查队列是否为空 bool empty() const { std::lock_guard<std::mutex> lock(mtx_); return queue_.empty(); } private: mutable std::mutex mtx_; // 互斥锁 std::condition_variable cv_; // 条件变量 std::queue<T> queue_; // 内部队列 }; Log类 Log.h #pragma once #include <iostream> #include <fstream> #include <cstring> #include <thread> #include <chrono> #include <mutex> #include <cstdio> #include <cstdarg> #include <atomic> #include "BlockingQueue.h" enum LogLevel { LEVEL_VERBOSE,LEVEL_INFO,LEVEL_WARN,LEVEL_ERROR,LEVEL_FATAL,LEVEL_OFF }; struct LogMsg { short m_LogLevel; std::string m_strTimestamp; std::string m_strLogMsg; }; class Log { private: std::ofstream m_ofLogFile; // 日志文件输出流 std::mutex m_lockFile; // 文件操作互斥锁 std::thread m_threadMain; // 后台日志处理线程 BlockingQueue<LogMsg> m_msgQueue; // 线程安全阻塞队列 short m_levelLog, m_levelPrint; // 文件和控制台日志级别阈值 std::atomic<bool> m_exit_requested{ false }; // 线程退出标志 std::string getTime(); // 获取当前时间戳 std::string level2str(short level, bool character_only); // 级别转字符串 void logThread(); // 后台线程函数 public: Log(short default_loglevel = LEVEL_WARN, short default_printlevel = LEVEL_INFO); ~Log(); void push(short level, const char* msg, ...); // 添加日志(支持格式化) void set_level(short loglevel, short printlevel); // 设置日志级别 bool open(std::string filename); // 打开日志文件 bool close(); // 关闭日志文件 }; Log.cpp #include "Log.h" std::string Log::getTime() { using sc = std::chrono::system_clock; std::time_t t = sc::to_time_t(sc::now()); char buf[20]; #ifdef _WIN32 std::tm timeinfo; localtime_s(&timeinfo,&t); sprintf_s(buf, "%04d.%02d.%02d-%02d:%02d:%02d", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec ); #else strftime(buf, 20, "%Y.%m.%d-%H:%M:%S", localtime(&t)); #endif return buf; } std::string Log::level2str(short level, bool character_only) { switch (level) { case LEVEL_VERBOSE: return character_only ? "V" : "Verbose"; case LEVEL_WARN: return character_only ? "W" : "Warning"; case LEVEL_ERROR: return character_only ? "E" : "Error"; case LEVEL_FATAL: return character_only ? "F" : "Fatal"; } return character_only ? "I" : "Info"; } void Log::logThread() { while (true) { LogMsg front = m_msgQueue.take(); // 阻塞直到有消息 // 处理文件写入 if (front.m_LogLevel >= m_levelLog) { std::lock_guard<std::mutex> lock(m_lockFile); // RAII 管理锁 if (m_ofLogFile) { m_ofLogFile << front.m_strTimestamp << ' ' << level2str(front.m_LogLevel, true) << ": " << front.m_strLogMsg << std::endl; } } // 处理控制台打印 if (front.m_LogLevel >= m_levelPrint) { printf("%s %s: %s\n", front.m_strTimestamp.c_str(), level2str(front.m_LogLevel, true).c_str(), front.m_strLogMsg.c_str()); } // 检查退出条件:队列为空且标志为真 if (m_exit_requested.load() && m_msgQueue.empty()) break; } return; } Log::Log(short default_loglevel, short default_printlevel) { set_level(default_loglevel, default_printlevel); m_threadMain = std::thread(&Log::logThread, this); } Log::~Log() { m_exit_requested.store(true); m_msgQueue.push({ LEVEL_INFO, getTime(), "Exit." }); // 唤醒可能阻塞的线程 if (m_threadMain.joinable()) m_threadMain.join(); close(); // 确保文件关闭 } void Log::push(short level, const char* msg, ...) { va_list args; va_start(args, msg); const int len = vsnprintf(nullptr, 0, msg, args); va_end(args); if (len < 0) return; std::vector<char> buf(len + 1); va_start(args, msg); vsnprintf(buf.data(), buf.size(), msg, args); va_end(args); m_msgQueue.push({level,getTime(),buf.data()}); } void Log::set_level(short loglevel, short printlevel) { m_levelLog = loglevel; m_levelPrint = printlevel; } bool Log::open(std::string filename) { m_lockFile.lock(); m_ofLogFile.open(filename.c_str(), std::ios::out); m_lockFile.unlock(); return (bool)m_ofLogFile; } bool Log::close() { m_lockFile.lock(); m_ofLogFile.close(); m_lockFile.unlock(); return false; } 说明 类/结构说明 LogLevel 枚举 定义日志级别:VERBOSE, INFO, WARN, ERROR, FATAL, OFF。 OFF不应被用于记录的日志等级,仅用于当需要关闭日志记录时将阈值设置为该项以实现所有日志都不记录 LogMsg 结构体 封装日志消息: m_LogLevel:日志级别。 m_strTimestamp:时间戳字符串。 m_strLogMsg:日志内容。 成员变量说明 变量 说明 m_ofLogFile 文件输出流,用于写入日志文件。 m_lockFile 互斥锁,保护文件操作。 m_threadMain 后台线程,处理日志消息的消费。 m_msgQueue 阻塞队列,存储待处理的日志消息。 m_levelLog 写入文件的最低日志级别(高于此级别的消息会被记录)。 m_levelPrint 打印到控制台的最低日志级别。 m_exit_requested 原子标志,控制日志线程退出。 函数说明 函数 说明 getTime 获取当前时间戳字符串(跨平台实现)。 level2str 将日志级别转换为字符串(如 LEVEL_INFO → "I" 或 "Info")。 logThread 后台线程函数:消费队列消息,写入文件或打印。 构造函数 初始化日志级别,启动后台线程。 析构函数 设置退出标志,等待线程结束,确保处理剩余消息。 push 格式化日志消息(支持可变参数)并推入队列。 set_level 动态设置日志级别和打印级别。 open/close 打开/关闭日志文件。 完整代码及测试样例下载: demo.zip
2025年04月19日
66 阅读
0 评论
2 点赞
C++使用WINAPI查询DNS解析记录
这篇文章是古早时期的博客上迁移过来的(什么赛博秽土转生), 如有错误欢迎指正 背景 为API配置了多条访问线路,以应对部分地区无法访问导致服务不可用的情况。开始是想到在网站里新建一个文件保存节点信息,发现行不通,联不通的地区根本无法获知其他线路;于是想到用DNS解析记录,用一个TXT解析记录来保存相应的节点数据,以备查询 实战 查询资料 一番Bing下来,发现大部分现有的文章都是使用socket来直接发送查询数据包,获取到的也都是A和CNAME类型的记录,方案不可行。最后,把目光锁定到了MSDN上的DnsQuery函数上,同时找到一篇样例:使用DnsQuery解析主机名 函数分析 从官方文档上得知,函数所需头文件为windns.h,需要包含引入库文件Ws2_32.lib和Dnsapi.lib,函数的参数如下: DNS_STATUS DnsQuery_A( [in] PCSTR pszName, [in] WORD wType, [in] DWORD Options, [in, out, optional] PVOID pExtra, [out, optional] PDNS_RECORD *ppQueryResults, [out, optional] PVOID *pReserved ); 参数解释: pszName是要查询的主机名; wType是查询类型,如A记录,CNAME,TXT等,具体的MSDN官方给出了文档:DNS Constants Options字面意思上理解是查询方式,我直接使用DNS_QUERY_STANDARD,具体也可以查询文档:DNS Constants pExtra和pReserved直接给NULL就行 ppQueryResults传入查询结果的指针,类型是PDNS_RECORD,这个需要传入变量值的引用。 返回值,DNS_STATUS类型,如果查询失败会返回Winerror.h中的相应错误码 使用 明白了使用方法,那就简单了: 我的TXT记录域名为test.iyoroy.cn,查询部分代码如下: PDNS_RECORD pDnsRecord; DNS_STATUS QueryRet = DnsQuery(L"test.iyoroy.cn", DNS_TYPE_TEXT, DNS_QUERY_STANDARD, NULL, &pDnsRecord, NULL); if (QueryRet) { MessageBox(GhWnd, L"DNS查询失败!\r\n将使用默认节点", L"Warning", MB_ICONERROR | MB_OK); } std::wstring strQueryRes = *pDnsRecord->Data.TXT.pStringArray;//这个就是查询结果 成功实现功能 后记 TXT记录会自动过滤掉空格、换行,因此我选择记录base64编码,使用时再解码并使用stringstream拆分空格。Base64解码我借鉴了这篇文章:C++进行base64编码和解码,以下是我用宽字节重写的解码函数 {collapse} {collapse-item label="展开代码"} //Function:Base64解密 static const std::wstring base64_chars = L"ABCDEFGHIJKLMNOPQRSTUVWXYZ" L"abcdefghijklmnopqrstuvwxyz" L"0123456789+/"; static inline bool is_base64(wchar_t c) { return (isalnum(c) || (c == '+') || (c == '/')); } std::wstring base64_decode(std::wstring const& encoded_string) { int in_len = encoded_string.size(); int i = 0; int j = 0; int in_ = 0; wchar_t char_array_4[4], char_array_3[3]; std::wstring ret; while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) { char_array_4[i++] = encoded_string[in_]; in_++; if (i == 4) { for (i = 0; i < 4; i++) char_array_4[i] = base64_chars.find(char_array_4[i]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (i = 0; (i < 3); i++) ret += char_array_3[i]; i = 0; } } if (i) { for (j = i; j < 4; j++) char_array_4[j] = 0; for (j = 0; j < 4; j++) char_array_4[j] = base64_chars.find(char_array_4[j]); char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4); char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2); char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3]; for (j = 0; (j < i - 1); j++) ret += char_array_3[j]; } return ret; } 拆分: wstringstream wDnsRec(base64_decode(*pDnsRecord->Data.TXT.pStringArray)); wstring wNodes[16]; unsigned int nNodes = 0; while (wDnsRec >> wNodes[nNodes]) nNodes++; {/collapse-item} {/collapse} 之后再选择可用节点就行 参考文章: https://learn.microsoft.com/zh-cn/previous-versions/troubleshoot/windows/win32/use-dnsquery-resolve-host-names https://learn.microsoft.com/en-us/windows/win32/dns/dns-constants https://blog.csdn.net/sky04/article/details/6881649
2025年03月20日
110 阅读
0 评论
2 点赞
小米845系列机器类原生调用红外摄像头进行人脸识别
背景 小米845系列的小米8、小米8屏幕指纹版有一颗前置红外摄像头用于面部识别,这样即使在无光的环境下仍可以使用面部解锁。然而如果使用LineageOS的设备树不加修改编译出来的类原生无法使用红外摄像头,只能使用普通的摄像头,而PixelExperience的设备树是可以的,故探究其原因。 原因 CameraID 首先是需要指定人脸识别模块使用红外摄像头。翻阅PixelExperience的相关设备树源码可以发现:在overlay中设置了调用CameraID为5的摄像头用作人脸识别。 https://github.com/PixelExperience-Devices/device_xiaomi_dipper/blob/fourteen/overlay/packages/apps/FaceUnlockService/app/src/main/res/values/config.xml <?xml version="1.0" encoding="utf-8"?> <resources> <integer name="override_front_cam_id">5</integer> <bool name="use_alternative_vendor_impl">true</bool> </resources> PixelExperience使用的是motorola的人脸解锁方案,而现在很多类原生使用的是AOSPA的ParanoidSense,因此上述设置的overlay并不适用。 查阅ParanoidSense源码可以看到:使用了Properties属性ro.face.sense_service.camera_id标识使用的摄像头。 https://github.com/AOSPA/android_packages_apps_ParanoidSense/blob/uvite/src/co/aospa/sense/camera/CameraUtil.kt#L32 val cameraIdProp = SystemProperties.get("ro.face.sense_service.camera_id") 因此理论上只要设置ro.face.sense_service.camera_id属性为5即可使其调用红外人脸摄像头。 vendor.camera.aux.packagelist属性 如果仅仅设置上述CameraID相关属性,那么人脸模块调用时会直接报错。翻阅Frameworks部分的代码可以得知:系统默认隐藏了除主前摄和主后摄之外的其他辅助摄像头(即AUXCamera),而红外摄像头也属于AUXCamera之一。 取LineageOS的Framework代码作为例子: https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L295-L301 /** * Returns the number of physical cameras available on this device. * The return value of this method might change dynamically if the device * supports external cameras and an external camera is connected or * disconnected. * * If there is a * {@link android.hardware.camera2.CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA * logical multi-camera} in the system, to maintain app backward compatibility, this method will * only expose one camera per facing for all logical camera and physical camera groups. * Use camera2 API to see all cameras. * * @return total number of accessible camera devices, or 0 if there are no * cameras or an error was encountered enumerating them. */ public static int getNumberOfCameras() { int numberOfCameras = _getNumberOfCameras(); if (!shouldExposeAuxCamera() && numberOfCameras > 2) { numberOfCameras = 2; } return numberOfCameras; } 不难看出,即使设备有大于2颗摄像头,若函数shouldExposeAuxCamera()返回为False则会强制指定摄像头数量为2,阻止程序访问CameraID>2的摄像头;再来看看这个判断函数的定义: https://github.com/LineageOS/android_frameworks_base/blob/lineage-21.0/core/java/android/hardware/Camera.java#L264-L278 /** * @hide */ public static boolean shouldExposeAuxCamera() { /** * Force to expose only two cameras * if the package name does not falls in this bucket */ String packageName = ActivityThread.currentOpPackageName(); if (packageName == null) return true; List<String> packageList = Arrays.asList( SystemProperties.get("vendor.camera.aux.packagelist", packageName).split(",")); List<String> packageExcludelist = Arrays.asList( SystemProperties.get("vendor.camera.aux.packageexcludelist", "").split(",")); return packageList.contains(packageName) && !packageExcludelist.contains(packageName); } 可以看出,假如包名在属性vendor.camera.aux.packagelist中并且不在vendor.camera.aux.packageexcludelist中就会返回为True,否则False。 那么解决方案就很明了了:将ParanoidSense的包名加到vendor.camera.aux.packagelist属性中即可让其能够调用到红外摄像头。 解决 将ParanoidSense的包名co.aospa.sense加到vendor.camera.aux.packagelist中: https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_sdm845-common/commit/28fe5bc41d49b31b4acb840bf167b70d70a40c61 设置ro.face.sense_service.camera_id属性以指定摄像头: https://github.com/YoriInstitute/crDroidAndroid_device_xiaomi_equuleus/commit/67518f130650e4592b5f4c7210248072058d48cc https://github.com/YoriInstitute/AOSPA_device_xiaomi_equuleus/blob/topaz/device.mk#L397-L399 在crDroid魔改的ParanoidSense中,因指定了其位于system_ext(https://gitlab.com/crdroidandroid/android_packages_apps_FaceUnlock/-/commit/545688260eb32ba19f348e84e3cae89ba29f20d1),故将这个属性加到system_ext的prop文件中。
2025年03月06日
150 阅读
0 评论
1 点赞
1
2