通过 WebRTC 在 Unity 中进行 P2P 数据传输

P2P Data transfer in Unity via WebRTC

所以Unity好像有wrapped WebRTC in a neat package。这看起来是个好消息,因为他们在没有先放置平衡的情况下就弃用了 UNET。随便。

我现在碰巧必须为某些游戏实现多人游戏,并且由于我的公司不想在没有对游戏玩家如何接受它的第一印象的情况下进行投资,所以我不得不在没有服务器来处理连接。所以我偶然发现了 WebRTC,其中 DataChannels 似乎非常适合我的用例,因为我将不得不传输代表游戏状态的几个字节(这是步调一致的,所以没问题)。

然而,对于我来说,我无法理解它是如何工作的。

看起来它通过 google STUN 服务器交换地址和其他数据,做了一些 offer\answer 恶作剧,从而建立了数据通道。但是我无法理解如何它知道需要连接2个设备,我无法理解为什么我的代码不起作用。我做了一个 class 连接本地和远程对等点,所以他们应该能够交换数据,对吗?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.WebRTC;

namespace REPLAYSRL.Forza4 {

    public class P2PManager : MonoBehaviour {

        //---------------------------------------------- VARIABILI PRIVATE

        private RTCPeerConnection localPeer, remotePeer; //Le istanze delle due rispettive sessioni peer 2 peer 
        private RTCDataChannel localDataChannel, remoteDataChannel; //I data channel per il peer locale e quello remoto per una comunicazione fullduplex
        private RTCOfferOptions OfferOptions = new RTCOfferOptions {
            iceRestart = false,
            offerToReceiveAudio = true,
            offerToReceiveVideo = false
        };
        private RTCAnswerOptions AnswerOptions = new RTCAnswerOptions {
            iceRestart = false,
        };

        //---------------------------------------------- VARIABILI PUBBLICHE

        /// <summary>
        /// Se è stata stabilita una connessione P2P.
        /// </summary>
        public bool Is_Connected { get; private set; } = false;

        /// <summary>
        /// Istanza singleton della classe
        /// </summary>
        public static P2PManager Instance;

        //---------------------------------------------- FUNZIONI PRIVATE

        private void OnIceConnectionChange(RTCPeerConnection peer, RTCIceConnectionState status) {
            Debug.Log(((peer == localPeer) ? "localPeer" : "remotePeer") + " Status: " + status);
        }

        private void OnIceCandidate(RTCPeerConnection peer, RTCIceCandidate candidate) {
            ((peer == localPeer) ? remotePeer : localPeer).AddIceCandidate(ref candidate);
            Debug.Log("Aggiunto candidato a " + ((peer == localPeer) ? "LocalPeer" : "RemotePeer"));
        }

        private RTCPeerConnection GetPairedPeer(RTCPeerConnection peer) {
            return (peer == localPeer) ? remotePeer : localPeer;
        }

        private string GetName(RTCPeerConnection peer) {
            return (peer == localPeer) ? "localPeer" : "remotePeer";
        }

        private IEnumerator P2PConnection(DataReceivedBehaviour drb) {
            RTCConfiguration peerCfg = default;
            peerCfg.iceServers = new RTCIceServer[]
            {
            new RTCIceServer { urls = new string[] { "stun:stun.l.google.com:19302" } }
            };

            localPeer = new RTCPeerConnection(ref peerCfg);
            localPeer.OnIceCandidate = (candidate => { OnIceCandidate(localPeer, candidate); });
            localPeer.OnIceConnectionChange = (state => { OnIceConnectionChange(localPeer, state); });
            /*
            localPeer.OnDataChannel = (channel => {
                //OnChannelCreate
                Debug.Log("Canale locale creato.");
                localDataChannel = channel;
                localDataChannel.OnMessage = (bytes => {
                    drb(bytes);
                });
            });
            */

            remotePeer = new RTCPeerConnection(ref peerCfg);
            remotePeer.OnIceCandidate = (candidate => { OnIceCandidate(localPeer, candidate); });
            remotePeer.OnIceConnectionChange = (state => { OnIceConnectionChange(remotePeer, state); });
            remotePeer.OnDataChannel = (channel => {
                //OnChannelCreate
                Debug.Log("Canale remoto creato");
                remoteDataChannel = channel;
                remoteDataChannel.OnMessage = (bytes => {
                    drb(bytes);
                });
            });
            

            RTCDataChannelInit dataChannelCfg = new RTCDataChannelInit(true);
            dataChannelCfg.id = 0;
            dataChannelCfg.reliable = true;
            
            localDataChannel = localPeer.CreateDataChannel("data", ref dataChannelCfg);
            localDataChannel.OnOpen = (() => {
                //Inizializzazioni
                Is_Connected = true;
            });
            localDataChannel.OnClose = (() => {
                //Distruzioni
                Is_Connected = false;
            });

            //Connessione
            var localOfferResult = localPeer.CreateOffer(ref OfferOptions);
            yield return localOfferResult;
            if (!localOfferResult.IsError) {
                var desc = localOfferResult.Desc;
                var localDescriptionResult = localPeer.SetLocalDescription(ref desc);
                yield return localDescriptionResult;
                var op2 = remotePeer.SetRemoteDescription(ref desc);
                yield return op2;
                var op3 = remotePeer.CreateAnswer(ref AnswerOptions);
                yield return op3;
                if (!op3.IsError) {
                    desc = op3.Desc;
                    var op4 = remotePeer.SetLocalDescription(ref desc);
                    yield return op4;
                    var op5 = localPeer.SetRemoteDescription(ref desc);
                    yield return op5;
                }
            }
        }

        //---------------------------------------------- FUNZIONI PUBBLICHE

        public delegate void DataReceivedBehaviour(byte[] bytes);

        public void Initialize(DataReceivedBehaviour dataReceivedBehaviour) {
            WebRTC.Initialize();
            StartCoroutine(P2PConnection(dataReceivedBehaviour));
        }

        public void SendToLocal(byte[] Value) {
            localDataChannel.Send(Value);
        }

        public void SendToLocal(string Value) {
            localDataChannel.Send(Value);
        }

        public void SendToRemote(byte[] Value) {
            remoteDataChannel.Send(Value);
        }

        public void SendToRemote(string Value) {
            remoteDataChannel.Send(Value);
        }

        public void Dispose() {
            if (localPeer != null)
                localPeer.Close();
            if (remotePeer != null)
                remotePeer.Close();
            WebRTC.Dispose();
        }

        //---------------------------------------------- FUNZIONI DI UNITY

        private void Awake() {
            if (Instance == null) {
                Instance = this;
            } else if (Instance != this) {
                Destroy(this);
            }
        }

    }
}

对意大利语的评论表示歉意。

我不知道,也许我弄错了。如果您碰巧对其他更好的选择有任何建议,我会洗耳恭听。

我觉得你的逻辑基本正确。我不知道它是否能解决您的问题,但为了让事情更清楚,我会调整您的 SDP 交换,这样描述对象就不会被覆盖。

//Connessione
var localOfferResult = localPeer.CreateOffer(ref OfferOptions);
yield return localOfferResult;
if (!localOfferResult.IsError) {
    var offer = localOfferResult.Desc;
    var localDescriptionResult = localPeer.SetLocalDescription(ref offer);
    yield return localDescriptionResult;
    var op2 = remotePeer.SetRemoteDescription(ref desc);
    yield return op2;
    var op3 = remotePeer.CreateAnswer(ref AnswerOptions);
    yield return op3;
    if (!op3.IsError) {
        var answer = op3.Desc;
        var op4 = remotePeer.SetLocalDescription(ref answer);
        yield return op4;
        var op5 = localPeer.SetRemoteDescription(ref answer);
        yield return op5;
    }
}

除此之外,您还收到任何日志消息吗?任一对等方的 ICE 连接状态是否发生变化?

另请注意,当您想采取下一步并在不同机器上拥有本地和远程对等点时,您很可能需要涉及某种信令服务器以允许 SDP offer/answer 和 ICE 候选人待兑换