#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 两种传输协议的客户端-服务器程序,对比分析它们在不同网络条件下的性能表现。TCP(Transmission Control Protocol)是互联网的核心传输协议,提供可靠的、面向连接的字节流传输服务;QUIC(Quick 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(``、`` 等) - QUIC 程序使用 Cloudflare 的 quiche 库(``),提供 QUIC 协议的 C 语言接口 - *网络模拟工具*: - `tc`(Traffic Control):Linux 内核流量控制工具,用于模拟丢包、延迟等网络条件 - `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/s,QUIC 的传输时间比 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-RTT(SYN、SYN-ACK、ACK),而 QUIC 的连接建立需要 1-RTT(ClientHello、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()