Go + webrpc 实战:人在外面,远程查家里 NAS 磁盘和目录

独立开发者笔记,非官方教程。Token 与 SDK 见 webrpc 控制台

人不在家里时,常会碰到两个很具体的问题:

  • 家里的 NAS / 小主机还活着吗?磁盘还剩多少空间?
  • 某个目录里有没有把备份跑完?

用 frp 可以硬把 Web 管理页或 SSH 透出来,但你要先维护一台公网机器,还要操心端口暴露面。之前我写过一篇更完整的双端 RPC 示例(Ping / GetDeviceInfo),这次换一个更贴近日常的痛点:用 JSON 发请求,远程查磁盘用量和列目录

下面示例依然是无公网 IP 场景:A 端挂在家里当 Agent,B 端在外面发起查询。

这次和之前示例有什么不同?

之前 这篇
载荷格式 `RPC Ping
业务 连通性测试 disk.usage / dir.list
代码结构 双文件各写一套 抽出公共 protocol.go 思路(文内合并展示)

协议长这样:

json 复制代码
// 请求
{"id":1,"method":"disk.usage"}
{"id":2,"method":"dir.list","params":{"path":"/Photos"}}

// 响应
{"id":1,"ok":true,"data":{"path":"/","total_gb":931.5,"free_gb":412.3}}
{"id":2,"ok":true,"data":{"entries":["2024","2025","backup"]}}

准备

  1. 控制台拿两个 Token:YOUR_TOKEN_A(家里 Agent)、YOUR_TOKEN_B(外面 Client)
  2. 下载对应平台 SDK,头文件和动态库放工程根目录
  3. Go 1.20+,能跑 CGO

目录:

text 复制代码
nas-remote-query/
  ├── libwebrpc-Mac.h
  ├── libwebrpc-Mac.dylib
  ├── agent/main.go      # 家里 NAS
  └── client/main.go     # 外面查询

公共:回调读帧 + JSON 类型

两端都要连 127.0.0.1:回调端口 读 SDK 推来的数据。帧格式与官网一致:sessionId(4) + type(1) + payload。数据流 type=2。

go 复制代码
type Request struct {
	ID     int               `json:"id"`
	Method string            `json:"method"`
	Params map[string]string `json:"params,omitempty"`
}

type Response struct {
	ID    int         `json:"id"`
	OK    bool        `json:"ok"`
	Data  interface{} `json:"data,omitempty"`
	Error string      `json:"error,omitempty"`
}

func readDataFrame(conn net.Conn) (sessionID uint32, payload []byte, err error) {
	sidBuf := make([]byte, 4)
	if _, err = io.ReadFull(conn, sidBuf); err != nil {
		return
	}
	sessionID = binary.BigEndian.Uint32(sidBuf)
	typ := make([]byte, 1)
	if _, err = io.ReadFull(conn, typ); err != nil {
		return
	}
	if typ[0] != 2 {
		return sessionID, nil, fmt.Errorf("unsupported frame type %d", typ[0])
	}
	lenBuf := make([]byte, 4)
	if _, err = io.ReadFull(conn, lenBuf); err != nil {
		return
	}
	n := binary.BigEndian.Uint32(lenBuf)
	payload = make([]byte, n)
	_, err = io.ReadFull(conn, payload)
	return
}

Agent:家里 NAS 处理 disk.usage 和 dir.list

agent/main.go 核心逻辑:

go 复制代码
func handleRequest(webrpc C.GoUintptr, sessionID uint32, raw []byte) {
	var req Request
	if err := json.Unmarshal(raw, &req); err != nil {
		reply(webrpc, sessionID, Response{OK: false, Error: "bad json"})
		return
	}
	log.Printf("[Agent] 收到请求: %s", string(raw))

	var resp Response
	resp.ID = req.ID
	switch req.Method {
	case "disk.usage":
		resp.OK = true
		resp.Data = diskUsage("/") // 见下方实现
	case "dir.list":
		path := req.Params["path"]
		if path == "" {
			path = "."
		}
		entries, err := listDir(path)
		if err != nil {
			resp.OK = false
			resp.Error = err.Error()
		} else {
			resp.OK = true
			resp.Data = map[string]interface{}{"entries": entries}
		}
	default:
		resp.OK = false
		resp.Error = "unknown method"
	}

	reply(webrpc, sessionID, resp)
}

func reply(webrpc C.GoUintptr, sessionID uint32, resp Response) {
	body, _ := json.Marshal(resp)
	log.Printf("[Agent] 回包: %s", body)
	go func() {
		cmsg := C.CString(string(body))
		defer C.free(unsafe.Pointer(cmsg))
		ret := C.WebrpcClient_SendData(webrpc, C.uint(sessionID), cmsg, C.int(len(body)), 3000)
		log.Printf("[Agent] SendData ret=%d", int(ret))
	}()
}

func diskUsage(path string) map[string]interface{} {
	// Linux/macOS 可用 syscall.Statfs;此处示意返回结构
	return map[string]interface{}{
		"path":     path,
		"total_gb": 931.5,
		"free_gb":  412.3,
	}
}

func listDir(path string) ([]string, error) {
	ents, err := os.ReadDir(path)
	if err != nil {
		return nil, err
	}
	names := make([]string, 0, len(ents))
	for _, e := range ents {
		names = append(names, e.Name())
	}
	return names, nil
}

Agent 的 main 与官网示例相同:WebrpcClient_New → 等 LoginStatusGetReceivePort → goroutine 里 net.Dial 回调端口,循环 readDataFrame 后调 handleRequest

Client:外面发 JSON 查询

go 复制代码
func main() {
	// ... New / Login / GetReceivePort 同上 ...
	go startCallbackReader(int(tcpPort)) // 打印收到的 JSON 响应

	peer := C.CString("YOUR_TOKEN_A")
	defer C.free(unsafe.Pointer(peer))
	sid := C.WebrpcClient_OpenSession(webrpc, peer, permission)
	if sid == 0 {
		log.Fatal("OpenSession 失败")
	}
	log.Printf("会话建立 sessionId=%d", uint32(sid))

	sendJSON(webrpc, sid, Request{ID: 1, Method: "disk.usage"})
	time.Sleep(time.Second)
	sendJSON(webrpc, sid, Request{
		ID: 2, Method: "dir.list",
		Params: map[string]string{"path": "/Photos"},
	})

	select {}
}

func sendJSON(webrpc C.GoUintptr, sid C.uint, req Request) {
	body, _ := json.Marshal(req)
	cmsg := C.CString(string(body))
	defer C.free(unsafe.Pointer(cmsg))
	ret := C.WebrpcClient_SendData(webrpc, sid, cmsg, C.int(len(body)), 5000)
	log.Printf("[Client] 发送 %s ret=%d", body, int(ret))
}

回调里收到响应后直接 log.Printf 打印 JSON;若要做得像同步 RPC,可以用 map[int]chan Responseid 等待,这里为短示例保持异步打印。

CGO 头(与官网一致)

go 复制代码
/*
#cgo CFLAGS: -I..
#cgo darwin LDFLAGS: -L.. -lwebrpc-Mac
#cgo linux,amd64 LDFLAGS: -L.. -lwebrpc-Linux
#cgo linux,arm64 LDFLAGS: -L.. -lwebrpc-Linux-arm64

#if defined(__APPLE__)
#include "libwebrpc-Mac.h"
#elif defined(__linux__) && defined(__aarch64__)
#include "libwebrpc-Linux-arm64.h"
#elif defined(__linux__)
#include "libwebrpc-Linux.h"
#endif
#include <stdlib.h>
*/
import "C"

跑起来

家里(Agent 先启动):

bash 复制代码
cd nas-remote-query/agent
export DYLD_LIBRARY_PATH=..   # Linux 用 LD_LIBRARY_PATH
go run .

外面(Client):

bash 复制代码
cd nas-remote-query/client
go run .

Agent 侧大致会看到:

Client 侧会在回调里打印带 free_gbentries 的 JSON。两端跨网(家宽 + 手机热点)测一次,比同机两个进程更有说服力。

三个容易踩的坑

  1. 回调里同步 SendData 会卡死读循环------回包务必放 goroutine。
  2. OpenSession 返回 0------Agent 没在线、Token 填反、或网络策略太严;先同机验证逻辑。
  3. JSON 和业务权限 ------示例为了短没做鉴权;真实 NAS 产品要在 Agent 里校验 caller、限制可列目录的路径,避免 dir.list 被扫全盘。

还能怎么扩展?

  • disk.usage 换成真实 syscall.Statfs(Linux)或 unix.Statfs(macOS)
  • file.pull:Agent 读小文件,SendData 分块回传;大文件用 SendFile
  • 请求里加 token 或 HMAC,做一层轻量鉴权
  • 把 Client 封成 CLI:nasctl disk / nasctl ls /Photos

小结

webrpc 不会替你实现「列目录」的业务逻辑,但它把 无公网 IP 下的会话和收发 收进了 SDK。这篇示例解决的是很具体的一刀:在外面用 JSON 问家里 NAS 还剩多少空间、目录里有什么------比管道字符串更接近真实产品,也比再开一条 frp 隧道更贴应用层。

完整 API 与多语言示例见 https://www.webrpc.cn/ 开发文档。Personal 档约 $5/年/2 Token,够 Agent + Client 各用一个。

相关推荐
newerp1 天前
Go net/http 标准库基础
后端·程序员·go
2651940511264801 天前
01-整体架构与高可用
go
程序员爱钓鱼1 天前
Go if 判断详解
前端·后端·go
名字还没想好☜2 天前
Go 的 unsafe.Pointer 实战:零拷贝 []byte↔string 转换与三条铁律
开发语言·后端·golang·go·unsafe
阿里云云原生2 天前
阿里云联合 Datadog,补齐 Go 可观测性最后短板
云原生·go
可观测性用观测云2 天前
零码改造!Go 语言应用上报观测云完整最佳实践
go
斐波那契数列2 天前
参考react 实现一个golang gui
go
程序员爱钓鱼2 天前
Go 运算符详解
后端·面试·go
angryshan3 天前
goLang配置断点流程
go·bug