WebRTC (Web Real-Time Communications) 是一项实时通讯技术,它允许网络应用或者站点,在不借助中间媒介的情况下,建立浏览器之间点对点(Peer-to-Peer)的连接,实现视频流和(或)音频流或者其他任意数据的传输。
这里简单介绍一个例子。在这个例子中,建立WebRTC连接的过程如下图所示:
这是一个简单基于python实现的服务端响应用户请求的例子。当客户端(Client)发起请求,服务器端(Server)与Client建立WebRTC连接,并把流媒体资源库中的demo-instruct.wav音频通过WebRTC连接的方式传输到Client,在Client这一端可以实时听到音频的播放。
webrtc的P2P连接依赖于实现在两端主体程序中的RTCPeerConnection对象(pc_client和pc_server),它们需要经历两个阶段的协商,才能建立连接。
媒体协商要做的事情,是让彼此了解对方的多媒体能力(上图中红色标记的步骤)。例如:webrtc默认使用V8编码和解码,如果Client不支持V8解码,如果没有媒体协商过程,那么即便是连接成功,Server把视频数据发给Client,对方也无法播放。进一步,如果Client支持VP8、H264多中编码格式,而Server支持VP9、H264,如果要保证两端的正常的编码、解码,最简单的办法是取它们的交集,H264。
媒体协商的过程在代码层实际上是交换了各自的sdp信息,这个过程也叫 offer/answer 过程(可能是因为Client端的sdp信息由createOffer()方法创建,而Server的sdp信息由createAnswer()方法创建)。在这个例子中,这个过程大致为,首先由Client通过普通http请求的方式将自身sdp信息Post给Server,然后Server将自身sdp信息作为响应返回给Client,它们各自使用以下语句,来设置自身的sdp,和对方的sdp。
#用于设置自身的多媒体特征sdp
pc.setLocalDescription()
#用于设置对方的多媒体特征sdp
pc.setRemoteDescription
sdp的具体格式可以分成三个部分,*号表示的是可选的。如下:
Session description
v= (protocol version)
o= (originator and session identifier)
s= (session name)
i=* (session information)
u=* (URI of description)
e=* (email address)
p=* (phone number)
c=* (connection information -- not required if included in all media)
b=* (zero or more bandwidth information lines)
[...One or more time descriptions ("t=" and "r=" lines)]
z=* (time zone adjustments)
k=* (encryption key)
a=* (zero or more session attribute lines)
[...Zero or more media descriptions]
Time description
t= (time the session is active)
r=* (zero or more repeat times)
Media description, if present
m= (media name and transport address)
i=* (media title)
c=* (connection information -- optional if included at session level)
b=* (zero or more bandwidth information lines)
k=* (encryption key)
a=* (zero or more media attribute lines)
举例如下:
v=0
o=jdoe 2890844526 2890842807 IN IP4 10.47.16.5
s=SDP Seminar
i=A Seminar on the session description protocol
u=[http://www.example.com/seminars/sdp.pdf](http://www.example.com/seminars/sdp.pdf)
[e](mailto:e=[email protected])[=[email protected]](mailto:e=[email protected]) (Jane Doe)
c=IN IP4 224.2.17.12/127
t=2873397496 2873404696
a=recvonly
m=audio 49170 RTP/AVP 0
m=video 51372 RTP/AVP 99
a=rtpmap:99 h263-1998/90000
当各端调用 setLocalDescription 后,WebRTC就开始建立网络连接,主要包括收集candidate、交换candidate和按优先级尝试连接,该过程被称为ICE(Interactive Connectivity Establishment,交互式连接建立)。其中每个 candidate 都包含IP地址、端口、传输协议、类型等信息。
根据 RFC5245 协议 ,WebRTC将 candidate分为了四个类型:host、srflx、prflx、relay,它们的优先级依次降低。
host:Host Candidate,根据主机的网卡数量决定,一般一个网卡对应一个ip地址,然后给每个ip随机分配一个端口生成。
srflx:Server Reflexive Candidate,根据STUN服务器获得的ip和端口生成。
prflx:Peer Reflexive Candidate,根据对端的ip和端口生成。
relay:Relayed Candidate,根据TURN服务器获得的ip和端口生成。
简单点来说,candidate的交换即告诉对方自己传输数据的方式。在PyWebRTC的这个例子中,candidate的交换是自动完成的。不需要额外编写代码。
以下是当Client和Server在同一个网段下,没有使用turnserver时的candidate信息
在offer中的candidate信息
a=candidate:3508291585 1 udp 2122260223 192.168.36.1 62937 typ host generation 0 network-id 1
a=candidate:3773578447 1 udp 2122194687 172.16.123.63 62938 typ host generation 0 network-id 2
a=candidate:2678043889 1 tcp 1518280447 192.168.36.1 9 typ host tcptype active generation 0 network-id 1
a=candidate:2926559295 1 tcp 1518214911 172.16.123.63 9 typ host tcptype active generation 0 network-id 2
在answer中的candidate信息
a=candidate:ae131dace505164676d6f637ba6d2232 1 udp 2130706431 172.16.123.63 62943 typ host
a=candidate:4f58facbc6fb04d2f88caee9bdc8c3c6 1 udp 2130706431 192.168.36.1 62944 typ host
a=candidate:c23b409001556fce7bcd691e8a5c284f 1 udp 1694498815 113.67.225.115 10944 typ srflx raddr 172.16.123.63 rport 62943
在实际情况中,我们更多面临由基于NAT所搭建的网络环境。NAT的用处主要有两个:
(1)解决IPv4地址不够用的问题,可以让多个主机共用一个公网IP。
(2)将主机隐藏在内网中,外网比较难访问到真实主机。
在这种情况下,基于webrtc的Client和Server无法建立对等连接,所以需要依赖turnserver的中继功能来转发,以解决处在不同网络域对等端的连接建立与通讯问题。
由于缺乏真实的Nat内外网环境,这里使用了两套虚拟网络机制来模拟内外网环境,这里记录一下搭建这套环境的过程。首先安装一个虚拟机(Ubuntu20.04),网络连接选择桥接模式,并且是自动获取ip。
这种模式下给虚拟机分配的ip会与宿主机(相对于虚拟机)处于同一网段,例如我当前宿主机(相对于虚拟机)的ip为172.16.123.63,而虚拟机的ip为172.16.123.118。这样就会形成一个“外网环境”,并且Client运行在外网环境中浏览器中。
在虚拟机中安装docker,利用docker虚拟网络模拟内网环境。注意创建容器时network_mode不能为host,因为这样将无法控制端口的开放,而在实际情况中,端口默认都是不开放的。创建容器时,docker会默认生成一个虚拟桥接网卡(使用ifconfig可以看到),这个网络与容器里查询到的ip处于一个网段。这样形成了一个“内网环境”。
如上图所示,在虚拟机中ifconfig查看网络可知,ens33是VMWare的虚拟网卡,br-7261d1118a42是它所安装的docker的其中一个虚拟网卡,可通过docker network ls 查看docker的所有虚拟网卡,如下图所示,这串数字与br-7261d1118a42对应
进去容器中查看它的ip,如下图所示,可以知道容器ip与虚拟机的虚拟网卡ip在统一网段。
最后总结,利用VMWare和Docker两套虚拟网络机制模拟的内外网网络环境可以用以下示意图来表示
由于Server和coturn部署在虚拟内网,而内网由docker模拟,所以Server和coturn需要制作成镜像。
FROM python:3
WORKDIR /usr/src/app
COPY pywebrtc ./
COPY turnserver.conf ./
COPY requirements.txt ./
COPY sources.list ./
RUN mv /etc/apt/sources.list /etc/apt/sources.list.bak \
&& cp ./sources.list /etc/apt/sources.list \
&& apt-get update \
&& apt-get install coturn -y \
&& apt-get install vim -y \
&& apt-get install net-tools \
&& pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple \
#&& openssl req -x509 -newkey rsa:2048 -keyout /etc/sslkey.pem -out /etc/sslcert.pem -days 99999 -nodes \
&& mv /etc/turnserver.conf /etc/turnserver.conf.bak \
&& cp turnserver.conf /etc/turnserver.conf \
&& rm /usr/src/app/turnserver.conf \
&& turnadmin -a -u demo -p 4080218913 -r 172.16.123.118 \
&& turnadmin -l
EXPOSE 8801
EXPOSE 3478
EXPOSE 3478/udp
EXPOSE 9000
EXPOSE 9000/udp
EXPOSE 59000-59010/udp
CMD [ "python", "main.py" ]
如上DockerFile所示,内容包括:
(1)基础镜像是python:3,因为Server是基于aiortc和tornado实现的python应用。
(2)修改镜像中系统软件源为国内源。
(3)安装coturn,vim,net-tools,安装Server运行所需要的依赖。
(3)替换镜像中coturn的配置文件turnserver.conf
(4)开放相关端口,需要对应turnserver.conf中的端口配置。8801端口用于交换sdp媒体描述信息和icecandidate网络信息(icecandidate可以手动设置);3478是tunserver的监听端口,在Client端指定turnserver地址时会用到。9000也是turnserver的监听端口,在turnserver.conf中对应tls-listening-port,使用这个端口会在TLS & DTLS基础上安全传输;59000-59010是turnserver 启动UDP中继通道的端口上下界。
镜像创建结束后,使用docker-compose.yaml创建容器。
version: '3.1'
services:
pywebrtcturn:
image: alan/pywebrtcturn:v1
container_name: pywebrtcturn
ports:
- "8801:8801"
- "3478:3478"
- "3478:3478/udp"
- "9000:9000"
- "9000:9000/udp"
- "59000-59010:59000-59010/udp"
# network_mode: "host"
使用docker-compose.yaml创建容器后,进入容器,使用以下命令创建公钥和秘钥
openssl req -x509 -newkey rsa:2048 -keyout /etc/sslkey.pem -out /etc/sslcert.pem -days 99999 -nodes
创建用户,但是在我们的例子里,这一步已在DockerFile里完成,进入容器后可以忽略这一步
turnadmin -a -u demo -p 4080218913 -r 172.16.123.118
设置turnserver的配置,/etc/turnserver.conf,如下所示
listening-ip=172.18.0.2
listening-port=3478
tls-listening-port=9000
relay-ip=172.18.0.2
relay-threads=50
external-ip=172.16.123.118
min-port=59000
max-port=59010
lt-cred-mech
Verbose
fingerprint
cert=/etc/sslcert.pem
pkey=/etc/sslkey.pem
realm=172.16.123.118
no-loopback-peers
no-multicast-peers
mobility
no-clis
这里需要特别注意几个ip的设置,listening-ip、relay-ip和external-ip等等,需要结合上面虚拟内外网网络环境的示意图来对应。几个端口的设置则要结合DockerFile和docker-compose.yaml来设置。
启动turnserver
turnserver
在前端页面作iceserver的配置便可
config.iceServers = [{url: 'turn:172.16.123.118:3478',username:"demo",credential:"4080218913"}];
在页面中点击start,等待一下,便可听到处于虚拟内网中Server返回的多媒体资源。
注意:turnserver有turn和stun两种方式,但是我用stun只成功了一次,我猜测是stun机制下配置中的min-port和max-port并没有生效,中继通道所使用的端口并没有控制在容器创建时预期的59000-59010范围内。
我研究webrtc的过程中有几个要点
(1)webrtc p2p通信指的是两侧的RTCPeerConnection对象,但是建立连接时需要帮助它们交换sdp媒体特征描述信息以及icecandidate网络传输信息,这一步可以基于用多种方式来实现。
(2)如何在有限的条件下模拟内外网环境。
(3)turnserver配置要准确,要清楚内网ip和外网ip具体分别指的是什么,要有一定的网络基础。
待更新
待更新