普通视图

发现新文章,点击刷新页面。
昨天以前治部少辅

Emscripten Fetch 接口的一个潜在内存泄漏问题

作者 治部少辅
2025年5月7日 18:04

近日发现了一个非常刁钻的可能引起基于 Emscripten 编译的 WASM 程序内存泄漏的问题。Emscripten 工具链提供了 Fetch 功能模块,这个模块允许我们调用浏览器的 fetch 接口来进行网络访问。

一个使用 fetch 接口的简单例子是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>
#include <emscripten/fetch.h>

void downloadSucceeded(emscripten_fetch_t *fetch) {
printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
// The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
emscripten_fetch_close(fetch); // Free data associated with the fetch.
}

void downloadFailed(emscripten_fetch_t *fetch) {
printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
emscripten_fetch_close(fetch); // Also free data on failure.
}

int main() {
emscripten_fetch_attr_t attr;
emscripten_fetch_attr_init(&attr);
strcpy(attr.requestMethod, "GET");
attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
attr.onsuccess = downloadSucceeded;
attr.onerror = downloadFailed;
emscripten_fetch(&attr, "myfile.dat");
}

Fetch API 提供了一些比较高阶的功能,一种一个比较重要的功能是,他可以将下载的内容缓存到 IndexDB 中,这个缓存机制能够突破浏览器自身的缓存大小的限制(一般超过 50MB 的文件浏览器的自动缓存机制会拒绝缓存)。但是这个缓存机制会导致内存泄漏。

1 泄漏产生的过程

在开头的例子中,我们需要再 onerror 和 onsuccess 回调中调用 emscripten_fetch_close 接口来关闭 fetch 指针代表的请求。在关闭过程中,fetch 使用的数据缓存区将会被回收。这个过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
EMSCRIPTEN_RESULT emscripten_fetch_close(emscripten_fetch_t* fetch) {
if (!fetch) {
return EMSCRIPTEN_RESULT_SUCCESS; // Closing null pointer is ok, same as with free().
}

// This function frees the fetch pointer so that it is invalid to access it anymore.
// Use a few key fields as an integrity check that we are being passed a good pointer to a valid
// fetch structure, which has not been yet closed. (double close is an error)
if (fetch->id == 0 || fetch->readyState > STATE_MAX) {
return EMSCRIPTEN_RESULT_INVALID_PARAM;
}

// This fetch is aborted. Call the error handler if the fetch was still in progress and was
// canceled in flight.
if (fetch->readyState != STATE_DONE && fetch->__attributes.onerror) {
fetch->status = (unsigned short)-1;
strcpy(fetch->statusText, "aborted with emscripten_fetch_close()");
fetch->__attributes.onerror(fetch);
}

fetch_free(fetch);
return EMSCRIPTEN_RESULT_SUCCESS;
}

可以看到,回收并非总会发生, emscripten_fetch_close 函数会对 fetch 的部分状态进行检查,如果检查失败,则会返回一个 EMSCRIPTEN_RESULT_INVALID_PARAM 的错误码,并且不会执行后续的清理过程(`fetch_free)。被检查的两属性中,fetch->id 是我们需要关注的对象。fetch->id 这个属性作为 fetch 的唯一标识符,是用来建立起 C++ 端的请求对象和 JS 端的请求对象的映射的。id 的值在 JS 端分配。查看源码中的 Fetch.js 文件,

1
2
3
4
5
6
7
8
9
10
11
12
function fetchXHR(fetch, onsuccess, onerror, onprogress, onreadystatechange) {
// ...

var id = Fetch.xhrs.allocate(xhr);
#if FETCH_DEBUG
dbg(`fetch: id=${id}`);
#endif
{{{ makeSetValue('fetch', C_STRUCTS.emscripten_fetch_t.id, 'id', 'u32') }}};

// ...

}

这是唯一的一处 id 复制。这段代码位于 fetchXHR 函数中,这意味着只有发起了 XHR 请求时,id 才会被分配。那么,如果缓存存在呢?这时不会调用 fetchXHR 函数(而是调用 fetchLoadCachedData 函数)。这意味着回调函数中我们试图调用 emscripten_fetch_close 函数来关闭请求并回收资源时,这个回收过程无法进行,这导致了内存泄漏。

2 怎么解决这个问题

要解决这个问题我们只需要强行让 fetch->id == 0 的检查无法通过即可,我们可以在 emscripten_fetch_close 调用前,强行设置 fetch->id 为一个非零值。那么什么值合适呢?如果我们取值和已有的请求的 id 相同,那么 emscripten_fetch_close 可能将那个请求关闭。研究 id 分配的过程(即 Fetch.xhrs.allocate 的实现)

1
2
3
4
5
6
// libcore.js
allocate(handle) {
var id = this.freelist.pop() || this.allocated.length;
this.allocated[id] = handle;
return id;
}

可以看到,id 是顺序分配的,且使用过的 id 会被回收使用(freelist)。因此我们可以设置一个较大的值,只要同一时间最大的并发请求数量不超过这个值,那就是安全的。我一般选择设置为 0xffff。 那么,正确的关闭请求的方式是:

1
2
3
4
if (fetch->id == 0) {
fetch->id = 0xffff;
}
emscripten_fetch_close(fetch);

引发 LTO 优化后续错误的一种可能及解决方法

作者 治部少辅
2024年5月31日 14:02

最近开发 C++ 的程序在链接阶段发生了一个 LTO 相关的错误。链接时优化(Link Time Optimization, LTO) 是一种编译优化技术,旨在通过在链接阶段执行全程序优化来提升代码性能和减少二进制文件大小。其主要原理是在链接阶段而不是在编译阶段进行更高级的优化,允许跨文件的全局优化。LTO 的基本原理和过程是:

  • 中间表示:在编译阶段,编译器将每个源文件编译为一种中间表示(如 LLVM bitcode),而不是直接生成目标代码。
  • 全局优化:在链接阶段,链接器将所有中间表示文件合并,生成一个完整的程序表示。此时,优化器可以进行跨文件的全局优化,如函数内联、去除冗余代码和跨模块优化。
  • 生成目标文件:优化完成后,链接器生成最终的可执行文件或库。

我遇到的错误信息中的管件一句是:

1
attempt to add bitcode file after LTO (htons)

可以看到错误信息提示我们链接系统试图在 LTO 优化之后添加 bitcode。并且在后面的括号中链接器提到了一个符号 htons,这是来自 libc 库的一个符号。基于的 https://github.com/emscripten-core/emscripten/issues/16836 这个链接中的启发,我发现了一个 LTO 优化可能面临的普遍性问题。这个问题是 LTO 会尝试移除一些它认为不需要的符号从而减少编译出来的程序的体积,但是这个判断可能会不准确。它可能将一些实际会被使用的符号删除。为了解决这个问题,我们可以为链接器指定链接输入,强制保留指定的符号以避免错误的移除。针对我遇到的情况,为链接器加上 -Wl,-u,htons 的配置即可解决问题。

  • -Wl: 这个标志告诉编译器将接下来的选项传递给链接器(ld)。-Wl,option 是传递给链接器的标准方法。
  • -u,symbol: 这个选项告诉链接器强制包含指定的符号 symbol。在这个情况下,-u,htons 和 -u,htonl 告诉链接器强制包含 htons 和 htonl 函数

Ubuntu 中如何彻底删除一个用户

作者 治部少辅
2023年8月16日 18:51

在 Ubuntu 中彻底删除一个用户涉及到几个步骤,不仅仅是删除用户,还可能包括删除与用户相关的数据。以下是步骤指南:

  1. 确保用户未登录: 在删除用户之前,最好确保该用户未登录。你可以使用 who 命令来检查哪些用户当前已登录。

    1
    who
  2. 删除用户: 使用 userdel 命令删除用户。如果你还希望删除用户的主目录和邮件池,可以使用 -r 选项。

    1
    sudo userdel -r username

    其中,username 是你想删除的用户的用户名。

    注意:-r 选项会删除用户的主目录(通常是 /home/username)。确保你已经备份了所有重要的数据!

  3. 检查文件系统: 即使删除了用户和其主目录,可能仍然在文件系统上遗留一些属于该用户的文件。你可以使用 find 命令来搜索这些文件:

    1
    sudo find / -user username

    这将列出所有属于 username 的文件。根据你的需要,你可以手动删除这些文件或更改它们的所有权。

  4. 删除用户的 cron 作业: 如果用户配置了任何 cron 作业,你还需要手动删除它们。检查 /var/spool/cron/crontabs/username 是否存在,如果存在,删除它。

    1
    sudo rm /var/spool/cron/crontabs/username
  5. 其他服务或配置: 如果该用户有其他特定的配置,例如在 /etc/sudoers 中的条目或在其他服务中的特殊访问权限,你需要手动检查并删除它们。

请在进行任何删除操作之前确保备份所有重要数据。确保你明确知道正在执行的操作,避免意外删除重要文件或配置。

读论文:一种音频事件识别方法

作者 治部少辅
2023年4月12日 15:43

本文的标题是 Adaptive Multi-scale Detection of Acoustic Events。这是清华大学电子系何亮老师团队在 2019 年的工作。

声音事件检测(AED或SED)的目标是预测给定音频段中目标事件的时间位置。这项任务在安全监测、声学预警和其他场景中起着重要作用。然而,数据不足和声学事件来源的多样性使AED任务成为一个艰巨的问题,特别是对于普遍采用数据驱动方法的情况。本文从分析声学事件的时频域特性入手,表明不同的声学事件具有不同的时频尺度特征。受到这个分析的启发,我们提出了一种自适应多尺度检测(AdaMD)方法。通过利用沙漏神经网络 (hourglass neural network) 和门控循环单元(gated recurrent unit, GRU)模块,我们的AdaMD在不同的时间和频率分辨率下产生多个预测。随后采用自适应训练算法,将多尺度预测相结合以增强整体能力。在2017年声学场景和事件的检测和分类(DCASE 2017)任务2、DCASE 2016任务3和DCASE 2017任务3上的实验结果表明,AdaMD在事件错误率(ER)和F1分数的指标上优于已发表的最先进竞争对手。我们收集的工厂机械数据集上的验证实验也证明了AdaMD的抗噪能力,提供了实际应用的证明。

1 Introduction

AED 问题的主要挑战:

  1. 数据极度不均衡;
  2. 事件具有多样化的特征;
  3. 时运频域尺度不一:这句话说的是不同的时间具有不同的时域长度,以定长音频输入为基础的检测模型的有效性存在问题;

AdaMD 的网络架构如上图所示。此网络由一个 CNN 网络和一个 RNN 网络组成。其中,CNN 部分的网络架构被称为 Hourglass,这种网络架构在计算机视觉领域被广泛应用于关键点检测,其优势在于其可以在多种时频分辨率的条件下进行特征提取。在 RNN 部分,作者采用了 Gate Recurrent Unit (GRU) 模块来处理 CNN 模型输出的每个通道,从而处理时域信息。GRU 的输出会经过一个上采样流程的处理,使得各个通道的输出具有相同的尺寸。

2 声音事件检测任务的类别

  1. 多事件检测:同一时间段内有多个事件发生,检测模块除了检测是否有事件发生,还需要给出事件的类别。
  2. 弱监督事件监测:理想情况下,标注数据应该包含事件的类别和起止时间。但是这样标注工作会比较多,如何在只标注的类别的情况下,让模型学习出事件的起止时间是一个挑战,目前这个问题巨解决的还不是很好;
  3. 异常事件检测:如何检测我们不知道的异常事件?

读论文:CADTransformer

作者 治部少辅
2023年4月11日 14:42

这次要读的文章是 CADTransformer: Panoptic Symbol Spotting Transformer for CAD Drawings

这篇论文介绍了一个名为CADTransformer的新框架,用于自动化CAD图纸中的全景符号识别任务。该任务需要识别和解析可数对象实例(如窗户、门、桌子等)和不可数的物品(如墙壁、栏杆等),并在CAD图纸中进行标记。该任务的主要难点在于图纸中符号的高度不规则的排序和方向。现有方法基于卷积神经网络(CNNs)和/或图神经网络(GNNs)来回归实例边界框并将预测结果转换为符号。相比之下,CADTransformer直接从CAD图形原始集合中进行标记,通过一对预测头同时优化线条级别的语义和实例符号识别。此外,该框架还通过几个可插拔的修改增强了主干网络,包括邻域感知自注意力、分层特征聚合和图形实体位置编码等。此外,该论文还提出了一种新的数据增强方法,称为随机层(Random Layer),通过CAD图纸的分层分离和重组来进行数据增强。最终,CADTransformer在最新发布的FloorPlanCAD数据集上,将先前的最先进水平从0.595提高到0.685,展示了该模型可以识别具有不规则形状和任意方向的符号。

1 Introduction

1.1 关于 CNN 网络的准确性问题

在 CAD 文件中对图元要素分类的任务在本文中被称为 Symbol Spotting。这类任务在建筑行业和其他工业领域都要广泛的应用前景。Symbol Spotting 任务分类的目标是具有几何特征或更多异质特征的图元要素,不同的是,传统的基于计算机视觉,尤其是基于 CNN 网络的图像分割任务,其分割的目标元素是同质化的像素。由于存在遮挡、非均匀聚集、制图风格差异等原因,Symbol Spotting 任务具有较大的难度。

传统的 Symbol Spotting 采用的是一种名为 Query-by-example,望文生义地来看,这些方法需要提前指定特定的符号构成,然后以匹配的方式寻找图面中与之类似的元素。这种方法显然无法适应现实中复杂多变的画图方式。近年来也涌现了一些基于机器学习的方法,他们提出使用 CNN 方法来解决 Symbol Spotting 问题。但是将 CAD 矢量数据本身转化成位图天然就引入了精度损失,这使得这些方法存在先天性不足。

以建筑行业为例,建筑 CAD 图纸具有“高对比度”(或者说“高灵敏度:)的特点,在一个 100 米乘 100 米的平面空间内,在围观层面上要求达到 1 毫米的精度。为了保持这个精度,我们必须将其转化成 10 万像素乘 10 万像素的位图,这个尺寸的图像处理起来代价过于高昂。而如果减少像素数量,则无法保持 1 毫米的精度。

这篇论文的团队在之前的发表的论文中提出了 FloorPlanCAD 数据集。基于这个数据集可以训练能够进行 Panoptic Segmentation Task 的模型。这类任务是综合了实例分割和语义分割的复合任务。在 CAD Symbol Spotting 场景,这意味着需要同时得到图元级别的类型归属和具有语音含义的每个建筑构件的识别。

本文的作者通过引入图卷积神经网络 (GCN) 来解决 Symbol Spotting 问题。GCN 网络可以给出每个图元的语义信息,同时还有一个 CNN 网络来给出“可数”构件的实例分割结果。

1.2 Transformer 的作用

基于 Transformer,可以在不预先定义图连接结构的情况下,基于注意力机制推断到 tokens 之间的全局关系 (Transformers reason global relationships across tokens without pre-defined graph connectivity, by instead learning with self-attention)。这使得 Transformer 可以在 panoptic symbol spotting 任务中替代 GCN 的作用。但是标准的 Transformer 在这类任务上的使用仍然存在一些挑战:

  1. Tokenization and position encoding of graph symbols:前者是编码的问题,后者则是典型的 ViT 引入的问题。典型的 ViT,即 Visual Transformer 会将每个图像划分成 或者 个 Patch,即 token,同时基于这个二维网格分布可以将这些 token 的顺序线性化,并组合在一起。不过在 CAD 数据中,图元数据的排列是无序的,且坐标数值属于连续的实数空间,并非离散化,这和栅格化的图像有根本性的不同。
  2. Inmense set of primitives in certain scenes. ViT 的全局注意力机制的复杂度相比于 token 的数量是四次方的,如果是处理 CAD 数据,由于图元的数量非常庞大,这个数量级的复杂度是无法处理的。
  3. Training data limitations. ViT 架构能够提供更多灵活性的同时,其对数据的需求量也更大了。

作者提出的 CADTransformer 旨在成为一个通用的框架,可以被轻易地和现有的 ViT 骨干网络整合起来。

CADTransformer 是作为一个完整的 ViT 处理 Pipe 的一部分被插入到图像处理流程中。

2 Methodology

2.1 Overview

一般意义上的 Panoptic symbol spotting 任务可以表述为 ,其中 表示一个基础图元。 分别是此图元的语义标签与实例索引。对于输入的 CAD 矢量图纸,我们首先将其拆解成基础图元要素(弧线、多段线等),并将图纸转换成栅格化后的位图。图元的嵌入的获取方式是将每个图元投影到一个预训练的 CNN 网络输出的二维特征图上。基于这个这样获取的嵌入,使用一个标注你的 Vision Transformer 以及作者提出的可插拔的附加网络,模型可学习到不同的 Token 之间的关系。Transformer 层后面跟着的是 Two-Branch heads 结构,我们可以通过这个结构来优化图元级预测的精度。本文提出的模型的整体结构如下图所示

具体而言,图元 Embeding 获取的方式是将图纸的栅格化图像输入 HRNetV2-W48 这个预训练网络。此网络是一个多级多分辨率的网络,我们取不同分辨率的分支,拼成一个 的张量。对于每个几何图元,我们将其中心点投影到这个张量的第二、三个维度张成的平面上,去第一个维度的特征值构成特征向量。这样最终得到所有 token 的 Embedding 就是 ,其中 是图元的数量, 是 Embedding 的维度。

2.2 Two-Branch heads

如前文所述,Two-Branch heads 提出的目的是优化预测精度。TBH 层以 Transformer 骨干网络输出的几何 Embedding 为输入。

2.2.1 语义分割

对于聚合后的 Transformer 特征输出 ,施加一个 MLP 层,可以生成一个语义分数 。损失函数使用交叉熵

2.2.2 实例分割

CAD 图纸场景中的实例分割结果无法简单地用 BoundingBox 来表示。作者提出了一个新的方法,不是在像素图像中预测 2D BoundingBox,而是针对每个图形实体预测一个偏移向量,以便将实例实体聚集在一个公共的实例中心周围。为了更好地将实例相关的图元聚类在一起,作者使用一系列的 MLP 来编码图元的 Embedding,进而产生 个偏移向量 。针对这个偏移量的训练使用 损失函数。

其中 是一个指示变量组成的向量,只有可数构件的图元参与这个计算。在经过这个 Offset 量处理之后,可以用聚类算法对偏移后的图元中心点进行聚类。

2.3 Improve Transformer Backbone Design for Better Spotting

ViT 骨干网络以上述编码层的输出,即 Tokenization Module 的输出作为输入,执行自注意力机制,并生成送往 Two-Branch Head 网络的特征表示。

2.3.1 Self-Attension with Neighbor Awareness

典型的 ViT 的架构由多个 Transformer 层组成,每层包含一个 self-attention 模块和一个前馈网络(Feed-Forward Network) 。自注意力机制的计算方式为:

其中 分别是 query, key 和 value 矩阵, 表示样本数量和隐藏层维度。

然而,图纸中图元的数量非常多,因此全局注意力复杂度太高。为了解决这个问题,作者提出以下改进方法。

2.3.2 Self-attention with k Neighbors

让注意力机制的计算只在图元最邻近的 k 个邻居之间进行计算,这样可以限制复杂度随着图纸规模增长的速度。这里的临近关系通过计算起始点和终止点的距离来实现。

其中 表示图元 的起始点和终止点, 是目标图元, 表示其他图元。增加注意力机制计算中考虑的临近的图元数量可以增加感知的范围。作者提出的 Transformer 主干网络划分成了若干个阶段,每经过一个阶段会提提高 的值。

2.3.3 Multi-resolution Feature Fusion

我们将不同阶段得到的特征输出整合起来,令

2.3.4 Graphic Entity Position Encoding

由于图元数据天然来自于二维欧式空间,因此可以的比较容易的定义位置编码:

2.4 Random Layer Augmentation

本章节介绍的是一种数据增强策略。这个策略很简单,就是利用 CAD 图纸的图层结构信息,将图层划分为是哪个类别,分别是 Thing layer, Stuff layer 和 Background Layer。我们让这些层内随机挑选出的若干层组成新的图纸。

解决在 Openvpn 客户端中部署的 Web 服务不可用的问题

作者 治部少辅
2023年4月4日 17:32

遇到这样一个蛋疼的问题,我有一台服务器,上面部署了一个 web 服务,同时我也想把这个服务器作为客户端连入一个 OpenVPN 虚拟网络,并且我希望服务器上的程序能够通过 OpenVPN 的网关来访问外部网络,这主要是为了隐藏服务器的身份。但是在 OpenVPN 连接之后,原有的 Web 服务将无法访问。通过调试分析可以法线,造成这一现象的原因是 Web 服务的响应包也被路由配置路由到 OpenVPN 的 tun0 接口中。这导致响应无法返回给原来的请求服务器。如何解决这个问题呢?

如果你在网络上搜索这类问题你会发现各种文章给出的方案一般都是为 OpenVPN 的配置文件添加 route-nopull 选项来阻止 OpenVPN 设置客户端路由,但是这会导致客户端的对外访问无法通过 VPN 进行。事实上,从 IP 路由的角度来看,如果我的服务器是 A,而某个访问 Web 服务的 IP 是 B,同时 B 也可能是 A 试图访问的外部的目标服务提供者。对传输层路由而言,A 给 B 的 Web 服务响应和 A 主动发往 B 的请求是无法区分的。从这个角度来看,要达成 inbound 和 outbound 流量分别路由似乎是不可能的。

但是其实我们有一个突破点:那就是 Web 服务总是在本机使用固定的端口和请求者通信,我们可以通过端口来区分 inbound 和 outbound 流量。首先我们输入下面的命令:

1
iptables -t mangle -A OUTPUT -p tcp -m multiport --sports 80,443 -j MARK --set-mark 1

在这个命令中,我们会让路由表观察从本地的 80 和 443 端口发出的包,并给这些包打上一个特定的标签(--set-mark 1 的作用)。然后输入下面的命令:

1
ip route add default via [gateway addr of eth0] dev eth0 table 100

这个命令会创建一个新的路由表 table 100,然后在这个表中加入一个默认路由,将流量送往原有的网关。然后再运行下面的命令:

1
ip rule add fwmark 1 table 100

这个命令会让带有标记 1 的包使用 table 100 路由表进行路由。运行完上面的配置后,web 服务就可以正常访问呢了。


以上方法是在 ChatGPT 的协助下摸索出来的,我的提问是:

如何通过 iptables 或者 route 命令,让访问 80 与 443 端口的包使用 tun0 接口,而访问其他端口的包使用 eth0 接口?

GPT 的回答是

可以使用iptables和route命令结合起来实现此目的。具体操作如下:

首先需要添加一条规则,将访问80和443端口的包标记为一个特定的标记值(比如"1"),使用tun0接口发送出去。iptables规则如下:

1
iptables -t mangle -A OUTPUT -p tcp -m multiport --dports 80,443 -j MARK --set-mark 1

接着需要添加一条规则,将标记为"1"的包使用tun0接口发送出去。route命令如下:

1
2
ip route add default dev tun0 table 100
ip rule add fwmark 1 table 100

最后需要添加一条规则,将所有未被标记的包使用eth0接口发送出去。route命令如下:

1
ip route add default dev eth0

以上三个步骤分别实现了:将访问80和443端口的包标记,并使用tun0接口发送出去;将标记为"1"的包使用tun0接口发送出去;将所有未被标记的包使用eth0接口发送出去。这样就实现了访问80和443端口的包使用tun0接口,访问其他端口的包使用eth0接口的效果。

介绍一种"新"的隧道技术: spiped

作者 治部少辅
2023年4月2日 16:47

在之前的文章SSH隧道:访问翻墙服务器的临时性手段 的时候介绍了一种使用 SSH 隧道进行端口映射,进而进行手动的流量混淆的技术。就稳定性来说这个方案是经受了考验的。在过去几年里,无论任何时候 SSH 隧道都是能够提供稳定可靠的链接的。不过,SSH 隧道的性能是一个问题。根据 Stackoverflow 上的 这个问题下的回答 来说:“当你通过TCP隧道传输TCP时,性能问题就会出现,因为您有两个层执行自适应纠正”。在这篇文章里面我介绍一个较新的方案。

1 简介

新的方案设计一个新的工具:spiped(github)。spiped是一种安全的、轻量级的加密通信工具,可用于在不安全的网络中传输敏感数据。它的设计目标是提供简单、易于配置和使用的加密通信,而不需要复杂的密钥管理或公钥基础设施。spiped使用对称密钥加密算法,可以在客户端和服务器之间建立加密通道。它可以用于保护各种类型的数据流,包括SSH、Telnet、HTTP和SMTP等协议。spiped的另一个优点是它可以在不同的平台上使用,包括Linux、FreeBSD、OpenBSD和Windows等操作系统。

2 使用方法

spiped 的使用非常简单。在 Ubuntu 上可以通过 apt 来安装。也可以自行编译。在完成安装以后,我们首先进入服务端,运行下面的命令生成一个秘钥:

1
dd if=/dev/urandom bs=32 count=1 of=keyfile

其中 keyfile 是生成的秘钥文件,注意将其保管到合适的位置。然后用下面的服务启动 spiped 服务端服务:

1
spiped -d -s '[0.0.0.0]:8025' -t '[127.0.0.1]:25' -k /path/to/keyfile

注意,不同于 ssh 使用同一个 TCP 链接来走所有的隧道流量,spiped 会为每个流创建一个独立的 TCP 链接。因此在做代理中介使用时,spiped 可能会需要创建数量较多的 TCP 链接。spiped-n 配置可以指定最大链接数量,这个数值默认是 100,对于代理应用来说这是不够的。你可以将其设置为 10000。

其中 -s 后的地址是服务端的监听地址,-t 的转发的目标地址,-d 表示将加密的流量从 -s 后的地址解密后发往 -t 后面的地址。这里我们可以填写 Shadowsocks 服务监听的地址。注意 Host 可以保持成 127.0.0.1 的形式。这样,我们可以在防火墙中把 Shadowssocks 的公网监听端口关闭,仅允许从加密隧道访问,这样可以提升安全性。

上面的命令在输入后会自动进入 Daemon 模式运行。

然后我们来到客户端(一般是跳板机),将服务端生成的秘钥文件下载到客户端,运行

1
spiped -e -s '[127.0.0.1]:25' -t $SERVERNAME:8025 -k /path/to/keyfile

其中 $SERVERNAME 是服务端的 IP 地址。注意用 [] 包裹的形式来填写。如果这里说的客户端也是 Shadowsocks 的机器,那么这里的源地址(-s 后的地址)可以写 127.0.0.1,但是如果是跳板机,那么应该写成 0.0.0.0

在上面的设置中,方括号内可以写入 IPv6 地址,但是注意 IPv6 的全网段监听地址就不能写 [0.0.0.0] 了,而是要写 [::]

客户端的程序也是会自动进入 Daemon 模式运行。

完成设置之后,访问客户端(跳板机)的 25 端口就相当于直接访问服务器的 25 端口。

3 后记

请注意,spiped:

  • 需要一个强密钥文件:通过-k选项指定的文件应该具有至少256位的熵。(dd if=/dev/urandom bs=32 count=1是你的好朋友。)
  • 需要来自/dev/urandom的强熵。(确保您的内核的随机数生成器在引导时被种子化!)
  • 不提供任何防止通过数据包计时泄露信息的保护:在spiped上运行telnet会保护密码不会被直接从网络中读取,但不会掩盖输入节奏。
  • 对于交互式会话,可以显着增加带宽使用:它以1024字节的数据包发送数据,并将较小的消息填充到此长度,因此如果不能与相邻字节合并,则1字节的写操作可能会扩展到1024字节。
  • 使用对称密钥——因此,任何可以连接到spiped“服务器”的人也可以冒充它。

如何使用 Proxifier 来进行流量代理控制

作者 治部少辅
2023年3月30日 22:33

Proxifier 是一款网络工具软件,可以帮助用户通过代理服务器进行网络访问,实现匿名化、翻墙、加速等功能。支持多种代理协议,规则管理,流量监控和日志记录等功能,提高网络连接的灵活性和智能化。注意 Proxifier 本身并不是一个代理软件,也不是一个 VPN 软件,其作用是控制其他其他软件访问代理的方式。

上一篇文章我提到了可以试用 Proxifier 来解决通过 Shadowsocks 代理访问呢 ChatGPT 频繁出现 Something Went Wrong 的问题,在本文中我们来展开讲一下具体的做法。

1 准备工作

1.1 软件下载

Proxifier 的官方网站是 https://www.proxifier.com/,其正版价格较贵,约 40 美元。不过,较早的 2.x 版本存在破解版。读者可以根据自己的经济状况来选择。

1.2 代理准备

如同开头提到,Proxifer 本身不是一个代理程序,读者应该自己提前准备好代理。这些代理中,无论是 Shadowsocks、V2Ray 还是其他常见的代理程序,都会在客户端本地重建一个 Socks5 代理,这一般都可以在代理客户端程序的配置或者服务器设置中看到。下图给出的是 Shadowsocks-NG 客户端在 macOS 中的系统配置,可以看到程序在本地 (localhost) 的 1086 端口创建了一个 Socks5 代理。

这个代理会被进一步转成 http 代理,从而被浏览器使用,这个转换过程是导致 ChatGPT 链接不稳定的元凶。接下来我们要使用 Proxifier 让 ChatGPT 直接使用 Socks5 代理。

2 设置 Proxifier

2.1 添加代理

我们以知乎上这篇文章为范本简要讲一下设置过程。首先我们需要将现有代理软件提供的 Socks5 服务告知 Proxifier,这通过添加代理服务器实现。

注意一般各种代理软件在本地创建的代理都是无需验证的,我们可以取消验证启用。输入设置之后,可以点击检查确认设置是否正确。

3 设置规则

为了让 ChatGPT 的访问能够通过 Proxifier 访问代理,我们需要配置 Proxifier 的代理规则。

添加代理规则的页面如下:

我们有两种方法引导 ChatGPT 的网络访问:

  1. 我们可以选择下载 ChatGPT Desktop 客户端,然后通过代理规则中应用程序过滤规则找到 ChatGPT 程序,然后让目标主机为空(这意味着所有的目标主机都会服从本规则);
  2. 将应用程序部分置空,然后在目标主机中填入 ChatGPT 访问的域名。经过我的试验发现,填入 chat.openai.com; challenges.cloudflare.com; *.openai.com; 即可。

事实上,因为 Proxifier 能够全局性地处理所有网络访问,因此,第二种方法对于使用 ChatGPT Desktop 的用户来说同样有效。下图是完整代理规则。

至此我们就完成了设置,你可能需要重启浏览器才能能让 Proxifier 成功地引导 ChatGPT 的流量。

BIM 中的几何表达方式

作者 治部少辅
2023年2月17日 16:31

三维几何表示时 BIM 模型的重要基础。这篇文章主要阐述 BIM 中几何模型设计原则与基本思想。

1 BIM 场景下的几何建模

一个建筑信息模型的包含的的建筑规划与维护所需要的所有相关信息。三维形态的几何苗猪相比于传统的二维图纸具有巨大的有事:

  1. 可以用统一模型描述整个建筑项目,而不需要拆解成多个图框与子项,进而可以确保不同部分的图纸内容的一致性;但仅凭三维模型,尚不足与生成与现行规范相符合的图纸,模型中还需要更多的信息,例如建筑类型,材料等;
  2. 三维模型可以更好地排查方案规划中的建筑元素冲突情况;
  3. 更便捷的算量计算;
  4. 更好地服务于后续计算与仿真;
  5. 能够更便捷地生成效果图;

一般而已,衡量一个 BIM 建模工具的几何能力的核心要素是其使用的几何建模型核心的质量。目前使用最为广泛的几何工具包括了 ACIS 和 ParaSolid。

2 Solid Modeling

建模三维实心几何体的方法有两种不同的方向:

  1. Explicit Modeling,即显式模型,此种方法通常是直接描述几何体的表面来描述实心体本身。这类方法通常也被称为 BRep (Boundary Representation);
  2. Implicit Modeling,即隐式模型,此方法是通过描述构造过程来描述几何体本身。

这两种方法在 BIM 建模工具中都会使用到,且都是 IFC 标准的一部分。

2.1 Explicit Modeling

2.1.1 BRep

Boundary Representation 是最为广泛使用的三维几何描述模型。其核心思想是构造一系列边界元素的树状结构。通常这种树状结构包含了点 (Vertex)、线 (Edge)、面 (Face)、体 (Body) 等。每个元素都是由更底层的子元素来描述。这个树状结构代表了几何体的拓扑结构,如下图所示:

整个拓扑结构我们可以用一个图数据结构来表示,这个图数据结构被称为 vertex-edge-face graph,简称为 vef 图。

在拓扑结构的基础上,还需要增加几何维度信息来才能充分表示几何体。如果偶几何体只包含了直线和平面,则只需要顶点的几何坐标信息就可以充分表示几何体。但是如果几何体中包含了曲线和曲面,则需要更多的几何描述信息。

用来描述拓扑信息的数据结构通是多个变长列表。体用围合它的面来表示,面则用围合它的边来表示,每个边由其起点和终点来表示。不过这种简单的数据结构只适用于简单的,没有孔洞和开口的几何体。为了表示更加复杂的几何体,我们必须要扩展这一数据结构。

ACIS 的几何数据结构设计。

上图给出了 ACIS 使用的几何数据结构设计,ACIS 在多种 CAD 和 BIM 软件中得到应用。在这个数据结构中,Body 有若干个彼此不连通的 Lump 组成、Lump 则由若干个 Shell 来描述(因此可以描述孔洞和开口)。每个 Shell 由若干个 Faces 来表示。每个 Face 由一个或者多个 Loop 来表示。由于每个 Face 可以有多个 Loop,因此可以表示含孔的多边形。

上述模型的一个重要特点是 Loop 本身并不直接指向边,而是通过 CoEdge 这个中间媒介。图中的下半部分显示了几何数据可以如何同拓扑数据整合起来。

2.1.2 基于三角面片的几何表达

这种方法是对 BRep 方法的简化。通过牺牲一定程度的精确度,可以将曲面表示为一系列的三角形。当然,三角形越小表示就越精确。三角面片表达方式通常用来做可视化。除了精度问题,三角面片表示方法也有更多的存储空间需求;

三角面片表达方式的底层数据结构的组织形式为 Indexed Face Set,其中每个顶点的坐标被存储为有序列表,每个三角面片由其在列表中的索引下表来定义。这种方法可以避免存储冗余的点坐标信息。同时避免因计算误差导致几何错误(三角面片之间出现错位)。

2.2 Implicit Modeling

如前面论述过的,Implicit 建模方法的核心思量是记录几何体被狗砸偶读过程,因此 Implicit Modeling 也被称为 procedural methods。BIM 和 CAD 软件在实践过程中会对生成的几何体提取快照,从而降低每次加载时的计算时间。

2.2.1 Cosntructive Solid Geometry (CSG)

CSG 是一个经典的面向构造过程的几何模型。CSG 模型在表示几何体时会选择一些基础元素(被称为 primitves),如立方体,圆柱体,棱锥结合几何布尔运算(交、并、差)等来创建更为复杂的几何体。

CSG 的一个例子。

表达能力的相对受限使得 CSG 模型在 BIM 场景中基本不会单独使用。

2.2.2 挤出和旋转方法

很多 CAD 或者 BIM 工具提供了通过挤出和旋转操作来代构造三维几何体的操作方法,其操作过程通常是将一个二维图形(通常是一个闭合曲线)沿着三维曲线移动。如果移动的路径是直线,则这个操作被称为 Extrustion,如果是曲线。则被称为 Sweep。作为出发点的二维图形在移动的过程中其朝向可以根据行进方向改变,亦可以保持不变。

旋转操作的定义非常直接,就是将一个二维梯形沿着一个三维上的轴进行旋转。

Lofting 是一种特殊的变种操作。在这个操作中用户需要定义两个面,程序会基于差值方法建立二者之间的过程,并在这一过程中构造出立方体(见上图)。

2.3 两种方法的对比

❌
❌