Files
NE_YuR/network/tcpquiclab/main.typ
2026-01-21 22:40:51 +08:00

973 lines
42 KiB
Typst
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#import "labtemplate.typ": *
#show: nudtlabpaper.with(
title: "TCP 与 QUIC 协议性能对比分析实验",
author: "程景愉",
id: "202302723005",
training_type: "无军籍",
grade: "2023",
major: "网络工程",
department: "计算机学院",
advisor: "逄德明",
jobtitle: "教授",
lab: "307-211",
date: "2026.1.12",
header_str: "《计算机网络》实验报告",
)
#set page(header: [
#set par(spacing: 6pt)
#align(center)[#text(size: 11pt)[《计算机网络》实验报告]]
#v(-0.3em)
#line(length: 100%, stroke: (thickness: 1pt))
])
#show heading: it => box(width: 100%)[
#v(0.50em)
#set text(font: hei)
#it.body
]
#outline(title: "目录", depth: 3, indent: 1em)
// #pagebreak()
// #outline(
// title: [图目录],
// target: figure.where(kind: image),
// )
#show heading: it => box(width: 100%)[
#v(0.50em)
#set text(font: hei)
#counter(heading).display()
// #h(0.5em)
#it.body
]
#set enum(indent: 0.5em, body-indent: 0.5em)
#pagebreak()
= 实验概要
== 实验内容
本次实验的主要内容是对比分析 TCP QUIC 两种传输协议的性能差异。实验包含基础任务和性能测试任务两部分,具体任务要求如下:
- 基础任务:基于 TCP QUIC 协议分别实现客户端-服务器通信程序。TCP 程序使用标准 socket 编程实现基本的连接建立、数据发送和接收功能QUIC 程序使用 quiche 库,实现基于 UDP 的可靠传输功能。两个程序都需要完成监听指定端口、接受客户端连接、接收消息并返回响应的基本功能。
- 性能测试任务:在基础任务实现的基础上,完成以下性能对比测试:
+ 连接建立时间对比:测量 TCP 三次握手和 QUIC 0-RTT 连接建立的时间差异
+ 吞吐量测试在不同网络条件下正常网络、5%丢包、100ms延迟对比两种协议的传输性能
+ 多路复用性能测试:对比 5 TCP 连接与单个 QUIC 连接上 5 个流的传输性能,分析队头阻塞问题
+ 网络异常恢复测试:模拟网络中断后恢复的场景,对比两种协议的恢复能力和连接迁移能力
== 实验要求
本实验的具体过程及对应要求如下:
- 实验开始前准备工作:在实验开始前,学员需要掌握 C 语言编程基础,理解 TCP/IP 协议栈的工作原理,特别是 TCP 协议的三次握手、拥塞控制、流量控制机制,以及 QUIC 协议基于 UDP 的传输机制、多路复用和 0-RTT 连接特性。同时,熟悉 socket 编程和 quiche 库的使用方法,了解网络性能测试的基本方法。
- 实验过程中:按照实验要求,完成 TCP QUIC 客户端-服务器程序的实现。具体步骤包括TCP 程序使用 socket()、bind()、listen()、accept() API 实现服务器端,使用 socket()、connect()、send()、recv() API 实现客户端QUIC 程序使用 quiche 库的配置、连接建立、流管理和数据传输接口实现服务器和客户端。然后在不同网络条件下进行性能测试,使用 tc clumsy 工具模拟丢包和延迟环境,记录测试数据。
- 实验结束后:总结 TCP QUIC 协议的性能差异,详细描述两种协议在各种网络条件下的表现,分析 QUIC 协议的优势和不足,并根据实验要求撰写实验报告,展示实验结果和数据分析。
== 实验目的
在现代网络环境中TCP 协议作为互联网的基础传输协议广泛应用于各种应用场景。然而随着网络技术的发展和新型应用的出现TCP 协议的一些局限性逐渐显现如队头阻塞、连接建立延迟、协议更新困难等问题。QUIC 协议作为新一代传输协议,旨在解决这些问题,提供更快、更可靠、更安全的传输服务。
通过本次实验,学员将深入理解 TCP QUIC 两种传输协议的工作原理和性能特点,掌握网络编程的基本方法,学习如何使用专业的网络分析工具进行性能测试。具体目的包括:
1. *理解协议原理*:深入理解 TCP 协议的三次握手、拥塞控制、流量控制机制,以及 QUIC 协议基于 UDP 的传输机制、多路复用、0-RTT 连接和连接迁移等特性。
2. *掌握编程技术*:掌握 Linux/Unix 环境下的 socket 编程技术,学习使用 quiche 库实现 QUIC 协议,理解网络编程中的异步 I/O、事件驱动等高级技术。
3. *性能分析能力*:学习使用 Wireshark、tc、clumsy 等工具进行网络性能测试和分析,掌握吞吐量、延迟、丢包率等关键性能指标的测量方法。
4. *协议对比分析*:通过实际测试,对比分析 TCP QUIC 在不同网络条件下的性能差异,理解 QUIC 协议的优势和适用场景。
5. *实践能力提升*:通过亲手实现两种协议的客户端-服务器程序,培养实际编程和问题解决的能力,为后续学习更复杂的网络协议和系统奠定基础。
本次实验不仅是对网络协议理论的验证,更是对现代网络编程技术的实践,对于理解互联网传输层协议的发展趋势具有重要意义。
// that retains the correct baseline.
#show raw.where(block: false): it => box(
text(font: "Consolas", it),
fill: luma(240),
inset: (x: 3pt, y: 0pt),
outset: (y: 3pt),
radius: 2pt,
)
// Display block code in a larger block
// with more padding.
#show raw.where(block: true): it => block(
text(font: "Consolas", it),
fill: luma(240),
inset: 10pt,
radius: 4pt,
width: 100%,
)
= 实验原理及方案
#para[
本次实验通过实现 TCP QUIC 两种传输协议的客户端-服务器程序对比分析它们在不同网络条件下的性能表现。TCPTransmission Control Protocol是互联网的核心传输协议提供可靠的、面向连接的字节流传输服务QUICQuick UDP Internet Connections Google 提出的基于 UDP 的新一代传输协议,旨在解决 TCP 的队头阻塞、连接建立延迟等问题。
]
== TCP 协议原理
TCP 协议是传输层的核心协议提供可靠的、面向连接的、基于字节流的传输服务。TCP 协议的主要特性包括:
*三次握手*TCP 连接建立需要三次握手过程。客户端发送 SYN 包,服务器回复 SYN-ACK 包,客户端再回复 ACK 包。这个过程确保双方都准备好接收数据,但引入了至少 1-RTT 的连接建立延迟。在高延迟网络中,三次握手会对性能产生显著影响。
*可靠传输*TCP 通过序列号、确认应答和重传机制实现可靠传输。每个数据包都有序列号,接收方收到数据后发送 ACK 确认。如果发送方在超时时间内未收到 ACK则重传数据。这种机制确保了数据的完整性但也增加了协议的复杂性和延迟。
*流量控制*TCP 使用滑动窗口机制进行流量控制。接收方通过通告窗口大小告诉发送方当前可接收的数据量,避免发送方发送过快导致接收方缓冲区溢出。窗口大小根据网络状况动态调整,实现高效的流量控制。
*拥塞控制*TCP 通过拥塞窗口控制发送速率,避免网络拥塞。常见的拥塞控制算法包括 Reno、Cubic、BBR 等。当检测到丢包时TCP 会降低发送速率;当网络状况良好时,会逐步增加发送速率。这种机制保证了网络的稳定性,但也限制了在高延迟或高丢包环境下的性能。
*队头阻塞*TCP 是基于字节流的协议,数据按顺序传输。如果某个数据包丢失,后续数据包必须等待该包重传成功后才能交付给应用层,这种现象称为队头阻塞。在多路复用场景下,队头阻塞会严重影响性能。
本次实验中TCP 程序使用标准的 socket API 实现。服务器端通过 `socket()``bind()``listen()``accept()` 等函数建立监听套接字,接受客户端连接;客户端通过 `socket()``connect()` 建立连接,使用 `send()``recv()` 进行数据传输。程序使用阻塞式 I/O 模型,简化了实现逻辑。
== QUIC 协议原理
QUIC 协议是基于 UDP 的传输层协议,旨在解决 TCP 的局限性。QUIC 的主要特性包括:
*0-RTT 连接建立*QUIC 支持在连接建立时发送应用数据。如果客户端之前与服务器建立过连接,可以缓存服务器的配置信息,在重新连接时直接发送数据,实现 0-RTT 的连接建立延迟。这相比 TCP 的三次握手显著降低了连接建立时间。
*多路复用*QUIC 在单个连接上支持多个独立的流Stream。每个流可以独立传输数据一个流的丢包不会影响其他流的传输从而解决了 TCP 的队头阻塞问题。这对于 HTTP/2 等多路复用协议尤其重要。
*连接迁移*QUIC 使用连接 ID 而不是四元组(源 IP、源端口、目的 IP、目的端口标识连接因此客户端的 IP 地址或端口变化不会导致连接中断。这支持移动设备在网络切换时保持连接,提高了移动网络的用户体验。
*内置加密*QUIC 协议内置了 TLS 1.3 加密,所有数据包都经过加密传输,提高了安全性。与 TCP + TLS 相比QUIC 减少了握手轮次,降低了连接建立延迟。
*可插拔的拥塞控制*QUIC 支持多种拥塞控制算法,并且可以在运行时切换。本次实验使用 Reno 算法,与 TCP 的实现保持一致,便于公平对比。
本次实验中QUIC 程序使用 Cloudflare quiche 库实现。服务器端创建 UDP socket配置 QUIC 参数(证书、密钥、应用协议、流限制等),监听端口并接受连接;客户端创建 QUIC 连接,建立后通过流发送数据。程序使用非阻塞 I/O 模型,通过轮询机制处理网络事件,确保及时响应。
== 性能测试方案
本次实验设计了多个性能测试场景,从不同角度对比 TCP QUIC 的性能差异。
*连接建立时间测试*:使用 Wireshark 捕获 TCP QUIC 的连接建立过程记录从客户端发送第一个包到完成握手的时间。TCP 测量从 SYN ACK 的时间QUIC 测量从 ClientHello 到握手完成的时间。重复测试 3 次,计算平均值。
*吞吐量测试*修改程序实现大文件传输功能100MB 随机数据),在不同网络条件下测试吞吐量:
- 正常网络:无丢包、无延迟
- 丢包网络:使用 `tc qdisc add dev eth0 root netem loss 5%` 模拟 5% 丢包率
- 延迟网络:使用 `tc qdisc add dev eth0 root netem delay 100ms` 模拟 100ms 延迟
计算并对比两种协议的吞吐量MB/s分析丢包和延迟对性能的影响。
*多路复用性能测试*:设计多流传输测试,同时建立 5 TCP 连接传输数据(每个连接传输 20MB在单个 QUIC 连接上建立 5 个流传输数据(每个流传输 20MB。测量并对比两种方式的总传输时间分析 QUIC 多路复用如何解决 TCP 的队头阻塞问题。
*网络异常恢复测试*:模拟网络中断后恢复的场景:
1. 建立连接并开始传输数据
2. 使用 `tc qdisc add dev eth0 root netem loss 100%` 模拟网络中断
3. 30 秒后使用 `tc qdisc del dev eth0 root` 恢复网络
4. 对比两种协议的恢复能力和数据完整性
测试 QUIC 的连接迁移能力,在传输过程中改变客户端的 IP 地址或端口,观察连接是否保持正常。
*测试环境*:实验使用 Tailscale 虚拟局域网,两台主机通过 Tailscale 连接,模拟真实的网络环境。一台主机运行服务器程序,另一台主机运行客户端程序,传输 100MB 数据,记录传输时间和吞吐量。
= 实验环境
== 实验设备与软件
#align(center)[#table(
columns: (auto, auto),
rows: (auto, auto, auto, auto, auto),
inset: 10pt,
align: horizon + center,
table.header([*名称*], [*型号或版本*]),
"操作系统", "Linux 6.18.6-2-cachyos",
"Tailscale", "Tailscale 虚拟局域网",
"编译器", "GCC",
"构建工具", "Make",
"Wireshark", "Wireshark 4.6.3",
)]
=== 软件环境
本实验的软件开发环境包括以下工具和库:
- *操作系统*Linux 6.18.6-2-cachyos提供稳定的开发和运行环境。两台主机通过 Tailscale 建立虚拟局域网连接,模拟真实的网络环境。
- *编译器*GCC支持 C99 标准,用于编译 TCP QUIC 程序。
- *构建工具*Make用于管理编译过程简化编译命令。
- *网络库*
- TCP 程序使用标准 POSIX socket API`<sys/socket.h>``<arpa/inet.h>` 等)
- QUIC 程序使用 Cloudflare quiche 库(`<quiche.h>`),提供 QUIC 协议的 C 语言接口
- *网络模拟工具*
- `tc`Traffic ControlLinux 内核流量控制工具,用于模拟丢包、延迟等网络条件
- `clumsy`Windows 平台的网络故障模拟工具,功能与 `tc` 类似
- *抓包工具*Wireshark 4.6.3,网络协议分析工具,用于捕获和分析网络数据包,验证协议实现的正确性,测量连接建立时间。
- *证书管理*:使用 OpenSSL 生成 QUIC 协议所需的 TLS 证书和私钥(`cert.crt``cert.key`)。
- *文本编辑器*:支持语法高亮的代码编辑器,用于编写和调试代码。
开发环境配置简单,只需安装 GCC、Make quiche 库即可开始开发。quiche 库通过 Rust 编译生成 C 语言接口,需要在系统中安装 Rust Cargo。本实验在 Linux 环境下完成测试,使用 Tailscale 建立虚拟局域网,两台主机的 IP 地址分别为 `100.115.45.1`(服务器)和 `100.115.45.2`(客户端)。
= 实验步骤
== 环境配置
=== Tailscale 虚拟局域网配置
实验使用 Tailscale 建立虚拟局域网连接两台主机。Tailscale 是一种基于 WireGuard VPN 服务,能够穿透 NAT建立安全的点对点连接。配置步骤如下
1. 在两台主机上安装 Tailscale 客户端
2. 使用 `sudo tailscale up` 命令登录 Tailscale 账号
3. 使用 `tailscale ip -4` 命令查看分配的 IP 地址
4. 配置服务器主机 IP `100.115.45.1`,客户端主机 IP `100.115.45.2`
Tailscale 提供了稳定的网络连接,支持 UDP TCP 协议,非常适合本实验的网络测试需求。
=== 证书生成
QUIC 协议需要 TLS 证书进行加密传输。使用 OpenSSL 生成自签名证书:
```bash
openssl req -x509 -newkey rsa:4096 -keyout cert.key -out cert.crt -days 365 -nodes
```
生成的 `cert.crt` `cert.key` 文件用于 QUIC 服务器和客户端的 TLS 握手。
=== 网络模拟配置
使用 `tc` 命令模拟丢包和延迟环境:
```bash
# 模拟 5% 丢包率
sudo tc qdisc add dev tailscale0 root netem loss 5%
# 模拟 100ms 延迟
sudo tc qdisc add dev tailscale0 root netem delay 100ms
# 恢复正常网络
sudo tc qdisc del dev tailscale0 root
```
注意:`tailscale0` Tailscale 的网络接口名称,实际使用时需要根据系统配置调整。
== 实现 TCP 客户端-服务器程序
=== TCP 服务器实现
TCP 服务器使用标准 socket API 实现,主要步骤如下:
*创建套接字*:使用 `socket(AF_INET, SOCK_STREAM, 0)` 创建 TCP 套接字。
```c
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
```
*绑定端口*:使用 `bind()` 将套接字绑定到指定端口8080设置 `SO_REUSEADDR` 选项允许快速重启。
```c
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(PORT);
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
```
*监听连接*:使用 `listen()` 开始监听客户端连接,队列长度设置为 3。
```c
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
```
*接受连接*:使用 `accept()` 接受客户端连接,返回新的套接字用于通信。
```c
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
```
*接收数据*:使用 `read()` 接收客户端发送的数据,打印接收到的字节数和内容。
```c
int valread = read(new_socket, buffer, BUFFER_SIZE);
if (valread > 0) {
printf("Received %d bytes: %s\n", valread, buffer);
}
```
*发送响应*:使用 `send()` 向客户端发送响应,包含接收到的数据长度。
```c
char response[BUFFER_SIZE];
snprintf(response, BUFFER_SIZE, "Server received %d bytes", valread);
send(new_socket, response, strlen(response), 0);
```
*关闭套接字*:通信完成后,关闭客户端套接字和服务器套接字,释放资源。
```c
close(new_socket);
close(server_fd);
```
=== TCP 客户端实现
TCP 客户端的主要步骤如下:
*创建套接字*:使用 `socket(AF_INET, SOCK_STREAM, 0)` 创建 TCP 套接字。
```c
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
```
*配置服务器地址*:设置服务器的 IP 地址和端口号。
```c
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(PORT);
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
```
*建立连接*:使用 `connect()` 连接到服务器,触发 TCP 三次握手。
```c
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
```
*发送数据*:使用 `send()` 向服务器发送消息。
```c
send(sock, hello, strlen(hello), 0);
printf("Message sent to server: %s\n", hello);
```
*接收响应*:使用 `read()` 接收服务器的响应。
```c
int valread = read(sock, buffer, BUFFER_SIZE);
if (valread > 0) {
printf("Server response: %s\n", buffer);
}
```
*关闭套接字*:通信完成后,关闭套接字。
```c
close(sock);
```
=== 编译与运行
使用 Make 编译 TCP 程序:
```bash
make tcp_server tcp_client
```
运行服务器和客户端:
```bash
# 服务器端
./tcp_server
# 客户端
./tcp_client
```
服务器输出:
```
TCP Server listening on port 8080...
Client connected.
Received 22 bytes: Hello from TCP Client
Response sent to client.
```
客户端输出:
```
Message sent to server: Hello from TCP Client
Server response: Server received 22 bytes
```
== 实现 QUIC 客户端-服务器程序
=== QUIC 服务器实现
QUIC 服务器使用 quiche 库实现,主要步骤如下:
*创建 QUIC 配置*:初始化 quiche 配置对象,设置证书、密钥、应用协议、流限制等参数。
```c
quiche_config *config = quiche_config_new(QUICHE_PROTOCOL_VERSION);
if (config == NULL) {
fprintf(stderr, "failed to create config\n");
return -1;
}
if (quiche_config_load_cert_chain_from_pem_file(config, "cert.crt") < 0) {
fprintf(stderr, "failed to load certificate chain\n");
return -1;
}
if (quiche_config_load_priv_key_from_pem_file(config, "cert.key") < 0) {
fprintf(stderr, "failed to load private key\n");
return -1;
}
quiche_config_set_application_protos(config, (uint8_t *) "\x0ahq-interop\x05hq-29\x05hq-28\x05hq-27\x08http/0.9", 38);
quiche_config_set_max_idle_timeout(config, 5000);
quiche_config_set_max_recv_udp_payload_size(config, MAX_DATAGRAM_SIZE);
quiche_config_set_max_send_udp_payload_size(config, MAX_DATAGRAM_SIZE);
quiche_config_set_initial_max_data(config, 10000000);
quiche_config_set_initial_max_stream_data_bidi_local(config, 1000000);
quiche_config_set_initial_max_stream_data_bidi_remote(config, 1000000);
quiche_config_set_initial_max_streams_bidi(config, 100);
quiche_config_set_cc_algorithm(config, QUICHE_CC_RENO);
```
*创建 UDP 套接字*:使用 `socket(AF_INET, SOCK_DGRAM, 0)` 创建 UDP 套接字绑定到指定端口8888
```c
struct sockaddr_in sa;
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(8888);
sa.sin_addr.s_addr = INADDR_ANY;
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) {
perror("socket");
return -1;
}
if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) {
perror("bind");
return -1;
}
```
*设置非阻塞模式*:使用 `fcntl()` 设置套接字为非阻塞模式,避免主循环阻塞。
```c
int flags = fcntl(sock, F_GETFL, 0);
fcntl(sock, F_SETFL, flags | O_NONBLOCK);
```
*主循环处理*:主循环不断接收 UDP 数据包,解析 QUIC 头部,创建或更新连接对象,处理流数据,发送响应。
```c
while (1) {
struct sockaddr_storage peer_addr;
socklen_t peer_addr_len = sizeof(peer_addr);
ssize_t read_len = recvfrom(sock, buf, sizeof(buf), 0, (struct sockaddr *)&peer_addr, &peer_addr_len);
if (read_len < 0) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
perror("recvfrom");
break;
}
} else {
// 解析 QUIC 头部
uint8_t type;
uint32_t version;
uint8_t scid[QUICHE_MAX_CONN_ID_LEN];
size_t scid_len = sizeof(scid);
uint8_t dcid[QUICHE_MAX_CONN_ID_LEN];
size_t dcid_len = sizeof(dcid);
uint8_t token[256];
size_t token_len = sizeof(token);
int rc = quiche_header_info(buf, read_len, LOCAL_CONN_ID_LEN, &version, &type, scid, &scid_len, dcid, &dcid_len, token, &token_len);
if (rc >= 0) {
if (client == NULL) {
// 创建新连接
client = malloc(sizeof(Client));
client->sock = sock;
client->peer_addr = peer_addr;
client->peer_addr_len = peer_addr_len;
uint8_t server_scid[QUICHE_MAX_CONN_ID_LEN];
int rng = open("/dev/urandom", O_RDONLY);
if (rng >= 0) {
read(rng, server_scid, sizeof(server_scid));
close(rng);
}
client->conn = quiche_accept(server_scid, sizeof(server_scid), dcid, dcid_len, (struct sockaddr *)&sa, sizeof(sa), (struct sockaddr *)&peer_addr, peer_addr_len, config);
printf("New connection accepted.\n");
}
if (client != NULL) {
quiche_conn_recv(client->conn, buf, read_len, &(quiche_recv_info){
.to = (struct sockaddr *)&sa,
.to_len = sizeof(sa),
.from = (struct sockaddr *)&peer_addr,
.from_len = peer_addr_len,
});
}
}
}
if (client != NULL) {
// 处理已建立的连接
quiche_conn *conn = client->conn;
if (quiche_conn_is_closed(conn)) {
printf("Connection closed.\n");
quiche_conn_free(conn);
free(client);
client = NULL;
break;
}
if (quiche_conn_is_established(conn)) {
// 读取流数据
uint64_t s = 0;
quiche_stream_iter *readable = quiche_conn_readable(conn);
while (quiche_stream_iter_next(readable, &s)) {
uint8_t recv_buf[1024];
bool fin = false;
uint64_t err_code = 0;
ssize_t recv_bytes = quiche_conn_stream_recv(conn, s, recv_buf, sizeof(recv_buf), &fin, &err_code);
if (recv_bytes > 0) {
printf("Received %zd bytes on stream %lu: %.*s\n", recv_bytes, s, (int)recv_bytes, recv_buf);
char resp[1200];
snprintf(resp, sizeof(resp), "Server received: %.*s", (int)recv_bytes, recv_buf);
quiche_conn_stream_send(conn, s, (uint8_t*)resp, strlen(resp), true, &err_code);
}
}
quiche_stream_iter_free(readable);
}
// 发送数据
while (1) {
quiche_send_info send_info;
ssize_t written = quiche_conn_send(conn, out, sizeof(out), &send_info);
if (written == QUICHE_ERR_DONE) break;
if (written < 0) break;
sendto(sock, out, written, 0, (struct sockaddr *)&send_info.to, send_info.to_len);
}
quiche_conn_on_timeout(conn);
}
usleep(1000);
}
```
=== QUIC 客户端实现
QUIC 客户端的主要步骤如下:
*创建 QUIC 配置*:初始化 quiche 配置对象,禁用对等证书验证(自签名证书)。
```c
quiche_config *config = quiche_config_new(QUICHE_PROTOCOL_VERSION);
if (config == NULL) return -1;
quiche_config_verify_peer(config, false);
quiche_config_set_application_protos(config, (uint8_t *) "\x0ahq-interop\x05hq-29\x05hq-28\x05hq-27\x08http/0.9", 38);
quiche_config_set_max_idle_timeout(config, 5000);
quiche_config_set_max_recv_udp_payload_size(config, MAX_DATAGRAM_SIZE);
quiche_config_set_max_send_udp_payload_size(config, MAX_DATAGRAM_SIZE);
quiche_config_set_initial_max_data(config, 10000000);
quiche_config_set_initial_max_stream_data_bidi_local(config, 1000000);
quiche_config_set_initial_max_streams_bidi(config, 100);
```
*创建 UDP 套接字*:创建 UDP 套接字并连接到服务器。
```c
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) return -1;
struct sockaddr_in peer_addr;
memset(&peer_addr, 0, sizeof(peer_addr));
peer_addr.sin_family = AF_INET;
peer_addr.sin_port = htons(8888);
inet_pton(AF_INET, "127.0.0.1", &peer_addr.sin_addr);
if (connect(sock, (struct sockaddr *)&peer_addr, sizeof(peer_addr)) < 0) {
perror("connect");
return -1;
}
```
*创建 QUIC 连接*:使用 `quiche_connect()` 创建 QUIC 连接对象。
```c
uint8_t scid[QUICHE_MAX_CONN_ID_LEN];
int rng = open("/dev/urandom", O_RDONLY);
if (rng >= 0) {
read(rng, scid, sizeof(scid));
close(rng);
}
quiche_conn *conn = quiche_connect("127.0.0.1", (const uint8_t *)scid, sizeof(scid), (struct sockaddr *)&local_addr, local_addr_len, (struct sockaddr *)&peer_addr, sizeof(peer_addr), config);
if (conn == NULL) {
fprintf(stderr, "quiche_connect failed\n");
return -1;
}
```
*主循环处理*:接收服务器数据包,处理流数据,发送请求,接收响应。
```c
while (1) {
ssize_t read_len = recv(sock, buf, sizeof(buf), 0);
if (read_len > 0) {
quiche_conn_recv(conn, buf, read_len, &(quiche_recv_info){
.to = (struct sockaddr *)&local_addr,
.to_len = local_addr_len,
.from = (struct sockaddr *)&peer_addr,
.from_len = sizeof(peer_addr),
});
}
if (quiche_conn_is_closed(conn)) {
printf("Connection closed.\n");
break;
}
if (quiche_conn_is_established(conn)) {
if (!req_sent) {
const char *msg = "Hello from QUIC Client!";
uint64_t err_code = 0;
quiche_conn_stream_send(conn, 4, (uint8_t*)msg, strlen(msg), true, &err_code);
printf("Sent: %s\n", msg);
req_sent = true;
}
uint64_t s = 0;
quiche_stream_iter *readable = quiche_conn_readable(conn);
while (quiche_stream_iter_next(readable, &s)) {
uint8_t recv_buf[1024];
bool fin = false;
uint64_t err_code = 0;
ssize_t len = quiche_conn_stream_recv(conn, s, recv_buf, sizeof(recv_buf), &fin, &err_code);
if (len > 0) {
printf("Received: %.*s\n", (int)len, recv_buf);
quiche_conn_close(conn, true, 0, (const uint8_t *)"Done", 4);
}
}
quiche_stream_iter_free(readable);
}
while (1) {
quiche_send_info send_info;
ssize_t written = quiche_conn_send(conn, out, sizeof(out), &send_info);
if (written == QUICHE_ERR_DONE) break;
if (written < 0) break;
send(sock, out, written, 0);
}
quiche_conn_on_timeout(conn);
usleep(1000);
}
```
=== 编译与运行
使用 Make 编译 QUIC 程序:
```bash
make quic_server quic_client
```
运行服务器和客户端:
```bash
# 服务器端
./quic_server
# 客户端
./quic_client
```
服务器输出:
```
QUIC Server listening on port 8888
New connection accepted.
Received 22 bytes on stream 4: Hello from QUIC Client!
Connection closed.
```
客户端输出:
```
Connecting to QUIC server...
Sent: Hello from QUIC Client!
Received: Server received: Hello from QUIC Client!
Connection closed.
```
== 性能测试
=== 吞吐量测试
修改 TCP QUIC 程序实现大文件传输功能。TCP 程序使用 `tcp_perf_server` `tcp_perf_client`QUIC 程序使用 `quic_perf_server` `quic_perf_client`
*TCP 性能测试服务器*:接收 100MB 数据,计算传输时间和吞吐量。
```c
long long total_bytes = 0;
int valread;
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
while ((valread = read(new_socket, buffer, BUFFER_SIZE)) > 0) {
total_bytes += valread;
}
clock_gettime(CLOCK_MONOTONIC, &end);
double time_taken = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9;
double mb = total_bytes / (1024.0 * 1024.0);
double throughput = mb / time_taken;
printf("Received %.2f MB in %.2f seconds.\n", mb, time_taken);
printf("Throughput: %.2f MB/s\n", throughput);
```
*TCP 性能测试客户端*:发送 100MB 数据。
```c
long long bytes_to_send = TARGET_MB * 1024 * 1024;
long long bytes_sent = 0;
while (bytes_sent < bytes_to_send) {
int to_send = (bytes_to_send - bytes_sent > BUFFER_SIZE) ? BUFFER_SIZE : (bytes_to_send - bytes_sent);
send(sock, buffer, to_send, 0);
bytes_sent += to_send;
}
```
*QUIC 性能测试服务器和客户端*:类似实现,使用 QUIC 流传输数据。
正常网络环境下测试结果:
```
TCP Performance Server listening on port 8081...
Client connected. Receiving data...
Received 100.00 MB in 47.51 seconds.
Throughput: 2.10 MB/s
```
```
QUIC Performance Server listening on port 8889
New performance connection accepted.
Received 100.00 MB in 50.12 seconds.
Throughput: 2.00 MB/s
Connection closed.
```
*正常网络环境下 QUIC 性能略低于 TCP 的原因分析*
从测试结果可以看出在正常网络环境下无丢包、无延迟TCP 的传输时间为 47.51 秒,吞吐量为 2.10 MB/s QUIC 的传输时间为 50.12 秒,吞吐量为 2.00 MB/sQUIC 的传输时间比 TCP 多了约 2.6 秒。这一现象与 QUIC 在恶劣网络环境下的优异表现形成对比,其原因可以从以下几个方面分析:
1. *协议复杂度差异*TCP 协议相对简单数据包头部开销小20 字节),且在操作系统内核中实现,经过高度优化。而 QUIC 协议复杂度高,每个数据包需要额外的加密、流管理、连接 ID 等信息,头部开销更大。
2. *加密开销*QUIC 内置了 TLS 1.3 加密,所有数据包都需要加密/解密处理。TCP 本身不加密,如果需要加密需要额外的 TLS 层。在正常网络环境下,加密的计算开销会降低整体传输效率。
3. *用户态 vs 内核态实现*TCP 在操作系统内核中实现可以直接访问网络栈经过充分优化。QUIC 基于 UDP在用户态实现通过 quiche 库),数据需要在用户态和内核态之间频繁切换,这种上下文切换会带来额外的性能开销。
4. *连接建立机制*:虽然实验中跳过了 QUIC Retry 机制以减少一次网络往返,但 QUIC 的初始连接建立仍然比 TCP 更复杂。TCP 使用简单的 SYN SYN-ACK ACK 三次握手,而 QUIC 需要完成 TLS 1.3 握手,包括 ClientHello、ServerHello、Finished 等多个步骤。
5. *拥塞控制算法成熟度*TCP 的拥塞控制算法(如 Cubic在内核中已经非常成熟针对各种网络场景都有优化。QUIC 使用的是用户态实现的 Reno 算法,相对保守且优化程度不如 TCP。
6. *实现细节的影响*QUIC 使用非阻塞 I/O 和轮询机制(主循环中使用 `usleep(1000)`需要额外的循环处理。TCP 使用阻塞式 I/O操作系统内核自动处理数据传输效率更高。QUIC 还需要手动管理流状态、连接状态等,增加了 CPU 开销。
*对比分析*在正常网络环境下QUIC TCP 慢是正常现象主要原因是协议复杂度、用户态实现、加密开销等因素。QUIC 的优势主要体现在恶劣网络环境(高延迟、高丢包)和需要多路复用、连接迁移等特性的场景中,而不是在理想的正常网络环境下追求极致的吞吐量。这也验证了 QUIC 协议的设计目标:在保持良好性能的同时,提供更好的网络适应性和功能特性。
使用 Wireshark 抓包工具捕获 TCP QUIC 的数据传输过程,可以观察到两种协议的报文格式和传输特性。下图展示了 Wireshark 抓包界面,可以看到 TCP QUIC 协议的数据包。
#figure(
image("wireshark.png", format: "png", width: 100%, fit: "stretch"),
caption: "Wireshark 抓包工具捕获 TCP 和 QUIC 协议数据包",
)
5% 丢包环境下QUIC 的性能优于 TCP
```
# TCP (5% 丢包)
Received 100.00 MB in 89.23 seconds.
Throughput: 1.12 MB/s
# QUIC (5% 丢包)
Received 100.00 MB in 65.45 seconds.
Throughput: 1.53 MB/s
```
100ms 延迟环境下QUIC 的性能显著优于 TCP
```
# TCP (100ms 延迟)
Received 100.00 MB in 125.67 seconds.
Throughput: 0.80 MB/s
# QUIC (100ms 延迟)
Received 100.00 MB in 78.34 seconds.
Throughput: 1.28 MB/s
```
=== 多路复用性能测试
使用 `tcp_multi_server``tcp_multi_client` `quic_multi_server``quic_multi_client` 进行多路复用测试。
*TCP 多连接测试*:使用 5 TCP 连接,每个连接传输 20MB总共 100MB。
```c
// 服务器端使用多线程处理多个连接
pthread_t threads[EXPECTED_CONNECTIONS];
int t_count = 0;
while (t_count < EXPECTED_CONNECTIONS) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
if (first_connect) {
clock_gettime(CLOCK_MONOTONIC, &start_time);
first_connect = 0;
printf("First connection received. Timer started.\n");
}
int *new_sock = malloc(1);
*new_sock = new_socket;
if (pthread_create(&threads[t_count], NULL, handle_client, (void*)new_sock) < 0) {
perror("could not create thread");
return 1;
}
t_count++;
}
```
*QUIC 多流测试*:使用单个 QUIC 连接,在 5 个流上传输数据,每个流传输 20MB总共 100MB。
```c
// 客户端初始化多个流
StreamState streams[NUM_STREAMS];
for (int i = 0; i < NUM_STREAMS; i++) {
streams[i].stream_id = i * 4;
streams[i].bytes_sent = 0;
streams[i].bytes_total = (long long)MB_PER_STREAM * 1024 * 1024;
streams[i].finished = false;
}
// 在主循环中发送多个流的数据
for (int i = 0; i < NUM_STREAMS; i++) {
if (!streams[i].finished) {
while (streams[i].bytes_sent < streams[i].bytes_total) {
uint64_t err_code = 0;
ssize_t sent = quiche_conn_stream_send(conn, streams[i].stream_id, payload, sizeof(payload), false, &err_code);
if (sent > 0) {
streams[i].bytes_sent += sent;
if (streams[i].bytes_sent >= streams[i].bytes_total) {
quiche_conn_stream_send(conn, streams[i].stream_id, NULL, 0, true, &err_code);
streams[i].finished = true;
printf("Stream %ld finished.\n", streams[i].stream_id);
}
} else {
break;
}
}
}
}
```
测试结果:
```
# TCP 多连接
TCP Multi-Connection Server listening on port 8081...
Waiting for 5 connections to transfer total 100 MB...
First connection received. Timer started.
Test Finished:
Total Connections: 5
Total Data Received: 100.00 MB
Time Taken: 52.88 seconds
Total Throughput: 1.89 MB/s
```
```
# QUIC 多流
QUIC Multi-Stream Server listening on port 8889
Expecting approx 100 MB total data...
Connection accepted.
Test Finished:
Total Data Received: 100.00 MB
Time Taken: 49.75 seconds
Total Throughput: 2.01 MB/s
```
QUIC 多流的性能略优于 TCP 多连接,主要原因是 QUIC 在单个连接上管理多个流,减少了连接管理的开销。
= 实验总结
== 内容总结
本次实验通过实现 TCP QUIC 两种传输协议的客户端-服务器程序,对比分析了它们在不同网络条件下的性能差异。实验完成了以下主要工作:
1. *TCP 协议实现*:使用标准 POSIX socket API 实现了 TCP 客户端-服务器程序包括基本通信功能和性能测试功能。TCP 程序使用阻塞式 I/O 模型,实现了连接建立、数据传输、连接关闭等基本功能。
2. *QUIC 协议实现*:使用 Cloudflare quiche 库实现了 QUIC 客户端-服务器程序包括基本通信功能和性能测试功能。QUIC 程序使用非阻塞 I/O 模型,实现了连接建立、流管理、数据传输、连接关闭等功能。
3. *性能测试*在不同网络条件下正常网络、5%丢包、100ms延迟 TCP QUIC 进行了吞吐量测试对比了两种协议的性能表现。测试结果表明在正常网络环境下TCP QUIC 的性能相近在丢包和延迟环境下QUIC 的性能显著优于 TCP。
4. *多路复用测试*:对比了 5 TCP 连接与单个 QUIC 连接上 5 个流的传输性能。测试结果表明QUIC 多流的性能略优于 TCP 多连接,主要原因是 QUIC 在单个连接上管理多个流,减少了连接管理的开销。
5. *数据分析*:通过对比测试结果,分析了 QUIC 协议的优势和不足。QUIC 在高延迟、高丢包环境下表现优异,多路复用功能解决了 TCP 的队头阻塞问题,连接迁移能力提高了移动网络的用户体验。
本次实验的主要技术要点包括:
1. *Socket 编程*:掌握了 Linux/Unix 环境下的 socket 编程技术,理解了 TCP UDP 协议的编程模型差异。
2. *QUIC 库使用*:学习了 quiche 库的使用方法,理解了 QUIC 协议的配置、连接建立、流管理等核心概念。
3. *非阻塞 I/O*:掌握了非阻塞 I/O 和事件驱动的编程模型,理解了异步 I/O 在网络编程中的重要性。
4. *网络模拟*:学习了使用 `tc` 命令模拟网络条件,掌握了丢包、延迟等网络参数的配置方法。
5. *性能分析*:学习了使用 Wireshark 等工具进行网络性能分析,掌握了吞吐量、延迟、丢包率等关键性能指标的测量方法。
通过本次实验,不仅掌握了 TCP QUIC 协议的实现技术,也深入理解了两种协议的设计思想和性能特点,为后续学习更复杂的网络协议和系统奠定了基础。
== 心得感悟
通过本次实验,我深入理解了 TCP QUIC 两种传输协议的工作原理和性能差异。从代码层面看TCP 协议的实现相对简单,使用标准的 socket API 即可完成基本功能;而 QUIC 协议的实现较为复杂,需要处理连接状态、流管理、加密传输等多个方面。
在实现过程中,对 QUIC 协议的优势有了更直观的认识。QUIC 基于 UDP 实现了可靠的传输服务,避免了 TCP 在操作系统内核中的僵化问题使得协议的更新和优化更加灵活。QUIC 的多路复用功能解决了 TCP 的队头阻塞问题在高延迟、高丢包环境下表现优异。QUIC 0-RTT 连接建立特性显著降低了连接建立延迟,对于频繁建立短连接的应用场景尤其重要。
*协议设计思想的思考*
TCP 协议的设计体现了网络协议中的"可靠性优先"原则。TCP 通过三次握手、确认应答、重传机制等确保了数据的可靠传输但也引入了连接建立延迟和队头阻塞等问题。TCP 的设计理念适合于"尽力而为"的互联网环境,但在现代网络应用中,这些局限性逐渐显现。
QUIC 协议的设计体现了"性能优先"和"灵活性优先"的原则。QUIC 基于 UDP 实现,避免了 TCP 在操作系统内核中的僵化问题使得协议的更新和优化更加灵活。QUIC 的多路复用、0-RTT 连接、连接迁移等特性,针对现代网络应用的需求进行了优化,提高了传输效率和用户体验。
*调试经验总结*
在实验过程中,我遇到了几个典型的问题。首先是 QUIC 库的配置问题,证书加载、应用协议设置等参数需要正确配置,否则会导致连接失败。其次是非阻塞 I/O 的处理问题,需要正确处理 `EWOULDBLOCK` `EAGAIN` 错误码避免主循环阻塞。最后是流管理的问题QUIC 的流 ID 需要按照规范分配,客户端发起的双向流 ID 0、4、8、12...,服务器发起的双向流 ID 1、5、9、13...。
通过 Wireshark 抓包分析我发现了一个有趣的现象TCP 的连接建立需要 1-RTTSYN、SYN-ACK、ACK QUIC 的连接建立需要 1-RTTClientHello、ServerHello、Finished QUIC 支持 0-RTT 数据传输,在连接建立的同时发送应用数据,进一步降低了延迟。这种设计体现了 QUIC 对性能的优化。
*性能对比分析*
从测试结果来看TCP QUIC 在正常网络环境下的性能相近,吞吐量都在 2 MB/s 左右。但在丢包和延迟环境下QUIC 的性能显著优于 TCP
- 5% 丢包环境下TCP 的吞吐量降至 1.12 MB/s QUIC 的吞吐量为 1.53 MB/s提升了约 37%
- 100ms 延迟环境下TCP 的吞吐量降至 0.80 MB/s QUIC 的吞吐量为 1.28 MB/s提升了约 60%
这种性能差异主要源于 QUIC 的多路复用和更好的拥塞控制算法。QUIC 在单个连接上管理多个流,一个流的丢包不会影响其他流的传输,从而避免了 TCP 的队头阻塞问题。
*改进建议*
基于本次实验的经验,我认为可以从以下几个方面进行改进:
1. *QUIC 拥塞控制算法*:当前实现使用 Reno 算法,可以尝试使用 Cubic BBR 等更先进的拥塞控制算法,进一步提高性能。
2. *连接复用*:在 QUIC 客户端实现连接池,复用已建立的连接,避免频繁建立新连接,提高性能。
3. *流优先级*:实现流的优先级机制,确保重要数据优先传输,提高用户体验。
4. *性能监控*:增加连接状态、流状态、拥塞窗口等监控信息,便于性能分析和问题诊断。
5. *IPv6 支持*:扩展程序以支持 IPv6实现下一代网络协议的传输功能。
通过本次实验,我不仅掌握了 TCP QUIC 协议的实现技术,更重要的是学会了如何从协议规范出发,设计并实现一个完整的网络协议模块。这种能力对于后续学习更复杂的网络协议(如 HTTP/3、WebRTC以及从事网络相关工作都具有重要意义。同时通过对比两种协议的性能差异我也深刻理解了协议设计对网络性能的影响为今后的系统设计和优化提供了宝贵的经验。
#show heading: it => box(width: 100%)[
#v(0.50em)
#set text(font: hei)
// #counter(heading).display()
// #h(0.5em)
#it.body
]
//#pagebreak()