写给初学者的 Git 极简教程

Git 是一个强大的版本控制系统,它允许多个开发者在同一个项目上协作,跟踪文件的变化,并且可以在不同的时间点恢复到之前的状态。对于初学者来说,理解 Git 的基本概念和操作是非常重要的。在这篇博客中,我们将详细介绍 Git 的使用,并通过具体的例子来演示常用的 Git 命令。

Git 的三个位置

在使用 Git 时,我们需要了解三个主要的位置:本地目录、缓存区(有时也称为暂存区)和远程仓库。

1
2
3
4
5
6
7

# 记住 git 的三个位置
本地目录 <--(cache)----> .git <----> remote
add #将代码添加到缓冲区
commit --> #将代码提交到仓库
push -> 推送到远程
pull <- 从远程拉取

本地目录

这是你在计算机上存放项目文件的地方。当你开始一个新项目或者克隆一个远程仓库时,Git 会在项目文件夹中创建一个隐藏的 .git 文件夹。这个文件夹包含了所有的版本控制信息。

缓存区(.git

当你对本地目录中的文件进行修改后,这些改动并不会立即提交到远程仓库。首先,你需要使用 git add 命令将这些修改添加到缓存区。缓存区是一个临时存放改动文件的地方,你可以在这里预览将要提交的改动。

远程仓库

远程仓库是存储你的项目代码的在线平台,例如 GitHub、GitLab 或 Bitbucket。通过 git pushgit pull 命令,你可以将本地的改动推送到远程仓库,或者从远程仓库拉取其他人的改动到本地。

基本操作流程

添加(add)和提交(commit

  1. 添加改动到缓存区

    1
    git add <文件名>

    如果你想添加所有改动的文件到缓存区,可以使用:

    1
    git add .

    这里的点(.)代表了当前目录下的所有改动文件。

  2. 提交改动到本地仓库

    1
    git commit -m "你的提交信息"

    -m 选项后面跟着的是你对这次提交的描述信息,它有助于你和你的团队成员理解这次提交的目的和内容。

推送(push)和拉取(pull

  1. 推送本地提交到远程仓库

    1
    git push 远程仓库名 分支名

    例如,如果你想推送到名为 origin 的远程仓库的 master 分支,你可以使用:

    1
    git push origin master
  2. 从远程仓库拉取最新改动

    1
    git pull 远程仓库名 分支名

    这个命令会将远程仓库的最新改动拉取到你的本地目录,并自动尝试合并。

查看 Git 状态

要查看当前 Git 的状态,包括哪些文件被修改但还没有暂存或提交,可以使用 git status 命令。这个命令会列出所有的改动文件,并标明它们的状态。

理解 origin

origin 是远程仓库的默认别名。每个远程仓库都可以有一个或多个别名。你可以使用 git remote -v 命令来查看远程仓库的地址和它们的别名。

理解分支

分支是 Git 中一个非常重要的概念。它允许你在不同的线路上进行开发,而不会相互干扰。master 通常是默认的主分支,所有的开发工作都是基于这个分支进行的。

创建和使用分支

创建新分支

1
git checkout -b 新分支名

这个命令会创建一个新的分支,并自动切换到这个分支。例如,如果你想创建一个名为 feature-x 的新分支,你可以使用:

1
git checkout -b feature-x

切换分支

1
git checkout 分支名

如果你想从 feature-x 分支切换回 master 分支,你可以使用:

1
git checkout master

合并分支

当你在一个新分支上完成开发后,你可能需要将这些改动合并回 master 分支。首先,确保你已经切换到 master 分支,然后使用 git merge 命令将其他分支合并进来:

1
git merge 要合并的分支名

例如,如果你想将 feature-x 分支的改动合并到 master 分支,你可以使用:

1
git merge feature-x

通过以上步骤,你可以有效地管理你的代码变更,并且与团队成员协作开发项目。希望这篇博客能帮助你更好地理解和使用 Git!

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

机器学习记录

机器学习记录

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

最简单的公式

$$
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 求子串绝对值最大

这两天学习了 rust 的语法

bookmark

尝试使用 rust 来写今天的 leetcode 的每日一题

https://leetcode.cn/problems/maximum-absolute-sum-of-any-subarray

给你一个整数数组 nums 。一个子数组 [numsl, numsl+1, ..., numsr-1, numsr] 的 和的绝对值 为 abs(numsl + numsl+1 + ... + numsr-1 + numsr) 。

请你找出 nums 中 和的绝对值 最大的任意子数组(可能为空),并返回该 最大值 。

边查文档边写的代码:

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
impl Solution {

pub fn max_abs(a:i32,b:i32) -> i32 {

if a <0{
a= -a
}
if b<0{
b= -b
}

if a > b {
a
}else{
b
}
}

pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
let mut res = 0;

let mut dp_max = vec![0;nums.length()];
let mut dp_min = vec![0;nums.length()];

for (i,num) in nums.iter().enumerate() {
if i == 0 {
dp_max[0] = *num
dp_min[0] = *num
res = Solution::max_abs(dp_max[0],dp_min[0])
}else{
let max_num = dp_max[i-1] + *num
if max_num > *num{
dp_max[i] = max_num
}else{
dp_max[i] =*num
}

let min_num = dp_min[i-1] + *num
if min_num < *num{
dp_min[i] = min_num
}else{
dp_min[i] = *num
}

max_num = Solution::max_abs(dp_max[i],dp_min[i])
res = Solution::max_abs(max_num,res)
}
}
res
}
}

编译错误提示

  • 忘记写 ;
1
2
3
4
5
6
7
error: expected `;`, found `dp_min`
--> src/main.rs:60:33
|
60 | dp_max[0] = *num
| ^ help: add `;` here
61 | dp_min[0] = *num
| ------ unexpected token
  • 获取数组长度 len 写成 length
1
2
|         let mut dp_max = vec![0;nums.length()];
| ^^^^^^ help: there is a method with a similar name: `len`
  • 未定义的结构体
1
2
34 | impl Solution {
| ^^^^^^^^ not found in this scope
  • 最后的返回没有分号
1
2
3
4
5
6
7
pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
| ---------------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
...
87 | res;
| - help: remove this semicolon to return this value
  • 计算最大绝对值函数的所有权
1
2
3
4
5
6
7
pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
| ---------------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
...
87 | res;
| - help: remove this semicolon to return this value
  • 所有权只读
1
2
3
4
5
6
7
8
70 |                 let max_num = dp_max[i-1] + *num;
| -------
| |
| first assignment to `max_num`
| help: consider making this binding mutable: `mut max_num`
...
84 | max_num = Solution::max_abs(dp_max[i],dp_min[i]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable

修改的结果

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
struct Solution{

}


impl Solution {

pub fn max_abs(a:i32,b:i32) -> i32 {
let mut a = a ;
let mut b = b ;
if a <0{
a= -a;
}
if b<0{
b= -b;
}

if a > b {
a
}else{
b
}
}

pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
let mut res = 0;

let mut dp_max = vec![0;nums.len()];
let mut dp_min = vec![0;nums.len()];

for (i,num) in nums.iter().enumerate() {
if i == 0 {
dp_max[0] = *num;
dp_min[0] = *num;
res = Solution::max_abs(dp_max[0],dp_min[0])
}else{
let mut max_num = dp_max[i-1] + *num;
if max_num > *num{
dp_max[i] = max_num;
}else{
dp_max[i] =*num;
}

let min_num = dp_min[i-1] + *num;
if min_num < *num{
dp_min[i] = min_num;
}else{
dp_min[i] = *num;
}

max_num = Solution::max_abs(dp_max[i],dp_min[i]);
res = Solution::max_abs(max_num,res);
}
}
res
}
}


fn main(){
Solution::max_absolute_sum(vec![1,-3,2,3,-4]);
}

总结

参看其他人写的代码

  • 直接使用 for i in 0..n 要比 nums.iter().enumerate() 要好一些
  • 数值型函数本身有 max min abs 函数,不需要自己写.
  • 计算时由于过程已经保存 dp 的结果也不用保存,只需要保存上一次的结果给下一次循环用即可.
  • rust 的语法确实很难描述,需要大量的实践,特别是开始写的时候变量的传递有些懵.

最终版本:

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

//f(n) 绝对值最大 = n 的 最大值和最小值的绝对值取大
// f(n) 最大值 = f(n-1) 的最大值+n 和 n 取大
// f(n) 最小值 = f(n-1) 的最小值+n 和 n 取小

impl Solution {
pub fn max_absolute_sum(nums: Vec<i32>) -> i32 {
let n = nums.len();
let mut res = 0;

let mut dp_max = 0;
let mut dp_min = 0;

for i in 0..n {
if i == 0 {
dp_max = nums[0];
dp_min = nums[0];
res = dp_max.abs().max(dp_min.abs());
}else{

let max_num = dp_max + nums[i];
dp_max = max_num.max(nums[i]);

let min_num = dp_min + nums[i];
dp_min = min_num.min(nums[i]);

res = res.max(dp_max.abs().max(dp_min.abs()));

}
}
res
}
}

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

微服务还是单体服务?

微服务还是单体服务?

前两天看到了腾讯云开发者的账号发布以一篇 QQ 浏览器服务的优化.

bookmark

在做架构设计时将微服务变成单体服务. 我在下面做了评价.

很多做设计的没有意识到 rpc 也是需要时间的,无限度的搞微服务,复杂性和网络调用链导致问题排查不下去. 一个单体应用加个缓存的事情,最后搞成一堆服务相互依赖调用,浪费机器、浪费人力.

在做架构拆分,很容易忘记初衷是什么,而是为了架构而架构.

在做微服务的时候,基本都是目前架构冗余,业务杂糅在一起,扩容困难. 或者是数据库表太多,大量的表关联查询缓慢,所以要做微服务架构的改造,跟随一起的还有,服务要跟着一起上容器.

单体应用与微服务并不冲突

做微服务拆分,但不要拆得那么细,比如有些过分的拆分恨不得 tab 就成为一个微服务.

本来一个很简单的业务,一个微服务/单体应用就够了, 非得拆出好几个来,比如一个业务有几个任务,业务把每个任务都拆成一个服务,可每个服务的80%的代码都是一样的,甚至数据都是一个数据库来源. 没有这个必要嘛. 而且过多的拆分会导致业务代码的割裂.

grpc 很快,但是也需要时间

grpc 使用 http2 协议,相比 http 协议对比,长链接和流要快很多,但是还是需要花时间的,每多一次的 grpc 的交互,网络请求会多出 1-2ms 的时间.

有些给前端的页面渲染拆成了多个模块,当想要把所有数据一次渲染出来.内部可能需要做几次甚至是十几次的 rpc 调用. 导致在网络上的时间就超过了 20ms. 还是不考虑高负载和网络抖动的情况下.

当 rpc 出现错误重试的成本要远远高于单体应用出错重试.

让缓存前置,让请求后置

当对服务进行拆分,有时候会出现这样的情况,每个微服务配套一套缓存和 DB.通常我们在内网微服务通信,然后通过 http 来提供外网服务.如果可能的话,可以尝试让对外的服务做前置的缓存.

不要过早的优化,容器并不是银弹

容器也是有损耗的, 使用容器会带来物理机大概 5% 的性能损耗,如果业务本身很稳定,也不是高并发的系统,没有什么扩容的需求,要不还是用物理机吧.

容器带来的无状态服务,扩容等让运维变得简单,但是同时也带来了查问题的成本变高,全链路的监控和运维的成本,对于小团队来说几乎是个黑洞.

做架构升级不要为了兼容旧的东西做妥协

在做架构升级会碰到很多的技术债,为了兼容可能是错误的技术债,不得不为此妥协,当妥协的内容越来越多的时候,会发现架构升级做了个寂寞. (这个和微服务没关系)

架构的目的,不是为了炫耀新技术,而是为了稳定,作为一个技术人员,要最求新技术,善用老技术.

从数据包看网络协议

从数据包看网络协议

数据包是怎么流转的 – 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

计算最大公约数

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

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