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;


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头部。












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











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





use etherparse::{Ipv4HeaderSlice, TcpHeaderSlice};








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




// 解析以太网帧(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;
}
};



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




// 解析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),
}







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

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



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


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


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










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




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










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






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





实现TCP三次握手 🤝


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




状态转换概述









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







处理SYN包(接受连接)















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








- 解析客户端SYN包中的初始序列号(IRS)和窗口大小。
- 为我们这一端选择一个初始序列号(ISS)。
- 构造一个SYN-ACK包(设置SYN和ACK标志)发送给客户端。
- 确认号(ACK)设置为
IRS + 1,表示我们已收到客户端的SYN。 - 序列号(SEQ)设置为我们的
ISS。
- 确认号(ACK)设置为
- 更新连接状态为
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: ðerparse::Ipv4Header,
tcp_header: ðerparse::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。













- 进行“可接受的ACK”检查,确保ACK号在预期的范围内。
- 如果ACK有效,更新发送序列号空间中的未确认指针(
una)。 - 将连接状态转换为
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)
}
}



/// 辅助函数:处理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包来发起关闭。










- 设置TCP头部的FIN标志。
- 发送数据包。
- 进入
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。





- 收到ACK包后,检查它是否确认了我们的FIN。
- 如果确认,进入
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来关闭它的数据流。














- 收到FIN包后,发送ACK进行确认。
- 进入
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(()); // 通知调用者删除此连接
}
}





总结与展望 🎯


在本节课中,我们一起学习了:
- TCP协议基础:了解了TCP在可靠数据传输中的作用。
- 环境搭建:使用Linux的TUN/TAP设备在用户空间创建虚拟网络接口,为实现协议栈打下基础。
- 数据包解析:利用
etherparsecrate解析IPv4和TCP头部,提取关键信息。 - 连接状态管理:设计了
Connection结构体和状态枚举,使用HashMap管理多个TCP连接。 - 三次握手实现:成功实现了服务器端对SYN包的响应(SYN-ACK),以及对ACK包的处理,完成了连接的建立。
- 连接终止:实现了服务器主动发起关闭(发送FIN)和响应对方FIN的过程,完成了连接的四次挥手。
我们目前实现了一个非常基础的、能够完成一次完整TCP连接建立和关闭流程的服务器。它还没有处理实际的数据传输、重传机制、流量控制、拥塞控制等复杂功能。
在接下来的课程中,我们将在此基础上,逐步实现:
- 数据的发送与接收。
- 重传队列与超时重传机制。
- 滑动窗口与流量控制。
- 基本的拥塞控制。
- 最终,使我们的TCP实现能够与真实的互联网服务器(如Web服务器)进行通信。






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



概述

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

回顾与准备工作

上一节我们介绍了如何使用TUN/TAP接口让用户空间程序能够处理原始网络数据包。我们基于RFC 793实现了TCP协议的基本状态机,支持连接的建立和拆除。
本节我们将在此基础上,为TCP协议栈添加数据读写功能。我们将设计类似于标准库的TcpStream和TcpListener接口,让用户能够以熟悉的方式使用我们的TCP实现。
接口设计思路
核心架构
我们需要设计一个能够管理多个TCP连接的中心化架构。以下是我们的设计思路:
- Interface结构体:作为TCP协议栈的入口点,管理所有连接状态
- TcpListener结构体:监听特定端口,接受传入连接
- 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,
}



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,
}


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. 数据缓冲区管理
为了实现高效的数据传输,我们需要管理两个关键缓冲区:


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

我们使用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(())
}
性能考虑
当前实现采用中心化的连接管理器和全局锁,虽然实现简单,但在高并发场景下可能存在性能瓶颈。未来可以考虑以下优化:




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

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







- 设计了
Interface、TcpListener和TcpStream的核心接口 - 实现了基于条件变量的阻塞机制
- 使用
VecDeque管理数据缓冲区 - 集成了数据包处理与缓冲区管理
- 扩展了TCP状态机以支持数据传输
虽然当前实现还有优化空间,但我们已经建立了一个功能完整的TCP协议栈基础框架。下一节课中,我们将继续完善这个实现,添加拥塞控制、超时重传等高级功能。














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

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







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




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



















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



























实现数据读取




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


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









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





完成这些修改后,我们的服务器就能够读取客户端发送的数据了。我们可以使用netcat或curl等工具进行测试。
实现数据写入与连接关闭







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










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










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








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



2. 计时器与重传






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



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









3. 实现shutdown方法






shutdown方法相对简单。它应当:
- 获取连接锁。
- 检查当前状态。如果处于
ESTABLISHED或SYN_RCVD状态,则将self.closed设置为true,并将连接状态改为FIN_WAIT_1。 - 释放锁并返回。
实际的FIN发送将由后续的on_tick调用处理。












4. 处理确认(ACK)










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

当前进展与挑战









按照上述思路实现后,我们的TCP栈在读取数据方面工作正常。服务器可以成功接收来自netcat或curl客户端发送的数据。
然而,在写入数据时遇到了问题。虽然代码能够生成看起来正确的TCP数据包(包含数据和FIN标志),并且Wireshark抓包显示协议交互符合预期,但Linux内核似乎没有确认(ACK)我们发送的数据包。这导致我们的重传计时器不断触发,而客户端收不到回复。
可能的原因包括:
- 我们的实现可能缺少某些TCP扩展(如窗口缩放、时间戳)。
- 内核可能因为某些校验(如校验和)问题而静默丢弃了我们的包。
- 将数据与FIN标志放在同一个包中发送可能不符合某些严格实现的要求。








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






总结






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




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

浙公网安备 33010602011771号