feat(overlay): 覆盖网络线序①–⑮ 代码与测试产物入库
覆盖网络线累积产物(此前只在工作区、未入版本库):
- 新增 relay 子系统 src/net/relay/**(wire/duplex/server/client/dialer/switcher/directory/identity/keys/placement/network/addr-override/main/index)
- 新增 src/worker/relay-tunnel.ts、src/web/routes/overlay.ts
- 新增观测/演练脚本 overlay-probe、overlay-failover-drill、overlay-keyring、overlay-holepunch、overlay-jitter、overlay-wan、overlay-relaykey-add、relay-mem-calibrate
- 新增测试 12 个(relay / relay-failover / remote-spawner / instance-port / overlay-{network,auth,bootstrap,identity} / remote-user-fs 等)
验收基线:npm test = 162 pass / 0 fail / 1 skip;--scene all = 12 PASS / 0 SKIP / 0 FAIL;overlay-probe = 12/12
This commit is contained in:
1 parent
640813e84e
commit
146c3d25ef
53 files changed
+16187
-63
No files matched your search
@@ -0,0 +1,357 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* 覆盖网络 序⑥ · S2/S5 一次性载荷与探针(⛔ 不进产品路径、⛔ 不进 package.json 依赖)。
|
||||
*
|
||||
* 为什么要有它:参数表 §3.5 的 `WAN_STEADY_THROUGHPUT` 与 §3.3 的 `PER_PLAYER_BW_LOCAL`
|
||||
* 都是 `待测` —— 两端都拿不到"可读大响应"(无凭据时只回 24–68 B 的 401/404)。
|
||||
* 本脚本只做一件事:**造可控载荷 + 量稳态速率**,供人工写回参数表。
|
||||
*
|
||||
* ## 四种角色
|
||||
* | 角色 | 跑在哪 | 干什么 |
|
||||
* |---|---|---|
|
||||
* | `--serve` | 被压的一端(106) | HTTP:`/blob?sec=N`(连续吐 N 秒)/`/blob?mb=N`/`POST /sink`(吞体) |
|
||||
* | `--download` | 挤压的一端(47) | 连 relay 回环端点 → 丢前 `--drop` 秒 → 量 `--secs` 秒 ⇒ **被压端 → 挤压端** |
|
||||
* | `--upload` | 挤压的一端(47) | 反向 POST 大流量(**尊重反压**)⇒ **挤压端 → 被压端** |
|
||||
* | `--echo-serve` / `--players` | 106 / 47 | S5 合成玩家:长度前缀回显 + 多连接消息率扫描 |
|
||||
*
|
||||
* ## 口径(⛔ 别丢,写回参数表时要抄)
|
||||
* - 速率 = **稳态段字节数 ÷ 稳态段秒数**(前 `--drop` 秒的字节**单独计数、不进分子**);
|
||||
* - `--upload` 的速率**必须**在 `write()` 返回 false 时停下等 `drain` —— 否则量的是 Node 的
|
||||
* 内存缓冲,不是链路(这正是"看起来很快、其实全在本地队列里"这个假象的成因)。
|
||||
*
|
||||
* 用法示例(远端):
|
||||
* node overlay-wan.cjs --serve --port 19777 --secs 600
|
||||
* node overlay-wan.cjs --download --port <relay回环口号> --secs 30 --drop 3
|
||||
* node overlay-wan.cjs --upload --port <relay回环口号> --secs 30 --drop 3 --mb 0
|
||||
*
|
||||
* @module scripts/overlay-wan
|
||||
*/
|
||||
|
||||
'use strict'
|
||||
|
||||
const http = require('node:http')
|
||||
const net = require('node:net')
|
||||
|
||||
function parseArgs(argv) {
|
||||
const out = {}
|
||||
for (let i = 0; i < argv.length; i++) {
|
||||
const a = argv[i]
|
||||
if (!a.startsWith('--')) continue
|
||||
const k = a.slice(2)
|
||||
const v = argv[i + 1]
|
||||
if (v === undefined || v.startsWith('--')) out[k] = true
|
||||
else {
|
||||
out[k] = isNaN(Number(v)) ? v : Number(v)
|
||||
i++
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const args = parseArgs(process.argv.slice(2))
|
||||
const log = (s) => process.stderr.write(`[wan] ${s}\n`)
|
||||
const out = (obj) => process.stdout.write(`### RESULT ### ${JSON.stringify(obj)}\n`)
|
||||
|
||||
const CHUNK = Buffer.alloc(64 * 1024, 0x41)
|
||||
|
||||
// ─────────────────────────── serve ───────────────────────────
|
||||
function serve(port, secs) {
|
||||
const server = http.createServer((req, res) => {
|
||||
const u = new URL(req.url, 'http://x')
|
||||
if (req.method === 'POST') {
|
||||
// 吞体:只数不清
|
||||
let n = 0
|
||||
req.on('data', (c) => {
|
||||
n += c.length
|
||||
})
|
||||
req.on('end', () => {
|
||||
res.writeHead(200, { 'content-type': 'text/plain', connection: 'close' })
|
||||
res.end(`sink ${n}\n`)
|
||||
log(`POST /sink ${n} B in ${Date.now() - t0} ms`)
|
||||
})
|
||||
var t0 = Date.now()
|
||||
return
|
||||
}
|
||||
if (u.pathname === '/small') {
|
||||
res.writeHead(200, { 'content-type': 'text/plain', connection: 'close' })
|
||||
res.end('ok-0123456789-0123456789\n')
|
||||
return
|
||||
}
|
||||
const sec = Number(u.searchParams.get('sec') || 0)
|
||||
const mb = Number(u.searchParams.get('mb') || 0)
|
||||
if (sec > 0) {
|
||||
res.writeHead(200, { 'content-type': 'application/octet-stream', connection: 'close' })
|
||||
const start = Date.now()
|
||||
let sent = 0
|
||||
const pump = () => {
|
||||
while (Date.now() - start < sec * 1000) {
|
||||
if (!res.write(CHUNK)) {
|
||||
sent += CHUNK.length
|
||||
res.once('drain', pump)
|
||||
return
|
||||
}
|
||||
sent += CHUNK.length
|
||||
}
|
||||
res.end()
|
||||
log(`GET /blob?sec=${sec} 送出 ${sent} B`)
|
||||
}
|
||||
pump()
|
||||
return
|
||||
}
|
||||
res.writeHead(200, { 'content-type': 'application/octet-stream', connection: 'close' })
|
||||
const total = mb * 1024 * 1024
|
||||
let sent = 0
|
||||
const pump = () => {
|
||||
while (sent < total) {
|
||||
if (!res.write(CHUNK)) {
|
||||
sent += CHUNK.length
|
||||
res.once('drain', pump)
|
||||
return
|
||||
}
|
||||
sent += CHUNK.length
|
||||
}
|
||||
res.end()
|
||||
}
|
||||
pump()
|
||||
})
|
||||
server.listen(port, '127.0.0.1', () => log(`serve 127.0.0.1:${port} secs=${secs}`))
|
||||
if (secs > 0) setTimeout(() => process.exit(0), secs * 1000)
|
||||
}
|
||||
|
||||
// ─────────────────────── download / upload ───────────────────────
|
||||
/** 从 `buf` 里切掉 HTTP 头,返回剩下的体;找不到头返回 null。 */
|
||||
function splitHeader(buf) {
|
||||
const i = buf.indexOf('\r\n\r\n')
|
||||
if (i < 0) return null
|
||||
return buf.subarray(i + 4)
|
||||
}
|
||||
|
||||
function download(port, secs, drop) {
|
||||
const t0 = Date.now()
|
||||
const sock = net.connect(port, '127.0.0.1')
|
||||
let hdrDone = false
|
||||
let tail = Buffer.alloc(0)
|
||||
let ramp = 0
|
||||
let steady = 0
|
||||
sock.on('connect', () => {
|
||||
sock.write(
|
||||
`GET /blob?sec=${Math.ceil(secs + drop + 6)} HTTP/1.1\r\nHost: wan\r\nConnection: close\r\n\r\n`,
|
||||
)
|
||||
})
|
||||
const finish = () => {
|
||||
try {
|
||||
sock.destroy()
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
out({
|
||||
mode: 'download',
|
||||
port,
|
||||
dropS: drop,
|
||||
steadyS: secs,
|
||||
rampBytes: ramp,
|
||||
steadyBytes: steady,
|
||||
steadyKBps: Math.round((steady / 1024 / secs) * 10) / 10,
|
||||
ms: Date.now() - t0,
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
sock.on('data', (b) => {
|
||||
if (!hdrDone) {
|
||||
tail = Buffer.concat([tail, b])
|
||||
const body = splitHeader(tail)
|
||||
if (body === null) return
|
||||
hdrDone = true
|
||||
b = body
|
||||
}
|
||||
const el = Date.now() - t0
|
||||
if (el < drop * 1000) ramp += b.length
|
||||
else if (el < (drop + secs) * 1000) steady += b.length
|
||||
else finish()
|
||||
})
|
||||
sock.on('error', (e) => {
|
||||
log(`sock error ${e.message}`)
|
||||
finish()
|
||||
})
|
||||
setTimeout(finish, (drop + secs + 20) * 1000)
|
||||
}
|
||||
|
||||
function upload(port, secs, drop) {
|
||||
const t0 = Date.now()
|
||||
const sock = net.connect(port, '127.0.0.1')
|
||||
let ramp = 0
|
||||
let steady = 0
|
||||
let acc = 0
|
||||
let started = false
|
||||
const head =
|
||||
'POST /sink HTTP/1.1\r\nHost: wan\r\nContent-Type: application/octet-stream\r\n' +
|
||||
'Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n'
|
||||
const frame = Buffer.concat([
|
||||
Buffer.from((CHUNK.length).toString(16) + '\r\n'),
|
||||
CHUNK,
|
||||
Buffer.from('\r\n'),
|
||||
])
|
||||
const finish = () => {
|
||||
try {
|
||||
sock.destroy()
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
out({
|
||||
mode: 'upload',
|
||||
port,
|
||||
dropS: drop,
|
||||
steadyS: secs,
|
||||
rampBytes: ramp,
|
||||
steadyBytes: steady,
|
||||
steadyKBps: Math.round((steady / 1024 / secs) * 10) / 10,
|
||||
ms: Date.now() - t0,
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
const pump = () => {
|
||||
const el = Date.now() - t0
|
||||
if (el >= (drop + secs) * 1000) return finish()
|
||||
let ok = true
|
||||
while (ok) {
|
||||
ok = sock.write(frame)
|
||||
if (el < drop * 1000) ramp += CHUNK.length
|
||||
else steady += CHUNK.length
|
||||
if (Date.now() - t0 >= (drop + secs) * 1000) break
|
||||
}
|
||||
if (!ok) sock.once('drain', pump)
|
||||
else setImmediate(pump)
|
||||
}
|
||||
sock.on('connect', () => {
|
||||
sock.write(head)
|
||||
started = true
|
||||
pump()
|
||||
})
|
||||
sock.on('error', (e) => {
|
||||
log(`sock error ${e.message}`)
|
||||
finish()
|
||||
})
|
||||
setTimeout(finish, (drop + secs + 30) * 1000)
|
||||
}
|
||||
|
||||
// ─────────────────────── S5:合成玩家 ───────────────────────
|
||||
function echoServe(port) {
|
||||
const server = net.createServer((c) => {
|
||||
c.setNoDelay(true)
|
||||
let buf = Buffer.alloc(0)
|
||||
c.on('data', (d) => {
|
||||
buf = Buffer.concat([buf, d])
|
||||
for (;;) {
|
||||
if (buf.length < 4) return
|
||||
const n = buf.readUInt32BE(0)
|
||||
if (buf.length < 4 + n) return
|
||||
const payload = buf.subarray(4, 4 + n)
|
||||
buf = buf.subarray(4 + n)
|
||||
c.write(payload)
|
||||
}
|
||||
})
|
||||
c.on('error', () => c.destroy())
|
||||
})
|
||||
server.listen(port, '127.0.0.1', () => log(`echo-serve 127.0.0.1:${port}`))
|
||||
}
|
||||
|
||||
function p95(arr) {
|
||||
if (arr.length === 0) return 0
|
||||
const s = [...arr].sort((a, b) => a - b)
|
||||
return s[Math.min(s.length - 1, Math.floor(s.length * 0.95))]
|
||||
}
|
||||
|
||||
function playOne(port, rate, secs, msgBytes, done) {
|
||||
const sentAt = []
|
||||
const rtts = []
|
||||
let sent = 0
|
||||
let recv = 0
|
||||
const sock = net.connect(port, '127.0.0.1')
|
||||
const frame = Buffer.alloc(4 + msgBytes, 0x42)
|
||||
frame.writeUInt32BE(msgBytes, 0)
|
||||
let buf = Buffer.alloc(0)
|
||||
let stopped = false
|
||||
const t0 = Date.now()
|
||||
const report = () => {
|
||||
if (stopped) return
|
||||
stopped = true
|
||||
const s = [...rtts].sort((a, b) => a - b)
|
||||
const half = rtts.map((x) => x / 2).sort((a, b) => a - b)
|
||||
done({
|
||||
sent,
|
||||
recv,
|
||||
lossPct: sent === 0 ? 0 : Math.round(((sent - recv) / sent) * 10000) / 100,
|
||||
rttP50: Math.round((s[Math.floor(s.length * 0.5)] || 0) * 10) / 10,
|
||||
rttP95: Math.round(p95(rtts) * 10) / 10,
|
||||
oneWayP50: Math.round((half[Math.floor(half.length * 0.5)] || 0) * 10) / 10,
|
||||
oneWayP95: Math.round(p95(half) * 10) / 10,
|
||||
})
|
||||
}
|
||||
sock.on('connect', () => {
|
||||
sock.setNoDelay(true)
|
||||
const timer = setInterval(() => {
|
||||
if (Date.now() - t0 > secs * 1000) {
|
||||
clearInterval(timer)
|
||||
sock.end()
|
||||
report()
|
||||
return
|
||||
}
|
||||
sentAt.push(Date.now())
|
||||
sock.write(frame)
|
||||
sent++
|
||||
}, Math.max(1, Math.round(1000 / rate)))
|
||||
})
|
||||
sock.on('data', (d) => {
|
||||
buf = Buffer.concat([buf, d])
|
||||
while (buf.length >= msgBytes) {
|
||||
buf = buf.subarray(msgBytes)
|
||||
const t = sentAt[recv]
|
||||
if (t !== undefined) rtts.push(Date.now() - t)
|
||||
recv++
|
||||
}
|
||||
})
|
||||
sock.on('error', (e) => log(`player err ${e.message}`))
|
||||
sock.on('close', report)
|
||||
}
|
||||
|
||||
function players(port, n, rate, secs, msgBytes) {
|
||||
const results = []
|
||||
let left = n
|
||||
for (let i = 0; i < n; i++) {
|
||||
setTimeout(() => {
|
||||
playOne(port, rate, secs, msgBytes, (r) => {
|
||||
results.push(r)
|
||||
if (--left === 0) {
|
||||
const agg = (k) => {
|
||||
const a = results.map((x) => x[k]).sort((x, y) => x - y)
|
||||
return a[Math.floor(a.length / 2)]
|
||||
}
|
||||
out({
|
||||
mode: 'players',
|
||||
players: n,
|
||||
rate,
|
||||
msgBytes,
|
||||
secs,
|
||||
perPlayerKBps: Math.round(((rate * msgBytes) / 1024) * 100) / 100,
|
||||
aggThroughputKBps: Math.round(((rate * msgBytes * n) / 1024) * 10) / 10,
|
||||
lossPctMax: Math.max(...results.map((r) => r.lossPct)),
|
||||
oneWayP50: agg('oneWayP50'),
|
||||
oneWayP95: agg('oneWayP95'),
|
||||
})
|
||||
process.exit(0)
|
||||
}
|
||||
})
|
||||
}, i * 60)
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────── main ───────────────────────────
|
||||
if (args.serve) serve(args.port, args.secs || 0)
|
||||
else if (args['echo-serve']) echoServe(args.port)
|
||||
else if (args.download) download(args.port, args.secs, args.drop)
|
||||
else if (args.upload) upload(args.port, args.secs, args.drop)
|
||||
else if (args.players) players(args.port, args.players, args.rate, args.secs, args['msg-bytes'] || 200)
|
||||
else {
|
||||
process.stderr.write('usage: --serve --port P [--secs N] | --download|--upload --port P --secs S --drop D | --echo-serve --port P | --players --port P --players N --rate R --secs S\n')
|
||||
process.exit(2)
|
||||
}
|
||||
Reference in new issue
Block a user