⌈项目⌋ 实现可靠完备的、与传输层无关的文件传输

在本文中,“上传”指 客户端(JS)将文件发送到服务端(Go),而“下载”则指 客户端从服务端获取文件。

文件下载的实现

Server 端(GO)

semaphore.go实现动态加权信号量分配,修改于golang.org/x/sync/semaphore

dchan.gosemaphore.go的基础上实现了一种带背压的 channel,使用 Little’s Law 简单方法自动调整缓冲区大小。所实现的 Write 和 Read 均为 Context-aware 的阻塞函数,可同时具有多个消费者和生产者。

这两个包允许通过 interface 传入可修改权重,可实现动态调整任务优先级。

之所以这样做,是因为实践证明 Gstreamer 的 Datachannel 的 buffer 缓冲区很大,无限制地压入待发送数据会导致很难低成本无开销地丢弃无用数据。同时,高带宽下避免磁盘 IO 繁忙,预缓冲足够的数据;低带宽时避免过度预读占用内存。

以下为不同带宽下的表现。

较低带宽下正常传输

较低带宽下终止传输

较高带宽下开始传输

较高带宽下传输完成

附图表说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// "github.com/guptarohit/asciigraph"
go func() {
data := make([][]float64, 3)

for {
select {
case <-newPeer.ctx.Done():
return
default:
data[0] = append(data[0], float64(newPeer.sendChan.InUse())/1024/1024)
data[1] = append(data[1], float64(newPeer.sendChan.Bps())/1024/1024)
data[2] = append(data[2], float64(newPeer.sendChan.Limit())/1024/1024)
for i := 0; i < len(data); i++ {
if len(data[i]) > 100 {
data[i] = data[i][len(data[i])-100:]
}
}

graph := asciigraph.PlotMany(data,
asciigraph.Precision(2),
asciigraph.Height(20),
asciigraph.SeriesColors(asciigraph.Red, asciigraph.Green, asciigraph.Blue),
asciigraph.SeriesLegends("allocated", "bps", "limit"),
asciigraph.Caption("graph of "+uid))
fmt.Println(graph)
time.Sleep(1000 * time.Millisecond)
}
}
}()

探索 WebRTC DataChannel 在不同消息体大小下的传输速率极限

浏览器为 Microsoft Edge,版本 139.0.3405.111 (正式版本) (64 位);使用dd if=/dev/urandom of=randomfile.dat bs=1m count=4096生成 4GB 文件,通过 WebRTC Datachannel 传输,ICE candidate 为 localhost udp,文件块大小为 12KB,下载耗时 140s。

第一次测试

第二次测试

第三次测试

文件分块为 60KB,下载耗时约 170s。
第一次测试

第二次测试

第三次测试

在不修改底层实现的前提下,将分块设置为小于默认 MTU 大小的 1KB,并不能突破 Gstreamer 的 Datachannel 的传输极限。

alt text

尝试修改分块大小,观察数据。当分块大小为 4KB 时,前几次测试前期能够始终保持 8K messagesReceived/s,随后浏览器卡住,无法完成下载测试。重启浏览器后,多次测试表现大致相同。

测试结果

Client 端

TODO

文件上传的实现

Server 端(GO)

combiner.go用于二叉合并,思想来自 blake。而blake3.go则是 blake3 的简单实现。

range.go待写,用于确定特定区间是否完成传输。

实现理由:为了实现一致性保证的断点续传,旧方案会先计算完整文件的哈希再上传,导致大文件上传时初始化阶段耗时长,上传进度始终为零,用户体感差。新方案改为分块计算哈希,边算边比较边传。

用于支持断点续传的临时文件

1
2
3
4
5
6
7
8
9
10
11
RDF/1
Version: 1
Algorithm: <0..15>
File-Size: <uint64> ; bytes
Chunk-Size: <uint64> ; bytes
Chunks: ceil(File-Size / Chunk-Size)
Chunk-Status-Length: ceil(Chunks / 8) ; bytes
Fingerprint-Length: L ; hex characters
Fingerprint-Encoding: packed-nibbles (ceil(L/2) bytes, odd L -> low nibble padded)
Trailer-Layout: [L:uint8][File-Size:uint64][Chunk-Size:uint64][Meta:uint8(V<<4|Alg)]
Byte-Order: big-endian for all uint64
1
2
3
4
5
+----------------------+----------------------+----------------------+------------------+
| Payload | Chunk-Status | Fingerprint | Trailer |
| (0..N) | ceil(Chunks/8) B | ceil(L/2) B | 18 B |
+----------------------+----------------------+----------------------+------------------+
^ EOF

Client 端

TODO