NAT小记

NAT小记

NAT 的分类

  • 全锥形: 一旦内部主机端口对(iAddr:iPort)被NAT网关映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);任何一个外部主机发送到(eAddr:ePort)的报文将会被转换后发到(iAddr:iPort)

    外部主机不限制 ip 和端口

  • 限制锥形:一旦内部主机端口对(iAddr:iPort)被映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);只有 (iAddr:iPort)向特定的外部主机hAddr发送过数据,主机hAddr从任意端口发送到(eAddr:ePort)的报文将会被转发到(iAddr:iPort)。

    外部主机限制ip不限制端口

  • 端口限制锥形: 一旦内部主机端口对(iAddr:iPort)被映射到(eAddr:ePort),所有后续的(iAddr:iPort)报文都会被转换为(eAddr:ePort);只有(iAddr:iPort)向特定的外部主机端口对(hAddr:hPort)发送过数据,由 (hAddr:hPort)发送到(eAddr:ePort)的报文将会被转发到(iAddr:iPort)。

    外部主机限制 ip 和端口

  • 对称型: NAT网关会把内部主机“地址端口对”和外部主机“地址端口对”完全相同的报文看作一个连接,在网关上创建一个公网“地址端口对”映射进行转换,只有收到报文的外部主机从对应的端口对发送回应的报文,才能被转换。即使内部主机使用之前用过的地址端口对去连接不同外部主机(或端口)时,NAT网关也会建立新的映射关系。

    外部主机的响应包才能发送

STUN、TURN、ICE

https://developer.aliyun.com/article/243540

STUN: 为终端提供一种能够获取自己经过NAT映射后的地址.

客户端向公网 STUN 服务器发送 Binding Request ,服务器收到后获取公网 IP:PORT,附加在 Binding Request 返回给客户端. 

TURN: TURN 作为通讯中间人,由服务器负责两方的数据转发.

ICE: 一种框架,可以整合现有的NAT穿透协议,尽可能的找到NAT穿透的数据通道.

打洞过程

  • 两个客户端处于同一 NAT 设备后

Untitled.png

当A向集中服务器发出消息请求与B进行连接,集中服务器S将B的外网地址二元组以及内网地址二元组发给A,同时把A的外网以及内网的地址二元组信息发给B。A和B发往对方公网地址二元组信息的UDP数据包不一定会被对方收到,这取决于当前的NAT设备是否支持不同端口之间的UDP数据包能否到达(即Hairpin转换特性),无论如何A与B发往对方内网的地址二元组信息的UDP数据包是一定可以到达的,内网数据包不需要路由,且速度更快。A与B推荐采用内网的地址二元组信息进行常规的P2P通信。

  • 两个客户端处于不同 NAT 设备后

Untitled.png

同上一个例子一样, A 和 B 得到了对方的外网 ip:port

当 A 往 NAT-B 发送 UDP 消息,经过 NAT-A ,并在 NAT-A 上生成会话表项,根据NAT类型可知除了全锥形NAT,NAT-B 认为设备 A 得消息未授权外网消息,会丢弃该数据包.

这时B设备向A发送一个UDP消息. NAT-B 上也会生成一个到NAT-A 的会话表项.

此时 NAT-A 和NAT-B 都有了对方在外网的二元组,打开了 A 和 B 之间的洞.A 和 B 可以开始数据传输.

  • 两个客户端位于两层 NAT 设备后

Untitled.png

当出现多层级的 NAT 时(这是我们常见的类型),我们通过外网服务器S来打洞,可能存在某个 NAT是最优的选择,但是外网服务器S并不能够观察到,只能够选择离服务器S最近的NAT-C来打洞.

NAT 设备在空闲状态下会对转换表进行清理,比如一些家用的路由设备保存 NAT 时间大概是2-3分钟.某些设备可能短的只有 20s .为了维持可以通过心跳包的方式来维持连接.在连接超时后进行重新打洞.

http://www.52im.net/thread-542-1-1.html

如何创建一个小的容器镜像

如何创建一个小的容器镜像

对 golang 程序上线到了容器,在使用 scratch 的时候遇到了一个小小的问题,顺手对编译做了一些整理. 以下:

编写一个简单的Go应用程序,例如 gin 的例子 **main.go**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

例子1: Dockerfile 编译

使用容器来编译运行程序

1
2
3
4
5
6
7
# 使用官方的 Golang 镜像作为基础镜像
FROM golang:1.20
WORKDIR /app
COPY . .
RUN go build -o main
EXPOSE 8080
CMD ["./main"]

通过构建的到了容器大小为 1.16GB

1
docker-golang-build-example1              latest               cd34e318087d   40 seconds ago   1.16GB

这个时候会容器的内容又一些大了. 可以看看 https://hub.docker.com/_/golang

golang:1.20 是基于 Debian 来作为基础镜像,本身就很大.

例子2: 使用 apline

我们可以看到提供了 alpine 作为基础镜像的包 golang:<version>-alpine ,只需要将 FROM 后面的镜像加上 alpine 即可

1
2
3
4
5
6
7
# 使用官方的 Golang alpine 镜像作为基础镜像
FROM golang:1.20-alpine
WORKDIR /app
COPY . .
RUN go build -o main
EXPOSE 8080
CMD ["./main"]

Untitled.png

使用 alpine 作为基础镜像会存在一些问题, 它使用了 musl libc 来代替 glibc ,小的镜像和稳定性的讨论会有一些不同的意见: https://news.ycombinator.com/item?id=10782897

镜像的大小从 1.16G 变成了 570M

例子3: 分段编译,使用 busybox

在编译过程中, go build 会拉取依赖,使用分段将编译和运行的镜像分开. golang 在运行时不需要编译环境,这里我直接使用 busybox 来作为基础镜像,同时例子2中使用 alpine 会存在一些问题,所以我们使用 busybox 的 glibc 的包.

1
2
3
4
5
6
7
8
9
10
11
FROM golang:1.20 AS build
WORKDIR /app
COPY . .
RUN go build -o main

# 最终阶段
FROM busybox:glibc
WORKDIR /app
COPY --from=build /app/main /app
EXPOSE 8080
CMD ["./main"]

Untitled.png

编译后得到了 15.5M 的镜像.

例子4: 静态编译,使用 scratch

在容器镜像中有一个 0M 的基础镜像 scratch ,它是所以镜像的基础.

容器本质上是基于Linux内核的Namespace、Cgroups和Union FS等技术对进程进行封装隔离的操作系统层面的虚拟化技术。

我们可以通过静态编译的方式,基于 scratch 来运行. 构建Go应用程序,并确保它是静态链接的。可以使用以下命令来构建:

1
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app main.go

在编译中添加了 -a -installsuffix cgo 两个条件,并指定了CGO_ENABLED=0:

-a : 强制重新构建
-installsuffix cgo: 指示编译器将生成的目标文件和包安装到一个特定的目录中,以便将与C语言绑定的包(使用**cgo**工具)与其他Go包区分开。
CGO_ENABLED=0 : cgo 工具标识为禁用

对 例子三 中的 dockerfile 做编译做调整,这个Dockerfile分为两个阶段:构建阶段和最终阶段。在构建阶段,它使用了官方的**golang镜像来构建Go应用程序,然后在最终阶段使用了scratch**基础镜像来创建最终的容器。

1
2
3
4
5
6
7
8
9
10
# 构建阶段
FROM golang:1.20 AS build
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app

# 最终阶段
FROM scratch
COPY --from=build /app/app /app
CMD ["/app"]

Untitled.png

编译后 docker-golang-build-example4 的大小从 15.5M 西江到了 11.2M, 这一点点的差值几乎就是 busybox 的存储占用.

要注意的问题

使用 scratch 镜像是一个空镜像,所以需要特别注意一些依赖和系统调用.

涉及到依赖时,需要解依赖,比如证书依赖:

COPY --from=builder /etc/ssl/certs/ /etc/ssl/certs/

涉及到系统调用,无法使用 scratch ,就需要考虑 apline 的方案.

例子5 加快编译速度

当把运行镜像的大小降下来后,还存在另一个问题,运行 go build 的时候都需要重新拉一遍golang 的环境. 可以通过挂在缓存的位置来减少应用程序编译时的时间.比如 golang 的 pkg目录 $GOPATH/pkg .在编译前提前做好 pkg 缓存,将缓存复制到目录中.

假设已经准备好了缓存目录 pkg. 在 例子4 中添加 COPY pkg/* /go/pkg/ 来加快编译速度.

1
2
3
4
5
6
7
8
9
10
11
# 构建阶段
FROM golang:1.20 AS build
WORKDIR /app
COPY pkg/* /go/pkg/
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app

# 最终阶段
FROM scratch
COPY --from=build /app/app /app
CMD ["/app"]

这个例子可能对 golang 的意义不是特别大, 对一些依赖比较多的 node 程序的 node_modele 目录缓存就很有用.

总结

通过四种方式,不断的降低了容器镜像的大小. 但是四种方式并没有特别的优劣之分,在不同的场景下,可以通过不同的策略来做镜像编译的方式.

  • 使用更小的镜像 apline 、busybox、scratch 来降低镜像大小
  • 使用分阶段构建来减少
  • 使用缓存来加快编译

在没有多阶段编译的时期,还有将多条命令写成一条的方式来降低容器的层数,或者借助一些工具来合并层.

以上例子放在了 github ,可以使用 docker-compose 来验证

docker-golang-build-example

机器学习记录

机器学习记录

不同的维度,看到的事物的形态也是不同的. 横看成岭侧成峰,远近高低各不同.

最简单的公式

$$
f(x)=XW+b
$$

有一堆数据,能够知道 x 和 y .现在我们通过一些方法来求出 Wb ,这样当有未知的 $X_n$ 通过 W 和 b 就能够求出 $f(x_n)$ 的值.

当把维度增加,公式就变成了

$$
f(x_1..xn)=X_1W_1+…X_nW_n+b
$$

https://microsoft.github.io/ai-edu/基础教程/

反向传播:

https://www.jiqizhixin.com/graph/technologies/7332347c-8073-4783-bfc1-1698a6257db3

https://zhuanlan.zhihu.com/p/40761721

正向传播: 传播信号从输入层到隐藏层到输出层,一层一层的传播,最后得到结果.

反向传播: 输出结果和真实结果存在误差,通过误差反向的传递给前面的个层,来调整网络的参数.

$$
f(x) = ax_1+bx_2
$$

如果 f(x) = z 而我们计算的值为y 那么误差为 m=z-y,那么我们需要通过误差来调整参数 a 、b 的值. 当误差值不断往前传播,最后通过误差计算出新的权重的过程.

https://cloud.tencent.com/developer/article/1897045

CNN: 卷积神经网络

http://arthurchiao.art/blog/cnn-intuitive-explanation-zh/

卷积的四种操作

  • 卷积 : 通过小矩阵对输入矩阵进行运算,学习图像特征,通过 ilter 保留像素空间关系.

Untitled.png

  • 非线性:卷积后通过一个称为 ReLU 的运算
  • 池化或降采样: 对卷积+ ReLU 的特征做降采样,比如44 降为22

Untitled.png

  • 分类/全连接

    Untitled.png

比如上图做两次卷积后,在做全连接.

卷积+降采样作为特征提取,全连接作为分类器

RNN 循环神经网络

http://fancyerii.github.io/books/rnn-intro/

Untitled.png

$x_t$ 为 t 时刻的输入

$s_t$ 为 t 时刻的隐状态,它可以看作是网络的记忆

$s_t = f(Ux_t+Ws_{t-1})$

使用上一层的输出和当前的输入作为当前的输入.

上一层的输入是前面权重计算,层数越多,前面层的影响越小(权重会越来越小),存在短期记忆的问题.

另外的

  • 双向 RNN
  • 深度双向 RNN
  • LSTM
  • GRU

transformer

https://cloud.tencent.com/developer/article/1897045

Transformer的优势在于,它可以不受梯度消失的影响,能够保留任意长的长期记忆。而RNN的记忆窗口很短;LSTM和GRU虽然解决了一部分梯度消失的问题,但是它们的记忆窗口也是有限的。

encoder-decoder 结构

https://luweikxy.gitbook.io/machine-learning-notes/seq2seq-and-attention-mechanism

给定的输入 encoder 后计算得到中间语义,使用 decoder 解码.

Untitled.png

attention 注意力机制

不在将输入编码成固定长度 .而是根据当前生成的新单词计算新的$C_i$

Untitled.png

transformer结构

https://www.tensorflow.org/tutorials/text/transformer?hl=zh-cn

https://erickun.com/2020/04/11/Transformer-原理-源码分析总结-Tensorflow官方源码/

Untitled.png

Transformer 结构分为编码器和解码器两部分. 编码器有 N 个层,解码器也有 N 个层.

Encoding

  • Positional Encoding

    https://www.cnblogs.com/emanlee/p/17137698.html

    transformer 是将所有词一起输入,并行操作,所以需要提供位置信息.

    位置嵌入的维度为[max sequence length ,embedding dimension] (输入的最大单句长度 、 词的维度)

    Untitled.png

    在论文中使用了 sin 和cos 函数的线性变换提供了模型的位置信息

    Untitled.png

    d:输出嵌入空间的维度

    pos:输入序列中的单词位置,0≤pos≤L-1

    这个公式的意义是,对于每个位置 pos 和每个维度 ,计算出对应的角度速率(angle_rate),用于位置编码中的计算。这样可以保证不同位置和不同维度的编码具有不同的特征,有助于模型更好地学习序列的位置关系。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def get_angles(pos,i,d_model,n):
    # pos/10000^(2*i/d_model)
    denominator = np.power(n, 2*(i/d_model))
    return pos/denominator

    def getPositionEncoding(seq_len,dim,n = 10000):
    PE = np.zeros((seq_len, dim))
    for pos in range(seq_len):
    for i in range(int(dim/2)):
    PE[pos,2*i] = np.sin(get_angles(pos,i,dim,n))
    PE[pos,2*i+1] = np.cos(get_angles(pos,i,dim,n))
    return PE

    疑问:

    使用 sin cos 交替的意义,奇数位和偶数位的位置描述不同? 如果直接使用随机数可行吗?

    2*i/d_model 在值域上的变化为 0-1-2,中值为1 ,对应的指数函数的变化不同,表

    示前半段的位置信息和后半段的位置信息不同?

在小说降临里面,对语言的描述是立体的,电影里面是一个环,而信息在环上延伸.最后学会了七支桶的语言,从而能够预测未来. 环描述了位置,也描述了信息.位置信息可能是相对的.

Multi- HeadAttention

https://imzhanghao.com/2021/09/15/self-attention-multi-head-attention/

https://www.cvmart.net/community/detail/5563

https://mp.weixin.qq.com/s/cJqhESxTMy5cfj0EXh9s4w

https://zhuanlan.zhihu.com/p/338817680

  • self Attention 自注意力机制

    Untitled.png

    1.初始化Q,K,V

    首先初始化三个权重矩阵$ W_Q 、 W_K、 W_V$ 然后将 X_embedding 与这三个权重矩阵相乘,得到 Q、K、V

    Untitled.png

    1. 计算 Self-Attention Score

    Untitled.png

    1. 对Self-Attention Socre进行缩放和归一化,得到Softmax Socre

    Untitled.png

    1. Softmax Socre乘以Value向量,求和得到Attention Value

Untitled.png

Multi-Head Attention的作用

将 Scaled Dot-Product Attention 的过程做 H 次(H=8)

把输出合并起来

$$
head_i = Attention(Q_i,K_i,V_i),i=1…8 \ MultiHead(Q,K,V) = Concact(head_1,…head_8)W^o
$$

Untitled.png

  • Add&Norm

    Add & Norm 由 Add 和 Norm 两个部分组成

    $$
    LayerNorm(X+MultiHeadAttention(X)) \
    LayerNorm(X+FeedForward(X))
    $$

    X 表示 MultiHeadAttention 或 FeedForward 的输入

    ADD 指的是 X + MultiHeadAttention(X) ,是一种残差链接,让网络指关注当前差异的部分.

Untitled.png

LayerNorm

https://zhuanlan.zhihu.com/p/492803886

对维度进行均值方差计算,LN 对 hidden 的维度做归一化操作.

Untitled.png

  • Feed Forward

    Feed Forward 是两个全连接层 ,第一层激活函数为 Relu ,第二层不使用激活函数

    $$
    max(0,XW_1+b_1)W_2+b_2
    $$

    X 是输入,Feed Forward 最终的到输出矩阵的维度与 X 一致.

encode

Untitled.png

Untitled.png

encode 的部分由 Multi-Head Attention , Add & Norm, Feed Forward, Add & Norm

decode

Untitled.png

解码器由三层结构组成

  • 第一层由带掩码(masked)的多头注意力层和一个残差连接

    Masked 操作目的是将后面的内容做掩盖,因为在生成过程中,生成了第i个字,才能生成第 i+1 个. 通过 masked 可以防止第i个字知道i+1的内容.

Untitled.png

Untitled.png

Untitled.png

Untitled.png

通过 masked 后的输出内容只有前 i 个字符的信息.

  • 第二层也是一个多头注意力层和一个残差连接

根据 Encoder 的输出 C 计算得到 K,V ,根据上一次输出的 Z 计算出 Q.进行多头注意力计算和残差连接. (通过掩码的 Q 去 Encode 的 K V 里查询出可能的内容. )

  • 第三层是前溃全连接层和一个残差连接.

计算方式与 encode 中的一致

最后线性层和 Softmax层

最后通过 Softmax 预测所有的单词

Untitled.png

链接

google develop 的llm简介: https://developers.google.com/machine-learning/resources/intro-llms?hl=zh-cn

Rust 小记

编程语言是用出来的, rust 文档已经看过几遍,但是依然不得要领. 通过和golang 做类比,以下是一些记录

变量默认不可变,变量可隐藏 .

let 不可变 ,let mut 可变,相比其他语言默认为可变 . 另外就是变了可以通过重新定义的方式.

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个不可变的变量
let x = 5;
// 下面的代码将会报错,因为x是不可变的
x = 10;

// 定义一个可变的变量
let mut y = 5;
// 修改y的值
y = 10;

// 重新定义一个变量z
let z = 5;
let z = z + 10; // z现在的值为15,但是它是一个新的变量

数据类型

整型、浮点型、布尔类型和字符类是基础类型,字符串切片、结构体 和 golang 大体一致.

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
// 整型类型
let x: i32 = 5;
println!("x = {}", x);
// 浮点型类型
let y: f64 = 3.14;
println!("y = {}", y);
// 布尔类型
let is_rust_fun: bool = true;
println!("is_rust_fun = {}", is_rust_fun);
// 字符类型
let c: char = 'A';
println!("c = {}", c);
// 字符串切片类型
let s: &str = "hello, world";
println!("s = {}", s);


// 结构体类型
// 定义一个结构体
struct Person {
name: String,
age: u8,
is_male: bool,
}
let person = Person {
name: String::from("Tom"),
age: 18,
is_male: true,
};

元组看使用示例是作为返回场景,golang 的多值返回一致. 其他地方的使用和结构体区别?写法的区别?

元组可以作为函数的返回值或者作为变量的值进行赋值。元组和结构体的区别在于元组的成员没有命名,而结构体的成员是有命名的。

在使用元组时需要注意,如果元组的成员是可变的,那么整个元组也是可变的。如果需要保证元组的成员不可变,可以使用 & 符号将元组的引用作为函数的返回值。

在同组值的时候可以很方便,比如表示坐标 Point(x,y)

看上去元组只时结构体的一种特殊表示.

函数返回值为表达式. 最后一个表达式的值作为函数的返回值.

如何提前返回?

rust 也有 return 关键字, 用于提前返回 ,那最后省略 return 的用意时什么?

在 Rust 中,函数的最后一个表达式的值将自动成为函数的返回值,因此可以省略 return 关键字。

1
2
3
4
5
6
7
8
// 使用 `return` 关键字
fn add_one(x: i32) -> i32 {
return x + 1;
}
// 省略 `return` 关键字
fn add_one(x: i32) -> i32 {
x + 1
}

循环

包括了 loop while for ,和 c 语言差不多.相比来说 golang 直接使用 for 来表示循环要简洁一些.

所有权

rust 的每个值都有一个所有权, 那么 = 的意思在其他语言里面的意思是: 把某值给予某变量. rust 里的意思是,把某值的拥有权交给谁.

前者的意思是,把东西放到你的仓库

后者的意思是给你一把仓库的钥匙,而且这个钥匙只有一把(只能有一个所有者,等你离开这里,这把钥匙就销毁,而且仓库也同时清理. (离开作用域,值被丢弃)

  • 值传递给函数时,所有权会交给函数.
  • println! 宏传递的是引用,不会转移所有权

bookmark

引用与借用

引用允许在没有所有权的情况下访问变量地址. 有点像 c 语言的指针的概念.

引用能对变量做什么?

引用的变量不可变,所以尝试修改会报错.可以使用 &mut 来定义可变引用 ,这里称之为借用.

借用: 不能在同一时间多次将变量作为可变变量借用. 也就是借用只能同一时间借给一个人,再次使用,只能等前一个还回来.(钥匙只有一把,只能借一个人)

在借用的同时,不能在做引用, 不可变的引用不希望变量被意外改变.

  • 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
  • 引用必须总是有效的。

枚举

在 golang 里面通常使用常变量的方式来定义一组变量,作为枚举值. rest 不久提供了枚举,还提供了一个通用枚举 Option

从后面的错误处理上看,rust 的枚举并不是单纯的枚举,而是通过枚举来设计了语言特性,比如 match 、错误处理

vector 和 array

vector 和 array 的区别?

vector 的大小不固定,可以动态的扩容. 区别是 vecter 是分配在栈上面.

  1. 大小可变性:Array 的大小是固定的,一旦定义就不能再改变。而 Vector 的大小是可变的,可以在程序运行时动态地添加或删除元素。
  2. 分配方式:Array 在栈上分配,而 Vector 在堆上分配。因为 Vector 的大小是可变的,所以需要在堆上分配内存。而 Array 的大小是固定的,所以可以在栈上分配内存,这样可以更快地访问元素。
  3. 索引访问:Array 的元素可以使用下标直接访问,比如 arr[0]。而 Vector 的元素也可以使用下标访问,但是需要使用 get 方法,比如 vec.get(0)。这是因为 Vector 的大小是可变的,有可能访问不存在的元素,所以需要在访问时进行一些检查。
  4. 用途:Array 适用于大小固定的情况,比如存储一组固定长度的数据。而 Vector 适用于大小不确定的情况,比如读取文件的内容、网络传输等场景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个数组
let arr = [1, 2, 3, 4];
// 定义一个向量
let mut vec = vec![1, 2, 3, 4];
// 访问数组的元素
println!("The first element of array is {}", arr[0]);
// 访问向量的元素
println!("The first element of vector is {}", vec[0]);
// 向向量中添加元素
vec.push(5);
// 计算数组的长度
println!("The length of array is {}", arr.len());
// 计算向量的长度
println!("The length of vector is {}", vec.len());

hashmap

hash map 用法和 golang 基本一样,需要注意一下所有权.

错误处理

rest 使用 Result<T,E> 的枚举来返回是否出现了错误,等同 golang 里面的返回 res,err := function(xxx)

Result<T,E> 定义了一些辅助方法来处理一些情况,比如

1
let greeting_file = File::open("hello.txt").unwrap();

unwrap 成功返回,失败直接 panic

expect 可以自定义错误信息.

可恢复错误: 当出现错误时,不是所有的错误都需要 panic. 所以可以将错误向上传播,使用 ? 来作为向上传播的标识符, ? 实际是一个宏,用来做 match 返回.

1
2
3
4
5
6
let mut f = match f {
// 打开文件成功,将file句柄赋值给f
Ok(file) => file,
// 打开文件失败,将错误返回(向上传播)
Err(e) => return Err(e),
};

? 给我的感觉是 golang 的 err ≠ nil 的方式相比不够优雅. 不过这都是建立在返回是枚举的基础上.

trait 类似于 golang 的 interface

(rust 的命名如果能和其他语言保持一致的话,可能更加好学一些吧,比如 match 和 switch )

智能指针

智能指针 像是为了解决对应引用问题定义的类型?

从文档来看是来源于 c++ 的概念

  • Box<T>,用于在堆上分配值

    • 解引用 Deref
    • 释放资源 Drop

    解引用可以将智能指针当普通指针来使用, 通过 deref 将引用返回

    释放资源在值离开作用域时调用(手动回收堆上的数据?)

  • Rc<T>,一个引用计数类型,其数据可以有多个所有者

    • 对一个Rc变量进行clone()时,不会将其内部的数据复制,只会增加引用计数。
    • 当一个Rc变量离开作用域被drop()时,只会减少引用计数,直到引用计数为零时,才会真正清除其拥有数据的堆内存。
    • Arc<T> : Atomic Rc 原子化的 Rc<T>,相比来说,带来线程安全和性能损耗
  • Ref<T> 和 RefMut<T>,通过 RefCell<T> 访问。( RefCell<T> 是一个在运行时而不是在编译时执行借用规则的类型)。

    • Cell 只适用于 Copy 类型,用于提供值,而 RefCell 用于提供引用
    • Cell 不会 panic,而 RefCell 会

并发

rust 提供了线程 、锁,提供了像 golang channel 的类型

async 提供了类似 golang 协程的概念.

总结

这里只是对rust 做了片面的理解, rust 所描述的没有垃圾回收,实际是任何时候,都明确变量在哪个位置被回收

定义好的变量在堆上还是在栈上也是确定的,所以回收的时机也是明确的.

所以不需要垃圾回收算法

rust 的设计上处处都体现着与其他语言的不同,变量、描述、定义,从语法上吸收了 c++ 的语法, 但依然过于复杂. 比如很多约定俗成的描述,依然换了一种描述方式,比如 match 而不是用 switch .

参考:

bookmark

bookmark

从数据包看网络协议

从数据包看网络协议

数据包是怎么流转的 – OSI 网络模型

Untitled.png

数据包在每一层的作用

Untitled.png

TCP/IP 模型更好地体现网络工作的逻辑方式

在 TCP/IP 模型中,四层包括:

  • 4. 应用程序层:这大致相当于 OSI 模型中的第 7 层。
  • 3. 传输层:对应于 OSI 模型中的第 4 层。
  • 2. 互联网层: 对应于 OSI 模型中的第 3 层。
  • 1. 网络访问层:结合了 OSI 模型中第 1 层和第 2 层的过程。

网络访问层:结合了 OSI 模型中第 1 层和第 2 层的过程

Untitled.png

帧头部 : 源 MAC 地址 目标 MAC 地址 Type

以太网MAC帧格式

Untitled.png

MTU(最大传输单元): MTU相当于在发寄快递的时候对包裹的限制,这个限制是不同的链路层对应的物理层的限制。

MAC帧中的数据长度规定为46-1500字节,ARP(地址解析协议)数据包的长度不够46字节要在后面补填充位,如果大于1500字节,必须要求网络层进行分片。
最大值1500称为以太网的最大传输单元(MTU)不同的网络类型有不同的MTU
如果一个数据包从以太网路由到拨号链路上,数据包长度大于拨号链路的MTU,则需要对数据包进行分片。

ARP协议: ARP是介于链路层到网络层之间的协议,它的作用是为了解析IP地址到MAC地址的映射

分析

Untitled.png

eth.type == 0x0800

eth.addr == ff:ff:ff:ff:ff:ff

互联网层: 对应于 OSI 模型中的第 3 层。

四元组 源地址:源端口 目标地址:目标端口

Netfilter 框架

iptables

NAT

使私有网络能够和公网互联。

Untitled.png

应用 LVS

传输层:对应于 OSI 模型中的第 4 层。

TCP && UDP

Untitled.png

TCP

Untitled.png

UDP

Untitled.png

TCP 三次握手 四次挥手 粘包 拆包

UDP

问题: 同一个端口能否同时监听 TCP 和 UDP

应用层协议设计 ,如果是你怎么来设计一个协议。

  1. 包分隔符:
  2. 包头
    1. 版本号
    2. 长度
    3. code
    4. 消息ID
    5. option
    6. 预留位
  3. 消息体
  4. 其他

请求包 / 响应包

Untitled.png

应用程序层:这大致相当于 OSI 模型中的第 7 层

HTTP 协议

Untitled.png

tcpdump 和 wireshark

libpocp

tcpdump -i eth0 port 80 -w result.pocp

tcpdump -i en0 udp -w upd.pcap

参考

什么是MTU(Maximum Transmission Unit)?MTU设置为多少合适?

数据链路层详解_HanSion.Z-CSDN博客_数据链路层

什么是网络层_以及网络传播协议中OSI模型与TCP/IP模型的区别 | Cloudflare

如何用 golang 生成比特币钱包

前一段时间看了区块链相关的内容,学习了一下bitcoin地址生成。内容来自网络。

第一步,随机选取一个32字节的数,大小介于1~0xFFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFE BAAE DCE6 AF48 A03B BFD2 5E8C D036 4141之间,作为私钥

PS: 助记词是将这个随机数按每 11位分组映射到 2048 个单词位得到 12 16 或 24 个单词。

18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725

第二步,使用椭圆曲线加密算法(ECDSA-SECP256k1)计算私钥所对应的非压缩公钥(共65字节,1字节0x04,32字节为x坐标,32字节为y坐标)。

0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6

第三步,计算公钥的SHA-256哈希值

600FFE422B4E00731A59557A5CCA46CC183944191006324A447BDB2D98D4B408

第四步,计算上一步哈希值的RIPEMD-160哈希值

010966776006953D5567439E5E39F86A0D273BEE

第五步,在上一步结果之间加入地址版本号(如比特币主网版本号”0x00”)

00010966776006953D5567439E5E39F86A0D273BEE

第六步,计算上一步结果的SHA-256哈希值

445C7A8007A93D8733188288BB320A8FE2DEBD2AE1B47F0F50BC10BAE845C094

第七步,再次计算上一步结果的SHA-256哈希值

D61967F63C7DD183914A4AE452C9F6AD5D462CE3D277798075B107615C1A8A30

第八步,取上一步结果的前4个字节(8位十六进制数)D61967F6,把这4个字节加在第五步结果的后面,作为校验(这就是比特币地址的16进制形态)

00010966776006953D5567439E5E39F86A0D273BEED61967F6

第九步,用base58表示法变换一下地址(这就是最常见的比特币地址形态)

16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM

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
package main

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"golang.org/x/crypto/ripemd160"
"log"
)

const VERSION = byte(0x00)
const CHECKSUM_LENGTH = 4

type BitcoinKeys struct {
PrivateKey *ecdsa.PrivateKey
PublicKey []byte
}

func GetBitcoinKeys() *BitcoinKeys {
b := &BitcoinKeys{nil, nil}
b.newKeyPair()
return b
}

func (b *BitcoinKeys) newKeyPair() {
curve := elliptic.P256()
var err error
b.PrivateKey, err = ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Panic(err)
}
b.PublicKey = append(b.PrivateKey.PublicKey.X.Bytes(), b.PrivateKey.PublicKey.Y.Bytes()...)
}

//获取地址
func (b *BitcoinKeys) GetAddress() []byte {
//1.ripemd160(sha256(publickey))
ripPubKey := GeneratePublicKeyHash(b.PublicKey)
//2.最前面添加一个字节的版本信息获得 versionPublickeyHash
versionPublickeyHash := append([]byte{VERSION}, ripPubKey[:]...)
//3.sha256(sha256(versionPublickeyHash)) 取最后四个字节的值
tailHash := CheckSumHash(versionPublickeyHash)
//4.拼接最终hash versionPublickeyHash + checksumHash
finalHash := append(versionPublickeyHash, tailHash...)
//进行base58加密
address := Base58Encode(finalHash)
return address
}

func GeneratePublicKeyHash(publicKey []byte) []byte {
sha256PubKey := sha256.Sum256(publicKey)
r := ripemd160.New()
r.Write(sha256PubKey[:])
ripPubKey := r.Sum(nil)
return ripPubKey
}

//通过地址获得公钥
func GetPublicKeyHashFromAddress(address string) []byte {
addressBytes := []byte(address)
fullHash := Base58Decode(addressBytes)
publicKeyHash := fullHash[1 : len(fullHash)-CHECKSUM_LENGTH]
return publicKeyHash
}

func CheckSumHash(versionPublickeyHash []byte) []byte {
versionPublickeyHashSha1 := sha256.Sum256(versionPublickeyHash)
versionPublickeyHashSha2 := sha256.Sum256(versionPublickeyHashSha1[:])
tailHash := versionPublickeyHashSha2[:CHECKSUM_LENGTH]
return tailHash
}

//检测比特币地址是否有效
func IsVaildBitcoinAddress(address string) bool {
adddressByte := []byte(address)
fullHash := Base58Decode(adddressByte)
if len(fullHash) != 25 {
return false
}
prefixHash := fullHash[:len(fullHash)-CHECKSUM_LENGTH]
tailHash := fullHash[len(fullHash)-CHECKSUM_LENGTH:]
tailHash2 := CheckSumHash(prefixHash)
if bytes.Compare(tailHash, tailHash2[:]) == 0 {
return true
} else {
return false
}
}

func main() {
keys := GetBitcoinKeys()
bitcoinAddress := keys.GetAddress()
fmt.Println("比特币地址:", string(bitcoinAddress))
fmt.Printf("比特币地址是否有效:%v\n:", IsVaildBitcoinAddress(string(bitcoinAddress)))
}


base58

Base58 采用数字、大写字母、小写字母,去除歧义字符 0(零)、O(大写字母 O)、I(大写字母i)、l(小写字母L),总计58个字符作为编码的字母表。

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
package main

import (
"bytes"
"math/big"
)

var b58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")

func Base58Encode(input []byte) []byte {
var result []byte

x := big.NewInt(0).SetBytes(input)

base := big.NewInt(int64(len(b58Alphabet)))
zero := big.NewInt(0)
mod := &big.Int{}

for x.Cmp(zero) != 0 {
x.DivMod(x, base, mod)
result = append(result, b58Alphabet[mod.Int64()])
}

ReverseBytes(result)

for _, b := range input {
if b == 0x00 {
result = append([]byte{b58Alphabet[0]}, result...)
} else {
break
}
}
return result

}

func Base58Decode(input []byte) []byte {
result := big.NewInt(0)
zeroBytes := 0
for _, b := range input {
if b != b58Alphabet[0] {
break
}
zeroBytes++
}
payload := input[zeroBytes:]
for _, b := range payload {
charIndex := bytes.IndexByte(b58Alphabet, b)
result.Mul(result, big.NewInt(int64(len(b58Alphabet))))
result.Add(result, big.NewInt(int64(charIndex)))
}

decoded := result.Bytes()
decoded = append(bytes.Repeat([]byte{byte(0x00)}, zeroBytes), decoded...)

return decoded
}

func ReverseBytes(data []byte) {
for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 {
data[i], data[j] = data[j], data[i]
}
}


lua package 面向对象开发

lua 作为一门简单的语言,在变量是只有 table 和非 table 的区别。

基本

1
2
3
4
5
6
7
local _M = {} 
function _M:foo()
...
end

return _M

在 lua 中 _M:foo() 就是 _M.foo(self) 。在使用时和个人习惯有关。这里我希望能够严格的区分库和对象.当函数为静态方法时,比如一些utils函数,使用点号(.)。当函数需要面向对象时,使用冒号(:)。

阅读更多

计算最大公约数

这是计算最大公约数的函数.辗转相除法

1
2
3
4
5
6
function gcd(a, b)
if b == 0 then
return a
end
return gcd(b, a % b)
end

它的计算其实是使用欧拉定理.

https://zh.wikipedia.org/wiki/欧拉定理_(数论)

证明

对于任何可以整除a和b的整数,那么它也一定能整除a-b

1.

假设 a b 都有公约数 n 且 a>b, 假设 $a=x_1n$, $b=x_2n$

那么$ a-b =(x_1-x_2)n$

  1. a=kb+t 那么 t=a-kb

    $\frac{t}{d} = \frac{a}{d} -\frac{kb}{d}$

    因为 a、b都能够被d整除

    所以 $\frac{a}{d}-\frac{kb}{d}$ 为整数,

    即 $\frac{t}{d}$ 为整数,所以 d 也是 t 的公约数.

在均衡负载中,对设置的权重求最大公约数,需要用到 gcd 函数.

当传入的值是非数字时, 比如传入 字符串 “0”, b==0 判断失效

执行 gcd(”0”,nil) ,再次执行 gcd(nil,nil) 导致出现死循环.

https://mp.weixin.qq.com/s?__biz=Mzg3Njc0NTgwMg==&mid=2247487272&idx=1&sn=038a30ce61706c97e3397eee982b1486&amp

go-singlefilght

singleflight

1
2
3
golang.org/x/sync/singleflight


singleflight 是 go 提供的一个扩展并发原语,主要是用来合并请求来降低服务压力。

code

1
2
https://cs.opensource.google/go/x/sync/+/036812b2:singleflight/singleflight.go

原理

实现了一个 Group 的 struct

1
2
3
4
5
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}

阅读更多

docker build alpine dns error

使用 alpine 构建 Ddockerfile ,导致 docker 打包失败,错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fetch https://mirrors.aliyun.com/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
fetch https://mirrors.aliyun.com/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
v3.13.4-69-g5bcff43ec5 [https://mirrors.aliyun.com/alpine/v3.13/main]
v3.13.4-66-g32aee0eba0 [https://mirrors.aliyun.com/alpine/v3.13/community]
OK: 13892 distinct packages available
fetch https://mirrors.aliyun.com/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
WARNING: Ignoring https://mirrors.aliyun.com/alpine/v3.13/main: DNS lookup error
fetch https://mirrors.aliyun.com/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
WARNING: Ignoring https://mirrors.aliyun.com/alpine/v3.13/community: DNS lookup error
ERROR: unable to select packages:
bash (no such package):
required by: world[bash]
curl (no such package):
required by: world[curl]
ERROR: Service 'microservice-users-rpc' failed to build : The command '/bin/sh -c apk update && apk add --no-cache curl bash' returned a non-zero code: 2
make: *** [docker-compose-up] Error 1

阅读更多