2024年7月

我们之前使用的是操作系统平台的线程,就称之为“系统线程”吧。虚拟线程是JDK维护的,原理跟WebFlux的底层实现差不多,都是工作线程分离。

要使用虚拟线程,需要使用JDK21以上,包括21。

虚拟线程可以创建很多很多

系统线程不能轻易创建太多,我们一直被教导创建线程是很重的活动。

        for (int i = 0; i < 1_000_000; i++) {
            new Thread(() -> {
                longAdder.increment();
                System.out.println(longAdder.longValue());
                try {
                    Thread.sleep(10000);
                } catch (Exception e) {
                    // deal with e
                }
            }).start();
        }

上面尝试创建百万个线程,线程都会休眠不结束。我用了一个
LongAdder
记录我的笔记本能实际创建多少线程。结果是4000多个,用了6秒:
image
改成虚拟线程就轻松成功:

        LongAdder longAdder = new LongAdder();
        for (int i = 0; i < 1_000_000; i++) {
            Thread.ofVirtual().start(() -> {
                longAdder.increment();
                System.out.println(longAdder.longValue());
                try {
                    Thread.sleep(100000);
                } catch (Exception e) {
                    // deal with e
                }
            });
        }

因为虚拟线程很轻量,所以不要使用线程池,可以很轻易的创建很多个。因为能创建很多,所以也不要使用 Thread Local 变量。

IO操作不好阻塞虚拟线程的使用

使用系统线程,必须通过线程池来处理多个任务,不然问题很严重:

    static void callService(String taskName) {
        try {
            System.out.println(Thread.currentThread() + " executing " + taskName);
            new URL("自己写一个http接口?sleep=2000").getContent();
            System.out.println(Thread.currentThread() + " completed " + taskName);

        } catch (Exception e) {
            // deal with e
        }
    }
	
try (ExecutorService executor = Executors.newFixedThreadPool(5)) {
	for (int i = 0; i <= 10; i++) {
		String taskName = "Task" + i;
		executor.execute(() -> callService(taskName));
	}
}

执行的时候你能看到,线程在执行结束前需要空闲等待任务的IO。毕竟每个任务都是在某一个线程上执行 —— 说这个干啥?
看一下虚拟线程

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 0; i <= 600; i++) {
                String taskName = "Task" + i;
                executor.execute(() -> callService(taskName));
            }
        }

这里创建了一个虚拟线程工厂(而不是线程池,记住不要使用虚拟化的线程池),它会给每个任务创建新的虚拟线程。
程序启动后,会立即打印600个"executing",而不是像系统线程那样只打印5个。
为了方便,我们少用几个任务来实验看一下输出:

VirtualThread[#50]/runnable@ForkJoinPool-1-worker-2 executing Task1
VirtualThread[#48]/runnable@ForkJoinPool-1-worker-1 executing Task0
VirtualThread[#51]/runnable@ForkJoinPool-1-worker-3 executing Task2
VirtualThread[#51]/runnable@ForkJoinPool-1-worker-2 completed Task2
VirtualThread[#48]/runnable@ForkJoinPool-1-worker-3 completed Task0
VirtualThread[#50]/runnable@ForkJoinPool-1-worker-1 completed Task1

仔细看,这里一共3个虚拟线程,因为工厂创建了三个,根据任务数来的。
但是每个任务都是在两个虚拟线程上:Task1 被worker-2接收,却被worker-1完成。

啥时候用

关键问题来了,我们总该使用虚拟线程吗?
对各种问题都通用的答案是:你没遇到问题就别想着解决问题。
如果的确有问题,想看看虚拟线程是否合适,可以看一下任务是否是IO密集型的。
对于计算密集型任务,系统线程比虚拟线程有效得多。
虚拟线程跟WebFlux一样,只能提升系统的吞吐量,并不能加快单个任务的完成时间。


BigDecimal是Java中用于高精度算术运算的类。当您需要精确地处理非常大或非常小的数字时,例如在金融计算中,它特别有用。由于众所周知得原因,Double这种类型在某些情况下会出现丢失精度的问题,所以在需要对较为敏感的数据(比如与金额有关的)进行运算时,我们都会用BigDecimal。但是,用BigDecimal不代表就一定没问题,我们今天就讨论一下关于BigDecimal的问题。

精度与刻度

要正确使用BigDecimal,首先要清楚精度(precision)和刻度(scale)的概念。

  • Precision(精度):表示数值的总位数,包括小数点前后的位数。例如,数值 123.45 的精度是 5,因为它有 5 位数字。

  • Scale(刻度):表示小数点后的位数。例如,数值 123.45 的刻度是 2,因为小数点后有 2 位数字。

举个例子,如果一个数值类型定义为 DECIMAL(7, 2),那么它的精度是 7,刻度是 2。这意味着这个数值最多可以有 7 位数字,其中 2 位在小数点后,5 位在小数点前。
(p.s. DECIMAL这个数值类型通常是用在数据库中的,JAVA中并没有这个类型。用这个例子是因为它可以最清晰地说明精度与刻度)

BigDecimal类中也有获取精度和刻度的方法

    BigDecimal num = new BigDecimal("12.1234");
	System.out.println(String.format("precision:%s scale:%s", num.precision(),num.scale()));

    //输出:precision:6 scale:4

除法中的刻度

在用BigDecimal做除法运算,使用divide方法的时候,可以指定刻度,也可以不指定。

当指定刻度,即保留几位小数的时候,需要指定进位模式(RoundingMode)。
可选的模式有UP、DOWN、CEILING、FLOOR、HALF_UP、HALF_DOWN、HALF_EVEN、UNNECESSARY。
JDK api中用一个表格比较了这几种模式的区别

Result of rounding input to one digit with the given rounding mode

Input Number UP DOWN CEILING FLOOR HALF_UP HALF_DOWN HALF_EVEN UNNECESSARY
5.5 6 5 6 5 6 5 6 throw ArithmeticException
2.5 3 2 3 2 3 2 2 throw ArithmeticException
1.6 2 1 2 1 2 2 2 throw ArithmeticException
1.1 2 1 2 1 1 1 1 throw ArithmeticException
1.0 1 1 1 1 1 1 1 1
-1.0 -1 -1 -1 -1 -1 -1 -1 -1
-1.1 -2 -1 -1 -2 -1 -1 -1 throw ArithmeticException
-1.6 -2 -1 -1 -2 -2 -2 -2 throw ArithmeticException
-2.5 -3 -2 -2 -3 -3 -2 -2 throw ArithmeticException
-5.5 -6 -5 -5 -6 -6 -5 -6 throw ArithmeticException

按指定的规则进位,保留几位小数,这没有问题。

如果不指定刻度呢?

    BigDecimal one = new BigDecimal("1");
	BigDecimal eight = new BigDecimal("8");
    System.out.println(one.divide(eight));//输出 0.125
	BigDecimal three = new BigDecimal("3");
    System.out.println(one.divide(three));// java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

当结果能除尽的时候正常处理,当除不尽即结果是无限循环小数的时候,程序抛出异常。
看一下源码:

public BigDecimal divide(BigDecimal divisor) {
        /*
         * Handle zero cases first.
         */
        if (divisor.signum() == 0) {   // x/0
            if (this.signum() == 0)    // 0/0
                throw new ArithmeticException("Division undefined");  // NaN
            throw new ArithmeticException("Division by zero");
        }

        // Calculate preferred scale
        int preferredScale = saturateLong((long) this.scale - divisor.scale);

        if (this.signum() == 0) // 0/y
            return zeroValueOf(preferredScale);
        else {
            /*
             * If the quotient this/divisor has a terminating decimal
             * expansion, the expansion can have no more than
             * (a.precision() + ceil(10*b.precision)/3) digits.
             * Therefore, create a MathContext object with this
             * precision and do a divide with the UNNECESSARY rounding
             * mode.
             */
            MathContext mc = new MathContext( (int)Math.min(this.precision() +
                                                            (long)Math.ceil(10.0*divisor.precision()/3.0),
                                                            Integer.MAX_VALUE),
                                              RoundingMode.UNNECESSARY);
            BigDecimal quotient;
            try {
                quotient = this.divide(divisor, mc);
            } catch (ArithmeticException e) {
                throw new ArithmeticException("Non-terminating decimal expansion; " +
                                              "no exact representable decimal result.");
            }

            int quotientScale = quotient.scale();

            // divide(BigDecimal, mc) tries to adjust the quotient to
            // the desired one by removing trailing zeros; since the
            // exact divide method does not have an explicit digit
            // limit, we can add zeros too.
            if (preferredScale > quotientScale)
                return quotient.setScale(preferredScale, ROUND_UNNECESSARY);

            return quotient;
        }
    }

MathContext mc = new MathContext( (int)Math.min(this.precision() +(long)Math.ceil(10.0*divisor.precision()/3.0),Integer.MAX_VALUE),RoundingMode.UNNECESSARY);

这一句算了一个刻度,然后进位方式选了UNNECESSARY。所以当除不尽的时候就抛异常了。

也就是说它断定,如果一个除法能除尽,它的小数位小于this.precision() +(long)Math.ceil(10.0*divisor.precision()/3.0)

这句话的意思是,如果 this 除以 divisor 的商是一个有限小数,那么这个小数的位数不会超过以下公式计算的结果:
\(a.precision() + \lceil \frac{10 \cdot b.precision}{3} \rceil\)
其中:
• a.precision() 是 this 的精度,即 this 的总位数。
• b.precision() 是 divisor 的精度,即 divisor 的总位数。
• ceil 是向上取整函数。
这个公式的作用是估算有限小数的最大位数。具体来说,它考虑了 this 和 divisor 的精度,并通过一个比例关系来确定商的小数位数上限。
举个例子,如果 this 的精度是 5,divisor 的精度是 3,那么公式计算如下:
\(5 + \lceil \frac{10 \cdot 3}{3} \rceil = 5 + \lceil 10 \rceil = 5 + 10 = 15\)
因此,商的有限小数位数不会超过 15 位。

插一句啊,这是为什么?

AI是这样回答的:

这个公式的提出者并没有明确的记录,但它基于数论中的一些基本原理。证明这个公式需要用到基本算术定理(Fundamental Theorem of Arithmetic),即每个整数都可以唯一地分解为素数的乘积。
证明思路

  1. 有限小数的条件:
    一个分数
    \(\frac{a}{b}\)
    只有在其分母 (b) 仅包含 2 和 5 作为素因子时,才会有一个有限小数。这是因为 10 的唯一素因子是 2 和 5。
  2. 小数位数的估算:
    假设我们有一个分数
    \(\frac{a}{b}\)
    ,其中 (a) 和 (b) 是整数,并且 (b) 仅包含 2 和 5 作为素因子。我们可以将 (b) 表示为
    \((2^m \times 5^n)\)
    ,其中 (m) 和 (n) 是非负整数。
  3. 小数位数的上限:
    为了使分数
    \(\frac{a}{b}\)
    变成一个整数,我们需要将其乘以
    \((10^{\max(m, n)})\)
    。因此,有限小数的最大位数可以通过以下公式估算:
    \(a.precision + \lceil \frac{10 \cdot b.precision}{3} \rceil\)
    其中 (a.precision) 是分子 (a) 的位数,(b.precision) 是分母 (b) 的位数。

具体例子
假设我们有两个数:
• this = 12345(精度为 5,因为有 5 位数字)
• divisor = 678(精度为 3,因为有 3 位数字)
我们想知道 12345 除以 678 的商,如果是有限小数,它的小数部分最多有多少位。根据公式:
\(5 + \lceil \frac{10 \cdot 3}{3} \rceil = 5 + \lceil 10 \rceil = 5 + 10 = 15\)
因此,12345 除以 678 的商,如果是有限小数,小数部分最多有 15 位。

额...是数论啊?那我走,打扰了,打扰了...

为什么你应该用字符串来构造BigDecimal

聪明的你应该早就发现了,BigDecimal的构造方法有很多个。应该用哪个呢?很多人都知道应该用字符串,可是为什么呢?
因为,当你不用字符串的时候,会用很多意想不到的惊喜。

    BigDecimal strnum = new BigDecimal("12.1234");
    System.out.println(String.format("precision:%s scale:%s", strnum.precision(),strnum.scale()));//输出 precision:6 scale:4

    BigDecimal num = new BigDecimal(12.1234);
    System.out.println(String.format("precision:%s scale:%s", num.precision(),num.scale()));//输出precision:50 scale:48

下面一个刻度是48,这是什么鬼?

再试试除法

    BigDecimal one = new BigDecimal("0.1");
	BigDecimal eight = new BigDecimal("8");
    System.out.println(one.divide(eight));//输出 0.0125

    BigDecimal _1 = new BigDecimal(0.1);
	BigDecimal _8 = new BigDecimal(8);
    System.out.println(_1.divide(_8));//输出 0.0125000000000000006938893903907228377647697925567626953125

为什么会出现这种结果?这个原因众所周知:

二进制(也称为基数2)不能精确表示某些十进制数,尤其是那些在十进制中有有限小数位但在二进制中需要无限小数位的数。例如,0.1 和 0.2 在二进制中无法精确表示。
这是因为二进制系统只能使用 0 和 1 来表示数值,而某些十进制数在转换为二进制时会变成无限循环小数。例如:
• 0.1 在二进制中表示为 0.00011001100110011...(无限循环)
• 0.2 在二进制中表示为 0.0011001100110011...(无限循环)
由于计算机的存储空间有限,这些无限循环小数只能被截断,从而导致精度损失。这就是为什么在使用浮点数进行计算时,可能会出现精度问题。

如果你看源码你会发现 public BigDecimal(double val) 和 public BigDecimal(String val)的实现完全不同。或许你也会想为什么呢?为什么要实现两套不同的呢?你就直接这样:

public BigDecimal(double val) {
    this(String.valueOf(val));
}

不就完了?

至于JDK团队实现两套的原因是什么?你知道吗?欢迎留言告诉我:)


著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
原文:
https://wangxuan.me/tech/2024/07/16/the-precision-and-scale-of-BigDecimal.html

原文:
https://hi.imzlh.top/2024/07/18.cgi
(预发布)

很久没有写完全折腾类文章了,这还得追溯到上次折腾S905L3A那会。
这篇文章很长,但是是小白级包学会。

为什么会有这篇文章?

这篇文章本来应该在4个月前就完成了,但是我一直都在折腾NAS软件
其中我也尝试了很多方法,奈何时间不足断断续续的

  • WebList
    ,前身叫做
    weborf
    ,是一个小巧的支持简单的WebDAV的程序
    我修改了一下UI和认证部分,改动不多但是
    性能不强
    ,且
    只能服务静态文件
    ,很快抛弃
  • MomentPHP
    ,前身是PHPMVC,经过两个月的扩充,已经很全能了
    可惜不知道是什么原因,总是
    莫名其妙出点问题

    性能也不可乐观
    ,上个月后就抛弃了
  • vList5
    ,前身是
    vList3
    ,同样是为了Nginx设计,只不过改成了
    njs
    作为平台
    继承了NGINX超强并发,使用了VUE绝对响应式,在昨天正式完成主要功能

至于为什么会这么折腾,完全是因为光猫限制比较大。详细的内容看原文即可,我们现在开始吧

准备一台光猫

这个材料很好找,一般的光猫都可以,但是有些光猫锁得很死,很考验大家的技术

为光猫解锁telnet

为什么是telnet呢

  • telnet方便,可以远程管理
  • 有些机器甚至tty串口都锁死了,只能破解telnet

case1:热门机型直接找

这一步就需要大家熟练使用搜索引擎了。
型号很好找,直接打开
管理页面
,去网上搜索就
轻松找到

我的是H60G

case2:找不到或失败

可以尝试一些工具,比如
针对ZTE机型的爆破工具
这里就不重点介绍了,感兴趣的自己研究

连接到光猫

下载
putty
,telnet连接到192.168.1.1
账号密码就是上一步爆破得来的,直接输入就行了

putty

有的畜生光猫默认给的权限不是root,这个时候root密码一般都是
aDm8H%MdA
建议固定到Windows剪贴板(
Win标
+
V
)上,下一次登录就很方便了

剪贴板

为光猫干掉TR069

成功

运营商有一个坑爹的东西叫做远程管理,能批量远程配置
一旦配置下发,你可能就又得再重新配置一次了(《从0开始的折腾之旅》)
因此为了杜绝后患,必须得把远程管理这个隐患干掉

sidbg or sendcmd

我接触过两种光猫,有两种管理工具,除了名字不一样,命令是一样的
你可以试试看哪种命令适合你的光猫

sendcmd 1 DB set WANC 0 Enable 0
sidbg 1 DB set WANC 0 Enable 0

然后你也可以顺便改一下超级密码,这样下次Web端登录会更方便
(比如我家的光猫就是账号
root
密码
toor
,怎么样好记吗)

sidbg 1 DB set DevAuthInfo 0 User [账号]
sidbg 1 DB set DevAuthInfo 0 Pass [密码]

查看架构

~ $ cat /proc/cpuinfo
processor
:
0
model name
:
ARMv7
Processor rev 1 (
v7l
)
BogoMIPS : 1594.16
Features
: half thumb fastmult edsp tls
CPU implementer : 0x41
CPU architecture : 7
CPU variant : 0x4
CPU part : 0xc09
CPU revision : 1

processor
:
1
model name
:
ARMv7
Processor rev 1 (
v7l
)
BogoMIPS : 1594.16
Features
: half thumb fastmult edsp tls
CPU implementer : 0x41
CPU architecture : 7
CPU variant : 0x4
CPU part : 0xc09
CPU revision : 1

Hardware : ZTE ZX279128 (Device Tree)
Revision : 0000
Serial : 0000000000000000

  • 首先看
    processor
    ,有两个,是古董级双核处理器
  • 再看
    model name
    ,是Armv7l,ARM的最后一代32位处理器
  • 最后是
    Features
    ,没有vfp,就是
    arm-eabi
    ,有vfp那就是
    arm-eabihf
  • 最后可以了解一下CPU,通过
    Hardware
    可以了解,比如我这一块CPU就是

    ZX279128S是一款功能强大的SoC(System on Chip)芯片。芯片内嵌ARM Cortex
    A9双核处理器
    以及丰富的外设,使用AXI高性能总线实现互联,CPU的频率达到
    1000MHz
    ,可实现复杂协议报文的处理。

armv7,但是
没有vfp
,那就当作是
armv5
,或者说是
armel
,别搞错了
mips的可能稍微难搞一点,下文可能需要自己编译软件

考虑安家位置

这个很重要,划重点
根目录大多是有CRC校检的或者直接打包进内核的,bin这类文件夹大多放在根目录。所以
想要重启后不消失或者不变砖
,建议找
/usr/data
,空间也大
或者这里更建议扔到自启动脚本周围,如我的光猫就保存到
/usr/local/osgi/
,方便
下文默认所有文件都保存到
/usr/local/osgi/

,如果不是,你可能需要修改nginx配置并替换所有出现的
/usr/local/osgi/

举个例子

准备软件

想要ctrl作为软件管理程序?自行参考
我写的文章
[懒人版一键复制]
这里假设你的光猫有curl自带,如果没有,参考
https://hi.imzlh.top/2024/03/02.cgi

# aria2
curl https://hi.imzlh.top/usr/uploads/2024/03/971539169.gz | gunzip - > aria2
# nginx
curl https://hi.imzlh.top/usr/uploads/2024/07/1871251897.gz | gunzip - > nginx
# natmap
curl https://hi.imzlh.top/usr/uploads/2024/07/1468987588.gz | gunzip - > natmap

配置nginx

首先,你需要安装vList5和一个
fancyindex美化包
,方法也非常简单
假设你的硬盘已经挂载到了
/mnt
,如果没有,请执行
mount /dev/sda /mnt

curl -L https://github.com/imzlh/vlist-njs/releases/download/master/vlist5.js -o /usr/local/osgi/vlist.js
cd /mnt
curl https://github.com/imzlh/vList5/releases/download/main/vlist5_latest.tgz -L | tar xz
mkdir .static
cd .static
curl https://hi.imzlh.top/usr/uploads/2024/07/2093537519.gz | tar xz

关于nginx配置文件这里直接抄作业就行了,保存到
/usr/local/osgi/etc/nginx/nginx.conf
我可是研究了将近一天研究出来了这一份高性能高并发的nginx配置
(B:你似乎很骄傲啊,你知不知道,你这一句就相当于赤裸裸地在说 “快夸我,快给我三连”啊?)
(别忘了新建文件夹,
mkdir /usr/local/osgi/etc/nginx/
)

worker_processes                                auto;

events {
        accept_mutex                            on;
        multi_accept                            on;
        use                                     epoll;
        worker_connections                      1024;
}

http {
        include                                 mime.types;
        default_type                            application/octet-stream;
        log_format                              main '$status $remote_addr $request';
        access_log                              /tmp/nginx/access.log main;

        tcp_nopush                              off;
        tcp_nodelay                             on;

        client_max_body_size                    256m;
        client_body_buffer_size                 128m;
        client_header_timeout                   1m;
        client_body_timeout                     10m;
        send_timeout                            10m;
        reset_timedout_connection               on;
        client_body_in_file_only                clean;
        client_body_in_single_buffer            on;

        gzip                                    on;
        gzip_min_length                         1024;
        gzip_buffers                            4 128k;
        gzip_types                              text/plain;
        gzip_vary                               on;

        output_buffers                          2 128k;
        postpone_output                         1460;
        sendfile                                on;
        sendfile_max_chunk                      256k;
        directio                                4m;
        keepalive_timeout                       1h;
        open_file_cache                         max=1024 inactive=1m;
        open_file_cache_min_uses                4;

        server_tokens                           off;

        proxy_cache_path                        /tmp/nginx/cache/ levels=1:2
                                                    keys_zone=thumb:1M inactive=10h max_size=256M;
        proxy_http_version                      1.1;
        ssl_session_cache                       shared:SSL:1m;

        dav_ext_lock_zone                       zone=dav:1m timeout=1h;

        js_shared_dict_zone                     zone=njs:1m type=string;
        resolver                                114.114.114.114 223.5.5.5 8.8.8.8;

        http2_recv_buffer_size                  128k;
        http2_chunk_size                        128k;
        http3_stream_buffer_size                128k;

        server {
                listen                          81;
                # listen                          [::]:8443 ssl;
                # listen                          [::]:444 quic;
                listen                          [::]:88;


                # http2                           on;
                # http3                           on;

                # ssl_certificate                 [证书cer/crt文件];
                # ssl_certificate_key             [证书key文件];
                # ssl_protocols                   TLSv1.2 TLSv1.3;
                # ssl_buffer_size                 32k;
                # ssl_early_data                  on;
                # ssl_session_timeout             10m;

                root                            /mnt/;
                index                           index.html;

                fancyindex                      on;
                fancyindex_localtime            on;
                fancyindex_exact_size           off;
                fancyindex_header               /.static/header.html;
                fancyindex_footer               /.static/footer.html;
                fancyindex_css_href             /.static/core.css;
                fancyindex_default_sort         name;
                fancyindex_show_path            off;
                fancyindex_hide_parent_dir      on;
                fancyindex_time_format          "%y/%m/%d %H:%M";

                charset_types                   *;
                charset                         utf-8;

                dav_methods                     PUT DELETE MKCOL COPY MOVE;
                dav_ext_methods                 PROPFIND OPTIONS LOCK UNLOCK;
                dav_access                      user:rw group:rw all:r;
                create_full_put_path            on;
                dav_ext_lock                    zone=dav;

                # js_path                               lib/nginx/;
                js_import                               /usr/local/osgi/lib/nginx/vlist.js;
                js_import                               /usr/local/osgi/lib/nginx/ddns.js;
                js_fetch_buffer_size            128k;
                js_fetch_max_response_buffer_size 4m;
                js_fetch_timeout                30s;
                js_fetch_verify                 off;

                add_header      Alt-Svc                         'h3=":444"; h2=":8443"; ma=86400';
                add_header      Access-Control-Allow-Origin     * always;
                add_header      Access-Control-Allow-Headers    "Content-Type, Authorization" always;

                location = /@api/{
                        js_var                  $authkey "[请自己设一个密码]";
                        js_content              vlist.main;
                }
        }
}

PS
想要SSL? 打开注释掉的内容即可

考虑:动态更新

在这么精简的系统内,如何处理
请求更新
呢?
那只有使用 curl 才行。如果没有请使用 wget也差不多(嘛,HTTPS就别想了)

参考readme.md,这是适用于natmap地自动更新脚本的参数列表

argv[0]: Script path
argv
1
:
Public address
(IPv4/IPv6)
argv
2
:
Public port
argv
3
: IP4P
argv
4
: Bind port (private port)
argv
5
: Protocol (TCP/UDP)
argv
6
: Private address (IPv4/IPv6)

所以脚本也很好写(保存到
/usr/local/osgi/update.sh
,别忘了
chmod +x update.sh
):

#!/bin/sh
# 换成自己的动态重定向服务,如我的是redirect.php
curl https://[你的服务地址]?addr=$1:$2
# wget也可以,但是不支https
wget http://[你的服务地址]?addr=$1:$2 -O - >> /dev/null

对于所有系统,直接使用
cat > update.sh
,输完了
Ctrl+D
即可
(如果系统很好心提供了
vi
,那就更方便了,比如我的H60G就很良心地提供了
busybox vi
)

#!/bin/sh
curl https://....?addr=$1:$2 ^D

这样就OK了

考虑:自启动

除非你家的光猫 7x24 小时开机,不然难免会想要自启动
我最开始修改了etc文件夹里的文件,第一台变砖第二台直接还原。
最后很简单,直接暴力干掉插件系统就行了(java...如果你有需要建议留着)

参考了
这篇文章
,不仅干掉了java插件系统,大幅度优化之外还提供了自启动管理
但是还是不能照抄,因为我们的java挂载不是镜像,而是完整的磁盘分区

Filesystem 1K-blocks Used Available Use% Mounted on
/dev/mtdblock12 30720 14464 16256 47% /usr/local/osgi
那就更好办了,直接find出手找到java文件
$ cd /usr/local/osgi/
/usr/local/osgi $ find ./ -name java
./local/j2re/bin/java

替换成以下内容就行了
(这里吐槽一下,垃圾光猫权限都不给足,还需要su升权,详情:
https://hi.imzlh.top/2024/03/11.cgi)

#!/bin/sh

if [ ! -f /tmp/services.log ]
then

        while true
        do
                curl -s http://www.gstatic.com/generate_204 && break
                sleep 10
        done

        echo aDm8H%MdA | su -c "/usr/local/osgi/rc.sh" -l root > /tmp/services.log &

else
        echo "services already started"
fi

exit 1

接下来是自启动脚本
/usr/local/osgi/rc.sh
,这个脚本里是有root权限的
这里,我们需要启动nginx和natmap
注意
千万别忘记
mkdir /tmp/nginx/
,不然nginx会启动报错的

cd /usr/local/osgi/
./natmap -4 -k 8 -s stunserver.stunprotocol.org -h baidu.com -t 192.168.1.2 -p 80 -d -e update.sh
mkdir /tmp/nginx/
./nginx

建议添加一行用来挂载USB磁盘

mount /dev/sda /mnt

保存重启
OK!全文完。

第二章 渲染在哪里开始?

牢记,按第一章介绍的 npm start 启动本地调式环境才可进行调式

如果是 example 文件夹内的例子还需要 serve . 开启本地静态服务器

image

上一章介绍了 PixiJS 源码调式环境的安装,以及基本的调试方法。本章要研究一下它是如何渲染的

渲染大致步骤:

  1. 注册渲染器 renderer

  2. TickerPlugin 的 ticker 会自动开启并调用注册的回调函数 'TickerListener'

  3. 'TickerListener' 回调内调用 Application render 方法

  4. Application render 方法会调用渲染器 this.renderer.render(this.stage) 并传入 stage

  5. stage 是即是显示对像又是容器,所以只要渲染器开始调用 stage 的 render 方法,就会渲染 stage 下的所有子对象从而实现整颗显示对象树的渲染

还是以 example/simple.html 例子为例

<script type="text/javascript">
const app = new PIXI.Application({ width: 800, height: 600 });  
document.body.appendChild(app.view);  

const sprite = PIXI.Sprite.from('logo.png');  
sprite.x = 100;  
sprite.y = 100;  
sprite.anchor.set(0.5);  
sprite.rotation = Math.PI / 4;  
app.stage.addChild(sprite);  

app.ticker.add(() => {  
    sprite.rotation += 0.01;  
});  
</script>

sprite 是 Sprite 对象的实例, Sprite 实例继承自: Container -> DisplayObject -> EventEmitter

朔源至最顶层是 EventEmitter, 这是一个高性能事件库

EventEmitter
https://github.com/primus/eventemitter3

至于为何它是高性能的,后面章节会顺便分析一下这个库

我们暂时不用去管这个 EventEmitter, 把它当做一个简单的事件收发库就行

先关注一下 DisplayObject,想要在画布中渲染,它必须得继承自 DisplayObject
/packages/display/src/DisplayObject.ts

所有 DisplayObject 都继承自 EventEmitter, 可以监听事件, 触发事件

DisplayObject.ts 源码 210 行 可以看到它是一个抽象类

export abstract class DisplayObject extends utils.EventEmitter<DisplayObjectEvents>

以下显示对象都继承实现了这个抽象类

PIXI.Container
PIXI.Graphics 
PIXI.Sprite   
PIXI.Text     
PIXI.BitmapText    
PIXI.TilingSprite  
PIXI.AnimatedSprite
PIXI.Mesh     
PIXI.NineSlicePlane
PIXI.SimpleMesh    
PIXI.SimplePlane   
PIXI.SimpleRope    

DisplayObject 有一个叫 render 的抽你方法需要子类实现

abstract render(renderer: Renderer): void;

render 方法就是各子类显示对像需要自己去实现绘制自己的方法

回到 example/simple.html 文件

app.stage 就是 Application 类的 stage 属性,它是一个 Container 对象,继承自 DisplayObject

stage 可以看作就是一棵显示对象树,而最顶层就是渲染方法就是 Application 的 render 方法

Application 实例化时它自身公开的 render 方法就被 TickerPlugin 插件的 init 方法调用了

/packages/ticker/TickerPlugin.ts
源码 68 行

ticker.add(this.render, this, UPDATE_PRIORITY.LOW); // 在ticker 内添加了 render() 回调

只要 ticker 开启,就会调用 Application 实例的 render 方法

/packages/app/src/Application.ts
第 70 - 90 行 构造函数与 render 方法

constructor(options?: Partial<IApplicationOptions>)
{
    // The default options
    options = Object.assign({
        forceCanvas: false,
    }, options);

    this.renderer = autoDetectRenderer<VIEW>(options);
    // console.log('hello', 88888);
    // install plugins here
    Application._plugins.forEach((plugin) =>
    {
        plugin.init.call(this, options);
    });
}

/** Render the current stage. */
public render(): void
{
    this.renderer.render(this.stage);
}

this.renderer 就是渲染器,把 this.stage 整个传到渲染器内渲染

往 stage 内添加子显示对象其实就是往一个 Container 内添加子显示对象,当然由于 Container 继承自 DisplayObject,所以 Container 也需要实现自己的 render 方法

/packages/display/src/Container.ts

render(renderer: Renderer): void
{
    // 检测是否需要渲染
    if (!this.visible || this.worldAlpha <= 0 || !this.renderable)
    {
        return;
    }

    // 如果是特殊的对象需要特殊的渲染逻辑
    if (this._mask || this.filters?.length)
    {
        this.renderAdvanced(renderer);
    }
    else if (this.cullable)
    {
        this._renderWithCulling(renderer);
    }
    else
    {
        this._render(renderer);

        for (let i = 0, j = this.children.length; i < j; ++i)
        {
            this.children[i].render(renderer);
        }
    }
}

这个 render 方法很简单,它接受一个 renderer 调用自己的 _render 后再遍历子显示对象调用子显示对象公开的 render 方法

就是一个显示对象树,从顶层开始调用往树了枝叶遍历调用 render 从而实现显示对象树的渲染

有一点需要注意,render 方法内显示它如果是一个 mask 遮罩或自带 filters 滤镜,那么需要调用更高极的渲染方法 renderAdvanced 或 _renderWithCulling,否则它先自己
this._render(renderer);

Container 本身自己的 _render 是空的,意味着它本身不会被渲染,只会被子显示对象渲染,但是继承实现它的子类,比如 Sprite,会去实现自己的 _render 方法覆盖实现渲染

renderer 渲染器

渲染器从哪里来的?

进入渲染器看看

渲染器是由 Application 类的构造函数内 autoDetectRenderer 判断返回的

渲染器类型分为三类:

export enum RENDERER_TYPE
{
    /**
     * Unknown render type.
     * @default 0
     */
    UNKNOWN,
    /**
     * WebGL render type.
     * @default 1
     */
    WEBGL,
    /**
     * Canvas render type.
     * @default 2
     */
    CANVAS,
}

我们找到 StartupSystem.ts 文件内的 defaultOptions 对象,将 hello 设为 true

static defaultOptions: StartupSystemOptions = {
    /**
        * {@link PIXI.IRendererOptions.hello}
        * @default false
        * @memberof PIXI.settings.RENDER_OPTIONS
        */
    hello: true,
};

本地服务器下打开 example/simple.html, 浏览器控制台会输出

image

图 2-1

由输出的 PixiJS 7.3.2 - WebGL 2 可知,现在使用的是 WebGL 2

Renderer 类就是我们现在用到的渲染器
/packages/core/src/Renderer.ts

进入到 Renderer.ts 文件可以看到此类继承自 SystemManager 并实现了 IRenderer 接口

export class Renderer extends SystemManager<Renderer> implements IRenderer

进入构造函数:
/packages/core/src/Renderer.ts
第 292 - 364 行:

constructor(options?: Partial<IRendererOptions>)
{
    super();

    // Add the default render options
    options = Object.assign({}, settings.RENDER_OPTIONS, options);

    this.gl = null;

    this.CONTEXT_UID = 0;

    this.globalUniforms = new UniformGroup({
        projectionMatrix: new Matrix(),
    }, true);

    const systemConfig = {
        runners: [
            'init',
            'destroy',
            'contextChange',
            'resolutionChange',
            'reset',
            'update',
            'postrender',
            'prerender',
            'resize'
        ],
        systems: Renderer.__systems,
        priority: [
            '_view',
            'textureGenerator',
            'background',
            '_plugin',
            'startup',
            // low level WebGL systems
            'context',
            'state',
            'texture',
            'buffer',
            'geometry',
            'framebuffer',
            'transformFeedback',
            // high level pixi specific rendering
            'mask',
            'scissor',
            'stencil',
            'projection',
            'textureGC',
            'filter',
            'renderTexture',
            'batch',
            'objectRenderer',
            '_multisample'
        ],
    };

    this.setup(systemConfig);

    if ('useContextAlpha' in options)
    {
        if (process.env.DEBUG)
        {
            // eslint-disable-next-line max-len
            deprecation('7.0.0', 'options.useContextAlpha is deprecated, use options.premultipliedAlpha and options.backgroundAlpha instead');
        }
        options.premultipliedAlpha = options.useContextAlpha && options.useContextAlpha !== 'notMultiplied';
        options.backgroundAlpha = options.useContextAlpha === false ? 1 : options.backgroundAlpha;
    }

    this._plugin.rendererPlugins = Renderer.__plugins;
    this.options = options as IRendererOptions;
    this.startup.run(this.options);
}

Renderer 类内有一堆的 runners, plugins, systems

runners 即所谓的 signal '信号', 可以理解为 生命周期+状态变更时就会触发

plugins 即为 Renderer 所专门使用的插件

systems 即为 Renderer 所使用的系统,它由各个系统组合形成了渲染器 Renderer,以一辆车举例,'系统'可以理解组成车子的各个子系统,比如空调系统,油路系统,传动系统 等等

在构造函数中调用的
this.setup(systemConfig)
就是安装渲染函数所需要用到的系统,它来自
/packages/core/system/SystemManager.ts

进入 SystemManager.ts 找到 setup 方法:

setup(config: ISystemConfig<R>): void
{
    this.addRunners(...config.runners);

    // Remove keys that aren't available
    const priority = (config.priority ?? []).filter((key) => config.systems[key]);

    // Order the systems by priority
    const orderByPriority = [
        ...priority,
        ...Object.keys(config.systems)
            .filter((key) => !priority.includes(key))
    ];

    for (const i of orderByPriority)
    {
        this.addSystem(config.systems[i], i);
    }
    console.log('看看runners里是什么:',this.runners)
}

可以看到,创建了很多个 Runner 对象存储在 this.runners 内

在 setup 函数最后一行打印看看 runners 里存了些啥

image

图 2-3

可以看到各个 Runner 对象的 items 里保存了所有的 system 当 Runner 被调用时,也即触发调用 items 内系统

找到 addSystem 方法:

addSystem(ClassRef: ISystemConstructor<R>, name: string): this
{
    const system = new ClassRef(this as any as R);

    if ((this as any)[name])
    {
        throw new Error(`Whoops! The name "${name}" is already in use`);
    }
    
    (this as any)[name] = system;

    this._systemsHash[name] = system;

    for (const i in this.runners)
    {
        this.runners[i].add(system);
    }

    /**
        * Fired after rendering finishes.
        * @event PIXI.Renderer#postrender
        */

    /**
        * Fired before rendering starts.
        * @event PIXI.Renderer#prerender
        */

    /**
        * Fired when the WebGL context is set.
        * @event PIXI.Renderer#context
        * @param {WebGLRenderingContext} gl - WebGL context.
        */

    return this;
}


(this as any)[name] = system;
这一句就把 实例化后的
const system = new ClassRef(this as any as R);
'系统' 按名称赋值到了 this 也即 Renderer 实例属性上了

所以通过 this.setup 后, 构造函数最后的 this.startup 属性 (StartupSystem) 可以访问,因为此时已经存在

根据注释,StartupSystem 就是用于负责初始化渲染器的,这是一切渲染的开始...

StartupSystem 的 run 方法
/packages/core/startup/StartupSystem.ts

第 56 - 69 行

run(options: StartupSystemOptions): void
{
    const { renderer } = this;
    console.log(renderer.runners.init)
    renderer.runners.init.emit(renderer.options);

    if (options.hello)
    {
        // eslint-disable-next-line no-console
        console.log(`PixiJS ${process.env.VERSION} - ${renderer.rendererLogId} - https://pixijs.com`);
    }

    renderer.resize(renderer.screen.width, renderer.screen.height);
}

第 58 行输出
console.log(renderer.runners.init)
看看名为 init 的 Runner 属性 items 内有 6 个系统需要触发 emit

image

图 2-3

再看看 Runner 类
/packages/core/runner/Runner.ts

根据注释:Runner是一种高性能且简单的信号替代方案。最适合在事件以高频率分配给许多对象的情况下使用(比如每帧!)

注释中举的例子已经很清晰的说明了 Runner 的使用场景了

Runner 类似 Signal 模式:

import { Runner } from '@pixi/runner';

const myObject = {
    loaded: new Runner('loaded'),
};

const listener = {
    loaded: function() {
        // Do something when loaded
    }
};

myObject.loaded.add(listener);

myObject.loaded.emit();

或用于处理多次调用相同函数

import { Runner } from '@pixi/runner';

const myGame = {
    update: new Runner('update'),
};

const gameObject = {
    update: function(time) {
        // Update my gamey state
    },
};

myGame.update.add(gameObject);

myGame.update.emit(time);

Signal 和 观察者模式 之间的主要区别在于实现方式和使用场景。观察者模式通常涉及一个主题(Subject)和多个观察者(Observers),主题维护观察者列表并在状态变化时通知观察者。

观察者模式更加结构化,观察者需要显式地注册和注销,而且通常是一对多的关系。

相比之下,Signal 更加简单和灵活,它通常用于处理单个事件或消息的订阅和分发。

Signal 不需要维护观察者列表,而是直接将事件发送给所有订阅者。

Signal 更加轻量级,适用于简单的事件处理场景,而观察者模式更适合需要更多结构和控制的情况。

renderer 的 render 函数

渲染器 Renderer 类内调用的 render 是名为 objectRenderer 的 ObjectRendererSystem 对象

render(displayObject: IRenderableObject, options?: IRendererRenderOptions): void
{
    this.objectRenderer.render(displayObject, options);
}

可以看到调用的是 ObjectRendererSystem 系统的 render 方法

/packages/core/src/render/ObjectRendererSystem.ts
第 49 - 125 行:

render(displayObject: IRenderableObject, options?: IRendererRenderOptions): void
{
    const renderer = this.renderer;

    let renderTexture: RenderTexture;
    let clear: boolean;
    let transform: Matrix;
    let skipUpdateTransform: boolean;

    if (options)
    {
        renderTexture = options.renderTexture;
        clear = options.clear;
        transform = options.transform;
        skipUpdateTransform = options.skipUpdateTransform;
    }

    // can be handy to know!
    this.renderingToScreen = !renderTexture;

    renderer.runners.prerender.emit();
    renderer.emit('prerender');

    // apply a transform at a GPU level
    renderer.projection.transform = transform;

    // no point rendering if our context has been blown up!
    if (renderer.context.isLost)
    {
        return;
    }

    if (!renderTexture)
    {
        this.lastObjectRendered = displayObject;
    }

    if (!skipUpdateTransform)
    {
        // update the scene graph
        const cacheParent = displayObject.enableTempParent();

        displayObject.updateTransform();
        displayObject.disableTempParent(cacheParent);
        // displayObject.hitArea = //TODO add a temp hit area
    }

    renderer.renderTexture.bind(renderTexture);
    renderer.batch.currentRenderer.start();

    if (clear ?? renderer.background.clearBeforeRender)
    {
        renderer.renderTexture.clear();
    }

    displayObject.render(renderer);

    // apply transform..
    renderer.batch.currentRenderer.flush();

    if (renderTexture)
    {
        if (options.blit)
        {
            renderer.framebuffer.blit();
        }

        renderTexture.baseTexture.update();
    }

    renderer.runners.postrender.emit();

    // reset transform after render
    renderer.projection.transform = null;

    renderer.emit('postrender');
}

displayObject.updateTransform(); 这一句,会遍历显示对象树,计算所有显示对象的 localTransform 和 worldTransform ,这对于正常渲染元素的样子与位置至关重要

displayObject.render(renderer);
这一句,也就是传进来的 stage 对象,遍历子显示对象的 render 并将渲染器传入。

最终会调用到 Sprite 内的 _render 方法就是我们加入到 stage 的 'logo.png'


/packages/sprite/src/Sprite.ts
的第 369 - 375 行

image

图 2-4

batch 就是 BatchSystem 的实例

batch 的当前渲染器 ExtensionType.RendererPlugin

再调用 batch 渲染器的 render(this) 将 this 即当前 Sprite 对象传入

batch 批处理渲染器

batch 渲染器定义
/packages/core/batch/src/BatchRenderer.ts

由 BatchRenderer.ts 定义的 extension 可知它是一个
ExtensionType.RendererPlugin
类型的扩展插件

在源码最后一行
extensions.add(BatchRenderer);
可知,它默认就被安装(实例化)到了 Renderer 上

正是由于默认被实例化安装了,所以才能在 图 2-5 Sprite.ts 的 _render 函数中调用
renderer.plugins[this.pluginName].render(this);

让我们看看 BatchRenderer.ts 的 render 函数

/**
 * Buffers the "batchable" object. It need not be rendered immediately.
 * @param {PIXI.DisplayObject} element - the element to render when
 *    using this renderer
 */
render(element: IBatchableElement): void
{
    if (!element._texture.valid)
    {
        return;
    }

    if (this._vertexCount + (element.vertexData.length / 2) > this.size)
    {
        this.flush();
    }

    this._vertexCount += element.vertexData.length / 2;
    this._indexCount += element.indices.length;
    this._bufferedTextures[this._bufferSize] = element._texture.baseTexture;
    this._bufferedElements[this._bufferSize++] = element;
}

可以看到,这个 render 并不是立即渲染,而是将渲染数据缓存起来,等到渲染的时候再进行渲染。

由这个类的注释信息可知,它的作用是先缓存需要渲染的 texture 数据,等待将 多个 texture 信息直接提交到GPU进行批量渲染, 以减少 draw 次数提高性能

在这个 render 函数最后一行加一个 debugger 看看

image
image

图 2-5

/packages/core/src/render/ObjectRendererSystem.ts
的 render 函数, 也就是第 104 - 107 行:

displayObject.render(renderer);

        // apply transform..
renderer.batch.currentRenderer.flush();

等到
displayObject.render(renderer);
显示对像树遍历收集完渲染数据后才 flush 推到 GPU

进入
/packages/core/batch/src/BatchRenderer.ts
找到 flush 第 625 - 646 行:

flush(): void
{
    if (this._vertexCount === 0)
    {
        return;
    }

    this._attributeBuffer = this.getAttributeBuffer(this._vertexCount);
    this._indexBuffer = this.getIndexBuffer(this._indexCount);
    this._aIndex = 0;
    this._iIndex = 0;
    this._dcIndex = 0;

    this.buildTexturesAndDrawCalls();
    this.updateGeometry();
    this.drawBatches();

    // reset elements buffer for the next flush
    this._bufferSize = 0;
    this._vertexCount = 0;
    this._indexCount = 0;
}

至此 flush() 函数,才是真正调用 webgl 处

_attributeBuffer 是一个 ViewableBuffer 的实例对象

而随后的 this.buildTexturesAndDrawCalls(); 会调用 buildTexturesAndDrawCalls -> buildDrawCalls -> packInterleavedGeometry

/packages/core/batch/src/BatchRenderer.ts
766 - 800 行

packInterleavedGeometry(element: IBatchableElement, attributeBuffer: ViewableBuffer, indexBuffer: Uint16Array,
    aIndex: number, iIndex: number): void
{
    const {
        uint32View,
        float32View,
    } = attributeBuffer;

    const packedVertices = aIndex / this.vertexSize;
    const uvs = element.uvs;
    const indicies = element.indices;
    const vertexData = element.vertexData;
    const textureId = element._texture.baseTexture._batchLocation;

    const alpha = Math.min(element.worldAlpha, 1.0);
    const argb = Color.shared
        .setValue(element._tintRGB)
        .toPremultiplied(alpha, element._texture.baseTexture.alphaMode > 0);

    // lets not worry about tint! for now..
    for (let i = 0; i < vertexData.length; i += 2)
    {
        float32View[aIndex++] = vertexData[i];
        float32View[aIndex++] = vertexData[i + 1];
        float32View[aIndex++] = uvs[i];
        float32View[aIndex++] = uvs[i + 1];
        uint32View[aIndex++] = argb;
        float32View[aIndex++] = textureId;
    }

    for (let i = 0; i < indicies.length; i++)
    {
        indexBuffer[iIndex++] = packedVertices + indicies[i];
    }
}

packInterleavedGeometry 内会将 element.vertexData 顶点数据, uvs, argb 等信息存入 attributeBuffer

indexBuffer 是用来存储 sprite 渲染时所需的顶点索引的缓冲区。

在渲染 sprite 时,引擎需要知道如何连接顶点以形成正确的形状,而这些连接顶点的顺序就是通过 _indexBuffer 中的数据来定义的。

每三个索引对应一个顶点,通过这些索引,引擎可以正确地连接顶点以渲染出 sprite 的形状。

如果你把 indexBuffer 打印出来可以看到有 12 个值, WebGL 绘制几何体都是由三角形组成的

矩形由2个三角形组成

let vertices = [
0.5, 0.5, 0.0,
-0.5, 0.5, 0.0,
-0.5, -0.5, 0.0, // 第一个三角形
-0.5, -0.5, 0.0,
0.5, -0.5, 0.0,
0.5, 0.5, 0.0, // 第二个三角形
]; // 矩形

有一条边是公共,这个时候可以索引缓冲区对象减少冗余的数据

索引缓冲对象全称是 Index Buffer Object(IBO),通过索引的方式复用已有的数据。

顶点位置数据只需要 4 个就足够了,公共数据使用索引代替。

const vertices = [
0.5, 0.5, 0.0, // 第 1 个顶点
-0.5, 0.5, 0.0, // 第 2 个顶点
-0.5, -0.5, 0.0, // 第 3 个顶点
0.5, -0.5, 0.0, // 第 4 个顶点
]; // 矩形

绘制模式为 gl.TRIANGLES 时,两个三角形是独立的,索引数据如下:

const indexData = [
0, 1, 2, // 对应顶点位置数据中 1、2、3 顶点的索引
0, 2, 3, // 对应顶点位置数据中 1、3、4 顶点的索引
]

这就是为什么Sprite.ts 类中
const indices = new Uint16Array([0, 1, 2, 0, 2, 3]);
如此定义的原因

相关知识可参考:
https://segmentfault.com/a/1190000041144928

接下来是 this.updateGeometry(); 简单来说它它会创建几何模型 和 shader

最后调用 this.drawBatches() 内调用 gl.drawElements() 将前面缓存整理好的 buffer 绘制到 GPU

不管是 Canvas context 还是 WebGL 都是非对象的过程式的调用,PixiJS 的 Renderer 封装了这些操作,让开发者更专注于业务逻辑。

将过程式的调用封装成对象

WebGL 想要渲染,原理:

顶点着色器 + 片段着色器, 顶点着色器确定顶点位置,片段着色器确定每个片元的像素颜色

组成的着色程序 program 后通过 gl.drawArrays 或 gl.drawElements 运行一个着色方法对绘制到 GPU 上

我们采取先整体再细节的方式阅读源码,WebGL 具体渲染挺复杂的,暂时可以略过,如果有兴趣可以参考 WebGL 教程

这是一个很好的 WebGL 教程
https://webglfundamentals.org/webgl/lessons/zh_cn/webgl-fundamentals.html

本章小节

本章通过分析 webgl 渲染器,顺带看了部分 PixiJS 的 system/SystemManager "系统设计", 咋一看确实很复杂

优秀的设计时分值得借鉴,完全可以运用到自己的项目或组件库内

我对 webgl 了解的十分粗浅但借助 debugger 还是可以一步一步分析出逻辑走向,道阻且长啊

最新的 PixiJS 已经支持 WebGPU 渲染了,学不动了...


注:转载请注明出处博客园:王二狗Sheldon池中物 (willian12345@126.com)

打开题目是三个页面

Hint中有提示

image

flag页面有个输入框抓包观察cookie发现多了一user就是回显内容

image

然后猜测有模板注入漏洞就开始尝试


'时代少年团队长乌萨奇的颜值一直被质疑'的文章内容
如何判断对方的模板?
常见模板有Smarty、Mako、Twig、Jinja2、Eval、Flask、Tornado、Go、Django、Ruby等。以下是一张广为流传的图:
image

这幅图的含义是通过这些指令去判断对方用的是什么模板,下面解释一下这幅图的意思:
绿色箭头是执行成功,红色箭头是执行失败。
首先是注入${7
7}没有回显出49的情况,这种时候就是执行失败走红线,再次注入{{7
7}}如果还是没有回显49就代表这里没有模板注入;如果注入{{7*7}}回显了49代表执行成功,继续往下走注入{{7
'7'}},如果执行成功回显7777777说明是jinja2模板,如果回显是49就说明是Twig模板。
然后回到最初注入${7*7}成功回显出49的情况,这种时候是执行成功走绿线,再次注入a{

comment*}b,如果执行成功回显ab,就说明是Smarty模板;如果没有回显出ab,就是执行失败走红线,注入${"z".join("ab")},如果执行成功回显出zab就说明是Mako模板。实际做题时也可以把指令都拿去测测看谁能对上。平时做题也可以多搜集不同模板对应的注入语句语法。
https://blog.csdn.net/2301_76690905/article/details/134257483


image

经过尝试是一个Twig模板注入
然后就是直接注入就行

先找到flag位置
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("find / -name 'f*'")}}
再
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}