Rust 入门笔记(网络安全工程师版 · 完整练习版)
目标:不追求"背下语法",而是通过"这段代码在干什么 + 为什么 Rust 要这么设计"理解概念,每章配 2~3 道题(含前面章节的知识点滚动巩固),并附案例和解释、答案。
目录
- 环境搭建 & Cargo
- 基础语法(变量、类型、控制流)
- 所有权系统
- 借用与引用
- 结构体、枚举、模式匹配
- 错误处理(Result / Option /
?) - 集合类型(Vec、HashMap、String)
- Trait 与泛型
- 闭包与迭代器
- 智能指针(Box、Rc、Arc、RefCell)
- 并发(线程、Mutex、channel)
- 异步编程(async/await、tokio)
- 案例一:TCP 端口扫描器
- 案例二:HTTP 请求调试工具
- 案例三:抓包分析
- GUI 案例一:egui
- GUI 案例二:Tauri
- GUI 案例三:Slint(从安装到运行全流程)
- 常见坑 & 记忆口诀
- 学习路径建议
每章末尾的"✏️ 练习"部分,建议先自己写代码跑一跑,再看"答案与解析",效果比直接看答案好得多。
1. 环境搭建 & Cargo
# 安装
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 常用命令
cargo new my_project # 新建项目
cargo build # 编译(debug)
cargo build --release # 编译(release,性能优化)
cargo run # 编译并运行
cargo check # 只检查语法/类型,不生成二进制(写代码时最常用,快)
cargo add reqwest # 添加依赖(等价于手动改 Cargo.toml)记忆点:Cargo = Rust 的"包管理器 + 构建工具",相当于 Python 的 pip+venv,或 Node 的 npm 合体。Cargo.toml 是项目配置文件(依赖写在这里),Cargo.lock 是依赖版本锁定文件(类似 package-lock.json,不要手动改)。
一个新建项目的标准目录结构:
my_project/
├── Cargo.toml # 项目配置、依赖列表
├── Cargo.lock # 依赖版本锁定(自动生成,不用手改)
└── src/
└── main.rs # 程序入口✏️ 练习
1. 用 cargo 创建一个叫 port_tool 的新项目,并添加 reqwest 依赖,写出完整命令。
答案与解析
cargo new port_tool
cd port_tool
cargo add reqwestcargo new 会自动生成 Cargo.toml、src/main.rs 并初始化 git 仓库;cargo add 会自动把依赖写进 Cargo.toml 的 [dependencies] 段,不用手动查版本号。
2. cargo build 和 cargo check 有什么区别?你在开发调试阶段应该更常用哪个,为什么?
答案与解析
cargo build 会完整编译并生成可执行文件(在 target/debug/ 下);cargo check 只做语法和类型检查,不生成二进制文件,所以速度快很多。开发阶段频繁修改代码时应该用 cargo check 快速验证有没有写错,等确认逻辑没问题了再用 cargo run/cargo build 真正跑起来。这个习惯在写大型网络工具、编译时间较长时特别有用。
3. Cargo.toml 和 Cargo.lock 分别的作用是什么?为什么 Cargo.lock 不应该手动修改?
答案与解析
Cargo.toml 是你自己维护的配置文件,写项目名、版本、依赖及其版本范围(比如 reqwest = "0.12" 表示允许 0.12.x 的任意兼容版本)。Cargo.lock 是 Cargo 自动生成的,记录了本次构建实际锁定使用的每一个依赖(包括间接依赖)的精确版本号,保证"在别的机器上/别的时间点 cargo build,装的版本完全一样",避免"我这能跑,你那跑不了"的问题。手动改它容易导致版本记录和实际安装的包不一致。
2. 基础语法
fn main() {
// 变量默认不可变(immutable)—— 这是 Rust 的设计哲学:默认安全
let x = 5;
// x = 6; // ❌ 编译报错
// 要修改就必须显式加 mut
let mut y = 5;
y = 6; // ✅
// 类型标注(大多数时候可以省略,编译器会推断)
let ip: &str = "192.168.1.1";
let port: u16 = 8080; // u16: 无符号16位整数,端口号刚好0~65535,很贴切
// 常量:必须标类型,全大写命名
const MAX_RETRY: u32 = 3;
// 常用数值类型
// i8/i16/i32/i64/i128 有符号整数
// u8/u16/u32/u64/u128 无符号整数(网络编程常用,比如字节流用 u8)
// f32/f64 浮点数
// bool, char, &str, String
// 控制流
if port == 80 {
println!("HTTP");
} else if port == 443 {
println!("HTTPS");
} else {
println!("其他端口: {}", port);
}
// for 循环遍历端口范围(左闭右开)
for p in 1..5 {
println!("扫描端口 {}", p);
}
// loop:无限循环,配合 break 返回值(网络重试逻辑常用)
let mut retry = 0;
let result = loop {
retry += 1;
if retry == MAX_RETRY {
break "达到最大重试次数";
}
};
println!("{}", result);
}记忆口诀:let 默认不可变,想改加 mut;类型能推断就不用写,写了更安全(尤其是网络字节操作时,明确 u8/u32 能避免溢出坑)。
✏️ 练习
1. 写一个函数 is_valid_port,接收一个 u32 类型的数字,判断它是否在合法端口范围内(1~65535),返回 bool。
答案与解析
fn is_valid_port(p: u32) -> bool {
p >= 1 && p <= 65535
}
fn main() {
println!("{}", is_valid_port(8080)); // true
println!("{}", is_valid_port(70000)); // false
println!("{}", is_valid_port(0)); // false
}这里故意用 u32 而不是 u16,是因为如果直接用 u16 存用户输入的数字,一旦超过 65535 会直接溢出/报错(release模式下会“环绕”,debug模式下会panic),不利于做校验。用更大范围的类型接收输入、再做范围判断,是网络工具里处理用户输入端口号的常见写法。
2. 用 for 循环遍历 1~10(包含10),把所有能被 3 整除的数字打印出来,格式为 "整除3: {数字}"。
答案与解析
fn main() {
for i in 1..=10 { // ..= 表示"包含右端点"
if i % 3 == 0 {
println!("整除3: {}", i);
}
}
}1..10 是左闭右开(不含10),1..=10 是闭区间(含10)。网络工具里扫描端口范围时经常需要"包含末尾端口",记得用 ..=。
3. 结合上一节的 Cargo 知识:新建一个项目 port_checker,在 main.rs 里写一个 loop,模拟"最多重试 5 次连接,每次重试次数+1,第5次时打印失败信息并跳出循环"。
答案与解析
cargo new port_checkerfn main() {
const MAX_RETRY: u32 = 5;
let mut retry = 0;
loop {
retry += 1;
println!("第 {} 次尝试连接...", retry);
if retry == MAX_RETRY {
println!("已达最大重试次数,连接失败");
break;
}
}
}这里 break 不带值,因为我们不需要从 loop 里"返回"什么,只是单纯跳出循环——对比第1章例子里 break "达到最大重试次数" 是带值的写法,用于把结果赋给外部的 let result = loop {...}。两种写法根据是否需要循环的"返回值"来选。
3. 所有权系统(Rust 最核心、最反直觉,也是必须吃透的部分)
这是 Rust 和其他语言最大的区别:没有垃圾回收(GC),也不用手动 free/delete,而是靠编译期规则管理内存。
三条铁律:
- 每个值在 Rust 中都有一个"所有者"(变量)。
- 同一时刻只能有一个所有者。
- 所有者离开作用域,值就被自动释放(drop)。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // ⚠️ 所有权"移动"(move)给了 s2,s1 失效!
// println!("{}", s1); // ❌ 编译报错:value borrowed after move
println!("{}", s2); // ✅ 只有 s2 有效
// 如果想让两个变量都能用,要么 clone(深拷贝),要么用引用(下一节)
let s3 = String::from("world");
let s4 = s3.clone(); // 深拷贝,s3 和 s4 都有效,但有性能开销
println!("{} {}", s3, s4);
}为什么这样设计? 传统语言里,两个变量指向同一块堆内存,容易出现"重复释放"或"悬垂指针"(use-after-free)——这正是 C/C++ 里大量安全漏洞(比如很多 CVE)的根源。Rust 用"移动语义"在编译期就把这类问题堵死。
记忆类比:把 String 想象成一把只有一个人能拿的钥匙。let s2 = s1 不是复制钥匙,而是把钥匙从 s1 手里拿走给 s2,s1 从此手里没钥匙了(不能再用)。
注意:像i32、bool、char这种简单类型是Copy的,赋值时是复制而不是移动,let a = 5; let b = a;之后 a 和 b 都能用。
✏️ 练习
1. 下面这段代码能编译通过吗?如果不能,错误在哪一行?请修复它。
fn main() {
let target = String::from("192.168.1.1");
let backup = target;
println!("扫描目标: {}", target);
println!("备份: {}", backup);
}
666