无线临时网络文件传输工具

该文章需要被重构!

这个项目我也不知道现在该不该将他设置成 public,毕竟是别人的毕业设计(捂脸逃)

不管怎样这里我就写一些制作思路吧,题目要求完成的是在 WANET 环境下实现文件的发送与接受,这里我选择的开发平台是 Linux,在网络相关 API 上感觉用起来比 Windows 舒服不少,同时 WANET 的创建也更加方便。

WANET 的最大特点就是,网络中的每一个节点都充当路由,所以整个网络中某一个节点的掉线是不会影响到其他设备的通讯,同时也可以动态的加入和移除节点。带来的麻烦也就是整张网络的带宽上不去,所以基于 TCP 的 HTTP 我们是不能用了,这里就需要自己实现一套。同时,网络的去中心化也使得不能像传统程序一样设置一个服务端和一组客户端,必须由自己实现一个网络发现的机制。结合以上两个特点,UDP 无疑成了最好的协议选择。

组件 IBSS 网络

WANET 在 Linux 上创建的方法很简单,你只需要安装好 iw 工具,然后通过如下几个命令来简单创建:

iw $iface set type ibss
ip link set $iface up
iw $iface ibss join $ssid $freq

其中,`$iface` 是指定的无线网卡,使用前记得查看是否支持 IBSS 模式,`$ssid` 则是接入点名称,对于部分操作系统可以直接加入这个网络,`$freq` 是网络的频率,实际值我选择了 2412 这个信道 1 对应的频率。

配置 IP 地址

仅仅启动了网络仍然是无法通讯的,原因是没有配置 TCP/IP 相关的属性。由于去中心化,我们不能使用传统的 DHCP 协议来分配 IP,只能有每个设备给自己分配一个 IP,所以防止冲突也比较重要,在这方面我选择了直接使用一个 `10.0.0.0/8` 这么个的大网段,其中三个数字随机产生,来满足地址的需求。

传送算法设计

文件发送方面,UDP 协议是无状态协议,同时大小最好也不要超过整个网络的 MTU 来保证可靠性,所以实际发送过程中会将文件分解成一个一个很小的切片,逐一发送。要实现可靠的文件传输,必然需要一个记录包送达的机制,我这里设计了一个比较简单(naive)的思路:定义一个数组,其中包含了数据包的传递状态,以及自身在文件中的偏移,以及这个分片的重传次数:

typedef struct WindowItem {
    uint32_t offset;
    uint8_t sendCount;
    uint32_t nextTick;
    bool isFinished = false;
    bool operator==(const WindowItem& other);
    WindowItem();
    WindowItem(uint32_t);
} WindowItem;

然后程序执行时,主循环每次循环都将扫描一次数组,对其中的元素,也就是数据包状态,执行以下操作:

  1. 检查这个数据包有没有收到 ACK 确认包,若收到,将他从数组移除
  2. 根据已完成的数据包数量,继续添加新的为发送的数据包
  3. 找到需要重新发送的包(超过若干 ticks 却没有收到 ACK 的,或者是刚加入的),并将它们发送
  4. 找到超出重试次数的包,如果有,则放弃发送,宣布失败,结束循环

以上这些过程被我称为一个 tick,为了防止网络拥塞和 CPU 过载,每个 tick 最后都会让该线程等待 10ms 的时间,来等待数据包的送达。

报文结构设计

下一步就是设计接收方拿到数据的处理方案了,如果每一次的数据报都只有文件内容,那么接收方将会很尴尬:他既不知道这个数据包时哪个会话里的,也不知道这个数据包在文件中的位置,要知道,UDP 作为无状态协议只保证数据的送达,不保证送达的顺序:

一个程序员遇到一个问题,他打算用 UDP 解决,现他在到遇了一大问个。题

所以对于每个数据包的格式,双方也必须都有明确的定义,我这里是这样的:

// 发送文件内容的报文结构
struct FilePacket {
  uint32_t offset;   // 该分片在文件中的位置
  uint8_t data[];    // 分片数据
}

当然还有其他的报文类型,这里暂时先不提,等用到再说。这样一番折腾后,我们的接受进程在接收到数据报文后,按照 offset 的偏移,将数据放到内存的缓冲区内,并发送 ACK 包。接收端的思路实在是太简单了,当时我甚至是用的 Node.js 写了原型来测试发送端(

接下来就是遇到的一个问题,对于一个接收端同时接收来次多个设备的文件,甚至是一个发送端同时发送的多个文件时,如何区分这个数据报的归属呢?这时候会话的概念就来了,HTTP 时通过每次传输都加上一个 Cookie 的 header 来实现会话,而我们则选择发送方广播寻找接受端时携带一个 UUID。同时,对于发送文件不同阶段也需要区分。整理一下,就是说所有的数据包都应该额外有两个字段的标记:会话UUID和阶段标记Tag。改进后的报文如下:

// 基础的报文结构,用于所有的传送
struct GenericHeader {
  uint8_t uuid[16];  // 会话 ID
  uint8_t tag;       // 报文类型标记
}

// 发送文件内容的报文结构
struct FilePacket {
  struct GenericHeader header;
  uint32_t offset;   // 该分片在文件中的位置
  uint8_t data[];    // 分片数据
}

其他几个类型的报文包括:

  • `DISCOVER` 报文:用于发现内网内的所有主机
  • `REQUEST` 报文:用于向发送方请求文件
  • `ACK` 报文:用于确认文件分片的接受
  • `FINISH` 报文:用于发送方向接收方宣告结束

UI 界面设计

题目要求使用 Qt 我还是比较高兴的,要使用 GTK 估计整个人都不好了(雾),有了 Qt Creator 创建界面还是很简单的,但是由于之前开发的程序都是纯命令行下的,要想接入到 Qt Application 还是比较蛋疼了,所以我就将之前的代码封装成了一个叫 `http` 的二进制程序,监听本地的 8000 端口,并执行操作。Qt 端程序就很方便了,只需要用 Qt Network 相关的东西去进行 HTTP 请求,就能拿到当前所有会话的状态,也能很轻松的创建请求。值得一提的是,这个 `http` 守护进程的 HTTP 模块完全是自己写的,代码也比较丑,这里就只放个头文件意思一下吧((

#pragma once

#include "common.h"
#include 

using handler_t = std::function;

class HttpServer {
private:
    boost::asio::io_service io_service;
    boost::asio::ip::tcp::acceptor acceptor;
    map handler;
    string decodeURL(string url);
public:
    HttpServer();
    void AddHandler(string, handler_t);
    void Run();
};

两边通讯大概就是这样:

// 刷新接收任务列表函数 1(HTTP 请求完成前)
void MainWindow::RefreshReceiveTaskList() {
    QNetworkAccessManager *manager = new QNetworkAccessManager();
    QNetworkRequest request;

    request.setUrl(QUrl("http://localhost:8000/receiving"));
    manager->get(request);

    connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(RefreshReceiveTaskListCallback(QNetworkReply *)));
}

// 刷新接收任务列表的函数 2(HTTP 完成后)
void MainWindow::RefreshReceiveTaskListCallback(QNetworkReply *reply) {
    QString response = reply->readAll();
    QStringList dataset = response.split(QChar('\n'));

    ui->receiveTable->setRowCount(0);

    for (int i = 0; i != dataset.length(); ++i) {
        QStringList rowData = dataset.at(i).split(QChar('\t'));
        if (rowData.length() != 4) {
            continue;
        }
        auto rowOffset = ui->receiveTable->rowCount();

        ui->receiveTable->insertRow(rowOffset);
        ui->receiveTable->setItem(rowOffset, 0, new QTableWidgetItem(rowData[0]));
        ui->receiveTable->setItem(rowOffset, 1, new QTableWidgetItem(rowData[1]));
        ui->receiveTable->setItem(rowOffset, 2, new QTableWidgetItem(rowData[2]));
        ui->receiveTable->setItem(rowOffset, 3, new QTableWidgetItem(rowData[3]));
    }
}

性能测试

题目的要求环境本身是有多个电脑互相发送,然而作为开发和测试的我只有一个笔记本和一个支持 IBSS 的 USB 无线网卡,所以我也只能做点小范围测试了。由于历史久远我也不记得具体速度能跑到多少,好像就比 HTTP 快那么一点点,也算是不亏了吧。

假模假样的总结

讲道理这个毕业设计真的是比我们学校的不知道高到哪里去了,涉及的范围还挺广的,至少得有 UDP 编程,Qt 编程,操作系统接口的调用(Linux 下 ioctl 就够了)和一点点算法设计的能力,对 C++ 和相关库也需要有个了解。相比而言我们学校的毕设我也做过几个,基本就是 C# WPF/WinForm 拖点控件,搞点文件保存数据或者上 SQLite,然后就没了,十分尴尬。