Nicksxs's Blog

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

目前我在家里用的路由器是个装了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

第二次就是

netty是java网络框架中非常有名,因为它把各种网络类型,阻塞非阻塞的都做了上层的抽象,非常值得学习,这边就先以一个简单的http server来做下实践,
主体的server代码

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
public static void main(String[] args) throws InterruptedException {

// boss 线程组
EventLoopGroup boss = new NioEventLoopGroup();
// worker 线程组
EventLoopGroup work = new NioEventLoopGroup();
try {
// 服务端启动类
ServerBootstrap bootstrap = new ServerBootstrap();
// 绑定线程组
bootstrap.group(boss, work)
// 设置服务端的tcp连接的最大排队连接数
.option(ChannelOption.SO_BACKLOG, 1024)
// 设置日志处理器
.handler(new LoggingHandler(LogLevel.DEBUG))
// 绑定Channel
.channel(NioServerSocketChannel.class)
// 实际的逻辑handler
.childHandler(new HttpServerInitializer());

// 绑定端口
ChannelFuture f = bootstrap.bind(new InetSocketAddress(8082)).sync();
System.out.println("server start up on port 8080");
f.channel().closeFuture().sync();
} finally {
boss.shutdownGracefully();
work.shutdownGracefully();
}
}

然后来看下 HttpServerInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
// 在这个Channel的pipeline上添加处理器
// 前三个都是netty提供的
pipeline.addLast(new HttpServerCodec());
pipeline.addLast(new HttpContentCompressor((CompressionOptions[]) null ));
pipeline.addLast(new HttpServerExpectContinueHandler());
// 这个是我们自己实现的逻辑
pipeline.addLast(new SimpleHttpServerHandler());
}
}

实际的代码也是很简略,我们用一个hello world统一响应所有的请求

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
public class SimpleHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
private static final byte[] CONTENT = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd' };

@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
// 刷写响应
ctx.flush();
}

@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, HttpObject msg) throws Exception {
// 先是从这个Channel读取到内容
System.out.println("channel read");
if (msg instanceof HttpRequest) {
// 判断是否为HttpRequest,这个其实依赖于前面netty自带的编码器
HttpRequest httpRequest = (HttpRequest) msg;
boolean keepAlive = HttpUtil.isKeepAlive(httpRequest);

// 包装http响应
FullHttpResponse response = new DefaultFullHttpResponse(httpRequest.protocolVersion(), OK, Unpooled.wrappedBuffer(CONTENT));

// 设置请求头
response.headers()
.set(CONTENT_TYPE, TEXT_PLAIN)
.setInt(CONTENT_LENGTH, response.content().readableBytes());

// 设置是否要连接保活
if (keepAlive) {
if (!httpRequest.protocolVersion().isKeepAliveDefault()) {
response.headers().set(CONNECTION, KEEP_ALIVE);
}
} else {
response.headers().set(CONNECTION, CLOSE);
}

// 写入channelFuture
ChannelFuture future = channelHandlerContext.write(response);

// 如果不保活,添加监听器
if (!keepAlive) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

上面就是这个简单的示例了,只是我在 channelRead0 入口建了个打印,但是这个再接收请求时会打印两次,可以作为个探讨题,我们下次来分析下

最近发现一个小问题,感觉挺有意思,我们来看下代码

1
2
3
4
public static void main(String[] args) {
Integer oneHundredYear = 36500;
System.out.println(oneHundredYear * 24 * 60 * 60 * 1000);
}

定义了个变量,用来表示100年的时间,然后计算出对应的时间戳
首先看下这个值

1
1094004736

如果对时间戳这些没概念的话,可能没发现有什么问题,但是好像也不对,乘以1000了,但是尾数是736
这个暂时我们先不管

1
2
3
4
5
public static void main(String[] args) {
Integer oneHundredYear = 36500;
System.out.println(oneHundredYear * 24 * 60 * 60 * 1000);
System.out.println(System.currentTimeMillis() + oneHundredYear * 24 * 60 * 60 * 1000);
}

然后来看下

1
2
1094004736
1726195843465

然后我们去看下这个时间戳,按正常理解应该是2124年的8月31日

但实际是不对的,只是12天后,跟100年这个差距天差地别了,这是为啥呢,可能大佬一眼就看出来了这是类型转换问题,但是比如说我这么改

1
2
3
4
5
public static void main(String[] args) {
Integer oneHundredYear = 36500;
System.out.println(oneHundredYear * 24 * 60 * 60 * 1000L);
System.out.println(System.currentTimeMillis() + oneHundredYear * 24 * 60 * 60 * 1000L);
}

或者

1
2
3
4
5
public static void main(String[] args) {
Integer oneHundredYear = 36500;
System.out.println(Long.valueOf(oneHundredYear * 24 * 60 * 60 * 1000));
System.out.println(System.currentTimeMillis() + Long.valueOf(oneHundredYear * 24 * 60 * 60 * 1000));
}

会发现还是一样,依旧是错误的
简单来说,就在于类型转换的时机,我们看下这个示例就知道了

1
2
3
4
5
6
7
public static void main(String[] args) {
Integer oneHundredYear = 36500;
System.out.println(oneHundredYear * 24);
System.out.println(oneHundredYear * 24 * 60);
System.out.println(oneHundredYear * 24 * 60 * 60);
System.out.println(oneHundredYear * 24 * 60 * 60 * 1000);
}

结果是这样

1
2
3
4
876000
52560000
-1141367296
1094004736

到乘第二个60的时候已经是负的了,因为已经超过了Integer的范围了
但是为啥用后面的示例转换成long类型还不行呢
这个就在于编译器是怎么做类型转换的
在第一个oneHundredYear跟24相乘的时候是认为两个Integer相乘,并且没有检查范围
只有在乘以显式申明的最后的1000的时候才会做转换
我们看下反编译

可以看到是先做int型的乘法,碰到有参数是long型时才会转类型
那么理论上其实我们只要在第二个60及之前申明long或者强转long就行了,这也是个很基础的问题,只是有时候写代码的时候直觉会以为加了个L就可以了,但实际是没那么简单

0%