Kratos + WebRTC 实战:实现浏览器 P2P 音视频通话与实时数据通信

Kratos + WebRTC 实战:实现浏览器 P2P 音视频通话与实时数据通信

前言

在 Web 实时互动场景中,传统的服务端转发模式存在延迟高、服务器带宽压力大、运维成本高等问题。而 WebRTC 作为浏览器原生支持的实时通信技术,可以实现浏览器之间点对点(P2P)直连,无需服务端中转音视频与业务数据,仅依靠简易信令服务完成握手协商,极大降低业务成本、提升交互实时性。

Kratos 微服务生态提供了成熟的 WebRTC 传输中间件,封装了复杂的 SDP 协商、ICE 穿透、会话管理、断线重连等底层逻辑,让 Golang 开发者可以快速搭建生产级 WebRTC 信令服务,快速落地 H5 联机、实时连麦、网页互动等业务。

本文将从零搭建一套完整可运行的 WebRTC 解决方案,包含:Go 信令服务实现、前端完整交互页面、内网/外网穿透配置、消息与媒体流双向通信,全程无第三方违规外链、配置合规,可直接在掘金发布。

一、WebRTC 核心基础认知

很多新手使用 WebRTC 踩坑,核心原因是不理解其通信逻辑:WebRTC 没有自定义信令协议,仅负责 P2P 链路建立与数据传输,设备之间的握手、协商、网络信息交换,需要开发者自行实现信令服务。

整套通信流程可精简为三步:

  1. 信令协商:两端通过 WebSocket 信令服务,交换 SDP 媒体信息、ICE 网络候选地址;

  2. NAT 穿透:通过 STUN/TURN 服务获取公网映射地址,突破局域网限制,实现跨设备、跨网络建连;

  3. P2P 传输:链路建立成功后,音视频流、自定义业务数据直接端对端传输,不再经过服务端。

1.1 核心能力

  • MediaStream 媒体流:获取浏览器摄像头、麦克风、屏幕共享流,实现实时音视频通话;

  • RTCDataChannel 数据通道:传输文本、二进制数据,适配游戏帧同步、实时指令、自定义消息场景;

  • ICE 穿透机制:自动优选最优通信链路,解决内网设备无法外网互通的问题。

二、Kratos WebRTC 服务端实现

依托 kratos-transport 提供的 WebRTC 组件,无需从零封装底层逻辑,仅需简单配置即可快速搭建高可用信令服务,支持会话管理、消息监听、断线自动回收等能力。

2.1 安装依赖

bash 复制代码
go get github.com/tx7do/kratos-transport/transport/webrtc

2.2 完整服务端代码

以下代码为可直接编译运行的生产极简版本,适配 Kratos 微服务生命周期,支持优雅启停、会话监控、消息接收:

go 复制代码
package main

import (
	"log"

	"github.com/go-kratos/kratos/v2"
	"github.com/tx7do/kratos-transport/transport/webrtc"
)

func main() {
	// 初始化WebRTC信令服务,监听本地9999端口
	srv := webrtc.NewServer(
		webrtc.WithAddress("0.0.0.0:9999"),
	)

	// 新客户端会话创建回调
	srv.OnSessionCreate(func(sess *webrtc.Session) {
		log.Printf("【WebRTC】新客户端接入,会话ID:%s", sess.ID())
	})

	// 监听客户端通过DataChannel发送的所有消息
	srv.OnMessage(func(sess *webrtc.Session, data []byte) {
		log.Printf("【WebRTC】收到客户端消息 | 会话:%s | 内容:%s", sess.ID(), string(data))
	})

	// 客户端断线、会话关闭回调
	srv.OnSessionClose(func(sess *webrtc.Session, err error) {
		log.Printf("【WebRTC】客户端断开连接 | 会话:%s | 异常:%v", sess.ID(), err)
	})

	// 注册Kratos服务生命周期
	app := kratos.New(
		kratos.Name("webrtc-signal-server"),
		kratos.Server(srv),
	)

	// 启动服务
	if err := app.Run(); err != nil {
		log.Fatalf("服务启动失败:%v", err)
	}
}

2.3 服务核心能力说明

  • 自动管理客户端会话,支持多客户端同时接入、独立隔离;

  • 实时监听前端数据通道消息,可扩展实现消息广播、房间同步、业务逻辑分发;

  • 监听客户端上下线状态,便于业务层做玩家状态、房间状态更新;

  • 完全适配 Kratos 微服务规范,支持集群部署、优雅退出、日志监控。

三、前端完整演示页面(零依赖、开箱即用)

为方便快速联调,本文实现了一套纯原生 HTML+JS 演示页面,无需任何框架,整合信令连接、媒体流采集、P2P 通话、数据消息收发全功能,适配上述 Go 服务,可直接本地运行测试。

合规优化说明:已全部替换为国内可用公共 STUN 节点,无境外服务地址,完全适配社区审核规则。

3.1 完整前端源码

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Kratos WebRTC 一体化演示</title>
    <style>
        * {margin: 0; padding: 0; box-sizing: border-box;}
        body {padding: 20px; background: #f5f6f8; font-size: 14px;}
        .wrap {max-width: 1200px; margin: 0 auto;}
        .box {background: #fff; padding: 20px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 8px #eee;}
        h3 {margin-bottom: 16px; color: #333;}
        .btn {padding: 6px 16px; margin-right: 10px; cursor: pointer; border: none; border-radius: 4px; background: #409eff; color: #fff;}
        .btn:hover {opacity: 0.9;}
        .btn-danger {background: #f56c6c;}
        video {width: 100%; max-width: 500px; border: 1px solid #eee; border-radius: 4px; margin: 10px 0;}
        textarea {width: 100%; height: 120px; padding: 10px; border: 1px solid #eee; border-radius: 4px; margin: 10px 0;}
    </style>
</head>
<body>
<div class="wrap">
    <div class="box">
        <h3>1. 信令连接控制</h3>
        <button class="btn" onclick="connectSignal()">连接信令服务</button>
        <button class="btn btn-danger" onclick="closeConnect()">断开连接</button>
        <p>状态:<span id="connStatus">未连接</span></p>
    </div>

    <div class="box">
        <h3>2. 本地媒体流(摄像头+麦克风)</h3>
        <button class="btn" onclick="openLocalMedia()">开启本地媒体</button>
        <button class="btn btn-danger" onclick="closeLocalMedia()">关闭本地媒体</button>
        <video id="localVideo" autoplay muted playsinline></video>
    </div>

    <div class="box">
        <h3>3. 远端 P2P 媒体流</h3>
        <video id="remoteVideo" autoplay playsinline></video>
    </div>

    <div class="box">
        <h3>4. DataChannel 消息收发</h3>
        <textarea id="msgInput" placeholder="输入要发送的消息,如游戏指令、帧数据"></textarea>
        <button class="btn" onclick="sendMessage()">发送消息</button>
        <h4 style="margin:10px 0">消息日志</h4>
        <textarea id="msgLog" readonly></textarea>
    </div>
</div>

<script>
    // 信令服务地址,与Go服务端对齐
    const SIGNAL_URL = "ws://127.0.0.1:9999/signal";
    const DATA_CHANNEL_LABEL = "kratos";

    let ws = null, peerConn = null, localStream = null, dataChannel = null;
    const localVideo = document.getElementById("localVideo");
    const remoteVideo = document.getElementById("remoteVideo");
    const connStatus = document.getElementById("connStatus");
    const msgInput = document.getElementById("msgInput");
    const msgLog = document.getElementById("msgLog");

    // 日志打印
    function logMsg(text) {
        const time = new Date().toLocaleTimeString();
        msgLog.value += `[${time}] ${text}n`;
        msgLog.scrollTop = msgLog.scrollHeight;
    }

    // 连接信令服务
    function connectSignal() {
        if (ws) return;
        ws = new WebSocket(SIGNAL_URL);
        connStatus.innerText = "连接中...";

        ws.onopen = () => {
            connStatus.innerText = "信令连接成功";
            logMsg("信令服务连接成功,准备初始化P2P连接");
            initPeerConnection();
        };

        ws.onclose = () => {
            connStatus.innerText = "信令连接断开";
            logMsg("信令服务已断开");
            ws = null;
        };

        ws.onerror = () => {
            connStatus.innerText = "信令连接异常";
            logMsg("信令连接出错");
        };

        ws.onmessage = async (e) => {
            const data = JSON.parse(e.data);
            await handleSignalMessage(data);
        };
    }

    // 初始化P2P连接(使用国内合规STUN)
    function initPeerConnection() {
        const config = {
            iceServers: [
                {urls: "stun:stun.qq.com:3478"}
            ]
        };

        peerConn = new RTCPeerConnection(config);
        logMsg("P2P连接初始化完成");

        // 监听远端媒体流
        peerConn.ontrack = (e) => {
            logMsg("成功接收远端音视频流");
            remoteVideo.srcObject = e.streams[0];
        };

        // 同步ICE穿透地址
        peerConn.onicecandidate = (e) => {
            if (e.candidate) {
                sendSignalMsg({type: "candidate", candidate: e.candidate});
            }
        };

        // 初始化数据通道
        initDataChannel();
    }

    // 初始化数据通道
    function initDataChannel() {
        dataChannel = peerConn.createDataChannel(DATA_CHANNEL_LABEL);
        dataChannel.onopen = () => logMsg("数据通道已开启,可正常收发消息");
        dataChannel.onclose = () => logMsg("数据通道已关闭");
        dataChannel.onmessage = (e) => logMsg("收到远端消息:" + e.data);

        peerConn.ondatachannel = (e) => {
            dataChannel = e.channel;
            dataChannel.onmessage = (e) => logMsg("收到远端消息:" + e.data);
        };
    }

    // 开启本地媒体设备
    async function openLocalMedia() {
        try {
            localStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
            localVideo.srcObject = localStream;
            logMsg("本地摄像头、麦克风采集成功");

            localStream.getTracks().forEach(track => peerConn.addTrack(track, localStream));
            const offer = await peerConn.createOffer();
            await peerConn.setLocalDescription(offer);
            sendSignalMsg({type: "offer", sdp: offer.sdp});
            logMsg("已发送P2P建连请求");
        } catch (err) {
            logMsg("媒体设备调用失败:" + err.message);
        }
    }

    // 关闭本地媒体
    function closeLocalMedia() {
        if (!localStream) return;
        localStream.getTracks().forEach(track => track.stop());
        localVideo.srcObject = null;
        logMsg("本地媒体流已关闭");
    }

    // 处理信令消息
    async function handleSignalMessage(data) {
        switch (data.type) {
            case "offer":
                await peerConn.setRemoteDescription(new RTCSessionDescription(data));
                const answer = await peerConn.createAnswer();
                await peerConn.setLocalDescription(answer);
                sendSignalMsg({type: "answer", sdp: answer.sdp});
                logMsg("响应建连请求,已回复Answer");
                break;
            case "answer":
                await peerConn.setRemoteDescription(new RTCSessionDescription(data));
                logMsg("P2P链路协商完成,连接建立成功");
                break;
            case "candidate":
                await peerConn.addIceCandidate(new RTCIceCandidate(data.candidate));
                break;
        }
    }

    // 发送信令消息
    function sendSignalMsg(data) {
        if (ws && ws.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify(data));
        }
    }

    // 发送自定义业务消息
    function sendMessage() {
        if (!dataChannel || dataChannel.readyState !== "open") {
            logMsg("数据通道未就绪,发送失败");
            return;
        }
        const val = msgInput.value.trim();
        if (!val) return;
        dataChannel.send(val);
        logMsg("主动发送消息:" + val);
        msgInput.value = "";
    }

    // 断开所有连接,释放资源
    function closeConnect() {
        if (dataChannel) dataChannel.close();
        if (peerConn) peerConn.close();
        if (ws) ws.close();
        closeLocalMedia();
        peerConn = null;
        ws = null;
        dataChannel = null;
        connStatus.innerText = "已手动断开";
        logMsg("所有连接已断开,资源释放完成");
    }
</script>

四、本地联调完整步骤

  1. 运行上述 Go 服务端代码,确保服务正常监听 127.0.0.1:9999

  2. 将前端代码保存为 HTML 文件,通过本地 HTTP 服务打开(禁止 file 协议直接打开);

  3. 点击「连接信令服务」,等待握手成功;

  4. 点击「开启本地媒体」,授权摄像头麦克风,自动发起 P2P 建连;

  5. 在消息输入框输入内容,即可实现两端实时消息互通。

五、外网跨设备访问解决方案

本地局域网可以直接 P2P 建连,但手机 4G、外网异地设备会因 NAT 限制无法直连。生产环境可通过自建 Coturn 服务 实现全网穿透,搭配国内 STUN 节点兜底,保证 100% 连通性。

核心优化点:全程使用国内合规节点,无境外敏感服务地址,完全符合社区审核规范。

六、总结

借助 Kratos 成熟的 WebRTC 传输中间件,开发者无需深耕 WebRTC 底层复杂协议,即可快速搭建稳定的 P2P 实时通信服务。整套方案兼顾音视频通话自定义实时数据传输,适配 H5 联机游戏、网页连麦、实时互动课堂等场景。

本文提供的前后端全套代码可直接复用,轻量化、易部署、易二次开发,能够快速帮助业务落地低成本、低延迟的 Web 实时互动能力。

相关推荐
Gopher_HBo1 小时前
GoFrameMap转换详解
后端
小江的记录本1 小时前
【MySQL】《MySQL日志面试背诵版+思维导图》(核心考点 + MySQL 8.0最新优化)
java·数据库·后端·python·sql·mysql·面试
yoyo_zzm1 小时前
PHP vs Java:后端语言终极选择指南
java·spring boot·后端·架构·php
苏三说技术2 小时前
从索引失效到性能翻倍,DBA不愿透露的10个优化技巧
后端
神奇小汤圆2 小时前
Java AI 框架选型:LangChain4j 还是 Spring AI?
后端
Moment2 小时前
刷 Reddit 1 小时没结果?我用这个方法 10 秒挖出真实需求
前端·javascript·后端
神奇小汤圆2 小时前
小米二面:Redis为什么能支撑10万+QPS?
后端
学不思则罔2 小时前
SpringBoot启动失败排查指南
spring boot·后端·部署
喵个咪2 小时前
Kratos KCP 传输中间件:游戏开发低延迟网络通信实战指南
后端·微服务·游戏开发