使用 systemd-nspawn 快速创建 Linux 容器

使用 `systemd-nspawn` 这个命令我们可以很方便的创建一个 Linux 容器,需要的只是一个使用 systemd 作为 init 的 Linux 发行版的根文件系统。通过创建容器,我们可以获得一个可以随便折腾而不用担心损坏的 Linux 环境。这里用 Ubuntu 16.04 和 CentOS 7 为例,整个过程可以说是非常简单(虽然比起 Docker 还是麻烦了点)

继续阅读使用 systemd-nspawn 快速创建 Linux 容器

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

该文章需要被重构!

这个项目我也不知道现在该不该将他设置成 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,然后就没了,十分尴尬。

serainTalk

serainTalk 是由 @ZephRay 取名和策划,由我和 @kasora 合作完成的论坛引擎,预期唯一用户大概就是 cnCalc 了,所以可能会成为专用引擎,不保证具有通用性,目前还在开发中。

目前采用的技术栈大概是这样的:`Node.js` 服务端软件,`Express` 服务端 Web 框架,`MongoDB` 数据库,`Vue.js` 前端 `MVVM` 框架,`Socket.io` `WebSocket` 通讯框架。

预览版地址

小实验:扩展 PS4 的存储

PS4 到手之后游戏买买买,500G的磁盘换成了1TB也快用完了,但是买2TB的话就感觉很麻烦,一来是要全部重新迁移数据,二是换下来的1TB WD Black也没其他地方用,就很难受。

查了一圈后发现,PS4 在某次系统更新后提供了将 USB 存储格式化为扩展存储的功能,也就是说可以用 USB3.0 的移动硬盘来装游戏。可能是有偏见吧我一直觉得移动硬盘是非常不靠谱的东西,不过这个特性倒是一个不错的拓展存储的切入点。

要说扩展存储,如果是普通的 PC 上有这个需求,解决方法非常多,比如我可以建立一个 SMB 协议的共享,Windows 和 macOS 原生兼容,Linux 只需要加载 cifs.ko 即可;当然 NFS 来实现也没有任何问题;如果对加密有需求,或是需要一个裸磁盘,还可以用 iSCSI 来做到。可是 PS4 目前没有破解,以上方法都不可行。

于是就想,PS4 支持 USB 存储的扩展,我能不能将上面提到的一些方式通过 USB 暴露给 PS4 来实现呢?比如这样一个设备:它本身是一个 USB 从机,同时有一个 GbE 接口,可以千兆访问到局域网里的 iSCSI Target,然后通过一些魔法,将 iSCSI Target 拿到的磁盘设备直通到 USB,让 PS4 认为这是一个大容量的 USB 存储。

继续阅读小实验:扩展 PS4 的存储

网易云音乐 Linux 版高分屏问题解决方案

网易云音乐 Linux 版推出很久了,肯定已经有很多人反馈过 HiDPI 下不能正常工作的问题了,然而似乎网易生娃不养娃,版本号 1.0.0 从一开始就没有变过(

在网上搜索 HiDPI 下字太小难以阅读就能找到最简单的解决方案:添加参数 `–force-device-scale-factor=2` 就能让他强制两倍渲染。不过这个方法在我这儿(ArchLinux,KDE)上并不是什么好的解决方案:启动程序时,程序按照环境 DPI 创建了窗体,但是内容部分则依然使用原分辨率渲染,效果就是只有坐上 1/4 能正常显示内容,剩下的都是白色的空白。只要我的全局 DPI 不是 100%,这些空白就一直存在。

继续阅读网易云音乐 Linux 版高分屏问题解决方案

简易 Web Terminal 的实现

说起来也是有趣,本来是研究一下 WebSocket 准备给论坛/博客增加实时更新之类的特性,结果看着看着就脑洞大开搞了这么个玩意儿((

首先明确一下,这里说的 Web Terminal 是指再网页中实现的,类似于终端模拟器的玩意儿。举例的话应该是类似于 Linode 的 LiSH 和 Visual Studio Code 中内置的那个终端,而不是 ConoHa 提供的 VNC 式的终端(其实那玩意儿是个远程桌面了)。最终目标的效果就是和 Secure Shell 类似:打开一个网页,就能启动一个网页所在服务器的 shell,比如到处都有的 bash 或者非常强大的 zsh,然后就可以与这个终端进行交互式的操作,比如使用 vim 编辑文件,或者查阅 man 中的手册。

继续阅读简易 Web Terminal 的实现

单页应用下实现动态的脚本加载

之前在写自己的博客框架的时候遇到了一个问题:文章中的 `script` 标签没有任何作用,以 Vue 为 MVVM 框架举个例子:


  

继续阅读单页应用下实现动态的脚本加载

初音未来 歌姬计划 Future Tone 游戏视频

前几天拿到了游戏仅有的两个金杯中的一个:節奏遊戲大師[FS],发个视频庆祝一下(大雾

第一个是 Extreme 难度的こちら、幸福安心委员会です,洗脑曲,这个难度可以说是相当简单:

继续阅读初音未来 歌姬计划 Future Tone 游戏视频

[作业] AES-128-ECB 实现

首先呢这份代码是这学期选修课的大作业,前后也花了点心思,于是还是贴到这儿,顺便给博客除除草ww

作业的题目有两大类,一类是使用常见的对称/非对称加密算法(DES/AES/RC5/RSA/Blowfish/blabla)实现一个可用的加解密工具,另一个则是实现一个HASH程序,类似于 md5sum 或者是 sha256sum 这种。我选择了做 AES 主要是有几个原因:

  1. 老师说了,杂凑算法于加解密算法比较起来,实现难度低,因此基础成绩就会相对低一点(然而我觉得两个都蛮简单的啊)。
  2. 在上课之前就已经接触过 AES 这个算法了(归功于 Shadowsocks 啦),用了这么久的东西,也算是对它的实现比较感兴趣的(然而怎么想都知道自己写的破代码性能会被 OpenSSL 吊着打)。

当然按照老师的说法,简单的实现只是最基本的要求,所以我这里打算做三分实现,分别是 CPU 单线程,CPU 多线程和 GPGPU。其实这里面核心代码都是完全一样的,无非就是分组加密和密钥更新上有点区别,但是听起来就高大上了很多(没毛病)。

继续阅读[作业] AES-128-ECB 实现