WebRTC in the real world: STUN, TURN and signaling

翻译☞http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/

!WebRTC中文社区原创,转载请注明出处,谢谢,水平有限,部分意思可能不到位,建议参考英文原帖!
WebRTC 可以p2p视频通话
但是...
WebRTC 仍然需要几个服务:
信令服务: 使客户端之间交换数据用来协调建立通话
NAT穿透服务:应付NATs和防火墙

这篇文章会教你怎么搭建信令服务,和用STUN/TURN服务去做nat穿透。另外,我们会解释WebRTC是怎么做到多端通话的。以及如何和VoIP/PSTN(电话)建立通话。
如果你对WebRTC还没有基础,我们强烈建议你先看下Getting Started With WebRTC
一.什么是信令服务(Signaling)?信令是一个协调沟通的过程,为了让一个WebRTC应用发起一个“通话”,客户端间需要交换以下信令信息:1.发起和关闭一个通话的控制信息;2.错误信息;3.媒体元数据,比如编码解码设置,带宽和媒体类型;4.Key数据,用于确保安全通讯;5.网络数据,比如主机在外网下的IP地址和端口。客户端的信令处理需要一种来回传递信息的方法,这种机制没有被WebRTC定义,你需要自己去创建它。下面我们将描绘几种构建信令服务的方法。在此之前,先讲几个概念……为什么WebRTC没有定义信令?为了避免冗余和最大化兼容已经确立的技术,WebRTC没有指定信令的方法和协议。-------------------------------(WebRTC设计思想是完全指定和控制媒体层,但是让signaling层尽量脱离应用,原因是不同的应用可能会使用不同的协议,比如已经存在的SIP或者Jingle呼叫协议等。这份协议中,需要交换的关键信息是多媒体会议的描述信息,包括在媒体层确定必要的传输方式和 媒体配置信息)------------------------------------------------JSEP的结构同样避免了让浏览器保存状态信息,如果让浏览器成为一个保存信令状态的机器,会出现一个问题,就是每次当页面重载的时候,信令会丢失。所以更好的方案是用服务器保存信令状态。

JSEP协议要求端对端之间需要发起(offer)和回应(answer)上面提到的数据。offer和answer用SDP(Session Description Protocol format信令描述协议格式)交流,像这样:
如果想知道所有的SDP代表的意思,可以看下这个链接:IEFT examples记住,WebRTC这样设计是为了让offer端和answer端能够在tweaked之前通过SDP文档设置好参数。举个例子: apprtc.appspot.com 里的preferAudioCodec()方法用来设置默认的编解码方式和比特率,SDP用JavaScript比较难操作,未来的版本可能会用JSON代替,但是SDP还是有一些优势的。二.RTCPeerConnection + 信令: offer,answer和candidateRTCPeerConnection 是WebRTC客户端在两端建立音视频通讯的API。初始化RTCPeerConnection进程需要两个步骤:1.确定当期的媒体条件,例如分辨率,编解码能力。这些是给offer和answer的原始数据。2.获得应用主机的网络地址(也就是candidate)一旦这些本地数据被确定好了,就必须通过信令机制在不同端交换。假设A想呼叫B,下面是整个offer/answer机制的细节:1.A创建一个RTCPeerConnection对象。2.A用RTCPeerConnection的createOffer()方法创建一个offer(用SDP协议描述)。3.A用他的offer设置本地描述setLocalDescription()。4.A序列化offer,并且用信令机制发送给B.5.B用A的offer调用setRemoteDescription()设置对方的描述,B的RTCPeerConnection就知道了A的配置了。6.B调用createAnswer(),如果成功会返回一个本地的session描述,既B的answer。7.B用她的answer设置为本地的描述,通过调用setLocalDescription().设置本地描述8.B用信令机制发送序列化后的answer给A。9.A设置B的answer为对方session描述,通过调用setRemoteDescription()设置对方的描述.(至此,A和B都设置了本地和对方的描述)A和B还需要交换网络信息。'finding candidates' 指的是用ICE framework.去发现网络接口和端口。1.A用一个onIceCandidate handler创建一个RTCPeerConnection对象。2.当网络candidates有效时这个handler会被调用。3.在这个handler里,A发送序列化的candidates数据给B,通过信令通道。4.当B从A获得一个candidate信息,她调用addIceCandidate()去给对方描述添加candidate。JSEP支持ICE Candidate Trickling技术(允许呼叫者在首次初始化offer后,逐次发送candidates给被呼叫者,这是为了让被呼叫者开始设置连接而不用等到全部的candidates到达)WebRTC 的信令编码下面是W3C code exampleW3C代码样例,概况了完整的signaling过程。样例假设已经有了信令机制:SignalingChannel。Signaling 会在下面探讨比较多的细节。
var signalingChannel = new SignalingChannel();
var configuration = {
'iceServers': [{
'url': 'stun:stun.example.org'
}]
};
var pc;

// call start() to initiate

function start() {
pc = new RTCPeerConnection(configuration);

// send any ice candidates to the other peer
pc.onicecandidate = function (evt) {
if (evt.candidate)
signalingChannel.send(JSON.stringify({
'candidate': evt.candidate
}));
};

// let the 'negotiationneeded' event trigger offer generation
pc.onnegotiationneeded = function () {
pc.createOffer(localDescCreated, logError);
}

// once remote stream arrives, show it in the remote video element
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};

// get a local stream, show it in a self-view and add it to be sent
navigator.getUserMedia({
'audio': true,
'video': true
}, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
}, logError);
}

function localDescCreated(desc) {
pc.setLocalDescription(desc, function () {
signalingChannel.send(JSON.stringify({
'sdp': pc.localDescription
}));
}, logError);
}

signalingChannel.onmessage = function (evt) {
if (!pc)
start();

var message = JSON.parse(evt.data);
if (message.sdp)
pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
// if we received an offer, we need to answer
if (pc.remoteDescription.type == 'offer')
pc.createAnswer(localDescCreated, logError);
}, logError);
else
pc.addIceCandidate(new RTCIceCandidate(message.candidate));
};

function logError(error) {
log(error.name + ': ' + error.message);
}

复制代码
查看“单页面”视频聊天的例子simpl.info/pc.可以在 控制台的lgo看到offer/answer 和candidate 的交换过程。如果你想了解更多,可以在Chrome浏览器打开 chrome://webrtc-internals 或在opera打开 opera://webrtc-internals下载完整的代码。三.成员发现机制(Peer discovery)这里有个问题: 我怎么发现谁可以通话?对于电话,我们有电话号码和目录。对于在线视频聊天,我们需要身份和业务管理系统和一种让用户开始会话的手段。WebRTC apps需要一种 让客户端标示自己以便可以开始和加入会话的方法。成员发现机制Peer discovery mechanisms没有被WebRTC定义,在这里我们不用做选择。这个过程可以像发送一个URL地址这么简单,对于视频聊天应用,比如 talky.io, tawk.com and browsermeeting.com,你通过分享一个通用链接邀请别人进入一个会话。开发者Chris Ball开发了一个有趣的实验:serverless-webrtc,可以让WebRTC呼叫参与者分享元数据,通过任何信息服务,比如IM,email或者信鸽。四.怎么创建一个signling服务?再说一遍:信令机制没有被WebRTC标准定义,无论你选择哪种 ,你需要一个中间服务器去交换信令信息和不同客户端间的应用数据。庆幸的是,信令信息很小,大部分交换都是在通话开始的时候。在测试 apprtc.appspot.comsamdutton-nodertc.jit.su 时,我们发现一个 视频会话,总共有大概30-45的信息被信令服务器处理,信息大小大概是10kB。除了相对要求不高的带宽,WebRTC 信令服务器不用花费过多的内存和进程,因为只需要转发信息和保持很少的会议状态数据(比如那个客户端被连接了)小贴士 :信令机制不仅可以用来交换会话元数据,也能用来传达应用数据。它就是个信息服务。五.从服务端推信息给客户端一个信令服务器需要是双向的:客户端到服务器和服务器到客户端。双向通讯违反了HTTP 客户端/服务端 请求/回复的模式,但是有一些发展多年的技术,例如long polling(长时间轮询) 被用来从服务端发送数据给一个运行中的web应用。最近,EventSource API 被广泛的应用,它允许“服务端发送事件”:数据通过HTTP从服务端发送给浏览器。这里有个简单的demo:simpl.info/es。EventSource被设计为一种消息传送方式,但是它可以跟XHR 结合做成一个交换signaling的服务:从一个呼叫者传递信息,由XHR 请求传递,推送给被呼叫者。WebSocket 是一种更自然的解放方案,它是为了全双工 客户端-服务端通讯设计的(信息可以在同一时间在两个端传递)。用纯WebSocket或者Server-Sent Events (EventSource) 做为signaling服务的优点是后端调用这些APIs可以用多种Web框架实现,在使用PHP,Python和Ruby的情况下。大约有四分之三的浏览器支持WebSocket ,更重要的是,所有支持WebRTC的桌面浏览器和移动浏览器都支持WebSocket。TLS(安全传输层协议)应该用于所有的链接,已确保信息不会被截断。同时用proxy traversal减少问题(更多关于WebSocket 和proxy traversal的资料可以看WebRTC chapterWebSocket Cheat Sheetapprtc.appspot.com 的信令是通过Google App Engine Channel API完成的,Google App Engine Channel API是使用了Comet技术(长时间轮询)让APP后端和web客户端 实现推送通讯功能。这里有个代码预演。另外一种方案,可以通过Ajax去轮询服务端获取signaling,但会导致一堆多余的网络请求,特别是在移动客户端。在一个会话被确定后,用户仍然需要去轮询signaling信息,因为会话可能会被其他用户改变或者终止。《WebRTC》这本书就用了这种经过优化轮询频率的方法。信令压缩虽然一个信令服务器在每一个客户端中花费相当小的带宽和CPU,但是一个普遍使用的应用可能需要从不同的地点处理很多信息,并且有很多高的并发数。一个大流量的WebRTC 应用需要心理服务端去处理相当大的负荷。这里我们不讲细节,下面有一些 处理高数据量,高性能的信息通讯设置:1.XMPP,最初被称为Jabber:一种被开发用来即时通讯的协议,可以用来做signaling。服务端可以用 ejabberd andOpenfire实现。JavaScript客户端,例如 Strophe.js 使用BOSH去模仿双向通讯流,但因为各种原因,BOSH可能不像WebSocket那么有效率。(Jingle 是一种支持视频和语音的XMPP扩展,WebRTC从libjingle库(Jingle的C++实现库)里使用了网络和传输组件 )2.像 ZeroMQ(据说TokBox服务端使用了)、OpenMQ的开源库。3.使用支持WebSocket商业的云服务平台。4.商业的WebRTC 平台,比如vLine.开发者Phil Leggetter提供了一系列信息服务器和第三方库列表在Real-Time Web Technologies Guide。用Node开发基于Sockket.io的信令服务下面有个例子,Socket.io可以轻易创建一个用于交换信息的服务。Socket.io非常适合WebRTC 的信令,因为它就是以“rooms”的概念设计的。这个demo不是一个产品级别的服务,但是能够应付小数量的用户。Socket.io通过下面的回调使用WebSocket: Adobe Flash Socket, AJAX long polling, AJAX multipart streaming, Forever Iframe and JSONP polling。Socket.io也被移植到后端版本,但是最广为人知的是Node版本。这个demo没有WebRTC,它只是展示怎么创建一个webapp的signaling。用控制台查看log,去看下客户端加入一个房间和交换数据发生了什么变化。WebRTC codelab会一步一步教你怎么整合这个demo变成一个完整的WEbRTC视频聊天应用。你可以从step 5 of the codelab repo下载源码或者在samdutton-nodertc.jit.su运行(用两个浏览器打开这个链接 )这是客户端的index.htl:

还有JavaScript文件main.js:
完整的服务端:
var static = require('node-static');
var http = require('http');
var file = new(static.Server)();
var app = http.createServer(function (req, res) {
file.serve(req, res);
}).listen(2013);

var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket){

// convenience function to log server messages to the client
function log(){
var array = ['>>> Message from server: '];
for (var i = 0; i < arguments.length; i++) {
array.push(arguments[i]);
}
socket.emit('log', array);
}

socket.on('message', function (message) {
log('Got message:', message);
// for a real app, would be room only (not broadcast)
socket.broadcast.emit('message', message);
});

socket.on('create or join', function (room) {
var numClients = io.sockets.clients(room).length;

log('Room ' + room + ' has ' + numClients + ' client(s)');
log('Request to create or join room ' + room);

if (numClients === 0){
  socket.join(room);
  socket.emit('created', room);
} else if (numClients === 1) {
  io.sockets.in(room).emit('join', room);
  socket.join(room);
  socket.emit('joined', room);
} else { // max two clients
  socket.emit('full', room);
}
socket.emit('emit(): client ' + socket.id + ' joined room ' + room);
socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room);

});

});

复制代码
要运行这个app,你需要安装Node, socket.io and node-static。可以在 nodejs.org下载Node,再安装 socket.io 和node-static,在终端运行Node Package Manager:npm install socket.ionpm install node-static启动服务,运行下面命令node server.js在浏览器打开 localhost:2013.用新的浏览器打开localhost:2013 ,用控制台看下发生了什么使用 RTCDataChannel交换信息初始化一个WebRTC会话,必须有一个信令 服务器。然而,一旦两端确定了 一个通话,理论上,RTCDataChannel可以接替信令通道,这可以减少信号的延迟。一旦信息直接在两端通讯,RTCDataChannel会帮忙减少带宽使用和进程开销。没有例子,但可以看下面:信令性能和扩展性1.RTCPeerConnection 不会搜集candidates,直到setLocalDescription() 被调用。这个被JSEP IETF draft.强制要求了。2.利用Trickle ICE(看上面解释):接收到candidates后立即调用addIceCandidate(),现成的信令服务 这里有一些可以用的WebRTC signaling服务端:
webRTC.io: 第一个抽象库 for WebRTC.
easyRTC: 一个完整的WebRTC包 a full-stack WebRTC package.
Signalmaster:一个使用 SimpleWebRTCJavaScrip客户端库的signaling服务

如果你一点都不想编码,你可以用完整的商业WebRTC平台,像vLine, OpenTok and Asterisk爱立信创建了一个 signaling server using PHP on Apache,在WebRTC早期的时候,现在这个已经被弃用了,但是如果你考虑到相似的情况,这个代码还是值得一看的。六.Signaling安全
Security is the art of making nothing happen.
Salman Rushdie

所有WebRTC 组件都被强制加密。
但是信令机制没有被WebRTC标准定义,所有确保信令安全取决于你,如果一个攻击者想去劫持信令,他们会导致会话中止,重定向链接和记录,改变或者注入内容。

一个牢固的信令最重要的功能是使用加密协议,HTTPS 和WSS (i.e TLS)可以确保信息不会非加密拦截。
同时,小心不要广播信令信息,不然攻击者可以使用相同的信令服务链接其他来电用户。

使用 TLS.去确保WebRTC应用的安全。
信令交互完之后,使用ICE去处理NATs和防火墙对于元数据的信令,WebRTC应用可以使用中间服务,但实际的媒体和数据流在一个会话确立后,RTCPeerConnection 尝试去直连客户端:P2P在一个简单的世界里,每一个WebRTC端都有一个唯一的地址,这样他可以与其他端交换数据,以便直接 通讯。

实际情况下,大多数设备都在一个或多个NAT层后面,有些有防毒软件阻碍确定的端口和协议,还有很多在代理和公司的防火墙后面。防火墙和NAT实际上可能由一些类似家庭wifi路由器产生的。
WebRTC 可以使用ICE框架去克服真实世界的复杂网络。为了实现这个功能,你的应用必须传ICE服务地址给RTCPeerConnection,如下所述。ICE 试着寻找最佳路线去连接对方,它会并行的寻找所有可能性,然后选择最有效的可行方式。 ICE首先会尝试用设备系统或网卡获取到的主机地址去建立连接;如果这个失败了(设备在NATs后面就会)ICE从STUN服务器获得外部的地址,如果这个也失败了,就用TURN中转服务器做通讯。
也就是说:
STUN服务器用来获取外部网络地址。
如果P2P失败的话,[size=14.4444446563721px]TURN服务器用来中继通讯。

每一个TURN服务器都支持STUN:一个TURN服务器是由一个STUN服务器加上中继功能。ICE也可以用来应付复杂的NAT设置:
事实上,NAT的”打洞“可能需要除了公共IP之外的端口地址。

WebRTC应用在iceServers配置对象(RTCPeerConnection constructor)里设置STUN and/or TURN服务器地址。
apprtc.appspot.com里这个值像这样:
{
'iceServers': [
{
'url': 'stun:stun.l.google.com:19302'
},
{
'url': 'turn:192.158.29.39:3478?transport=udp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
},
{
'url': 'turn:192.158.29.39:3478?transport=tcp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
}
]
}

复制代码

一旦RTCPeerConnection 有了这些信息,ICE会自动启动:RTCPeerConnection 使用ICE框架计算出两端间最佳路线,需要STUN和TURN服务器。

STUNNATs会给它的设备提供一个内部网络IP地址,但这个地址不能在外网使用,因为没有外网的地址,所有WebRTC没办法做连接,为解决这个问题,WebRTC使用了STUN。STUN服务架设在外网,它有一个简单的任务:获取一个发送请求的设备(运行在NAT后边的应用)的IP和端口,然后返回这个地址。换句话说,应用使用STUN服务器发现它的外网IP和端口,这个过程确保了一个WebRTC端获得它自己的公共地址,然后通过signaling机制发送这个信息给另一端,这样就可以建立起一个直接连接。(在实际中,不同的NATs有不同的工作方式,可能有多个NAT层,但是原理是一样的)STUN服务器不需要做太多工作和存储太多东西,所以简单的STUN服务器可以应付大量的请求。根据 webrtcstats.com的统计,使用STUN方式建立WebRTC通话的成功率有86%的。

TURNRTCPeerConnection 会试着用UDP在两端建立一个直连,如果失败了,RTCPeerConnection 会改用TCP,如果这个再失败了,TURN服务器会被作为后备方案使用,在两端间中继数据。
重述:TURN是在两端间中转视频/语音/数据 流,而不是发送数据。

TURN 有个公共地址,所以每个端即使在防火墙或者代理后面,也能访问到。
TURN有个简单的任务,中转数据流,但不像STUN,TURN会花费大量带宽。所有,TURN需要够强壮。


图表表示TURN的作用:单纯的STUN不起作用,客户端就会转向使用TURN。
部署 STUN 和 TURN 服务器作为测试,谷歌公布了一个公共的STUN服务,stun.l.google.com:19302, apprtc.appspot.com用的就是这个。作为一个产品级别的 STUN/TURN服务器,我们建议使用 rfc5766-turn-server,STUN 和TURN的源码可以从code.google.com/p/rfc5766-turn-server获取,这个链接也包括了部署的资料。A VM image for Amazon Web Services is also available.本社区也发布了部署教程:部署教程一个可代替的TURN服务器是restrund,可以在source code 下载到,下面介绍在谷歌Compute Engine部署resrund的步骤:
Open firewall as necessary, for tcp=443, udp/tcp=3478
Create four instances, one for each public IP, Standard Ubuntu 12.06 image
Set up local firewall config (allow ANY from ANY)
Install tools:sudo apt-get install makesudo apt-get install gcc
Install libre from creytiv.com/re.html
Fetch restund from creytiv.com/restund.html and unpack
wget hancke.name/restund-auth.patch and apply with patch -p1 < restund-auth.patch
Run make, sudo make install for libre and restund
Adapt restund.conf to your needs (replace IP addresses and make sure it contains the same shared secret) and copy to /etc
Copy restund/etc/restund to /etc/init.d/
Configure restund:Set LD_LIBRARY_PATHCopy restund.conf to /etc/restund.confSet restund.conf to use the right 10. IP address
Run restund
Test using stund client from remote machine: ./client IP:port

七.突破p2p:多人会议WebRTC你可以需要看下Justin Uberti提议的IETF标识:请求TURN服务的API很容易想象到一些场景不只是一对一的视频通话,举个例子,公司小组需要一个视频会议,或者一个公开的演讲,一个演讲者面对数百(或者数千)的观看者。一个WebRTC应用可以使用多个RTCPeerConnections,这样每一个端可以连接其他端形成一个网络。talky.io就是使用这种方法实现,对于少数的用户,可以很好的工作。但是进程和带宽开销会非常大,特别是移动客户端。

在一个星型结构里,一个WebRTC客户端可以选择一个端去分布数据流给所有的用户,你可以自己设计重新分配机制的服务和构造区实现这种方式(werrtc.org提供了一个样例sample client application)从Chrome31和Opera18 开始,从一个RTCPeerConnection 获取的媒体流,可以作为对方的输入:这里有个demosimpl.info/multi。这样可以确保更灵活的结构,因为它可以允许web应用通过选择哪个用户可以连接去控制一个通话 路由。多点控制部件MCU(Multipoint Control Unit)
大量用户通话的更好解决方案是使用Multipoint Control Unit(MCU)。这是一个在大量参与者间分布媒体的桥接服务器。MCUs可以在一个视频 会议里处理不同的分辨率,编解码,和帧速率。对于多端会议,有很多因素要考虑:最重要的是,从多个源里,怎么显示多个视频和混合音频。像 vLine 的云平台也在致力于优化传输路由。

你可以去买一个MCU硬件或者自己搭一个。

Cisco MCU背部有几个开源的MCU硬件款可以选,例如 Licode(以前称为Lynckia) 生产的开源 MCU for WebRTC; OpenTok 平台的Mantis.突破浏览器: VoIP, telephones 和 messagingWebRTC 的标准让浏览器和不同设备不同平台,例如手机或者一个视频会议系统,进行通话称为可能。SIP是一种信令协议,用来做VoIP和视频系统。为了让WebRTC和SIP端通讯,WebRTC需要一个代理服务器去调解信令。信令一定会经过网关,但是一旦会话建立,视频和语音就能在两端传输。PSTN,公共电话交换网络,是旧式模拟电话的交换网络。为了WebRTC和电话进行通话,必须通过一个PSTN网关。同理,要让WebRTC跟Jingle端(像IM客户端)通讯,需要一个中间XMPP服务器。Jingle作为XMPP的扩展,用来实现视频和语音能够作为信息服务:现在的WebRTC就是基于C++实现libjingle 库发展来的,Jingle最初是Google Talk的技术。一堆应用库,平台让WebRTC能在实际中通讯:
sipML5:一个 开源的 JavaScript SIP 客户端
jsSIP: JavaScript SIP库
Phono: 开源JavaScript phone API, 作为一个插件
Zingaya: 一个嵌入式电话部件
Twilio: 音频和信息
Uberconference: 会议技术

sipML5 的开发者也开发了webrtc2sip的网关Tethr and Tropo have demonstrated a framework for disaster communications 'in a briefcase', using an OpenBTS cell to enable communications between feature phones and computers via WebRTC. Telephone communication without a carrier! 发现更多WebRTC codelab: 一步一步教你怎么打造一个视频和文本聊天应用,使用Socket.io Signaling服务。在Node上面跑。2013 Google I/O WebRTC presentation WebRTC领头人Justin Uberti.
Chris Wilson 关于WebRTC的介绍:Introduction to WebRTC Apps.
WebRTC Book 这本书有很多关于数据、signaling、网络拓扑结构的细节。
WebRTC and Signaling: What Two Years Has Taught Us:介绍为什么把signaling脱离出标准是个好主意
A Practical Guide to Building WebRTC Apps:提供很多WebRTC的技术和基础建设信息。
WebRTC chapter 深入研究WebRTC的结构,使用案例和性能。翻译☞http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/
!WebRTC中文社区原创,转载请注明出处,谢谢,水平有限,部分意思可能不到位,建议参考英文原帖!
WebRTC 可以p2p视频通话
但是...
WebRTC 仍然需要几个服务:
信令服务: 使客户端之间交换数据用来协调建立通话
NAT穿透服务:应付NATs和防火墙

这篇文章会教你怎么搭建信令服务,和用STUN/TURN服务去做nat穿透。另外,我们会解释WebRTC是怎么做到多端通话的。以及如何和VoIP/PSTN(电话)建立通话。
如果你对WebRTC还没有基础,我们强烈建议你先看下Getting Started With WebRTC
一.什么是信令服务(Signaling)?信令是一个协调沟通的过程,为了让一个WebRTC应用发起一个“通话”,客户端间需要交换以下信令信息:1.发起和关闭一个通话的控制信息;2.错误信息;3.媒体元数据,比如编码解码设置,带宽和媒体类型;4.Key数据,用于确保安全通讯;5.网络数据,比如主机在外网下的IP地址和端口。客户端的信令处理需要一种来回传递信息的方法,这种机制没有被WebRTC定义,你需要自己去创建它。下面我们将描绘几种构建信令服务的方法。在此之前,先讲几个概念……为什么WebRTC没有定义信令?为了避免冗余和最大化兼容已经确立的技术,WebRTC没有指定信令的方法和协议。-------------------------------(WebRTC设计思想是完全指定和控制媒体层,但是让signaling层尽量脱离应用,原因是不同的应用可能会使用不同的协议,比如已经存在的SIP或者Jingle呼叫协议等。这份协议中,需要交换的关键信息是多媒体会议的描述信息,包括在媒体层确定必要的传输方式和 媒体配置信息)------------------------------------------------JSEP的结构同样避免了让浏览器保存状态信息,如果让浏览器成为一个保存信令状态的机器,会出现一个问题,就是每次当页面重载的时候,信令会丢失。所以更好的方案是用服务器保存信令状态。

JSEP协议要求端对端之间需要发起(offer)和回应(answer)上面提到的数据。offer和answer用SDP(Session Description Protocol format信令描述协议格式)交流,像这样:
如果想知道所有的SDP代表的意思,可以看下这个链接:IEFT examples记住,WebRTC这样设计是为了让offer端和answer端能够在tweaked之前通过SDP文档设置好参数。举个例子: apprtc.appspot.com 里的preferAudioCodec()方法用来设置默认的编解码方式和比特率,SDP用JavaScript比较难操作,未来的版本可能会用JSON代替,但是SDP还是有一些优势的。二.RTCPeerConnection + 信令: offer,answer和candidateRTCPeerConnection 是WebRTC客户端在两端建立音视频通讯的API。初始化RTCPeerConnection进程需要两个步骤:1.确定当期的媒体条件,例如分辨率,编解码能力。这些是给offer和answer的原始数据。2.获得应用主机的网络地址(也就是candidate)一旦这些本地数据被确定好了,就必须通过信令机制在不同端交换。假设A想呼叫B,下面是整个offer/answer机制的细节:1.A创建一个RTCPeerConnection对象。2.A用RTCPeerConnection的createOffer()方法创建一个offer(用SDP协议描述)。3.A用他的offer设置本地描述setLocalDescription()。4.A序列化offer,并且用信令机制发送给B.5.B用A的offer调用setRemoteDescription()设置对方的描述,B的RTCPeerConnection就知道了A的配置了。6.B调用createAnswer(),如果成功会返回一个本地的session描述,既B的answer。7.B用她的answer设置为本地的描述,通过调用setLocalDescription().设置本地描述8.B用信令机制发送序列化后的answer给A。9.A设置B的answer为对方session描述,通过调用setRemoteDescription()设置对方的描述.(至此,A和B都设置了本地和对方的描述)A和B还需要交换网络信息。'finding candidates' 指的是用ICE framework.去发现网络接口和端口。1.A用一个onIceCandidate handler创建一个RTCPeerConnection对象。2.当网络candidates有效时这个handler会被调用。3.在这个handler里,A发送序列化的candidates数据给B,通过信令通道。4.当B从A获得一个candidate信息,她调用addIceCandidate()去给对方描述添加candidate。JSEP支持ICE Candidate Trickling技术(允许呼叫者在首次初始化offer后,逐次发送candidates给被呼叫者,这是为了让被呼叫者开始设置连接而不用等到全部的candidates到达)WebRTC 的信令编码下面是W3C code exampleW3C代码样例,概况了完整的signaling过程。样例假设已经有了信令机制:SignalingChannel。Signaling 会在下面探讨比较多的细节。var signalingChannel = new SignalingChannel();
var configuration = {
'iceServers': [{
'url': 'stun:stun.example.org'
}]
};
var pc;

// call start() to initiate

function start() {
pc = new RTCPeerConnection(configuration);

// send any ice candidates to the other peer
pc.onicecandidate = function (evt) {
if (evt.candidate)
signalingChannel.send(JSON.stringify({
'candidate': evt.candidate
}));
};

// let the 'negotiationneeded' event trigger offer generation
pc.onnegotiationneeded = function () {
pc.createOffer(localDescCreated, logError);
}

// once remote stream arrives, show it in the remote video element
pc.onaddstream = function (evt) {
remoteView.src = URL.createObjectURL(evt.stream);
};

// get a local stream, show it in a self-view and add it to be sent
navigator.getUserMedia({
'audio': true,
'video': true
}, function (stream) {
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
}, logError);
}

function localDescCreated(desc) {
pc.setLocalDescription(desc, function () {
signalingChannel.send(JSON.stringify({
'sdp': pc.localDescription
}));
}, logError);
}

signalingChannel.onmessage = function (evt) {
if (!pc)
start();

var message = JSON.parse(evt.data);
if (message.sdp)
pc.setRemoteDescription(new RTCSessionDescription(message.sdp), function () {
// if we received an offer, we need to answer
if (pc.remoteDescription.type == 'offer')
pc.createAnswer(localDescCreated, logError);
}, logError);
else
pc.addIceCandidate(new RTCIceCandidate(message.candidate));
};

function logError(error) {
log(error.name + ': ' + error.message);
}

复制代码
查看“单页面”视频聊天的例子simpl.info/pc.可以在 控制台的lgo看到offer/answer 和candidate 的交换过程。如果你想了解更多,可以在Chrome浏览器打开 chrome://webrtc-internals 或在opera打开 opera://webrtc-internals下载完整的代码。三.成员发现机制(Peer discovery)这里有个问题: 我怎么发现谁可以通话?对于电话,我们有电话号码和目录。对于在线视频聊天,我们需要身份和业务管理系统和一种让用户开始会话的手段。WebRTC apps需要一种 让客户端标示自己以便可以开始和加入会话的方法。成员发现机制Peer discovery mechanisms没有被WebRTC定义,在这里我们不用做选择。这个过程可以像发送一个URL地址这么简单,对于视频聊天应用,比如 talky.io, tawk.com and browsermeeting.com,你通过分享一个通用链接邀请别人进入一个会话。开发者Chris Ball开发了一个有趣的实验:serverless-webrtc,可以让WebRTC呼叫参与者分享元数据,通过任何信息服务,比如IM,email或者信鸽。四.怎么创建一个signling服务?再说一遍:信令机制没有被WebRTC标准定义,无论你选择哪种 ,你需要一个中间服务器去交换信令信息和不同客户端间的应用数据。庆幸的是,信令信息很小,大部分交换都是在通话开始的时候。在测试 apprtc.appspot.comsamdutton-nodertc.jit.su 时,我们发现一个 视频会话,总共有大概30-45的信息被信令服务器处理,信息大小大概是10kB。除了相对要求不高的带宽,WebRTC 信令服务器不用花费过多的内存和进程,因为只需要转发信息和保持很少的会议状态数据(比如那个客户端被连接了)小贴士 :信令机制不仅可以用来交换会话元数据,也能用来传达应用数据。它就是个信息服务。五.从服务端推信息给客户端一个信令服务器需要是双向的:客户端到服务器和服务器到客户端。双向通讯违反了HTTP 客户端/服务端 请求/回复的模式,但是有一些发展多年的技术,例如long polling(长时间轮询) 被用来从服务端发送数据给一个运行中的web应用。最近,EventSource API 被广泛的应用,它允许“服务端发送事件”:数据通过HTTP从服务端发送给浏览器。这里有个简单的demo:simpl.info/es。EventSource被设计为一种消息传送方式,但是它可以跟XHR 结合做成一个交换signaling的服务:从一个呼叫者传递信息,由XHR 请求传递,推送给被呼叫者。WebSocket 是一种更自然的解放方案,它是为了全双工 客户端-服务端通讯设计的(信息可以在同一时间在两个端传递)。用纯WebSocket或者Server-Sent Events (EventSource) 做为signaling服务的优点是后端调用这些APIs可以用多种Web框架实现,在使用PHP,Python和Ruby的情况下。大约有四分之三的浏览器支持WebSocket ,更重要的是,所有支持WebRTC的桌面浏览器和移动浏览器都支持WebSocket。TLS(安全传输层协议)应该用于所有的链接,已确保信息不会被截断。同时用proxy traversal减少问题(更多关于WebSocket 和proxy traversal的资料可以看WebRTC chapterWebSocket Cheat Sheetapprtc.appspot.com 的信令是通过Google App Engine Channel API完成的,Google App Engine Channel API是使用了Comet技术(长时间轮询)让APP后端和web客户端 实现推送通讯功能。这里有个代码预演。另外一种方案,可以通过Ajax去轮询服务端获取signaling,但会导致一堆多余的网络请求,特别是在移动客户端。在一个会话被确定后,用户仍然需要去轮询signaling信息,因为会话可能会被其他用户改变或者终止。《WebRTC》这本书就用了这种经过优化轮询频率的方法。信令压缩虽然一个信令服务器在每一个客户端中花费相当小的带宽和CPU,但是一个普遍使用的应用可能需要从不同的地点处理很多信息,并且有很多高的并发数。一个大流量的WebRTC 应用需要心理服务端去处理相当大的负荷。这里我们不讲细节,下面有一些 处理高数据量,高性能的信息通讯设置:1.XMPP,最初被称为Jabber:一种被开发用来即时通讯的协议,可以用来做signaling。服务端可以用 ejabberd andOpenfire实现。JavaScript客户端,例如 Strophe.js 使用BOSH去模仿双向通讯流,但因为各种原因,BOSH可能不像WebSocket那么有效率。(Jingle 是一种支持视频和语音的XMPP扩展,WebRTC从libjingle库(Jingle的C++实现库)里使用了网络和传输组件 )2.像 ZeroMQ(据说TokBox服务端使用了)、OpenMQ的开源库。3.使用支持WebSocket商业的云服务平台。4.商业的WebRTC 平台,比如vLine.开发者Phil Leggetter提供了一系列信息服务器和第三方库列表在Real-Time Web Technologies Guide。用Node开发基于Sockket.io的信令服务下面有个例子,Socket.io可以轻易创建一个用于交换信息的服务。Socket.io非常适合WebRTC 的信令,因为它就是以“rooms”的概念设计的。这个demo不是一个产品级别的服务,但是能够应付小数量的用户。Socket.io通过下面的回调使用WebSocket: Adobe Flash Socket, AJAX long polling, AJAX multipart streaming, Forever Iframe and JSONP polling。Socket.io也被移植到后端版本,但是最广为人知的是Node版本。这个demo没有WebRTC,它只是展示怎么创建一个webapp的signaling。用控制台查看log,去看下客户端加入一个房间和交换数据发生了什么变化。WebRTC codelab会一步一步教你怎么整合这个demo变成一个完整的WEbRTC视频聊天应用。你可以从step 5 of the codelab repo下载源码或者在samdutton-nodertc.jit.su运行(用两个浏览器打开这个链接 )这是客户端的index.htl:

还有JavaScript文件main.js:
完整的服务端:var static = require('node-static');
var http = require('http');
var file = new(static.Server)();
var app = http.createServer(function (req, res) {
file.serve(req, res);
}).listen(2013);

var io = require('socket.io').listen(app);

io.sockets.on('connection', function (socket){

// convenience function to log server messages to the client
function log(){
var array = ['>>> Message from server: '];
for (var i = 0; i < arguments.length; i++) {
array.push(arguments[i]);
}
socket.emit('log', array);
}

socket.on('message', function (message) {
log('Got message:', message);
// for a real app, would be room only (not broadcast)
socket.broadcast.emit('message', message);
});

socket.on('create or join', function (room) {
var numClients = io.sockets.clients(room).length;

log('Room ' + room + ' has ' + numClients + ' client(s)');
log('Request to create or join room ' + room);

if (numClients === 0){
  socket.join(room);
  socket.emit('created', room);
} else if (numClients === 1) {
  io.sockets.in(room).emit('join', room);
  socket.join(room);
  socket.emit('joined', room);
} else { // max two clients
  socket.emit('full', room);
}
socket.emit('emit(): client ' + socket.id + ' joined room ' + room);
socket.broadcast.emit('broadcast(): client ' + socket.id + ' joined room ' + room);

});

});

复制代码
要运行这个app,你需要安装Node, socket.io and node-static。可以在 nodejs.org下载Node,再安装 socket.io 和node-static,在终端运行Node Package Manager:npm install socket.ionpm install node-static启动服务,运行下面命令node server.js在浏览器打开 localhost:2013.用新的浏览器打开localhost:2013 ,用控制台看下发生了什么使用 RTCDataChannel交换信息初始化一个WebRTC会话,必须有一个信令 服务器。然而,一旦两端确定了 一个通话,理论上,RTCDataChannel可以接替信令通道,这可以减少信号的延迟。一旦信息直接在两端通讯,RTCDataChannel会帮忙减少带宽使用和进程开销。没有例子,但可以看下面:信令性能和扩展性1.RTCPeerConnection 不会搜集candidates,直到setLocalDescription() 被调用。这个被JSEP IETF draft.强制要求了。2.利用Trickle ICE(看上面解释):接收到candidates后立即调用addIceCandidate(),现成的信令服务 这里有一些可以用的WebRTC signaling服务端:webRTC.io: 第一个抽象库 for WebRTC.
easyRTC: 一个完整的WebRTC包 a full-stack WebRTC package.
Signalmaster:一个使用 SimpleWebRTCJavaScrip客户端库的signaling服务

如果你一点都不想编码,你可以用完整的商业WebRTC平台,像vLine, OpenTok and Asterisk爱立信创建了一个 signaling server using PHP on Apache,在WebRTC早期的时候,现在这个已经被弃用了,但是如果你考虑到相似的情况,这个代码还是值得一看的。六.Signaling安全 Security is the art of making nothing happen.
Salman Rushdie
所有WebRTC 组件都被强制加密。
但是信令机制没有被WebRTC标准定义,所有确保信令安全取决于你,如果一个攻击者想去劫持信令,他们会导致会话中止,重定向链接和记录,改变或者注入内容。

一个牢固的信令最重要的功能是使用加密协议,HTTPS 和WSS (i.e TLS)可以确保信息不会非加密拦截。
同时,小心不要广播信令信息,不然攻击者可以使用相同的信令服务链接其他来电用户。
使用 TLS.去确保WebRTC应用的安全。
信令交互完之后,使用ICE去处理NATs和防火墙对于元数据的信令,WebRTC应用可以使用中间服务,但实际的媒体和数据流在一个会话确立后,RTCPeerConnection 尝试去直连客户端:P2P在一个简单的世界里,每一个WebRTC端都有一个唯一的地址,这样他可以与其他端交换数据,以便直接 通讯。

实际情况下,大多数设备都在一个或多个NAT层后面,有些有防毒软件阻碍确定的端口和协议,还有很多在代理和公司的防火墙后面。防火墙和NAT实际上可能由一些类似家庭wifi路由器产生的。
WebRTC 可以使用ICE框架去克服真实世界的复杂网络。为了实现这个功能,你的应用必须传ICE服务地址给RTCPeerConnection,如下所述。ICE 试着寻找最佳路线去连接对方,它会并行的寻找所有可能性,然后选择最有效的可行方式。 ICE首先会尝试用设备系统或网卡获取到的主机地址去建立连接;如果这个失败了(设备在NATs后面就会)ICE从STUN服务器获得外部的地址,如果这个也失败了,就用TURN中转服务器做通讯。也就是说:
STUN服务器用来获取外部网络地址。
如果P2P失败的话,[size=14.4444446563721px]TURN服务器用来中继通讯。
每一个TURN服务器都支持STUN:一个TURN服务器是由一个STUN服务器加上中继功能。ICE也可以用来应付复杂的NAT设置:
事实上,NAT的”打洞“可能需要除了公共IP之外的端口地址。
WebRTC应用在iceServers配置对象(RTCPeerConnection constructor)里设置STUN and/or TURN服务器地址。
apprtc.appspot.com里这个值像这样:
{
'iceServers': [
{
'url': 'stun:stun.l.google.com:19302'
},
{
'url': 'turn:192.158.29.39:3478?transport=udp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
},
{
'url': 'turn:192.158.29.39:3478?transport=tcp',
'credential': 'JZEOEt2V3Qb0y27GRntt2u2PAYA=',
'username': '28224511:1379330808'
}
]
}

复制代码

一旦RTCPeerConnection 有了这些信息,ICE会自动启动:RTCPeerConnection 使用ICE框架计算出两端间最佳路线,需要STUN和TURN服务器。

STUNNATs会给它的设备提供一个内部网络IP地址,但这个地址不能在外网使用,因为没有外网的地址,所有WebRTC没办法做连接,为解决这个问题,WebRTC使用了STUN。STUN服务架设在外网,它有一个简单的任务:获取一个发送请求的设备(运行在NAT后边的应用)的IP和端口,然后返回这个地址。换句话说,应用使用STUN服务器发现它的外网IP和端口,这个过程确保了一个WebRTC端获得它自己的公共地址,然后通过signaling机制发送这个信息给另一端,这样就可以建立起一个直接连接。(在实际中,不同的NATs有不同的工作方式,可能有多个NAT层,但是原理是一样的)STUN服务器不需要做太多工作和存储太多东西,所以简单的STUN服务器可以应付大量的请求。根据 webrtcstats.com的统计,使用STUN方式建立WebRTC通话的成功率有86%的。

TURNRTCPeerConnection 会试着用UDP在两端建立一个直连,如果失败了,RTCPeerConnection 会改用TCP,如果这个再失败了,TURN服务器会被作为后备方案使用,在两端间中继数据。重述:TURN是在两端间中转视频/语音/数据 流,而不是发送数据。
TURN 有个公共地址,所以每个端即使在防火墙或者代理后面,也能访问到。
TURN有个简单的任务,中转数据流,但不像STUN,TURN会花费大量带宽。所有,TURN需要够强壮。

图表表示TURN的作用:单纯的STUN不起作用,客户端就会转向使用TURN。
部署 STUN 和 TURN 服务器作为测试,谷歌公布了一个公共的STUN服务,stun.l.google.com:19302, apprtc.appspot.com用的就是这个。作为一个产品级别的 STUN/TURN服务器,我们建议使用 rfc5766-turn-server,STUN 和TURN的源码可以从code.google.com/p/rfc5766-turn-server获取,这个链接也包括了部署的资料。A VM image for Amazon Web Services is also available.本社区也发布了部署教程:部署教程一个可代替的TURN服务器是restrund,可以在source code 下载到,下面介绍在谷歌Compute Engine部署resrund的步骤:Open firewall as necessary, for tcp=443, udp/tcp=3478
Create four instances, one for each public IP, Standard Ubuntu 12.06 image
Set up local firewall config (allow ANY from ANY)
Install tools:sudo apt-get install makesudo apt-get install gcc
Install libre from creytiv.com/re.html
Fetch restund from creytiv.com/restund.html and unpack
wget hancke.name/restund-auth.patch and apply with patch -p1 < restund-auth.patch
Run make, sudo make install for libre and restund
Adapt restund.conf to your needs (replace IP addresses and make sure it contains the same shared secret) and copy to /etc
Copy restund/etc/restund to /etc/init.d/
Configure restund:Set LD_LIBRARY_PATHCopy restund.conf to /etc/restund.confSet restund.conf to use the right 10. IP address
Run restund
Test using stund client from remote machine: ./client IP:port

七.突破p2p:多人会议WebRTC你可以需要看下Justin Uberti提议的IETF标识:请求TURN服务的API很容易想象到一些场景不只是一对一的视频通话,举个例子,公司小组需要一个视频会议,或者一个公开的演讲,一个演讲者面对数百(或者数千)的观看者。一个WebRTC应用可以使用多个RTCPeerConnections,这样每一个端可以连接其他端形成一个网络。talky.io就是使用这种方法实现,对于少数的用户,可以很好的工作。但是进程和带宽开销会非常大,特别是移动客户端。

在一个星型结构里,一个WebRTC客户端可以选择一个端去分布数据流给所有的用户,你可以自己设计重新分配机制的服务和构造区实现这种方式(werrtc.org提供了一个样例sample client application)从Chrome31和Opera18 开始,从一个RTCPeerConnection 获取的媒体流,可以作为对方的输入:这里有个demosimpl.info/multi。这样可以确保更灵活的结构,因为它可以允许web应用通过选择哪个用户可以连接去控制一个通话 路由。多点控制部件MCU(Multipoint Control Unit)大量用户通话的更好解决方案是使用Multipoint Control Unit(MCU)。这是一个在大量参与者间分布媒体的桥接服务器。MCUs可以在一个视频 会议里处理不同的分辨率,编解码,和帧速率。对于多端会议,有很多因素要考虑:最重要的是,从多个源里,怎么显示多个视频和混合音频。像 vLine 的云平台也在致力于优化传输路由。

你可以去买一个MCU硬件或者自己搭一个。

Cisco MCU背部有几个开源的MCU硬件款可以选,例如 Licode(以前称为Lynckia) 生产的开源 MCU for WebRTC; OpenTok 平台的Mantis.突破浏览器: VoIP, telephones 和 messagingWebRTC 的标准让浏览器和不同设备不同平台,例如手机或者一个视频会议系统,进行通话称为可能。SIP是一种信令协议,用来做VoIP和视频系统。为了让WebRTC和SIP端通讯,WebRTC需要一个代理服务器去调解信令。信令一定会经过网关,但是一旦会话建立,视频和语音就能在两端传输。PSTN,公共电话交换网络,是旧式模拟电话的交换网络。为了WebRTC和电话进行通话,必须通过一个PSTN网关。同理,要让WebRTC跟Jingle端(像IM客户端)通讯,需要一个中间XMPP服务器。Jingle作为XMPP的扩展,用来实现视频和语音能够作为信息服务:现在的WebRTC就是基于C++实现libjingle 库发展来的,Jingle最初是Google Talk的技术。一堆应用库,平台让WebRTC能在实际中通讯:sipML5:一个 开源的 JavaScript SIP 客户端
jsSIP: JavaScript SIP库
Phono: 开源JavaScript phone API, 作为一个插件
Zingaya: 一个嵌入式电话部件
Twilio: 音频和信息
Uberconference: 会议技术

sipML5 的开发者也开发了webrtc2sip的网关Tethr and Tropo have demonstrated a framework for disaster communications 'in a briefcase', using an OpenBTS cell to enable communications between feature phones and computers via WebRTC. Telephone communication without a carrier! 发现更多WebRTC codelab: 一步一步教你怎么打造一个视频和文本聊天应用,使用Socket.io Signaling服务。在Node上面跑。2013 Google I/O WebRTC presentation WebRTC领头人Justin Uberti.Chris Wilson 关于WebRTC的介绍:Introduction to WebRTC Apps.
WebRTC Book 这本书有很多关于数据、signaling、网络拓扑结构的细节。
WebRTC and Signaling: What Two Years Has Taught Us:介绍为什么把signaling脱离出标准是个好主意
A Practical Guide to Building WebRTC Apps:提供很多WebRTC的技术和基础建设信息。
WebRTC chapter 深入研究WebRTC的结构,使用案例和性能。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容