SoStraw头像
关注
P2P文件传输不用WebRTC复杂配置,RPC调用直接传大文件封面图

P2P文件传输不用WebRTC复杂配置,RPC调用直接传大文件

我是 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 程序:

  1. 接收端先登录,挂上回调,准备好保存目录。
  2. 发送端登录后 OpenSession 到接收端 Token。
  3. 发送端对本地 ./demo.bin(换成绝对路径)调一次 SendFile。
  4. 接收端把收到的内容写到你配置的保存路径;发送端打印 ret=1 即对端收完。

你只要改:两枚 Token、发送文件路径、接收端保存目录。同机两个进程可先验证;跨网时把接收端放另一台设备即可。不需要公网 IP。

在这里插入图片描述


复制粘贴跑通 Demo(Go)

准备

  1. webrpc准备两枚 Token(发送端、接收端各一)及密码。
  2. 下载当前系统对应的动态库与头文件,放在工程根目录。
  3. 在发送端目录放一个测试文件,例如:
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

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--