文章目录

Rust 入门笔记(网络安全工程师版 · 完整练习版)

目标:不追求"背下语法",而是通过"这段代码在干什么 + 为什么 Rust 要这么设计"理解概念,每章配 2~3 道题(含前面章节的知识点滚动巩固),并附案例和解释、答案。

目录

  1. 环境搭建 & Cargo
  2. 基础语法(变量、类型、控制流)
  3. 所有权系统
  4. 借用与引用
  5. 结构体、枚举、模式匹配
  6. 错误处理(Result / Option / ?
  7. 集合类型(Vec、HashMap、String)
  8. Trait 与泛型
  9. 闭包与迭代器
  10. 智能指针(Box、Rc、Arc、RefCell)
  11. 并发(线程、Mutex、channel)
  12. 异步编程(async/await、tokio)
  13. 案例一:TCP 端口扫描器
  14. 案例二:HTTP 请求调试工具
  15. 案例三:抓包分析
  16. GUI 案例一:egui
  17. GUI 案例二:Tauri
  18. GUI 案例三:Slint(从安装到运行全流程)
  19. 常见坑 & 记忆口诀
  20. 学习路径建议
每章末尾的"✏️ 练习"部分,建议先自己写代码跑一跑,再看"答案与解析",效果比直接看答案好得多。

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 reqwest

cargo new 会自动生成 Cargo.tomlsrc/main.rs 并初始化 git 仓库;cargo add 会自动把依赖写进 Cargo.toml[dependencies] 段,不用手动查版本号。

2. cargo buildcargo check 有什么区别?你在开发调试阶段应该更常用哪个,为什么?


答案与解析

cargo build 会完整编译并生成可执行文件(在 target/debug/ 下);cargo check 只做语法和类型检查,不生成二进制文件,所以速度快很多。开发阶段频繁修改代码时应该用 cargo check 快速验证有没有写错,等确认逻辑没问题了再用 cargo run/cargo build 真正跑起来。这个习惯在写大型网络工具、编译时间较长时特别有用。

3. Cargo.tomlCargo.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_checker
fn 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,而是靠编译期规则管理内存。

三条铁律:

  1. 每个值在 Rust 中都有一个"所有者"(变量)。
  2. 同一时刻只能有一个所有者。
  3. 所有者离开作用域,值就被自动释放(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 从此手里没钥匙了(不能再用)。

注意:像 i32boolchar 这种简单类型是 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);
}

版权属于:厂长
本文链接:https://bishepro.com/archives/rustnote.html
本站未注明转载的文章均为原创,并采用 CC BY-NC-SA 4.0 授权协议,转载请注明来源,谢谢!