Nicksxs's Blog

What hurts more, the pain of hard work or the pain of regret?

docker镜像拉取目前是个大问题,前阵子出现了比较大规模的封禁,导致很多原有的方案无法使用,其中包括阿里云的私有镜像地址,导致我折腾了半天
本来是可以在 https://cr.console.aliyun.com/cn-beijing/instances/mirrors 上打开,阿里云用户都有各自的私有地址

1
https://xxxxxxxx.mirror.aliyuncs.com

然后可以通过这个方式修改

1
2
3
4
5
6
7
8
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://xxxxxxxx.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker

这样修改以后查看下日志

1
journalctl -xe --no-pager -u docker

但还是拉不下来,后来发现还是拉不下来
应该也是之前被封的原因,
后面重要找到了一个
配置使用这个就可以了

1
2
3
4
5
{
"registry-mirrors": [
"https://dockerproxy.cn"
]
}

或者拉取镜像的前缀加上这个地址
docker pull dockerproxy.cn/yangchuansheng/derper:latest
也可以这么拉取,说实话docker镜像现在真的很必要,据说这次是因为传了某些敏感的AI语音
参考下面的图

首先是安装Caddy,

1
2
3
4
5
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

安装完成就会自动启动Caddy,
这时我们就可以来实践反向代理,并且自带https,

1
caddy reverse-proxy --from :2080 --to :9000

这样就可以构建一个2080到9000的反向代理
如果需要做域名的反向代理
可以这样

1
caddy reverse-proxy --from example.com --to :9000

可以在本地开启一个9000端口的http服务

1
php -S localhost:9000

然后请求本地

1
curl -v https://localhost

就能看到响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
*   Trying 127.0.0.1:443...
* Connected to localhost (127.0.0.1) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
* subject: [NONE]
* start date: Oct 6 12:54:37 2024 GMT
* expire date: Oct 7 00:54:37 2024 GMT
* subjectAltName: host "localhost" matched cert's "localhost"
* issuer: CN=Caddy Local Authority - ECC Intermediate
* SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x55b8f32f5eb0)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET / HTTP/2
> Host: localhost
> user-agent: curl/7.81.0
> accept: */*
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 404
< alt-svc: h3=":443"; ma=2592000
< content-type: text/html; charset=UTF-8
< date: Sun, 06 Oct 2024 13:37:43 GMT
< host: localhost
< server: Caddy
< content-length: 533
<
* TLSv1.2 (IN), TLS header, Supplemental data (23):
<!doctype html><html><head><title>404 Not Found</title><style>
body { background-color: #fcfcfc; color: #333333; margin: 0; padding:0; }
h1 { font-size: 1.5em; font-weight: normal; background-color: #9999cc; min-height:2em; line-height:2em; border-bottom: 1px inset black; margin: 0; }
h1, p { padding-left: 10px; }
code.url { background-color: #eeeeee; font-family:monospace; padding:0 2px;}
</style>
* Connection #0 to host localhost left intact
</head><body><h1>Not Found</h1><p>The requested resource <code class="url">/</code> was not found on this server.</p></body></html>

然后在php侧服务器就能看到

1
2
3
4
[Sun Oct  6 21:34:29 2024] PHP 8.1.2-1ubuntu2.19 Development Server (http://localhost:9000) started
[Sun Oct 6 21:37:43 2024] 127.0.0.1:38708 Accepted
[Sun Oct 6 21:37:43 2024] 127.0.0.1:38708 [404]: GET / - No such file or directory
[Sun Oct 6 21:37:43 2024] 127.0.0.1:38708 Closing

还能用配置的形式

1
2
3
4
5

demo.domain.com {
# 反向代理的地址
reverse_proxy 127.0.0.1:xxxx
}

再运行caddy reload就能启动,还是很方便的

目前我在家里用的路由器是个装了ImmortalWrt的NX30 Pro路由器,由于内存只有256兆,默认没带网速统计和限制网速的插件,对于网络速度限制就有点困难,刚好这次网上找到了一个可以用iptables统计网速的脚本,简单记录下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

#!/bin/sh
echo "Collecting data..."
echo ""
cat /proc/net/arp | grep : | grep ^192 | grep -v 00:00:00:00:00:00| awk '{print $1}'> mac-arp
iptables -N UPLOAD
iptables -N DOWNLOAD
while read line;do iptables -I FORWARD 1 -s $line -j UPLOAD;done < mac-arp
while read line;do iptables -I FORWARD 1 -d $line -j DOWNLOAD;done < mac-arp
sleep 1
echo "Download speed:"
echo ""
iptables -nvx -L FORWARD | grep DOWNLOAD | awk '{print $2/1024/1" KB/s ",$1/10" packets/s", $9}' | sort -n -r
echo ""
echo "Upload speed:"
echo ""
iptables -nvx -L FORWARD | grep UPLOAD | awk '{print $2/1024/1" KB/s ",$1/10" packets/s", $8}' | sort -n -r
while read line;do iptables -D FORWARD -s $line -j UPLOAD;done < mac-arp
while read line;do iptables -D FORWARD -d $line -j DOWNLOAD;done < mac-arp
iptables -X UPLOAD
iptables -X DOWNLOAD

首先是通过arp记录内网ip地址,然后添加UPLOADDOWNLOAD链,
然后为mac-arp文件中每个ip地址添加FORWARD规则,将流量导到UPLOADDOWNLOAD
等待1秒,统计下载和上传速度,计算KB/s,packets/s,再清理规则
然后对于上传占用高的,我们可以用

1
2
iptables -t mangle -I FORWARD 1 -s 192.168.x.x -m limit --limit 800/s --limit-burst 1000 -j ACCEPT
iptables -t mangle -I FORWARD 2 -s 192.168.x.x -j DROP

限制通过包的数量为800,不过这个不绝对精确

Termux是安卓下一个终端工具,一开始以为就是一些极客的高端玩具,在安卓下编程用,实际的实用性不太强,直到之前稍微研究了下,还真的是个神器,这个神器的原因在于三方面
第一点,它其实不只是个终端工具,而是个类似于iterm2 + brew的组合拳神器
自带了pkg包管理工具

还可以通过termux-change-repo来更改包的安装源
有了这个工具,我们就可以在手机里运行一些简单的程序,比如跑个php脚本,因为可以安装php

甚至可以安装nginx,在手机里跑个web应用,可以通过安装git来拉取我们的代码,实现手机开发可能比较难,但是在手机上运行代码还是可以的
第二点是这个工具可以开启安卓文件的访问,默认它是限定在应用目录下的,我们可以通过以下命令来请求访问安卓文件目录权限

1
termux-setup-storage

然后我们去到 /storage/emulated/0 目录下,就发现安卓的文件就在这个目录下,举个例子这样我们就可以用脚本来处理我的照片文件
第三点是其实目前的手机处理器性能已经到了一个非常不错的阶段,比如我看了下我的渣渣安卓机是高通骁龙870处理器,它是个21年出的手机处理器,性能大致接近于桌面端i5 4590
870的geekbench跑分,单核1141,多核3317
i5 4590的geekbench跑分,单核1142,3165
单核非常接近,多核还稍微领先,并且最近也看到有用最新的高通骁龙8gen3来跑端侧大模型的,其实说明了目前手机的性能真的达到了很高的水平,提供了更多的可玩性,虽然从另一个角度来讲,870在运行现在最新版本的手机应用时会发烫变卡,那是因为手机应用往往要做非常多的事情,后台保活,前台的各种图形渲染,包括微信,支付宝都有了内置的小程序,那么都需要一套资源隔离机制来实现运行态的维护,并且各种内容也搞得花里胡哨都需要资源,我们只是运行简单的脚本,手机的可用性不比很多云服务器差,一般云服务器我们个人用户都买的是比较低配的,最低1核1g的配置,远不如现在的手机强大,可玩性是真的不错的。

上次用netty写的一个玩具http server,发现了一个问题,为啥channelRead0方法会被调用两次,这里我们来研究下
我们在收到http请求的时候需要经过的一个必要的过程就是编解码,而这里我们用的是 io.netty.handler.codec.http.HttpServerCodec

1
pipeline.addLast(new HttpServerCodec());

它继承了 io.netty.channel.CombinedChannelDuplexHandler

1
2
3
4
public HttpServerCodec(HttpDecoderConfig config) {
this.queue = new ArrayDeque();
this.init(new HttpServerRequestDecoder(config), new HttpServerResponseEncoder());
}

在初始化的时候调用了 io.netty.channel.CombinedChannelDuplexHandler#init
传入参数

1
2
3
4
public HttpServerCodec(HttpDecoderConfig config) {
this.queue = new ArrayDeque();
this.init(new HttpServerRequestDecoder(config), new HttpServerResponseEncoder());
}

这个 HttpServerRequestDecoder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private final class HttpServerRequestDecoder extends HttpRequestDecoder {
HttpServerRequestDecoder(HttpDecoderConfig config) {
super(config);
}

protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
int oldSize = out.size();
super.decode(ctx, buffer, out);
int size = out.size();

for(int i = oldSize; i < size; ++i) {
Object obj = out.get(i);
if (obj instanceof HttpRequest) {
HttpServerCodec.this.queue.add(((HttpRequest)obj).method());
}
}

}

这个decode方法就会调用到父类的decode方法 io.netty.handler.codec.http.HttpObjectDecoder#decode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
if (this.resetRequested.get()) {
this.resetNow();
}

int toRead;
int toRead;
ByteBuf line;
switch (this.currentState) {
case SKIP_CONTROL_CHARS:
case READ_INITIAL:
try {
line = this.lineParser.parse(buffer);
if (line == null) {
return;
}

String[] initialLine = this.splitInitialLine(line);

assert initialLine.length == 3 : "initialLine::length must be 3";

this.message = this.createMessage(initialLine);
this.currentState = HttpObjectDecoder.State.READ_HEADER;
} catch (Exception var9) {
out.add(this.invalidMessage(this.message, buffer, var9));
return;
}
case READ_HEADER:
try {
State nextState = this.readHeaders(buffer);
if (nextState == null) {
return;
}

this.currentState = nextState;
switch (nextState) {
case SKIP_CONTROL_CHARS:
this.addCurrentMessage(out);
out.add(LastHttpContent.EMPTY_LAST_CONTENT);
this.resetNow();
return;
case READ_CHUNK_SIZE:
if (!this.chunkedSupported) {
throw new IllegalArgumentException("Chunked messages not supported");
}

this.addCurrentMessage(out);
return;
default:
if (this.contentLength == 0L || this.contentLength == -1L && this.isDecodingRequest()) {
this.addCurrentMessage(out);
out.add(LastHttpContent.EMPTY_LAST_CONTENT);
this.resetNow();
return;
}

assert nextState == HttpObjectDecoder.State.READ_FIXED_LENGTH_CONTENT || nextState == HttpObjectDecoder.State.READ_VARIABLE_LENGTH_CONTENT;

this.addCurrentMessage(out);
if (nextState == HttpObjectDecoder.State.READ_FIXED_LENGTH_CONTENT) {
this.chunkSize = this.contentLength;
}

return;
}
} catch (Exception var10) {
out.add(this.invalidMessage(this.message, buffer, var10));
return;
}
case READ_CHUNK_SIZE:
try {
line = this.lineParser.parse(buffer);
if (line == null) {
return;
}

toRead = getChunkSize(line.array(), line.arrayOffset() + line.readerIndex(), line.readableBytes());
this.chunkSize = (long)toRead;
if (toRead == 0) {
this.currentState = HttpObjectDecoder.State.READ_CHUNK_FOOTER;
return;
}

this.currentState = HttpObjectDecoder.State.READ_CHUNKED_CONTENT;
} catch (Exception var8) {
out.add(this.invalidChunk(buffer, var8));
return;
}
case READ_CHUNKED_CONTENT:
assert this.chunkSize <= 2147483647L;

toRead = Math.min((int)this.chunkSize, this.maxChunkSize);
if (!this.allowPartialChunks && buffer.readableBytes() < toRead) {
return;
}

toRead = Math.min(toRead, buffer.readableBytes());
if (toRead == 0) {
return;
}

HttpContent chunk = new DefaultHttpContent(buffer.readRetainedSlice(toRead));
this.chunkSize -= (long)toRead;
out.add(chunk);
if (this.chunkSize != 0L) {
return;
}

this.currentState = HttpObjectDecoder.State.READ_CHUNK_DELIMITER;
case READ_CHUNK_DELIMITER:
toRead = buffer.writerIndex();
toRead = buffer.readerIndex();

while(toRead > toRead) {
byte next = buffer.getByte(toRead++);
if (next == 10) {
this.currentState = HttpObjectDecoder.State.READ_CHUNK_SIZE;
break;
}
}

buffer.readerIndex(toRead);
return;
case READ_VARIABLE_LENGTH_CONTENT:
toRead = Math.min(buffer.readableBytes(), this.maxChunkSize);
if (toRead > 0) {
ByteBuf content = buffer.readRetainedSlice(toRead);
out.add(new DefaultHttpContent(content));
}

return;
case READ_FIXED_LENGTH_CONTENT:
toRead = buffer.readableBytes();
if (toRead == 0) {
return;
}

toRead = Math.min(toRead, this.maxChunkSize);
if ((long)toRead > this.chunkSize) {
toRead = (int)this.chunkSize;
}

ByteBuf content = buffer.readRetainedSlice(toRead);
this.chunkSize -= (long)toRead;
if (this.chunkSize == 0L) {
out.add(new DefaultLastHttpContent(content, this.trailersFactory));
this.resetNow();
} else {
out.add(new DefaultHttpContent(content));
}

return;
case READ_CHUNK_FOOTER:
try {
LastHttpContent trailer = this.readTrailingHeaders(buffer);
if (trailer == null) {
return;
}

out.add(trailer);
this.resetNow();
return;
} catch (Exception var7) {
out.add(this.invalidChunk(buffer, var7));
return;
}
case BAD_MESSAGE:
buffer.skipBytes(buffer.readableBytes());
break;
case UPGRADED:
toRead = buffer.readableBytes();
if (toRead > 0) {
out.add(buffer.readBytes(toRead));
}
}

}

这个方法里会做实际的byte转换成message或者说string的转换
而在我们把消息转换完成,

会在最后加一个 io.netty.handler.codec.http.LastHttpContent#EMPTY_LAST_CONTENT
这样其实我的message就会变成两条,也就是为啥channelRead0会被调用两次,这样的目的也是让我的业务代码可以分辨是否请求已经读完,并且可以在读完以后有一些其他操作空间
可以看到第一次调用channelRead0的msg

第二次就是

0%