我是 webrpc 作者。SDK、Token 和开发文档在 。
两台设备互传安装包、素材、镜像,很多人第一反应是上 WebRTC:信令、ICE、DataChannel,再自己定义分片和落盘。浏览器里开实时音视频,这套很合适;做 原生 App / 桌面端「像调一次 RPC 一样把文件推过去」,配置和胶水代码往往比业务还长。
这篇只讲一件事:用 webrpc 在会话上直接 SendFile,把本机文件推到对端。 不展开 WebRTC 信令 / ICE / DataChannel 细节;也不重复「30GB 怎么调优」长文。技术点只保留四句:绝对路径、调用阻塞、返回 1 表示对端收完、收端按帧写盘。
webrpc 是面向无公网 IP 的跨平台 P2P 通信 SDK:Token 标识设备,登录后建加密会话,再用 SendData / SendFile 收发。官网:webrpc.cn
不想写代码、先体验传大文件:装 File2File。个人网盘再加 mywebdisk。

为什么传文件会想到 WebRTC,又容易卡住
WebRTC 的强项是浏览器实时媒体。你要「跨 NAT 传一份磁盘上的文件」时,通常还要自己补齐:
- 信令怎么交换、房间怎么鉴权
- DataChannel 上怎么切包、怎么命名、怎么确认收完
- Native 端怎么接,进度和失败怎么提示
能做成,但工程面大。若产品本来就不是浏览器会议,而是两台电脑 / 手机和家里 NAS 互传文件,连通和会话 可以交给 P2P SDK,业务侧只保留「打开会话 → 指定路径发送」。
webrpc:会话建好之后,一次 SendFile
主线与官网各语言示例一致:
New → LoginStatus 成功
→ GetReceivePort,本机连 127.0.0.1 读回调
→ OpenSession(对端 Token)
→ SendFile(本机文件绝对路径)
和「发一条 JSON」的差别,只是把 SendData 换成 SendFile。心智仍是 RPC:拨号成功,调一次发送,看返回值。
四个必须记住的点:
| 点 | 含义 |
|---|---|
| 绝对路径 | SendFile 传入本机 绝对路径;对端回调里看到的文件名也是这份完整路径字符串 |
| 阻塞 | 调用线程一直等到整次发送结束才返回;不要塞在 UI 线程或回调读循环里 |
| 返回值 | 1 = 文件发完且 对端已接受成功;0 = 失败 |
| 收端写盘 | 回调 type=1 是文件流;可能多帧(底层约按 10MB 一片),每帧立刻 append,不要在内存里攒整文件 |
小消息、指令继续用 SendData;磁盘上的文件用 SendFile。两者怎么选另有专文,这里不展开。

推文件 Demo 要达成什么
文末是一对 发送端 / 接收端 Go 程序:
- 接收端先登录,挂上回调,准备好保存目录。
- 发送端登录后
OpenSession到接收端 Token。 - 发送端对本地
./demo.bin(换成绝对路径)调一次SendFile。 - 接收端把收到的内容写到你配置的保存路径;发送端打印
ret=1即对端收完。
你只要改:两枚 Token、发送文件路径、接收端保存目录。同机两个进程可先验证;跨网时把接收端放另一台设备即可。不需要公网 IP。

复制粘贴跑通 Demo(Go)
准备
- webrpc准备两枚 Token(发送端、接收端各一)及密码。
- 下载当前系统对应的动态库与头文件,放在工程根目录。
- 在发送端目录放一个测试文件,例如:
dd if=/dev/urandom of=demo.bin bs=1m count=8
(Windows 可用任意小文件改名 demo.bin。)
目录
webrpc-sendfile-demo/
├── libwebrpc-Mac.h # 按平台替换
├── libwebrpc-Mac.dylib
├── demo.bin # 发送端要传的文件(示例名)
├── sender/
│ └── main.go
└── receiver/
└── main.go
接收端 receiver/main.go
把 SAVE_DIR 改成你本机要落盘的目录(需已存在)。收到文件流后按帧 append;文件名取发送路径的最后一段,避免把对方绝对路径当成本机目录结构。
package main
/*
#cgo CFLAGS: -I..
#cgo darwin LDFLAGS: -L.. -lwebrpc-Mac
#cgo linux,amd64 LDFLAGS: -L.. -lwebrpc-Linux
#cgo linux,arm64 LDFLAGS: -L.. -lwebrpc-Linux-arm64
#cgo windows LDFLAGS: -L.. -lwebrpc-Windows
#if defined(__APPLE__)
#include "libwebrpc-Mac.h"
#elif defined(_WIN32)
#include "libwebrpc-Windows.h"
#elif defined(__linux__) && defined(__aarch64__)
#include "libwebrpc-Linux-arm64.h"
#elif defined(__linux__)
#include "libwebrpc-Linux.h"
#else
#error "unsupported platform"
#endif
#include <stdlib.h>
*/
import "C"
import (
"encoding/binary"
"fmt"
"io"
"log"
"net"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
"unsafe"
)
// ===== 只改这里 =====
const (
token = "YOUR_TOKEN_RECEIVER"
pass = "YOUR_PASSWORD_RECEIVER"
saveDir = "/tmp/webrpc-recv" // 改成你的保存目录(请先 mkdir)
)
func waitLogin(h C.GoUintptr) {
deadline := time.Now().Add(60 * time.Second)
for time.Now().Before(deadline) {
st := C.WebrpcClient_LoginStatus(h)
log.Printf("[recv] LoginStatus=%d", int(st))
if st != 0 {
return
}
time.Sleep(time.Second)
}
log.Fatal("[recv] 登录超时")
}
func readCallback(port int, ready chan struct{}) {
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
log.Printf("[recv] 连回调失败: %v", err)
return
}
defer conn.Close()
log.Printf("[recv] 回调已连接 127.0.0.1:%d", port)
close(ready)
// 同一路径可能分多帧到达,按「逻辑文件名」追加写入
writers := map[string]*os.File{}
for {
sidBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, sidBuf); err != nil {
log.Printf("[recv] 读 sessionId: %v", err)
return
}
sid := binary.BigEndian.Uint32(sidBuf)
typ := make([]byte, 1)
if _, err := io.ReadFull(conn, typ); err != nil {
return
}
switch typ[0] {
case 1: // 文件流
nameLenBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, nameLenBuf); err != nil {
return
}
nameLen := binary.BigEndian.Uint32(nameLenBuf)
nameBytes := make([]byte, nameLen)
if _, err := io.ReadFull(conn, nameBytes); err != nil {
return
}
remoteName := string(nameBytes) // 通常是发送端的绝对路径
base := filepath.Base(remoteName)
if base == "" || base == "." || base == string(filepath.Separator) {
base = "recv.bin"
}
outPath := filepath.Join(saveDir, base)
dataLenBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, dataLenBuf); err != nil {
return
}
dataLen := binary.BigEndian.Uint32(dataLenBuf)
chunk := make([]byte, dataLen)
if _, err := io.ReadFull(conn, chunk); err != nil {
return
}
f, ok := writers[outPath]
if !ok {
_ = os.MkdirAll(saveDir, 0o755)
f, err = os.OpenFile(outPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
if err != nil {
log.Printf("[recv] 打开保存文件失败: %v", err)
continue
}
writers[outPath] = f
log.Printf("[recv] session=%d 开始写入 %s (来自 %s)", sid, outPath, remoteName)
}
if _, err := f.Write(chunk); err != nil {
log.Printf("[recv] 写盘失败: %v", err)
continue
}
log.Printf("[recv] session=%d 已追加 %d 字节 → %s", sid, dataLen, outPath)
case 2: // 数据流:本 Demo 跳过
lenBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, lenBuf); err != nil {
return
}
n := binary.BigEndian.Uint32(lenBuf)
io.CopyN(io.Discard, conn, int64(n))
default:
log.Printf("[recv] 未知 type=%d", typ[0])
}
}
}
func main() {
if err := os.MkdirAll(saveDir, 0o755); err != nil {
log.Fatalf("[recv] 创建保存目录失败: %v", err)
}
ctoken := C.CString(token)
cpass := C.CString(pass)
cperm := C.CString("")
defer C.free(unsafe.Pointer(ctoken))
defer C.free(unsafe.Pointer(cpass))
defer C.free(unsafe.Pointer(cperm))
h := C.WebrpcClient_New(ctoken, cpass, cperm)
if h == 0 {
log.Fatal("[recv] WebrpcClient_New 失败")
}
defer C.WebrpcClient_Free(h)
waitLogin(h)
port := int(C.WebrpcClient_GetReceivePort(h))
ready := make(chan struct{})
go readCallback(port, ready)
select {
case <-ready:
case <-time.After(5 * time.Second):
log.Fatal("[recv] 回调未连上")
}
log.Printf("[recv] 在线,文件将写入 %s Ctrl+C 退出", saveDir)
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
}
发送端 sender/main.go
把 sendPath 改成 demo.bin 的绝对路径;peerToken 填接收端那枚 Token。SendFile 在独立逻辑里调用(本示例在 main 里发完即退出),避免堵在回调读线程上。
package main
/*
#cgo CFLAGS: -I..
#cgo darwin LDFLAGS: -L.. -lwebrpc-Mac
#cgo linux,amd64 LDFLAGS: -L.. -lwebrpc-Linux
#cgo linux,arm64 LDFLAGS: -L.. -lwebrpc-Linux-arm64
#cgo windows LDFLAGS: -L.. -lwebrpc-Windows
#if defined(__APPLE__)
#include "libwebrpc-Mac.h"
#elif defined(_WIN32)
#include "libwebrpc-Windows.h"
#elif defined(__linux__) && defined(__aarch64__)
#include "libwebrpc-Linux-arm64.h"
#elif defined(__linux__)
#include "libwebrpc-Linux.h"
#else
#error "unsupported platform"
#endif
#include <stdlib.h>
*/
import "C"
import (
"encoding/binary"
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
"time"
"unsafe"
)
// ===== 只改这里 =====
const (
token = "YOUR_TOKEN_SENDER"
pass = "YOUR_PASSWORD_SENDER"
peerToken = "YOUR_TOKEN_RECEIVER"
// 改成 demo.bin 的绝对路径,例如 /Users/你/webrpc-sendfile-demo/demo.bin
sendPath = "/ABS/PATH/TO/demo.bin"
)
func waitLogin(h C.GoUintptr) {
deadline := time.Now().Add(60 * time.Second)
for time.Now().Before(deadline) {
st := C.WebrpcClient_LoginStatus(h)
log.Printf("[send] LoginStatus=%d", int(st))
if st != 0 {
return
}
time.Sleep(time.Second)
}
log.Fatal("[send] 登录超时")
}
func readCallback(port int, ready chan struct{}) {
conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
if err != nil {
log.Printf("[send] 连回调失败: %v", err)
return
}
defer conn.Close()
close(ready)
// 发送端本 Demo 不处理对端回包,读掉帧避免阻塞
for {
sidBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, sidBuf); err != nil {
return
}
typ := make([]byte, 1)
if _, err := io.ReadFull(conn, typ); err != nil {
return
}
switch typ[0] {
case 2:
lenBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, lenBuf); err != nil {
return
}
n := binary.BigEndian.Uint32(lenBuf)
io.CopyN(io.Discard, conn, int64(n))
case 1:
nameLenBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, nameLenBuf); err != nil {
return
}
nameLen := binary.BigEndian.Uint32(nameLenBuf)
io.CopyN(io.Discard, conn, int64(nameLen))
dataLenBuf := make([]byte, 4)
if _, err := io.ReadFull(conn, dataLenBuf); err != nil {
return
}
dataLen := binary.BigEndian.Uint32(dataLenBuf)
io.CopyN(io.Discard, conn, int64(dataLen))
}
}
}
func main() {
abs, err := filepath.Abs(sendPath)
if err != nil {
log.Fatal(err)
}
if st, err := os.Stat(abs); err != nil || st.IsDir() {
log.Fatalf("[send] 找不到文件,请改 sendPath: %s", abs)
}
log.Printf("[send] 将发送 %s (%d 字节)", abs, mustSize(abs))
ctoken := C.CString(token)
cpass := C.CString(pass)
cperm := C.CString("")
defer C.free(unsafe.Pointer(ctoken))
defer C.free(unsafe.Pointer(cpass))
defer C.free(unsafe.Pointer(cperm))
h := C.WebrpcClient_New(ctoken, cpass, cperm)
if h == 0 {
log.Fatal("[send] WebrpcClient_New 失败")
}
defer C.WebrpcClient_Free(h)
waitLogin(h)
port := int(C.WebrpcClient_GetReceivePort(h))
ready := make(chan struct{})
go readCallback(port, ready)
select {
case <-ready:
case <-time.After(5 * time.Second):
log.Fatal("[send] 回调未连上")
}
cpeer := C.CString(peerToken)
defer C.free(unsafe.Pointer(cpeer))
sid := C.WebrpcClient_OpenSession(h, cpeer, cperm)
if sid == 0 {
log.Fatal("[send] OpenSession=0:确认接收端已在线,peerToken 填对端 Token")
}
log.Printf("[send] OpenSession 成功 sessionId=%d", uint32(sid))
cpath := C.CString(abs)
defer C.free(unsafe.Pointer(cpath))
start := time.Now()
// 阻塞直到成功或失败;1 = 对端已接受整文件
ret := C.WebrpcClient_SendFile(h, sid, cpath)
log.Printf("[send] SendFile ret=%d 耗时 %v", int(ret), time.Since(start))
if ret == 1 {
fmt.Println("发送成功:对端已接受文件")
} else {
fmt.Println("发送失败:检查对方在线、路径、网络后重试")
os.Exit(1)
}
}
func mustSize(p string) int64 {
st, err := os.Stat(p)
if err != nil {
return 0
}
return st.Size()
}
怎么跑
终端 1(接收端,先开):
cd webrpc-sendfile-demo/receiver
# 按需改 saveDir,并 mkdir
CGO_ENABLED=1 go run .
终端 2(发送端):
cd webrpc-sendfile-demo/sender
# 确认 sendPath 指向 demo.bin 的绝对路径
CGO_ENABLED=1 go run .
期望:
- 发送端:
SendFile ret=1,打印「发送成功」 - 接收端:日志里出现「开始写入 …/demo.bin」和若干「已追加 … 字节」
- 打开
saveDir,能看到收到的demo.bin
把 sendPath 换成更大的安装包或素材路径即可测大文件;收端仍按帧 append,峰值内存不会按整文件涨。OpenSession 为 0 或 SendFile 为 0 时,先确认接收端进程在线、Token 没填反,再重试。
跑通之后
- 产品里可先
SendData约定文件名 / 是否接收,再SendFile;本 Demo 为最短路径直接推送。 - 需要进度条时,用
SendData询问对端已写字节(File2File 同款思路),SDK 不提供进度 API。 - 失败时对端磁盘可能有半成品,可用临时文件名,确认成功后再改名。
- 浏览器实时会议仍优先看 WebRTC;原生设备间传文件,优先把连通交给 webrpc,业务只保留路径与落盘策略。
配置好路径和保存目录,先把 demo.bin 跑通,再换成你的大文件即可。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq974816077/article/details/166494113




