独立开发者笔记,非 webrpc 官方文章。Token 请用自己的占位替换。

人不在电脑旁边时,有时只想用手机看一眼家里机器上的某张图。发微信会压缩,传网盘又绕,自己搭 frp 还得弄公网机器。后来我用 webrpc 试了一条更直接的路:电脑用 Go 守着图片,手机用 Java App 点一下「拉取」,经 P2P 会话把文件拉过来显示。

webrpc 本身是面向无公网 IP 的跨平台 P2P 通信 SDK——Token 标识设备,登录后建加密会话,再用 SendData / SendFile 收发。官网:https://www.webrpc.cn/

注意方向:是手机预览电脑上的图,不是手机往电脑传图。

电脑侧示例文件:

在这里插入图片描述

手机 App 界面(预览成功后):

在这里插入图片描述

为什么不用网盘 / 邮箱 / frp?

做法 痛点
发微信/邮箱 压缩、有痕、不适合私有文件
传到网盘再下 绕一圈云,隐私与延迟都差
frp 开 HTTP 静态目录 要公网 VPS,还要管暴露面
webrpc 手机和电脑都可在 NAT 后,优先 P2P,API 接近 RPC

一句话:要的是「跨网预览自家电脑文件」,不是「先拥有公网入口」。

架构(电脑推文件,手机预览)

┌─────────────────┐         webrpc P2P          ┌──────────────────┐
│ 电脑 Go Agent   │  ◄── OpenSession / 拉取 ──► │ 安卓 Java App    │
│ Token = PC      │  ── SendFile(image.png) ──► │ Token = PHONE    │
│ 持有 image.png  │                              │ ImageView 显示   │
└─────────────────┘                              └──────────────────┘

交互约定(故意写短,便于抄):

  1. 手机登录,开回调线程,等待文件帧
  2. 手机 OpenSession(PC),发送文本:PULL|image.png
  3. 电脑回调收到请求,对本 sessionId 执行 SendFile("./image.png")
  4. 手机回调 type=1 文件流:落盘 → BitmapFactoryImageView

准备

  1. https://www.webrpc.cn/ 注册,准备两个 Token:YOUR_TOKEN_PC / YOUR_TOKEN_PHONE
  2. 下载 macOS/Linux SDK(电脑)Android SDK(手机)
  3. 电脑放好 image.png(可用本文同目录样例)
  4. Android Studio,JNA 或官网 Android 绑定方式加载 libwebrpc(下文按与桌面 Java 相同的 JNA 心智写核心;真机请换成 Android .sojniLibs

目录示意:

pc-photo-preview/
  ├── image.png
  ├── pc/main.go              # 电脑发送端
  └── android/                # 仅核心类,非整包工程
        ├── WebrpcBridge.java
        └── PreviewActivity.java

一、电脑端 Go:收到 PULL 就 SendFile

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
#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"

import (
	"encoding/binary"
	"fmt"
	"io"
	"log"
	"net"
	"strings"
	"time"
	"unsafe"
)

func main() {
	token := C.CString("YOUR_TOKEN_PC")
	pass := C.CString("YOUR_PASSWORD_PC")
	perm := C.CString("")
	defer C.free(unsafe.Pointer(token))
	defer C.free(unsafe.Pointer(pass))
	defer C.free(unsafe.Pointer(perm))

	cli := C.WebrpcClient_New(token, pass, perm)
	if cli == 0 {
		log.Fatal("New failed")
	}
	for C.WebrpcClient_LoginStatus(cli) == 0 {
		time.Sleep(2 * time.Second)
	}
	port := int(C.WebrpcClient_GetReceivePort(cli))
	log.Printf("[PC] 登录成功,回调端口=%d,等待手机 PULL…", port)
	go serveCallback(cli, port)
	select {}
}

func serveCallback(cli C.GoUintptr, port int) {
	conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	for {
		sidBuf := make([]byte, 4)
		if _, err := io.ReadFull(conn, sidBuf); err != nil {
			return
		}
		sid := binary.BigEndian.Uint32(sidBuf)
		typ := make([]byte, 1)
		if _, err := io.ReadFull(conn, typ); err != nil {
			return
		}
		if typ[0] != 2 { // 只处理数据请求;文件帧按需跳过
			continue
		}
		lenBuf := make([]byte, 4)
		io.ReadFull(conn, lenBuf)
		n := binary.BigEndian.Uint32(lenBuf)
		payload := make([]byte, n)
		io.ReadFull(conn, payload)

		req := string(payload)
		log.Printf("[PC] 收到请求 session=%d: %s", sid, req)
		if strings.HasPrefix(req, "PULL|") {
			name := strings.TrimPrefix(req, "PULL|")
			if name == "" {
				name = "image.png"
			}
			go sendFile(cli, sid, name)
		}
	}
}

func sendFile(cli C.GoUintptr, sid uint32, path string) {
	cpath := C.CString(path)
	defer C.free(unsafe.Pointer(cpath))
	start := time.Now()
	ret := C.WebrpcClient_SendFile(cli, C.uint(sid), cpath)
	log.Printf("[PC] SendFile %s ret=%d elapsed=%v", path, int(ret), time.Since(start))
}

跑起来大致是这样:

在这里插入图片描述

# 把 image.png 与动态库放在同级后
cd pc && go run .

二、安卓端 Java:核心只要两块

下面是可嵌入的核心类,不是完整 Android Studio 工程。你需要自行:

  • 把 Android 版 libwebrpc-*.so 放进 jniLibs
  • 用 JNA 或 JNI 暴露与官网一致的 C API(下列接口名与 开发文档 · Java 对齐)
  • AndroidManifest 按需声明网络权限

1)回调里收文件帧并回调 UI

文件帧格式(官网):

sessionId:u32 BE | type:u8=1 | nameLen:u32 | name | dataLen:u32 | fileBytes
// PreviewCallback.java —— 读本机回调 TCP,遇到文件就交给 Activity
final class PreviewCallback implements Runnable {
  interface Listener {
    void onImageBytes(String fileName, byte[] data);
    void onText(String text);
  }

  private final long handle;
  private final int port;
  private final Listener listener;
  private final WebrpcLib lib; // JNA 接口,同官网 Java 示例

  PreviewCallback(WebrpcLib lib, long handle, int port, Listener listener) {
    this.lib = lib;
    this.handle = handle;
    this.port = port;
    this.listener = listener;
  }

  @Override public void run() {
    try (Socket socket = new Socket("127.0.0.1", port);
         DataInputStream in = new DataInputStream(socket.getInputStream())) {
      while (true) {
        int sessionId = in.readInt();
        int type = in.readUnsignedByte();
        if (type == 2) {
          int n = in.readInt();
          byte[] data = in.readNBytes(n);
          listener.onText(new String(data, StandardCharsets.UTF_8));
        } else if (type == 1) {
          int nameLen = in.readInt();
          String name = new String(in.readNBytes(nameLen), StandardCharsets.UTF_8);
          int dataLen = in.readInt();
          byte[] file = in.readNBytes(dataLen);
          listener.onImageBytes(name, file);
        }
      }
    } catch (IOException e) {
      Log.e("webrpc", "callback ended", e);
    }
  }
}

2)Activity:登录 → 拉取 → ImageView

// PreviewActivity.java(关键路径)
public class PreviewActivity extends AppCompatActivity {
  private WebrpcLib lib;
  private long handle;
  private ImageView imageView;
  private TextView statusView;

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_preview);
    imageView = findViewById(R.id.imagePreview);
    statusView = findViewById(R.id.status);
    findViewById(R.id.btnPull).setOnClickListener(v -> pullFromPc());

    new Thread(this::bootWebrpc).start();
  }

  private void bootWebrpc() {
    lib = Native.load("webrpc", WebrpcLib.class); // 按你的 .so 名调整
    handle = lib.WebrpcClient_New("YOUR_TOKEN_PHONE", "YOUR_PASSWORD_PHONE", "");
    while (lib.WebrpcClient_LoginStatus(handle) == 0) {
      try { Thread.sleep(2000); } catch (InterruptedException ignored) {}
    }
    int port = lib.WebrpcClient_GetReceivePort(handle);
    runOnUiThread(() -> statusView.setText("已登录,回调端口 " + port));

    new Thread(new PreviewCallback(lib, handle, port, new PreviewCallback.Listener() {
      @Override public void onImageBytes(String fileName, byte[] data) {
        Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
        runOnUiThread(() -> {
          imageView.setImageBitmap(bmp);
          statusView.setText("已预览:" + fileName + " (" + data.length + " bytes)");
        });
      }
      @Override public void onText(String text) {
        runOnUiThread(() -> statusView.setText(text));
      }
    }), "webrpc-cb").start();
  }

  private void pullFromPc() {
    new Thread(() -> {
      int sid = lib.WebrpcClient_OpenSession(handle, "YOUR_TOKEN_PC", "");
      if (sid == 0) {
        runOnUiThread(() -> statusView.setText("OpenSession 失败:电脑端是否在线?"));
        return;
      }
      String req = "PULL|image.png";
      int ret = lib.WebrpcClient_SendData(handle, sid, req, req.length(), 5000);
      runOnUiThread(() -> statusView.setText("已请求拉取,SendData ret=" + ret));
    }).start();
  }

  @Override protected void onDestroy() {
    if (handle != 0) lib.WebrpcClient_Free(handle);
    super.onDestroy();
  }
}

布局里一个 ImageView + 按钮「从电脑拉取 image.png」即可。跑通后手机界面就是文首那张 App 截图:标题「电脑图片预览」、中间显示 image.png、底部拉取按钮。


三、怎么联调

  1. 先开电脑 go run .,确认登录成功
  2. 再开 App,确认手机登录成功
  3. 手机点「从电脑拉取 image.png」
  4. 电脑日志出现 PULL|image.pngSendFile ok
  5. 手机 ImageView 显示与 image.png 相同的画面

两端可以一个在家宽、一个在 4G,这才是无公网 IP 场景的意义。

四、什么场景适合这么做?

比较适合:私有相册预览、现场看工控截图、家庭电脑/NAS 上的单张图片快速查看。

不太适合:浏览器里的实时视频会议(更该看 WebRTC);只是临时把一个网页目录暴露出去(frp 往往更快)。

没有公网 IP 时,链路可以收成:电脑 Go 登录并等待 PULL|文件名,收到后 SendFile;手机 Java 登录、OpenSession 到电脑 Token、发拉取请求,在回调文件帧里解码显示。双方都不必做端口映射。

五、几个坑(安卓侧多留意)

  1. 必须用 Android 对应 ABI 的 so(armeabi-v7a / arm64-v8a),不要拿桌面 dylib 硬塞
  2. 回调在子线程,更新 ImageView 必须 runOnUiThread
  3. 大图内存:可先写缓存文件再 BitmapFactory.Options.inSampleSize 抽样
  4. OpenSession 返回 0:电脑未登录、Token 填反、或网络握手失败——先同 Wi‑Fi 验证
  5. 回包/发文件不要堵死回调读循环——SendFile 放在电脑侧收到请求后的新 goroutine(上文已如此)

小结

这篇演示的是一条很常见却常被「网盘/frp」绕远的需求:手机预览电脑本地图片
webrpc 提供的是跨 NAT 的会话与 SendFile;Go 负责守文件,Java 负责请求与 ImageView。完整 SDK、Token 与多语言文档见官网:https://www.webrpc.cn/

仅用于合法业务。握手无法保证任意网络 100% 成功,产品层请做超时与重试。

Logo

一站式 AI 云服务平台

更多推荐