WebRTC 入门教程(二)|WebRTC信令控制与STUN/TURN服务器搭建


#1

前言

本文将向大家介绍两个方面的知识:

  • WebRTC信令控制
  • STUN/TURN服务器的搭建

在前面的文章中已经向大家介绍了如何构建信令服务器。但构建的信令服务器是如何工作的?那些消息需要信令服务器控制和中转?这些此前并没有做详细的说明,而本文将对这些问题做详细的讨论。

另一方面,在真实的网络中,WebRTC是如何进行NAT穿越的呢?如果穿越不成功,我们又该如何保证用户服务的呢?这些知识也将在本文中给出答案。

信令

WebRTC 信令控制的架构图如下所示:

信令服务器用于交换三种类型的信息:

  • 会话控制消息:初始化/关闭,各种业务逻辑消息以及错误报告。
  • 网络相关:外部可以识别的IP地址和端口。
  • 媒体能力:客户端能控制的编解码器、分辩率,以及它想与谁通讯。

下面我们就来详细讨论一下这三类消息:

会话控制消息

会话控制消息比较简单,像房间的创建与销毁、加入房间、离开房间、开启音频/关闭音频、开启视频/关闭视频等等这些都是会话控制消息。

对于一个真正商业的WebRTC信令服务器,还有许多的会话控制消息。像获取房间人数、静音/取消静音、切换主讲人、视频轮询、白板中的画笔、各种图型等等。但相对来说都是一引起比较简单的消息。

在我们之前的例子中,服务端只处理了一个会话消息 create or join,即房间的创建与加入消息。代码如下:

...

socket.on('create or join', function(room) {

    var clientsInRoom = io.sockets.adapter.rooms[room];
    var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;

    if (numClients === 0) {
      socket.join(room);
      logger.debug('Client ID ' + socket.id + ' created room ' + room);
      socket.emit('created', room, socket.id);

    } else if (numClients === 1) {
      io.sockets.in(room).emit('join', room);
      socket.join(room);
      socket.emit('joined', room, socket.id);
      io.sockets.in(room).emit('ready');
    } else { // max two clients
      socket.emit('full', room);
    }
});
  
...
  

该代码的逻辑非常简单,当收到 create or join 消息后,判断房间里当前人数,如果房间里的人数为 0,说明是第一个人进来,此时,需要向连接的客户端发送 created 消息;如果房间里的人数为 1,说明是第二个人进来,需要向客户端发送 joined消息;否则发送 full 消息,说明房间已满,因为目前一个房间最多只允许有两个人。

网络信息消息

网络信息消息用于两个客户端之间交换网络信息。在WebRTC中使用 ICE 机制建立网络连接。

在WebRTC的每一端,当创建好 RTCPeerConnection 对象,且调用了setLocalDescription 方法后,就开始收集 ICE候选者 了。

在WebRTC中有三种类型的候选者,它们分别是:

  • 主机候选者
  • 反射候选者
  • 中继候选者

主机候选者,表示的是本地局域网内的 IP 地址及端口。它是三个候选者中优先级最高的,也就是说在 WebRTC 底层,首先会偿试本地局域网内建立连接。

反射候选者,表示的是获取 NAT 内主机的外网IP地址和端口。其优先级低于 主机候选者。也就是说当WebRTC偿试本地连接不通时,会偿试通过反射候选者获得的 IP地址和端口进行连接。

其结构如下图所示:

在上面这幅图中可以看到,WebRTC通过 STUN server 获得自己的外网IP和端口,然后通过信令服务器与远端的WebRTC交换网络信息。之后双方就可以偿试建立 P2P 连接了。

以上就是我们通常所说的 P2P NAT 穿越。在WebRTC内部会探测用户的 NAT 类型,最终采用不同的方法进行 NAT 穿越。不过,如果双方都是 对称NAT 类型,是无法进行 P2P NAT 穿越的,此时只能使用中继了。

中继候选者,表示的是中继服务器的IP地址与端口,即通过服务器中转媒体数据。当WebRTC客户端通信双方无法穿越 P2P NAT 时,为了保证双方可以正常通讯,此时只能通过服务器中转来保证服务质量了。

所以 中继候选者的优先级是最低的,只有上述两种候选者都无法进行连接时,才会使用它。

在 WebRTC 信令服务器端,收到网络消息信令,即 message 消息时,不做任何处理,直接转发。代码如下:

socket.on('message', function(message) {
     socket.broadcast.emit('message', message);
});

客户端接收到 message 消息后,会做进一步判断。如果消息类型为 candidate,即 网络消息信令时,会生成 RTCIceCandidate 对象,并将其添加到 RTCPeerConnection 对象中,从而使 WebRTC 在底层自动建立连接。 其代码如下:

socket.on('message', function(message) {
  ...
  } else if (message.type === 'candidate') {
    var candidate = new RTCIceCandidate({
      sdpMLineIndex: message.label,
      candidate: message.candidate
    });
    pc.addIceCandidate(candidate);
  } else if (...) {
    ...
  }
});

交换媒体能力消息

在WebRTC中,媒体能力最终通过 SDP 呈现。在传输媒体数据之前,首先要进行媒体能力协商,看双方都支持那些编码方式,支持哪些分辨率等。协商的方法是通过信令服务器交换媒体能力信息。

WebRTC 媒体协商的过种如上图所示。

  • 第一步,Amy 调用 createOffer 方法创建 offer 消息。offer 消息中的内容是 Amy 的 SDP 信息。
  • 第二步,Amy 调用 setLocalDescription 方法,将本端的 SDP 信息保存起来。
  • 第三步,Amy 将 offer 消息通过信令服务器传给 Bob。
  • 第四步,Bob 收到 offer 消息后,调用 setRemoteDescription 方法将其存储起来。
  • 第五步,Bob 调用 createAnswer 方法创建 answer 消息, 同样,answer 消息中的内容是 Bob 的 SDP 信息。
  • 第六步,Bob 调用 setLocalDescription 方法,将本端的 SDP 信息保存起来。
  • 第七步,Bob 将 anwser 消息通过信令服务器传给 Amy。
  • 第八步,Amy 收到 anwser 消息后,调用 setRemoteDescription 方法,将其保存起来。

通过以上步骤就完成了通信双方媒体能力的交换。

上以就是信令服务器应该处理的所有消息,这些消息组成了信令服务器最基本的信令,每一个都必不可少,否则的话双方就无法进行最终的通信了。

在WebRTC 通讯时,光有信令是远远不够的。因为 WebRTC真正要传输的是媒体数据,信令只不过是其中的一部分。在WebRTC中他会尽可能的通过P2P进行数据的传输,但在 P2P穿越不成功时怎么办呢?

那就需要通过媒体中继服务器进行媒体数据的转发,下面我们就来看一下如何搭建媒体中继服务器吧。

搭建 STUN/TURN

在公网搭建一套 STUN/TURN 服务并不难。首先要有一台云主机,云主机的获我就不做介绍了,大家去某个云厂商购买就好了。

目前比较流行的 STUN/TURN 服务器是 coturn,使用它搭建 STUN/TURN 服务非常的方便。

下面我们就来看一下它的基本步骤:

  • 获取 coturn 源码

    git clone https://github.com/coturn/coturn.git
    
  • 编译安装

    cd coturn
    ./configure --prefix=/usr/local/coturn
    sudo make -j 4 && make install
    
  • 配置 coturn

    网上有很多关于 coturn 的配置文章,搞的很复杂。大多数人都是从网上拷贝转发的,其中有很多错误。其实只要使用 coturn 的默认设置就可以了,我这里整理了一份,如下:

    listening-port=3478        #指定侦听的端口
    external-ip=39.105.185.198 #指定云主机的公网IP地址
    user=aaaaaa:bbbbbb         #访问 stun/turn服务的用户名和密码
    realm=stun.xxx.cn          #域名,这个一定要设置
    

    所以,只需将上面 4 行配置项写入到 /usr/local/coturn/etc/turnserver.conf 配置文件中,你的 stun/turn 服务就配置好了。

  • 启动 stun/turn 服务

    cd /usr/local/coturn/bin
    turnserver -c ../etc/turnserver.conf
    
  • 测试 stun/turn 服务

    打开 trickle-ice ,按里面的要求输入 stun/turn 地址、用户和密码后就可以探测stun/turn服务是否正常了。

    以我们的配置为例,输入的信息分别是:

    • STUN or TURN URI 的值为: turn:stun.xxx.cn
    • 用户名为: aaaaaa
    • 密码为: bbbbbb

    测试的结果如下图所示:

    从上图我们可以看到该服务提供了 stun(srflx)和turn(relay)两种服务。

STUN/TURN布署好后,我们就可以使用它进行多媒体数据的传输了,再也不怕因为 NAT 和防火墙的原因导致双方无法通信的问题了。

小结

本文首先向大家详细介绍了 WebRTC 三种类型信令消息的控制与交换。然后给出了 STUN/TURN 服务器的布署、配置以及如何进行测试。

这里需要特别强调的是,STUN/TURN的布署虽然非常简单,但像 WebRTC 一样,其背后的原理确很复杂。由于篇幅的原因,我这里并没有向大家做详细的介绍,感兴趣的同学可以将其做为了一切入点进行深入的研究。


WebRTC入门教程(三) | Android 端如何使用 WebRTC
RTC 开发者社区 | 长期征文活动
#2

因为NAT多种多样, 穿越有时候真的困难


#3

是的,在国内 NAT 穿越是很困难的事儿!


#5

你好,在启动是时候遇到这个报错,楼主大概知道是什么引起的吗?

0: Domain name:
0: Default realm: stun.xxx.cn #域名,这个一定要设置
0: ERROR:
CONFIG ERROR: Empty cli-password, and so telnet cli interface is disabled! Please set a non empty cli-password!
0:
CONFIGURATION ALERT: you specified long-term user accounts, (-u option)
but you did not specify the long-term credentials option
(-a or --lt-cred-mech option).
I am turning --lt-cred-mech ON for you, but double-check your configuration.


#6

这个错误没关系,不影响turn服务的


#7

是这样的


#8

我没有域名,然后将域名那个设置写成了IP地址,然后测试是这样的,找了很多博客,还是没有解决,没有思路,到底是什么问题。。。
image


#9

将stun换成turn试试


#10

刚看到您的慕课上的课,买来学习学习。
感觉这方面刚起步,靠自己研究比其他技术要难一些。。。

有想买课的童鞋请先联系我哈哈哈,买完了才给我一个二维码,扫码买双方都能拿到36块钱。。。 亏了


#11

你好,请问这是成功了吗?
为什么会多出1个IP?
是我自己本地的网络地址吗?


#12

是安装好了


#13

请问下我安卓端用wifi可以视频,用4G不可以。是什么原因,需要配置turn服务器吗


#14

是的


#15

请教下,我在window服务器上启动coturn后,联通4G可以视频了,但是电信始终不行,是配置问题吗?请问下有没有其他办法解决?


#16

通过turn服务,无论你是电信还是联通都是可以通过的。可以不行的话,说明你的turn服务有问题或者说你的客户端没有与turn服务建立连接


#17


测试结果。
我android 中使用 iceServers.add(new PeerConnection.IceServer(“turn:139.159.216.139:3478”,“name”,“pass”));


#18

relay的IP 地址为什么是内网IP地址?这显然不对


#19

我在window服务器下,cygwin交叉编译TURN服务器。

start .\turnserver -v -L 0.0.0.0 -f -m 3 --min-port=65533 --max-port=65535 -a -u name:pass -realm=mycompany.org --max-allocate-timeout=10
start .\peerconnection_server


#20

这是启动日志,是少了什么吗?

0: log file opened: turn_5644_2019-06-04.log
0:
RFC 3489/5389/5766/5780/6062/6156 STUN/TURN Server
Version Coturn-4.5.0.8 ‘dan Eider’
0:
Max number of open files/sockets allowed for this process: 3200
0:
Due to the open files/sockets limitation,
max supported number of TURN Sessions possible is: 1000 (approximately)
0:

==== Show him the instruments, Practical Frost: ====

0: TLS supported
0: DTLS supported
0: DTLS 1.2 supported
0: TURN/STUN ALPN supported
0: Third-party authorization (oAuth) supported
0: GCM (AEAD) supported
0: OpenSSL compile-time version: OpenSSL 1.0.2n 7 Dec 2017 (0x100020ef)
0:
0: SQLite is not supported
0: Redis is not supported
0: PostgreSQL is not supported
0: MySQL is not supported
0: MongoDB is not supported
0:
0: Default Net Engine version: 2 (UDP thread per network endpoint)

=====================================================

0: WARNING: Cannot find config file: turnserver.conf. Default and command-line settings will be used.
0: Listener address to use: 0.0.0.0
0: WARNING: Cannot find config file: turnserver.conf. Default and command-line settings will be used.
0: Domain name:
0: Default realm: ealm=mycompany.org
0: WARNING: cannot find certificate file: turn_server_cert.pem (1)
0: WARNING: cannot start TLS and DTLS listeners because certificate file is not set properly
0: WARNING: cannot find private key file: turn_server_pkey.pem (1)
0: WARNING: cannot start TLS and DTLS listeners because private key file is not set properly
0: Relay address to use: 0.0.0.0
0: Cannot create pid file: /var/run/turnserver.pid
0: pid file created: turnserver.pid
0: IO method (main listener thread): poll
0: IPv4: On this platform, I am using alternative behavior of TTL according to RFC 5766.
0: IPv6: On this platform, I am using alternative behavior of TTL (HOPLIMIT) according to RFC 6156.
0: IPv4: On this platform, I am using alternative behavior of TOS according to RFC 5766.
0: WARNING: I cannot support STUN CHANGE_REQUEST functionality because only one IP address is provided
0: Wait for relay ports initialization…
0: relay 0.0.0.0 initialization…
0: relay 0.0.0.0 initialization done
0: Relay ports initialization done
0: IO method (general relay thread): poll
0: turn server id=0 created
0: IO method (general relay thread): poll
0: turn server id=1 created
0: IO method (udp listener/relay thread): poll
0: IO method (general relay thread): poll
0: turn server id=2 created
0: turn server id=128 created
0: IPv4. UDP listener opened on: 0.0.0.0:3478
0: IPv4. TCP listener opened on : 0.0.0.0:3478
0: Total UDP servers: 1
0: Total General servers: 3
0: IO method (auth thread): poll
0: IO method (auth thread): poll
0: IO method (admin thread): poll
0: IPv4. CLI listener opened on : 127.0.0.1:5766