# FileTransmission **Repository Path**: LisnX/file-trans ## Basic Information - **Project Name**: FileTransmission - **Description**: 《开源软件设计》 课程大作业:基于UDP的可靠大文件传输 - **Primary Language**: C++ - **License**: Not specified - **Default Branch**: develop - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 4 - **Forks**: 0 - **Created**: 2023-12-18 - **Last Updated**: 2024-12-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Readme ## 项目概况 ### 基本状况 项目地址:[LisnX/FileTransmission](https://gitee.com/LisnX/file-trans) 项目包含两个应用: - `server`服务器端,负责接收数据包 - `client`客户读端,负责发送数据包 使用`cloc`工具统计代码行数: ```text ------------------------------------------------------------------------------- Language files blank comment code ------------------------------------------------------------------------------- C++ 4 13 43 200 C/C++ Header 3 23 22 84 ------------------------------------------------------------------------------- SUM: 7 36 65 284 ------------------------------------------------------------------------------- ``` 项目源码共**284**行,使用**C++**语言编码。 ### 设计思路 - 缓冲区均设计为**50KB**大小,每次发送的数据单元**MTU**为1KB大小。 - 基于UDP的**无连接特性**,收发数据包都在一个线程中完成,不选择另起新的线程。 - 为了协调发送与接收的速率,使用了**发送窗口**(50个数据包),使用**位图**来记录数据包的传达状态。 - 使用自定缓冲区替代`ofstream` 默认的缓冲区,减少读写调用。 ![image-20231219165816935](http://img.lisnlxy.cn/uPic/image-20231219165816935.png) 主要的数据结构设计如下 ![image-20231219171714341](http://img.lisnlxy.cn/uPic/image-20231219171714341.png) 每个发送窗口的对象交互时序图如下: ![image-20231221151620712](http://img.lisnlxy.cn/uPic/image-20231221151620712.png) 软件版本迭代的设计如下 ![image-20231219172204985](http://img.lisnlxy.cn/uPic/image-20231219172204985.png) ### 关键代码 用于循环发送数据包的`send()`方法 ```cpp // Sender.cpp bool Sender::send() { SendFlag sendFlag = NormalSend; size_t sendBytes = -1; // 重制位图 check_set.reset(); while (sendFlag && read()) { auto it = sendList.cbegin(); while (it != sendList.cend()) { sendBytes = sendto(sock, &(*it), sizeof(Packet), 0, (sockaddr*)(&destin_addr), sizeof(destin_addr)); ++it; } // 读ACK和信息重传 int wait_ack_count = sendList.size(); do { // 接受 ACK 信息 recvAck(wait_ack_count); // 重传数据,并检查位图,计算未收到ACK的数据包的个数 wait_ack_count = resend(); } while (wait_ack_count != 0); if (ifs.eof()) { // 到达eof,说明剩余数据已经发送完成 sendFlag = StopSend; } } return sendBytes != -1; } ``` 用于循环接收数据包的`receive()`方法 ```cpp // Receiver.cpp bool Receiver::receive() { int recv_bytes; sockaddr_in clnt_addr{}; socklen_t socklen = sizeof(clnt_addr); int packet_number = RECEIVE_WINDOW_NUM; ACK ack{}; // 接收数据 int buffer_index{}; // buffer pointer bool recv_flag = true; // whether continue to receive data from client Packet packet{}; while (recv_flag) { recv_bytes = recvfrom(sock, &packet, sizeof(Packet), 0, (sockaddr *)(&clnt_addr), &socklen); // 检查数据是否接受过 if (!check_set.test(packet.id)) { // 拷贝 && 根据 id 重新排序 memcpy(buffer + packet.id * sizeof(Packet), &packet, sizeof(Packet)); ++buffer_index; check_set.set(packet.id); } // 重新封装 ack ack.id = packet.id; // 发送 ACK 信息 sendto(sock, &ack, sizeof(ACK), 0, (sockaddr *)(&clnt_addr), socklen); // window 发送完成 if (packet.ctr_msg == Finished) { packet_number = packet.id + 1; // 此时 Finished 包正好为最后一个接受到的 if (buffer_index == packet.id + 1) { recv_flag = false; } } if (buffer_index == packet_number) { ava_pkt_num = buffer_index; write(); // 重新设置bitwise check_set.reset(); ava_pkt_num = 0; buffer_index = 0; if (packet_number != RECEIVE_WINDOW_NUM) { // Finished包接受到了,但是window没有接收完全 recv_flag = false; } } } if (recv_bytes > 0) { return true; } return false; } ``` ## 项目构建 项目结构如下: ```text . ├── CMakeLists.txt ├── Packet.h ├── Receiver.cpp ├── Receiver.h ├── Sender.cpp ├── Sender.h ├── client.cpp ├── server.cpp └── cxxopts.hpp ``` 构建命令: ```bash mkdir build cd build cmake .. make -j ``` ```cmake # CMakeLists.txt cmake_minimum_required(VERSION 3.20) # 可以自行更改版本 project(FileTransmission) set(CMAKE_CXX_STANDARD 11) add_executable(client client.cpp Packet.h Sender.cpp Sender.h cxxopts.hpp) add_executable(server server.cpp Packet.h Receiver.cpp Receiver.h) ``` 其中,如果出现`CMake` 版本不兼容,`CMake`的版本可以自行更改。 ## 系统分析 ### 可靠性分析 #### 测试目的 验证系统的传输是否可靠,传输的过程中是否发生数据的丢失。 #### 测试方法 传输一个长文本文件,判断传输前后是否发生数据丢失。 测试文件 ```text -rw-r--r-- 1 lisn staff 9304119 12 16 12:12 input.txt ``` 测试环境 - `server` 运行于云服务器当中,版本为 **CentOS 8** - `client` 运行于本机环境中,版本为 **MacOS 14.2 (23C64)** 在新的版本中,加入了`cxxopts`作为命令行的参数解析器,使用如下命令传输: ```shell # 命令中隐藏了我的服务器公网ip ./client --ip xxx.xx.xxx.xx --port 1234 --file ./input.txt ``` #### 测试结果 服务端接收到数据与客户端大小一致,且没有发生编码错误,证明传输的可靠性已经被保证: ```text // 接收端接收数据 -rw-r--r-- 1 root root 9304119 Dec 18 17:09 output.txt // 本机传出的数据 -rw-r--r-- 1 lisn staff 9304119 12 16 12:12 input.txt ``` ![image-20231218171929071](http://img.lisnlxy.cn/uPic/image-20231218171929071.png) ![image-20231218172005785](http://img.lisnlxy.cn/uPic/image-20231218172005785.png) 使用 `Beyond Compare` 工具对比文本,文本没有编码错误且顺序一致: ![image-20231219165408821](http://img.lisnlxy.cn/uPic/image-20231219165408821.png) ### 性能测试 #### 测试目的 测试应用**所占的内存大小**,**传输速度**等性能指标 #### 测试方法 在本地环境中建立应用,使用活动监视器来获得程序运行的相应数据。测试数据为本地生成的**1G**大小的样本数据。 测试系统为**MacOS 14.2 (23C64)** #### 测试结果 如图所示 ![image-20231219164720064](http://img.lisnlxy.cn/uPic/image-20231219164720064.png) 传输速度的峰值大概在**235MB/s**,换算成带宽大概能跑满**1880M**的带宽。 ![image-20231219164922984](http://img.lisnlxy.cn/uPic/image-20231219164922984.png) ![image-20231219165002788](http://img.lisnlxy.cn/uPic/image-20231219165002788.png) - `server`端的内存占用在开始传输时为 **1.3MB**左右 - `client`端的内存占用在开始传输时为 **1.3MB**左右 使用`Profiler`分析`server` ![image-20231219172558767](http://img.lisnlxy.cn/uPic/image-20231219172558767.png) 可以看到读写文件占程序运行的小部分,证明缓冲区设计是合理的 同理,分析`client` ![image-20231219172653033](http://img.lisnlxy.cn/uPic/image-20231219172653033.png) 占程序运行主要部分的是socket编程相关的系统调用,缓冲区设计合理