Rust-实现-TCP-笔记-全-

Rust 实现 TCP 笔记(全)

001:基础搭建与三次握手 👨‍💻

概述

在本节课中,我们将开始一个相对宏大的项目:使用Rust语言从头开始实现TCP协议。TCP是互联网的基础协议之一,它使得互联网上的两台主机能够以可靠的方式进行通信,确保数据不会丢失,并且接收方接收数据的顺序与发送方发送的顺序一致。互联网本身并不保证这些特性,因此TCP协议通过在网络中为数据排序、在数据包丢失时进行重传等机制来实现可靠传输。

我们的目标是实现一个能与真实服务器通信的TCP实现。这意味着我们将实现TCP的核心功能,但不会涉及高级扩展或复杂的拥塞控制算法。我们将主要遵循RFC 793规范,并参考其他相关RFC文档。


准备工作与环境搭建 🛠️

在开始编码之前,我们需要搭建一个合适的网络环境。通常,如果你想自己实现TCP,会遇到一个问题:操作系统内核已经实现了TCP协议栈。为了让我们的用户空间程序能够处理网络数据包,我们需要“劫持”原本发送给内核的数据包。

使用TUN/TAP设备

为了解决上述问题,我们将使用Linux的TUN/TAP功能。TUN/TAP允许我们在用户空间创建一个虚拟网络接口。内核会将这个接口视为一个真实的网卡。

  • 工作原理:当内核尝试向这个虚拟接口发送数据包时,数据包会被重定向到我们的用户空间程序。同样,当我们的程序向这个接口写入数据时,内核会认为这些数据是从网络接收到的。
  • 优势:这使我们能够在一个受控的环境中实现自己的TCP/IP协议栈,而不会与内核的协议栈冲突。

我们将使用Rust的 tun-tap crate来方便地创建和使用TUN接口。

项目初始化

首先,我们创建一个新的Rust二进制项目,并添加必要的依赖。

// Cargo.toml
[dependencies]
tun-tap = "0.1"
etherparse = "0.8"

main.rs 中,我们首先尝试创建一个TUN接口并接收数据包,以验证基础设置是否正常工作。

use tun_tap::Iface;

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_106.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_108.png)

fn main() -> std::io::Result<()> {
    // 创建一个TUN接口,命名为 `tun0`
    let nic = Iface::without_packet_info("tun0", tun_tap::Mode::Tun)?;

    let mut buf = [0u8; 1500]; // 以太网MTU通常是1500字节
    loop {
        // 从接口读取数据包
        let nbytes = nic.recv(&mut buf[..])?;
        println!("read {} bytes: {:?}", nbytes, &buf[..nbytes]);
    }
}

运行此程序需要 CAP_NET_ADMIN 权限。我们可以通过一个脚本来自动化构建、设置权限和运行的过程。


解析网络数据包 📦

当我们的程序开始接收数据包后,我们需要解析它们。从TUN接口读取到的是原始的IP数据包(因为我们使用的是TUN模式,它处理的是网络层数据)。

解析IP头部

IP数据包遵循RFC 791定义的格式。为了节省时间,我们使用 etherparse crate来解析IP头部。

以下是处理接收到的数据包的主要逻辑:

  1. 检查以太网帧类型,过滤掉非IPv4的数据包。
  2. 使用 etherparse 解析IPv4头部。
  3. 根据IP头部的协议字段,进一步处理TCP数据包。

use etherparse::{Ipv4HeaderSlice, TcpHeaderSlice};

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_191.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_193.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_195.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_197.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_199.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_200.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_202.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_204.png)

// 在循环中处理接收到的数据
let nbytes = nic.recv(&mut buf[..])?;

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_206.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_208.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_210.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_212.png)

// 解析以太网帧(TUN接口省略了链路层头部,直接是IP包)
// 这里我们假设前4个字节是TUN的元信息(如果使用with_packet_info),但我们使用了without_packet_info
let ip_packet = match Ipv4HeaderSlice::from_slice(&buf[..nbytes]) {
    Ok(p) => p,
    Err(_) => {
        eprintln!("Ignoring non-IPv4 packet");
        continue;
    }
};

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_214.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_216.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_218.png)

// 检查协议类型,只处理TCP(协议号6)
if ip_packet.protocol() != 0x06 {
    continue; // 忽略非TCP数据包
}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_220.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_222.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_223.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_225.png)

// 解析TCP头部
let tcp_slice = &buf[ip_packet.slice().len()..nbytes];
let tcp_packet = match TcpHeaderSlice::from_slice(tcp_slice) {
    Ok(p) => p,
    Err(_) => {
        eprintln!("Failed to parse TCP header");
        continue;
    }
};

// 打印连接四元组信息:源IP、源端口、目标IP、目标端口
println!(
    "{}:{} -> {}:{}",
    ip_packet.source_addr(),
    tcp_packet.source_port(),
    ip_packet.destination_addr(),
    tcp_packet.destination_port()
);

管理TCP连接状态

TCP是面向连接的协议。每个连接由一个四元组唯一标识:(源IP, 源端口, 目标IP, 目标端口)。我们需要为每个活跃的连接维护状态。

我们将定义一个 Connection 结构体来保存连接状态,并使用一个 HashMap 来管理所有活跃连接。

use std::collections::HashMap;
use std::net::Ipv4Addr;

#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
struct Quad {
    src: (Ipv4Addr, u16),
    dst: (Ipv4Addr, u16),
}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_261.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_263.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_265.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_267.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_269.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_271.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_273.png)

struct Connection {
    // 连接状态机(例如:Listen, SynReceived, Established, FinWait1等)
    state: State,
    // 发送序列号空间状态
    send: SendSequenceSpace,
    // 接收序列号空间状态
    recv: RecvSequenceSpace,
    // 其他字段,如IP和TCP头部模板、重传队列等
}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_275.png)

// 发送序列号空间 (RFC 793 Section 3.2, Figure 4)
struct SendSequenceSpace {
    una: u32, // 已发送但未确认的最老序列号
    nxt: u32, // 下一个要使用的序列号
    wnd: u16, // 发送窗口大小
    up: bool, // 紧急指针标志
    wl1: usize, // 用于窗口更新的段序列号
    wl2: usize, // 用于窗口更新的段确认号
    iss: u32,  // 初始发送序列号
}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_277.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_279.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_281.png)

// 接收序列号空间 (RFC 793 Section 3.2, Figure 5)
struct RecvSequenceSpace {
    nxt: u32, // 期望接收的下一个序列号
    wnd: u16, // 接收窗口大小
    up: bool, // 紧急指针标志
    irs: u32, // 初始接收序列号
}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_283.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_285.png)

enum State {
    Closed,
    Listen,
    SynReceived,
    Established,
    FinWait1,
    FinWait2,
    // ... 其他状态
}

main 函数中,我们根据接收到的数据包的四元组,在 HashMap 中查找或创建对应的 Connection,并将数据包交给它处理。

let mut connections: HashMap<Quad, Connection> = HashMap::new();

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_310.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_312.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_314.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_316.png)

// 在数据包处理循环中
let quad = Quad {
    src: (ip_packet.source_addr(), tcp_packet.source_port()),
    dst: (ip_packet.destination_addr(), tcp_packet.destination_port()),
};

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_317.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_319.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_321.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_323.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_324.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_326.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_327.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_329.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_331.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_333.png)

// 获取或创建连接,并处理数据包
let connection = connections.entry(quad).or_insert_with(|| {
    // 如果连接不存在,尝试接受新连接(例如,处理SYN包)
    Connection::accept(ip_packet, tcp_packet, &nic)
});

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_335.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_337.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_339.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_341.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_343.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_345.png)

// 如果连接存在,调用其 on_packet 方法处理数据包
if let Some(conn) = connection {
    conn.on_packet(ip_packet, tcp_packet, &data_payload, &nic)?;
}


实现TCP三次握手 🤝

TCP使用三次握手来建立连接。状态机是理解这个过程的关键。我们将根据RFC 793中定义的状态转换图来实现。

状态转换概述

  1. LISTEN (服务器):服务器在某个端口上监听连接请求。
  2. SYN-RECEIVED:当服务器收到客户端的SYN包后,进入此状态。它必须回复一个SYN-ACK包。
  3. ESTABLISHED:当服务器收到客户端对SYN-ACK的ACK回复后,连接建立,进入此状态。

处理SYN包(接受连接)

当我们的程序(作为服务器)在 LISTEN 状态(或初始状态)收到一个SYN包时,需要执行以下操作:

  1. 解析客户端SYN包中的初始序列号(IRS)和窗口大小。
  2. 为我们这一端选择一个初始序列号(ISS)。
  3. 构造一个SYN-ACK包(设置SYN和ACK标志)发送给客户端。
    • 确认号(ACK)设置为 IRS + 1,表示我们已收到客户端的SYN。
    • 序列号(SEQ)设置为我们的 ISS
  4. 更新连接状态为 SYN-RECEIVED,并初始化发送和接收序列号空间。

impl Connection {
    /// 尝试接受一个新连接(处理SYN包)
    fn accept<'a>(
        ip_header: Ipv4HeaderSlice<'a>,
        tcp_header: TcpHeaderSlice<'a>,
        nic: &Iface,
    ) -> Option<Connection> {
        // 只处理SYN包(且不能是ACK包)
        if !tcp_header.syn() || tcp_header.ack() {
            return None;
        }

        let mut conn = Connection {
            state: State::SynReceived,
            send: SendSequenceSpace {
                iss: 0, // 简化:实际应随机生成
                una: 0,
                nxt: 1, // ISS + 1,因为SYN消耗一个序列号
                wnd: 10, // 初始窗口大小
                up: false,
                wl1: 0,
                wl2: 0,
            },
            recv: RecvSequenceSpace {
                irs: tcp_header.sequence_number(),
                nxt: tcp_header.sequence_number().wrapping_add(1), // IRS + 1
                wnd: tcp_header.window_size(),
                up: false,
            },
            // ... 初始化其他字段
        };

        // 发送 SYN-ACK 响应
        conn.send_synack(ip_header, tcp_header, nic).ok()?;

        Some(conn)
    }

    fn send_synack(
        &mut self,
        ip_header: Ipv4HeaderSlice,
        tcp_header: TcpHeaderSlice,
        nic: &Iface,
    ) -> std::io::Result<usize> {
        // 构建TCP头部
        let mut synack = etherparse::TcpHeader::new(
            tcp_header.destination_port(), // 我们的源端口
            tcp_header.source_port(),      // 目标端口
            self.send.iss,                 // 我们的序列号
            self.recv.nxt,                 // 确认号 (IRS + 1)
        );
        synack.syn = true;
        synack.ack = true;
        synack.window_size = self.send.wnd;

        // 构建IP头部(交换源和目标地址)
        let mut ip = etherparse::Ipv4Header::new(
            synack.header_len() as u16, // 负载长度(只有TCP头,无数据)
            64, // TTL
            etherparse::IpNumber::TCP,
            ip_header.destination_addr(), // 源地址(我们的地址)
            ip_header.source_addr(),      // 目标地址
        );

        // 将IP和TCP头部写入缓冲区并发送
        self.write_to_nic(&ip, &synack, &[], nic)
    }

    /// 通用函数:将IP包写入NIC
    fn write_to_nic(
        &mut self,
        ip_header: &etherparse::Ipv4Header,
        tcp_header: &etherparse::TcpHeader,
        data: &[u8],
        nic: &Iface,
    ) -> std::io::Result<usize> {
        let mut buf = [0u8; 1500];
        let mut unwritten = &mut buf[..];

        // 写入IP头部
        ip_header.write(&mut unwritten);
        // 写入TCP头部
        tcp_header.write(&mut unwritten);
        // 写入数据
        unwritten.write_all(data)?;

        // 计算实际写入的字节数
        let written = buf.len() - unwritten.len();
        nic.send(&buf[..written])?;
        Ok(written)
    }
}

处理ACK包(完成握手)

当处于 SYN-RECEIVED 状态的连接收到一个ACK包时,需要验证这个ACK是否确认了我们发送的SYN。

  1. 进行“可接受的ACK”检查,确保ACK号在预期的范围内。
  2. 如果ACK有效,更新发送序列号空间中的未确认指针(una)。
  3. 将连接状态转换为 ESTABLISHED

impl Connection {
    fn on_packet<'a>(
        &mut self,
        ip_header: Ipv4HeaderSlice<'a>,
        tcp_header: TcpHeaderSlice<'a>,
        data: &[u8],
        nic: &Iface,
    ) -> std::io::Result<()> {
        // 首先进行序列号有效性检查(RFC 793 Section 3.3)
        if !self.segment_acceptable(tcp_header, data) {
            // 如果段不可接受,可能需要发送ACK(TODO)
            return Ok(());
        }

        match self.state {
            State::SynReceived => {
                // 在SYN-RECEIVED状态,我们期望一个ACK来确认我们的SYN
                if !tcp_header.ack() {
                    return Ok(());
                }
                // 检查ACK是否确认了我们的SYN (self.send.iss)
                let ack_num = tcp_header.acknowledgment_number();
                if self.acceptable_ack_number(ack_num) {
                    // 更新未确认的序列号
                    self.send.una = ack_num;
                    // 连接建立!
                    self.state = State::Established;
                    println!("Connection established!");
                    // 这里我们可以选择立即关闭连接作为测试
                    self.initiate_close(nic)?;
                } else {
                    // ACK无效,可能发送RST(TODO)
                }
            }
            State::Established => {
                // 处理已建立连接的数据包...
                // 例如:处理数据、FIN包等
                self.handle_established(ip_header, tcp_header, data, nic)?;
            }
            // ... 处理其他状态
            _ => {}
        }
        Ok(())
    }

    /// 检查ACK号是否可接受(简化版)
    fn acceptable_ack_number(&self, ack_num: u32) -> bool {
        // ACK号必须大于已发送未确认的序列号(self.send.una)
        // 并且小于等于下一个要发送的序列号(self.send.nxt)
        // 注意:需要处理32位环绕运算
        wrapping_lt(self.send.una, ack_num) && wrapping_le(ack_num, self.send.nxt)
    }
}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_506.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_508.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/e709d71a082b45418e3b70befbd3ce3f_510.png)

/// 辅助函数:处理32位无符号整数的环绕比较
fn wrapping_lt(a: u32, b: u32) -> bool {
    (a.wrapping_sub(b) as i32) < 0
}
fn wrapping_le(a: u32, b: u32) -> bool {
    (a.wrapping_sub(b) as i32) <= 0
}


实现连接终止 🚪

TCP使用四次挥手来优雅地关闭连接。为了简化,在我们的测试中,服务器在建立连接后立即发起关闭。

发送FIN包

ESTABLISHED 状态,我们可以通过发送一个FIN包来发起关闭。

  1. 设置TCP头部的FIN标志。
  2. 发送数据包。
  3. 进入 FIN-WAIT-1 状态,等待对方对FIN的ACK。

impl Connection {
    fn initiate_close(&mut self, nic: &Iface) -> std::io::Result<()> {
        // 构建一个FIN包
        let mut fin = etherparse::TcpHeader::new(
            // ... 端口号使用连接中存储的
            self.tcp.src_port,
            self.tcp.dst_port,
            self.send.nxt, // 当前序列号
            self.recv.nxt, // 当前期望接收的序列号
        );
        fin.fin = true;
        fin.ack = true;

        // 发送FIN包
        self.write_to_nic(&self.ip, &fin, &[], nic)?;

        // 更新下一个序列号(FIN消耗一个序列号)
        self.send.nxt = self.send.nxt.wrapping_add(1);
        self.state = State::FinWait1;
        Ok(())
    }
}

处理对FIN的ACK

FIN-WAIT-1 状态,我们等待对方确认我们的FIN。

  1. 收到ACK包后,检查它是否确认了我们的FIN。
  2. 如果确认,进入 FIN-WAIT-2 状态,等待对方的FIN。

// 在 on_packet 的 match 语句中
State::FinWait1 => {
    if tcp_header.ack() && self.acceptable_ack_number(tcp_header.acknowledgment_number()) {
        // 对方确认了我们的FIN
        self.send.una = tcp_header.acknowledgment_number();
        // 检查是否所有已发送数据(包括FIN)都被确认了
        if self.send.una == self.send.nxt {
            self.state = State::FinWait2;
        }
    }
}

处理对方的FIN并最终关闭

FIN-WAIT-2 状态,我们等待对方也发送FIN来关闭它的数据流。

  1. 收到FIN包后,发送ACK进行确认。
  2. 进入 TIME-WAIT 状态(在我们的简单实现中可能直接关闭连接)。

State::FinWait2 => {
    if tcp_header.fin() {
        // 收到对方的FIN,发送ACK
        let mut ack = etherparse::TcpHeader::new(
            self.tcp.src_port,
            self.tcp.dst_port,
            self.send.nxt,
            self.recv.nxt.wrapping_add(1), // 确认对方的FIN (IRS + data + FIN)
        );
        ack.ack = true;
        self.write_to_nic(&self.ip, &ack, &[], nic)?;

        // 更新接收序列号(FIN消耗一个序列号)
        self.recv.nxt = self.recv.nxt.wrapping_add(1);

        // 连接可以关闭了(简化:实际应等待2MSL的TIME-WAIT状态)
        println!("Connection closing.");
        // 从连接映射中移除这个连接
        return Ok(()); // 通知调用者删除此连接
    }
}


总结与展望 🎯

在本节课中,我们一起学习了:

  1. TCP协议基础:了解了TCP在可靠数据传输中的作用。
  2. 环境搭建:使用Linux的TUN/TAP设备在用户空间创建虚拟网络接口,为实现协议栈打下基础。
  3. 数据包解析:利用 etherparse crate解析IPv4和TCP头部,提取关键信息。
  4. 连接状态管理:设计了 Connection 结构体和状态枚举,使用 HashMap 管理多个TCP连接。
  5. 三次握手实现:成功实现了服务器端对SYN包的响应(SYN-ACK),以及对ACK包的处理,完成了连接的建立。
  6. 连接终止:实现了服务器主动发起关闭(发送FIN)和响应对方FIN的过程,完成了连接的四次挥手。

我们目前实现了一个非常基础的、能够完成一次完整TCP连接建立和关闭流程的服务器。它还没有处理实际的数据传输、重传机制、流量控制、拥塞控制等复杂功能。

在接下来的课程中,我们将在此基础上,逐步实现:

  • 数据的发送与接收。
  • 重传队列与超时重传机制。
  • 滑动窗口与流量控制。
  • 基本的拥塞控制。
  • 最终,使我们的TCP实现能够与真实的互联网服务器(如Web服务器)进行通信。

本节课的代码已经能够与像 netcat 这样的TCP客户端进行基本的握手和关闭交互,这是一个重要的里程碑。请继续关注后续课程,我们将一起完善这个TCP实现。

002:实现TCP数据流接口 🚀

概述

在本节课中,我们将继续使用Rust实现用户空间的TCP协议栈。上一节我们完成了TCP连接建立和拆除的基本框架,本节我们将重点实现TCP数据流的读写接口,让我们的TCP协议栈能够真正传输数据。

回顾与准备工作

上一节我们介绍了如何使用TUN/TAP接口让用户空间程序能够处理原始网络数据包。我们基于RFC 793实现了TCP协议的基本状态机,支持连接的建立和拆除。

本节我们将在此基础上,为TCP协议栈添加数据读写功能。我们将设计类似于标准库的TcpStreamTcpListener接口,让用户能够以熟悉的方式使用我们的TCP实现。

接口设计思路

核心架构

我们需要设计一个能够管理多个TCP连接的中心化架构。以下是我们的设计思路:

  1. Interface结构体:作为TCP协议栈的入口点,管理所有连接状态
  2. TcpListener结构体:监听特定端口,接受传入连接
  3. TcpStream结构体:表示已建立的TCP连接,支持数据读写

数据结构关系

Interface (连接管理器)
├── 管理所有TCP连接状态
├── 处理网络数据包
├── 提供TcpListener创建接口
└── 协调多个TcpStream的并发访问

实现步骤

1. 定义核心数据结构

首先,我们定义接口和连接管理器的基本结构:

pub struct Interface {
    handle: Arc<Mutex<ConnectionManager>>,
    join_handle: JoinHandle<()>,
}

struct ConnectionManager {
    connections: HashMap<Quad, TcpConnection>,
    pending: HashMap<u16, VecDeque<Quad>>,
    // 连接状态和缓冲区
}

2. 实现TcpListener

TcpListener负责监听特定端口并接受连接:

pub struct TcpListener {
    handle: Arc<Mutex<ConnectionManager>>,
    port: u16,
}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/9c2a8d9517815d56e3f7ba056f67ae8c_21.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/9c2a8d9517815d56e3f7ba056f67ae8c_23.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/9c2a8d9517815d56e3f7ba056f67ae8c_25.png)

impl TcpListener {
    pub fn bind(handle: Arc<Mutex<ConnectionManager>>, port: u16) -> io::Result<Self> {
        // 在连接管理器中注册端口监听
        Ok(TcpListener { handle, port })
    }
    
    pub fn accept(&mut self) -> io::Result<TcpStream> {
        // 等待并接受新连接
    }
}

3. 实现TcpStream

TcpStream表示一个已建立的TCP连接,支持数据读写:

pub struct TcpStream {
    handle: Arc<Mutex<ConnectionManager>>,
    quad: Quad,
}

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/9c2a8d9517815d56e3f7ba056f67ae8c_43.png)

![](https://github.com/OpenDocCN/cs-notes-pt1-zh/raw/master/docs/rs-impl-tcp/img/9c2a8d9517815d56e3f7ba056f67ae8c_45.png)

impl Read for TcpStream {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        // 从接收缓冲区读取数据
    }
}

impl Write for TcpStream {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        // 向发送缓冲区写入数据
    }
    
    fn flush(&mut self) -> io::Result<()> {
        // 确保所有数据都已发送
    }
}

4. 数据缓冲区管理

为了实现高效的数据传输,我们需要管理两个关键缓冲区:

  1. 接收缓冲区:存储从网络接收但应用尚未读取的数据
  2. 发送缓冲区:存储应用已写入但尚未发送到网络的数据

我们使用VecDeque作为环形缓冲区:

struct TcpConnection {
    incoming: VecDeque<u8>,    // 接收缓冲区
    unacked: VecDeque<u8>,     // 发送缓冲区(未确认)
    // 其他连接状态...
}

5. 阻塞机制实现

为了让read()accept()等操作能够正确阻塞,我们使用条件变量:

struct ConnectionManager {
    pending_var: Condvar,      // 新连接通知
    receive_var: Condvar,      // 数据可读通知
    // 其他字段...
}

impl TcpStream {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let mut cm = self.handle.lock().unwrap();
        loop {
            // 检查是否有数据可读
            if cm.is_receive_closed(self.quad) && cm.incoming.is_empty() {
                return Ok(0);  // 连接已关闭
            }
            if !cm.incoming.is_empty() {
                // 从缓冲区读取数据
                return self.read_from_buffer(&mut cm, buf);
            }
            // 等待数据到达
            cm = self.handle.receive_var.wait(cm).unwrap();
        }
    }
}

6. 数据包处理集成

我们需要修改数据包处理逻辑,将接收到的数据存入相应连接的缓冲区:

fn process_packet(&mut self, packet: TcpPacket) {
    let quad = packet.quad();
    if let Some(conn) = self.connections.get_mut(&quad) {
        let availability = conn.on_packet(packet);
        
        // 根据数据包处理结果通知等待的线程
        if availability.contains(Available::READ) {
            self.receive_var.notify_all();
        }
        if availability.contains(Available::WRITE) {
            self.send_var.notify_all();
        }
    }
}

7. 连接状态管理

我们需要扩展TCP连接状态机,支持数据传输阶段:

#[derive(Debug, Clone, Copy)]
enum TcpState {
    Established,      // 连接已建立,可以传输数据
    FinWait1,         // 已发送FIN,等待ACK
    FinWait2,         // 收到对FIN的ACK,等待对方FIN
    TimeWait,         // 双方都已关闭连接
    // 其他状态...
}

impl TcpConnection {
    fn is_receive_closed(&self) -> bool {
        matches!(self.state, 
            TcpState::FinWait2 | 
            TcpState::TimeWait | 
            TcpState::Closed
        )
    }
}

使用示例

以下是如何使用我们实现的TCP接口:

fn main() -> io::Result<()> {
    // 创建TCP接口
    let interface = Interface::new()?;
    
    // 监听端口
    let listener = interface.bind(8080)?;
    
    // 接受连接
    let mut stream = listener.accept()?;
    
    // 读取数据
    let mut buf = [0; 1024];
    let n = stream.read(&mut buf)?;
    println!("读取到 {} 字节数据", n);
    
    // 写入数据
    stream.write_all(b"Hello, world!")?;
    stream.flush()?;
    
    Ok(())
}

性能考虑

当前实现采用中心化的连接管理器和全局锁,虽然实现简单,但在高并发场景下可能存在性能瓶颈。未来可以考虑以下优化:

  1. 细粒度锁:为每个连接使用独立的锁,减少锁竞争
  2. 无锁数据结构:使用无锁环形缓冲区提高并发性能
  3. 异步接口:提供异步API支持,提高IO效率

总结

本节课我们一起学习了如何为TCP协议栈实现数据流接口。我们主要完成了以下工作:

  1. 设计了InterfaceTcpListenerTcpStream的核心接口
  2. 实现了基于条件变量的阻塞机制
  3. 使用VecDeque管理数据缓冲区
  4. 集成了数据包处理与缓冲区管理
  5. 扩展了TCP状态机以支持数据传输

虽然当前实现还有优化空间,但我们已经建立了一个功能完整的TCP协议栈基础框架。下一节课中,我们将继续完善这个实现,添加拥塞控制、超时重传等高级功能。


关键概念总结

  • TCP数据流接口:提供类似标准库的读写接口
  • 环形缓冲区:使用VecDeque高效管理数据
  • 条件变量:实现线程间的协调与通知
  • 连接状态管理:扩展状态机支持数据传输阶段

通过本节课的学习,你应该已经掌握了如何为网络协议栈设计用户友好的API接口,以及如何管理并发访问下的数据缓冲区。这些知识不仅适用于TCP实现,也适用于其他需要处理并发IO的系统编程场景。

003:实现数据传输与关闭

在本节课中,我们将继续使用Rust实现TCP协议。我们将重点实现TCP连接的数据读写功能,并处理连接的优雅关闭。我们将学习如何处理接收到的数据、如何发送数据,以及如何管理发送窗口和重传计时器。

课程回顾

上一节我们实现了TCP连接的基本建立与拆除。本节中,我们来看看如何让TCP连接能够传输实际的数据。

我们当前的代码库已经能够处理TCP的三次握手和四次挥手,但readwrite方法还不能处理实际的数据。我们的目标是让TCPStreamread方法能够读取对端发送的数据,write方法能够向对端发送数据,并实现shutdown方法来优雅地关闭连接。

实现数据读取

首先,我们需要修改on_packet方法中处理接收数据的部分。当前,当连接处于ESTABLISHED状态并收到数据时,我们只是简单地断言数据为空并关闭连接。现在,我们需要将这些数据存入缓冲区,并通知等待读取的线程。

以下是处理接收数据的关键步骤:

  1. 计算有效数据:TCP数据包的序列号可能指向我们已经接收过的字节。我们需要根据receive.next(期望接收的下一个字节的序列号)和当前数据包的序列号,计算出本次数据包中我们真正需要接收的新数据部分。
    • 公式:unread_data_at = max(0, (receive.next.wrapping_sub(seq)) as usize)
    • 如果unread_data_at >= data.len(),说明这个数据包不包含新数据(可能是重传的FIN包),我们可以忽略其数据部分。
  2. 存储数据:将计算出的新数据切片(data[unread_data_at..])添加到incoming字节队列中。
  3. 更新接收状态:更新receive.next,将其指向下一个期望接收的字节序列号。这需要加上我们实际接收的新数据长度,如果数据包包含FIN标志,还需要额外加1(因为FIN占用一个逻辑序列号)。
    • 代码:self.receive.next = seq.wrapping_add((data.len() + fin_offset) as u32);
  4. 唤醒读者:我们的availability机制会自动检测incoming队列非空,并设置READ标志。事件循环会据此唤醒正在read调用中等待的线程。

完成这些修改后,我们的服务器就能够读取客户端发送的数据了。我们可以使用netcatcurl等工具进行测试。

实现数据写入与连接关闭

实现数据写入(write)和连接关闭(shutdown)更为复杂,因为它们涉及到发送窗口管理、数据重传和计时器。

1. 发送窗口与未确认数据

TCP使用滑动窗口机制进行流量控制。发送方需要维护:

  • send.unacknowledged: 已发送但尚未被确认的第一个字节的序列号。
  • send.next: 下一个将要发送的字节的序列号。
  • send.window: 接收方通告的窗口大小,即允许发送方在未收到确认前可以发送的最大数据量。
  • unacked: 一个字节队列,存储已发送但未被确认的数据,用于可能的重新传输。

当我们调用stream.write()时,数据被添加到unacked队列的尾部。真正的发送逻辑由on_tick方法(计时器触发)或当有新数据且窗口允许时驱动。

2. 计时器与重传

为了实现可靠传输,我们需要一个计时器来触发数据重传。我们在事件循环中使用poll系统调用,设置一个短暂的超时(例如1毫秒),定期检查所有连接。

在每个连接的on_tick方法中,我们需要:

  1. 检查是否需要重传:查找unacked队列中最早发送但未被确认的数据包,计算其等待时间。如果等待时间超过了动态计算的重传超时(RTO),则触发重传。
    • RTO基于平滑的往返时间(SRTT)估算,公式可简化为:RTO = max(1秒, SRTT * 1.5)
  2. 检查是否可以发送新数据:如果不需要重传,则检查发送窗口是否有剩余空间(send.window - (send.next - send.unacknowledged))。如果有空间且unacked队列中有未发送的数据,则发送尽可能多的新数据。
  3. 处理FIN:如果连接已调用shutdownself.closedtrue),且FIN尚未发送,并且发送窗口允许(即FIN的逻辑序列号在窗口内),则在数据包中设置FIN标志。发送FIN后,连接状态应转移到FIN_WAIT_1

3. 实现shutdown方法

shutdown方法相对简单。它应当:

  1. 获取连接锁。
  2. 检查当前状态。如果处于ESTABLISHEDSYN_RCVD状态,则将self.closed设置为true,并将连接状态改为FIN_WAIT_1
  3. 释放锁并返回。

实际的FIN发送将由后续的on_tick调用处理。

4. 处理确认(ACK)

当收到对端发来的ACK时,我们需要:

  1. 清理已确认数据:从unacked队列头部删除所有已被确认的字节。
  2. 更新发送状态:将send.unacknowledged更新为ACK号。
  3. 更新RTT估计:如果这个ACK确认了我们记录的某个发送时间点,我们可以用当前时间减去发送时间来估算RTT,并更新SRTT。
  4. 唤醒写者/刷新者:如果unacked队列变空,意味着所有数据都已确认,可以唤醒等待flush完成的线程。

当前进展与挑战

按照上述思路实现后,我们的TCP栈在读取数据方面工作正常。服务器可以成功接收来自netcatcurl客户端发送的数据。

然而,在写入数据时遇到了问题。虽然代码能够生成看起来正确的TCP数据包(包含数据和FIN标志),并且Wireshark抓包显示协议交互符合预期,但Linux内核似乎没有确认(ACK)我们发送的数据包。这导致我们的重传计时器不断触发,而客户端收不到回复。

可能的原因包括:

  • 我们的实现可能缺少某些TCP扩展(如窗口缩放、时间戳)。
  • 内核可能因为某些校验(如校验和)问题而静默丢弃了我们的包。
  • 将数据与FIN标志放在同一个包中发送可能不符合某些严格实现的要求。

尽管写入功能未能完全与标准TCP栈互操作,但我们已经实现了TCP数据传输的所有核心逻辑组件:滑动窗口、重传计时器、ACK处理、以及优雅关闭。代码结构已经为构建一个功能完整的TCP实现奠定了基础。

总结

本节课中我们一起学习了如何为TCP连接添加数据传输能力。我们实现了:

  • 数据读取:正确解析接收到的数据段,处理序列号,将数据存入缓冲区并通知读取者。
  • 数据写入与重传框架:建立了基于滑动窗口的发送逻辑、未确认数据缓冲区(unacked)以及由计时器驱动的重传机制。
  • 连接关闭:实现了shutdown方法,用于启动连接关闭流程,并处理FIN标志的发送。
  • ACK处理:更新发送状态,清理已确认数据,并估算RTT。

虽然在与标准内核TCP栈的互操作性上遇到了挑战,但我们已经深入探讨了实现一个用户态TCP协议栈所需面对的主要设计问题和复杂性。希望本系列课程能帮助你更好地理解TCP协议和Rust系统编程。

posted @ 2026-03-29 09:25  布客飞龙II  阅读(22)  评论(0)    收藏  举报