Coding的痕迹

一位互联网奔跑者的网上日记

0%

软件工程综合实训 基于UDP的文件传输工具

五周的课程设计课结束了。后两周我和同学一起基于 UDP 使用 C++ 语言实现了一个文件传输工具。这篇文章记述了项目相关的一些细节, 但不是一篇正式的软件工程文档。项目地址见 Github

项目背景和需求分析

去年暑假时,有次我想把约 800GB 的硬盘备份文件通过网络传输给同学。 我们觉得自己算是“半个专业人士”,便不考虑快递硬盘的方式传送,想了很多办法。因为我所在的地方有公网地址,方案大体上是我在本地建立一个服务器,使用一般的文件传输工具传输。由于是跨省跨运营商,存在以下问题:

  1. 网络速度波动
  2. 每 48 小时动态公网 IP 出现 IP 变化,传输中断
  3. 丢包自动重传

我们需要以下特性:

  1. 将带宽跑满
  2. 支持断点续传
  3. 尽可能顺序读写,减少磁头移动

如果可能,还希望支持:

  1. 支持 UDP 减少传输量(虽然和 TCP 相似)
  2. 传输加密
  3. 支持差错校验和分块重传

否决掉 QQ 等工具,也排除 Windows 文件共享,最后选择使用 HTTP 协议(不记得为什么没使用 FTP 了)。后来才知道,类似需求可以使用 Linux 下常用的 scprsync 工具,scp 不支持但 rsync 支持断点续传。

我在本地架设一个 IIS,开放公网端口让客户端下载,带宽约能跑满,但在约两天的传输后,接收方(同学)文件校验失败。 我们总结了上述特性,尽管目前主流的 HTTP、FTP、SMB 等文件传输协议支持广、功能强大,但仍不支持上述的全部特性。我们想自己实现一个文件传输工具,我们希望它能支持断点续传、加密传输、校验等特性,如果不能做到创新与创造,那就当造个轮子吧!

趁这次课程设计的机会,实现一个简单版本的传输工具吧。项目标题就定为:基于 UDP 协议的文件传输工具。

概要设计

运行环境

由于项目使用了 Windows Socket 技术,当前仅支持 Windows XP 及以后的 Windows 操作系统。

编译器要求:支持 C++ 11 及以上。

系统结构

项目最终产品包括两个程序:发送端(Client)和接收端(Server)。每个程序均包含网络通信、界面控制、文件读写三大模块。

  • 网络通信

    封装网络请求,为控制层提供平台和协议无关的消息传输,简化程序开发、同时为支持跨平台打下基础。

    如果可能,我们想动手实现 TCP 的差错校验和重传等,同时实现一个滑动窗口(Sliding window)。考虑到网络是耗时最长的因素,且 C++ 下异步实现复杂(看了下 asio,决定以后再研究),因此使用同步方法实现。

  • 输入和输出控制

    输入主要是通过解析命令行参数控制程序流程、行为。

    输出主要是更友好地显示文件传输过程,输出有好的错误提示和程序执行进度。

  • 文件读写

    封装文件操作,程序实现磁盘缓存,减少磁盘读写。

系统结构示意图

局限

根据需求,此项目不使用图形用户界面,相反,它基于命令行进行操作。项目的重点在数据结构、算法、计算机组成、计算机网络与程序设计上。

输入输出

服务端启动时,需要指定服务端监听的地址、端口号。如果服务端开启了自动保存,还需要指定自动保存文件的路径。

客户端启动时,也需要指定服务端监听的地址、端口号用于“连接”,同时客户端还需要指定客户端名称以及发往服务端文件的路径。

客户端和服务端交互时,输出友好的提示并展示任务进度。此外,若服务器未开启自动保存,应支持显示用户确认信息。

传输流程

客户端与服务端传输流程如下:

  1. 需要文件传输时,首先启动服务端(Server),服务端进入监听状态。随后,运行客户端。
  2. 客户端(Client)发送文件传送请求,包含客户端名称、文件名、文件大小等信息。
  3. 服务端响应接收或拒绝。
  4. 开始传输。

Sequence Diagram

在当前版本的程序中,缺少 Server 端对 Client 端发送文件进行校验和重传的机制。

详细设计

说实话,在详细设计阶段,应该包含对类设计、类间关系的描述。但由于项目经验少,在项目编写中很难将接口写得“舒适”、简洁,或将模块层次划分得很细致。因此,实际情况往往是先编写代码然后再画图。使用 Jetbrains 有关工具甚至可以自动生成类图。

类设计

Protocol

Protocol 模块包括五个类。Frame 规范了每个数据包的字段格式,并支持序列化和反序列化。注意,每一个数据包还包括 crc32 字段,但这对上层是透明的,因此在结构中隐藏了。包的序列化和反序列化时会自动进行加校验头和验证校验操作。

类图1

UdpSocket

网络模块仅有 UdpSocket 一个类,他可以为程序提供一个平台无关的网络接口。类设计如下:

类图2

Buffer

文件读写模块有 ReadBufferWriteBuffer 两个类。

类图3

Controller

控制层有 AntClientAntServer 两个类。由于类间关系复杂,这里简化了一下。

类图4

测试

项目里我们只基于 Google Test 做了一些单元测试,详情见代码,这里不多说。

一点感想

项目从6月23日初始化,到昨天(7月1日)答辩过去一周多,我思考最多的是如何写出可复用、可扩展、高质量的代码——这也正是软件工程所研究的。如,在编写 UdpSocket 类的时候,我参考了 Qt5 Network 模块的实现,定义了一个新的结构 UdpSocket,从而简化代码开发。

在底层模块,尤其是序列化和反序列化代码编写时,我对函数基本编写了单元测试——这真是一个费力的活。在“软件测试”课程中我们学过对函数画出基本路径进行路径测试、构造边界值测试等等,在实际情况中这显得非常复杂:编写软件容易,保证软件质量不易。

在我写过的 bug 中,有这样一段代码:

1
2
3
4
void push_u16(std::vector<uint8_t> &output_buffer, const uint16_t x) {
push_u8(output_buffer, x >> 8);
push_u8(output_buffer, x & 127);
}

这段代码是为序列化服务的,目的是将一个 u16 整数附加到 vector<uint8_t> 后面。然而该部分代码对应的测试用例并没有找出代码中的问题:第三行的 127 应该改为 255 (即右8位二进制位全为1),而是因为另一个测试用例才检查出的问题,但却耗费了我很多的时间出定位错误。这也给我一个深刻的体会:测试不能发现所有的错误,但好的测试用例能找到尽可能多的问题。

答辩的时候自豪地对老师展示我的项目,因为我们的项目是近期新做的,没有用其他课程所写的代码,老师的评价还不错。下来后做了个测试,单机传输 400M 的文件居然只能达到不到 1MB/s, CPU 占用在 5%~10%,同时还伴有内存泄漏的情况,着实给我和另一名同学泼了一盆冷水。在代码编写中不熟悉 C++11 的右值引用、所有权和存储管理导致函数调用时出现了大量拷贝,现在看来应该与此有一定关系。做项目如同做题,在这过程中能不断发现自己的问题、激发自己对计算机程序设计的兴趣,后续再优化这个问题吧。

我也因此,更加喜欢 Rust 了。原来不用手动管理指针、内存多么幸福!内存是个难关,值得深入研究,体会工程上的各种解决方案。

以上。

写于 2021 年 7 月 2 日。