如何使用 JavaScript 创建 WebRTC UDP 连接

本文将介绍如何使用 JavaScript 创建 WebRTC UDP 连接的步骤。

第 1 步:要求

我们将使用 Node.js 服务器和普通浏览器 JavaScript 客户端。因此,需要:

  • Node.js
  • websocketwrtc节点包,npm i -g node-pre-gyp安装wrtc

第 2 步:WebSocket 连接

在创建 WebRTC 连接之前,需要一种不同的数据交换方法。可以使用 WebSockets 或普通 HTTP 请求。在这里我们将使用 WebSockets。

先从一个简单的 WebSocket 服务器开始:

// server.js

const http = require( "http" );
const WebSocketServer = require( "websocket" ).server;
const wrtc = require( "wrtc" );

// 创建一个 http 服务器
const server = http.createServer();
server.listen( 12345 ); 

// 创建一个 websocket 服务器
const wsServer = new WebSocketServer( { httpServer: server } );

let peerConnection; // 稍后用于 wrtc

// 等待 websocket 连接
wsServer.on( "request" , ( request ) =>
{
    const connection = request.accept( null , request.origin );

    connection.on( "message" , async ( message ) =>
    {
        var msg = JSON.parse( message.utf8Data );
        // [...]
    } );
} );

// 发送 websocket 消息的辅助函数
function sendWebSocketMessage( connection , type , data )
{
    var json = {};
    json.type = type;
    json.data = data;
    connection.sendUTF( JSON.stringify( json ) );
}

在客户端,还需要一个函数来连接 WebSocket 服务器:

// client.html

let webSocket;

function connectWebSocket()
{
    webSocket = new WebSocket( "ws://localhost:12345/" );
    webSocket.onmessage = async ( msg ) =>
    {
        msg = JSON.parse( msg.data );
        // [...]
    };
}

// 用于发送 WebSocket 消息的辅助函数
function sendWebSocketMessage( type , data )
{
    var json = {};
    json.type = type;
    json.data = data;
    webSocket.send( JSON.stringify( json ) );
}

第 3 步:WebRTC 连接

成功建立 WebSocket 连接后,就可以开始创建 WebRTC 连接这一稍显复杂的过程了。具体做法是初始化一个新的 RTCPeerConnection 对象,然后通过现有的 WebSocket 连接创建并向服务器发送要约。我们还必须实现一个 onicecandidate 处理程序,它负责向服务器发送可用的 ICE 候选。

// client.html

let peerConnection;

async function connectWebRTC()
{
    // 一个用于 WebRTC 协商的 stun 服务器
    var config = {
        iceServers: [ {
            urls: [ "stun:stun.l.google.com:19302" ]
        } ]
    };

    // 创建 WebRTC 对等连接
    peerConnection = new RTCPeerConnection( config );

    // WebRTC negotiations
    peerConnection.onicecandidate = ( event ) =>
    {
        if ( event.candidate )
        {            
            sendWebSocketMessage( "icecandidate" , event.candidate );
        }
    };

    // [...] 稍后会在这里创建一个数据通道

    // 创建一个 offer
    const offer = await peerConnection.createOffer();
    // 本地描述 offer
    await peerConnection.setLocalDescription( offer );

    // 将 offer 发送到服务器
    sendWebSocketMessage( "call" , offer );
}

在服务器端,我们必须对客户端的 offer 做出反应,这看起来与客户端版本非常相似(事实上,我们也可以从服务器调用,从客户端 answer,因为它是点对点的)。唯一不同的是,使用客户端的 offer 并创建一个answer,然后将其发送回客户端。

// server.js

    connection.on( "message" , async ( message ) =>
    {
        var msg = JSON.parse( message.utf8Data );
        if ( msg.type === "call" )
        {
            // 创建 WebRTC 对等连接
            peerConnection = new wrtc.RTCPeerConnection();

            // WebRTC negotiations
            peerConnection.onicecandidate = ( event ) =>
            {
                if ( event.candidate )
                {
                    sendWebSocketMessage( connection , "icecandidate" , event.candidate );
                }
            };

            // [...] 稍后会在这里处理数据通道

            // 从客户端获得的 offer 是远程描述
            await peerConnection.setRemoteDescription( new wrtc.RTCSessionDescription( msg.data ) );
            // 为 offer 创建一个 answer
            const answer = await peerConnection.createAnswer();
            // answer 是一个本地描述
            await peerConnection.setLocalDescription( answer );

            // 将 answer 发送给客户端
            sendWebSocketMessage( connection , "answer" , answer );
        }
        // 处理来自客户端的 ice candidates
        else if ( msg.type === "icecandidate" )
        {
            var candidate = new wrtc.RTCIceCandidate( {
                sdpMLineIndex: msg.data.sdpMLineIndex,
                candidate: msg.data.candidate
            } );
            try 
            {
                await peerConnection.addIceCandidate( candidate );
            }
            catch ( exception )
            {
                console.log( "exception: " , exception );
            }
        }
    } );

由于服务器对客户端的 offer 发送了 answer ,因此我们还必须在客户端对 answer 做出反应:

// client.html

    webSocket.onmessage = async ( msg ) =>
    {
        msg = JSON.parse( msg.data );
        if ( msg.type === "answer" )
        {
            // 从服务器得到的answer是远程描述
            await peerConnection.setRemoteDescription( new RTCSessionDescription( msg.data ) );
        }
        // 处理来自服务器的 ice candidates
        else if ( msg.type === "icecandidate" )
        {
            var candidate = new RTCIceCandidate( {
                sdpMLineIndex: msg.data.sdpMLineIndex,
                candidate: msg.data.candidate
            } );
            await peerConnection.addIceCandidate( candidate );
        }
    };

完成所有这些后,我们应该能够在客户端和服务器之间成功建立 WebRTC 连接。不过,WebRTC 只有在有内容要传输时才会开始协商连接细节。通常,这将是来自网络摄像头或麦克风的媒体流。但在本例中,我们想传输其他任意数据,这可以通过数据通道来实现。

第 4 步:创建数据通道

在任何一方都可以创建数据通道。不过,Node.js 的 wrtc 软件包似有问题,我只能在客户端创建数据通道时才能让它工作。

// client.html

    // 创建数据通道
    // 参考 https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/createDataChannel
    dataChannel = peerConnection.createDataChannel( "dataChannel" , {} );
    dataChannel.onopen = () => { console.log( "data channel onopen" ); };
    dataChannel.onclose = () => { console.log( "data channel onclose" ); };
    dataChannel.onmessage = ( event ) =>
    {
        console.log( "data channel onmessage: "+event.data );
    };

在服务器上,必须实现 ondatachannel 回调。在本例中,服务器只需向客户端回复同样的内容:

// server.js

    peerConnection.ondatachannel = ( event ) =>
    {
        dataChannel = event.channel;
        dataChannel.onopen = () => { console.log( "data channel onopen" ); };
        dataChannel.onclose = () => { console.log( "data channel onclose" ); };
        dataChannel.onmessage = ( event ) =>
        {
            // 对于这个简单的例子,只需回复相同的内容
            dataChannel.send( "server reply: "+event.data );
        };
    };

最后,使用以下命令在客户端发送数据:

// client.html

dataChannel.send( message );

完整实例

这是一个非常简单的示例,只有一个客户端。对于真正的游戏,服务器应该能够处理多个客户端。不过,添加这个功能也并不复杂,只需为每个客户端存储 WebSocket 和 WebRTC 连接,并向所有客户端而非仅向一个客户端发送广播即可。

另一种方法是根本不在服务器端添加 WebRTC。您也可以在客户端之间创建点对点连接,直接进行通信。不过,你仍然需要 WebSocket 服务器来转发 WebRTC offers 和 ice candidates。

虽然处理 WebSocket 连接看似是一个恼人且不必要的步骤,但它也有一个很好的作用:在游戏开发中,发送的大部分数据包都是实体位置和状态更新,你可以忍受少量的数据包丢失。但是,某些信息必须到达,比如重要的游戏状态更新。你可以在 UDP 上建立一个简单的 ACK 系统。但在这种情况下,我们已经有了 TCP WebSocket 连接,它可以保证数据包的到达。因此,我们可以使用 WebSockets 发送重要信息,而使用 WebRTC 发送其他信息。

作者:Christian Behler

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/webrtc/57225.html

(0)

相关推荐

发表回复

登录后才能评论