Refactor Mihomo traffic and memory handling to use streaming GET requests
- Updated `GATEWAY_RUN.md` to clarify the use of streaming HTTP GET for traffic and memory metrics instead of WebSocket connections, addressing potential issues with WebSocket stability behind nginx. - Modified the Svelte component to implement streaming GET requests for `/traffic` and `/memory`, improving data retrieval and reducing connection issues. - Enhanced the `recordRates` function to handle totals from the streaming response, ensuring accurate metric tracking.
This commit is contained in:
+2
-2
@@ -112,7 +112,7 @@ services:
|
||||
|
||||
и в `config.yaml` для нужного сервера: `mihomo_base_url_env: MIHOMO_CONTROLLER_URL`, `mihomo_authorization_env: TELEMT_MIHOMO_AUTH`.
|
||||
|
||||
За **reverse proxy** (nginx) перед панелью убедитесь, что для WebSocket проксируются заголовки `Upgrade` и `Connection`.
|
||||
За **reverse proxy** (nginx) перед панелью убедитесь, что для WebSocket проксируются заголовки `Upgrade` и `Connection`. Панель Mihomo тянет трафик и память **потоковым HTTP** (не WS); при задержке или «залипании» графиков за nginx включите для upstream шлюза `proxy_buffering off` (или эквивалент), чтобы не буферизовать длинный ответ.
|
||||
|
||||
<a id="mihomo-debug-400"></a>
|
||||
|
||||
@@ -126,7 +126,7 @@ services:
|
||||
|-----|--------|
|
||||
| **REST** (GET `/version`, `/proxies`, `/connections`, POST к API контроллера и т.д.) | Тот же путь, что и прокси к Telemt: `http.NewRequest` + `RoundTrip` (`internal/proxy/forward.go`, общая логика `newAliasForward`). Так совпадает с рабочим `curl` к upstream. |
|
||||
| **Не** единый `httputil.ReverseProxy` на весь Mihomo | Для обычного HTTP `ReverseProxy` нередко даёт **400** у строгих upstream при том, что прямой запрос работает. |
|
||||
| **WebSocket** (`/traffic`, `/memory`, …) | Отдельно `httputil.ReverseProxy` + `Rewrite` (`internal/proxy/mihomo.go`). |
|
||||
| **WebSocket** (`/traffic`, `/memory`, …) | На upstream — отдельно `httputil.ReverseProxy` + `Rewrite` (`internal/proxy/mihomo.go`). **Веб-панель** для графиков скорости и памяти использует **потоковый HTTP GET** к тем же путям (как в Mihomo без `Upgrade`): так же проходит через шлюз, что и `/connections`, без WebSocket за nginx. |
|
||||
| Заголовок **Host** | Не пересылается с клиента; на upstream уходит authority из `mihomo_base_url` (как для Telemt и `base_url`). |
|
||||
|
||||
Проверки конфигурации:
|
||||
|
||||
@@ -20,7 +20,10 @@ export function mihomoUrl(alias: string, path: string): string {
|
||||
return `${gatewayBase()}/api/${encodeURIComponent(alias)}/mihomo/${p}`;
|
||||
}
|
||||
|
||||
/** WebSocket к тому же origin (ws / wss). */
|
||||
/**
|
||||
* WebSocket к тому же origin (ws / wss).
|
||||
* Для /traffic и /memory во фронте предпочтительнее потоковый GET через `mihomoUrl` — тот же прокси, что и для REST; WS из браузера часто рвётся за nginx.
|
||||
*/
|
||||
export function mihomoWsUrl(alias: string, path: string): string {
|
||||
const p = path.replace(/^\/+/, '');
|
||||
const base = gatewayBase();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
fetchMihomoJson,
|
||||
fetchMihomoMeta,
|
||||
mihomoPut,
|
||||
mihomoWsUrl
|
||||
mihomoUrl
|
||||
} from '$lib/api/client.js';
|
||||
import type {
|
||||
MihomoConnectionsResponse,
|
||||
@@ -48,8 +48,6 @@
|
||||
const MAX_POINTS = 72;
|
||||
let lastTick = $state<number | null>(null);
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
let wsTraffic: WebSocket | null = null;
|
||||
let wsMemory: WebSocket | null = null;
|
||||
|
||||
let proxiesLoading = $state(false);
|
||||
let proxiesErr = $state<string | null>(null);
|
||||
@@ -58,8 +56,17 @@
|
||||
let testing = $state<string | null>(null);
|
||||
let testingGroup = $state<string | null>(null);
|
||||
|
||||
function recordRates(now: number, up: number, down: number) {
|
||||
if (lastTick != null) {
|
||||
/** Mihomo отдаёт upTotal/downTotal в потоке /traffic — используем их; иначе накапливаем из up/down. */
|
||||
function recordRates(
|
||||
now: number,
|
||||
up: number,
|
||||
down: number,
|
||||
totals?: { upTotal: number; downTotal: number }
|
||||
) {
|
||||
if (totals) {
|
||||
totalUp = totals.upTotal;
|
||||
totalDown = totals.downTotal;
|
||||
} else if (lastTick != null) {
|
||||
const dt = (now - lastTick) / 1000;
|
||||
if (dt > 0 && dt < 5) {
|
||||
totalUp += up * dt;
|
||||
@@ -90,38 +97,89 @@
|
||||
}
|
||||
}
|
||||
|
||||
function connectWs(a: string) {
|
||||
if (!browser) return;
|
||||
wsTraffic?.close();
|
||||
wsMemory?.close();
|
||||
lastTick = null;
|
||||
/**
|
||||
* Потоковый GET /traffic и /memory (как в hub/route/server.go Mihomo: без Upgrade — chunked JSON/s).
|
||||
* Работает через тот же HTTP-прокси, что и /connections (Authorization на шлюзе).
|
||||
* WebSocket с браузера часто рвётся за nginx или без заголовков — метрики остаются нулями.
|
||||
*/
|
||||
async function pumpMihomoTrafficStream(a: string, signal: AbortSignal) {
|
||||
try {
|
||||
wsTraffic = new WebSocket(mihomoWsUrl(a, 'traffic'));
|
||||
wsTraffic.onmessage = (ev) => {
|
||||
try {
|
||||
const o = JSON.parse(ev.data as string) as { up?: number; down?: number };
|
||||
recordRates(performance.now(), Number(o.up) || 0, Number(o.down) || 0);
|
||||
} catch {
|
||||
/* ignore */
|
||||
const res = await fetch(mihomoUrl(a, 'traffic'), { signal });
|
||||
if (!res.ok || !res.body) return;
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = '';
|
||||
while (!signal.aborted) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
for (;;) {
|
||||
const nl = buf.indexOf('\n');
|
||||
if (nl < 0) break;
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (!line) continue;
|
||||
try {
|
||||
const o = JSON.parse(line) as {
|
||||
up?: number;
|
||||
down?: number;
|
||||
upTotal?: number;
|
||||
downTotal?: number;
|
||||
};
|
||||
const up = Number(o.up) || 0;
|
||||
const down = Number(o.down) || 0;
|
||||
const totals =
|
||||
typeof o.upTotal === 'number' && typeof o.downTotal === 'number'
|
||||
? { upTotal: o.upTotal, downTotal: o.downTotal }
|
||||
: undefined;
|
||||
recordRates(performance.now(), up, down, totals);
|
||||
} catch {
|
||||
/* ignore line */
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
/* aborted or network */
|
||||
}
|
||||
}
|
||||
|
||||
async function pumpMihomoMemoryStream(a: string, signal: AbortSignal) {
|
||||
try {
|
||||
wsMemory = new WebSocket(mihomoWsUrl(a, 'memory'));
|
||||
wsMemory.onmessage = (ev) => {
|
||||
try {
|
||||
const o = JSON.parse(ev.data as string) as { inuse?: number };
|
||||
memKB = Number(o.inuse) || 0;
|
||||
histMem = [...histMem, memKB];
|
||||
while (histMem.length > MAX_POINTS) histMem.shift();
|
||||
} catch {
|
||||
/* ignore */
|
||||
const res = await fetch(mihomoUrl(a, 'memory'), { signal });
|
||||
if (!res.ok || !res.body) return;
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buf = '';
|
||||
while (!signal.aborted) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
buf += decoder.decode(value, { stream: true });
|
||||
for (;;) {
|
||||
const nl = buf.indexOf('\n');
|
||||
if (nl < 0) break;
|
||||
const line = buf.slice(0, nl).trim();
|
||||
buf = buf.slice(nl + 1);
|
||||
if (!line) continue;
|
||||
try {
|
||||
const o = JSON.parse(line) as { inuse?: number };
|
||||
memKB = Number(o.inuse) || 0;
|
||||
histMem = [...histMem, memKB];
|
||||
while (histMem.length > MAX_POINTS) histMem.shift();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
/* aborted or network */
|
||||
}
|
||||
}
|
||||
|
||||
async function runTrafficMemoryStreams(a: string, signal: AbortSignal) {
|
||||
while (!signal.aborted) {
|
||||
await Promise.all([pumpMihomoTrafficStream(a, signal), pumpMihomoMemoryStream(a, signal)]);
|
||||
if (signal.aborted) break;
|
||||
await new Promise((r) => setTimeout(r, 3000));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,20 +222,27 @@
|
||||
$effect(() => {
|
||||
const a = alias;
|
||||
if (!a || !browser) return;
|
||||
lastTick = null;
|
||||
totalUp = 0;
|
||||
totalDown = 0;
|
||||
upBps = 0;
|
||||
downBps = 0;
|
||||
memKB = 0;
|
||||
histUp = [];
|
||||
histDown = [];
|
||||
histMem = [];
|
||||
void loadMeta(a);
|
||||
connectWs(a);
|
||||
const streamAc = new AbortController();
|
||||
void runTrafficMemoryStreams(a, streamAc.signal);
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
pollTimer = setInterval(() => void pollConnections(a), 2000);
|
||||
void pollConnections(a);
|
||||
return () => {
|
||||
streamAc.abort();
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
pollTimer = null;
|
||||
}
|
||||
wsTraffic?.close();
|
||||
wsMemory?.close();
|
||||
wsTraffic = null;
|
||||
wsMemory = null;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -269,8 +334,6 @@
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollTimer) clearInterval(pollTimer);
|
||||
wsTraffic?.close();
|
||||
wsMemory?.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user