边缘网关 + Agent 的端口中继:Agent 主动连出到网关,把内网目标挂到网关端口上; 通过邀请链接按设备授权,可选审批,首次连接起计时,到期、闲置自动收回。 - portrelay-server:边缘网关(端口池、白名单与授权租约、邀请与授权网页) - portrelay-agent:Agent(yamux 隧道、本地 API 与网页面板、流量统计) - mac-native:SwiftUI 菜单栏 App
128 lines
3.2 KiB
Go
128 lines
3.2 KiB
Go
package client
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
rateStep = 5 * time.Second
|
|
ratePoints = 120 // 10 分钟
|
|
totalSeries = "__total__"
|
|
)
|
|
|
|
// RateSeries is a fixed-length rate history (oldest first). The last point is the
|
|
// current, still-accumulating bucket so the chart moves every second.
|
|
type RateSeries struct {
|
|
End int64 `json:"end"` // 最后一个点所在 5 秒桶的起始时间(Unix 秒)
|
|
Step int `json:"step"` // 每个点的秒数
|
|
Up []int64 `json:"up"` // 上行 bytes/s(客户端 → 目标)
|
|
Down []int64 `json:"down"` // 下行 bytes/s(目标 → 客户端)
|
|
}
|
|
|
|
type rateBucket struct {
|
|
start time.Time
|
|
up, down int64
|
|
}
|
|
|
|
type rateHistory struct {
|
|
done []rateBucket // completed buckets, oldest first, at most ratePoints
|
|
curStart time.Time
|
|
curUp, curDown int64
|
|
curSamples int64
|
|
lastSampleBucket time.Time
|
|
}
|
|
|
|
func (h *rateHistory) add(now time.Time, up, down int64) {
|
|
bs := now.Truncate(rateStep)
|
|
if h.curStart.IsZero() {
|
|
h.curStart = bs
|
|
}
|
|
if bs.After(h.curStart) {
|
|
if h.curSamples > 0 {
|
|
h.done = append(h.done, rateBucket{h.curStart, h.curUp / h.curSamples, h.curDown / h.curSamples})
|
|
}
|
|
if len(h.done) > ratePoints {
|
|
h.done = h.done[len(h.done)-ratePoints:]
|
|
}
|
|
h.curStart, h.curUp, h.curDown, h.curSamples = bs, 0, 0, 0
|
|
}
|
|
h.curUp += up
|
|
h.curDown += down
|
|
h.curSamples++
|
|
}
|
|
|
|
// series renders the last ratePoints buckets ending at now's bucket; missing buckets are zero
|
|
func (h *rateHistory) series(now time.Time) RateSeries {
|
|
end := now.Truncate(rateStep)
|
|
s := RateSeries{End: end.Unix(), Step: int(rateStep / time.Second), Up: make([]int64, ratePoints), Down: make([]int64, ratePoints)}
|
|
first := end.Add(-time.Duration(ratePoints-1) * rateStep)
|
|
put := func(start time.Time, up, down int64) {
|
|
if start.Before(first) || start.After(end) {
|
|
return
|
|
}
|
|
i := int(start.Sub(first) / rateStep)
|
|
s.Up[i], s.Down[i] = up, down
|
|
}
|
|
for _, b := range h.done {
|
|
put(b.start, b.up, b.down)
|
|
}
|
|
if h.curSamples > 0 {
|
|
put(h.curStart, h.curUp/h.curSamples, h.curDown/h.curSamples)
|
|
}
|
|
return s
|
|
}
|
|
|
|
// rateRecorder keeps per-tunnel and total rate histories in memory (近 10 分钟,不落盘)
|
|
type rateRecorder struct {
|
|
mu sync.Mutex
|
|
hist map[string]*rateHistory
|
|
}
|
|
|
|
func newRateRecorder() *rateRecorder {
|
|
return &rateRecorder{hist: make(map[string]*rateHistory)}
|
|
}
|
|
|
|
// record adds one sample per tunnel (rates in bytes/s) plus the total
|
|
func (r *rateRecorder) record(now time.Time, rates map[string][2]int64) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
var tu, td int64
|
|
for id, v := range rates {
|
|
h := r.hist[id]
|
|
if h == nil {
|
|
h = &rateHistory{}
|
|
r.hist[id] = h
|
|
}
|
|
h.add(now, v[0], v[1])
|
|
tu += v[0]
|
|
td += v[1]
|
|
}
|
|
t := r.hist[totalSeries]
|
|
if t == nil {
|
|
t = &rateHistory{}
|
|
r.hist[totalSeries] = t
|
|
}
|
|
t.add(now, tu, td)
|
|
}
|
|
|
|
func (r *rateRecorder) series(id string, now time.Time) RateSeries {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if h := r.hist[id]; h != nil {
|
|
return h.series(now)
|
|
}
|
|
return (&rateHistory{}).series(now)
|
|
}
|
|
|
|
// forget drops the history of tunnels that are no longer enabled
|
|
func (r *rateRecorder) retain(enabled map[string]bool) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
for id := range r.hist {
|
|
if id != totalSeries && !enabled[id] {
|
|
delete(r.hist, id)
|
|
}
|
|
}
|
|
}
|