webrtc-信令与连接

/post/webrtc-sinalling article cover image

sinaling信令

信令是在两个设备之间发送控制信息以确定通信协议、信道、媒体编解码器和格式以及数据传输方法以及任何所需的路由信息的过程,因为信令在规范中并没有定义,所以开发者需要自己实现这一过程

信息协议:SIP或XMPP

双向通信信道: WebSocket或XMLHttpRequest以及持久化api(google channel api)

<Callout> 还值得注意的是,用于执行信令的信道甚至不需要通过网络。 一个 Peer 可以输出一个数据对象,这个数据对象可以被打印出来,然后物理携带(步行或由信鸽)直到进入另一个设备,然后由该设备输出响应,并以同种方式返回, 直到 WebRTC 对等连接打开。 这将带来非常高的延迟,但也是可以做到的。 </Callout>

信令期间交换的信息

信令期间需要交换的信息有三种基本类型:

  • 控制消息:用于设置、打开、关闭通信通道并处理错误。
  • 为了建立连接所需的信息:设备间能够彼此交谈所需的 IP 寻址和端口信息。
  • 媒体能力协商:交互双方可以理解哪些编解码器和媒体数据格式? 这些都需要在 WebRTC 会议开始之前达成一致。

<Callout type="info"> 只有在信令成功完成之后,打开 WebRTC 对等连接的过程才真正开始 <p>在信令期间,信令服务器实际上不需要理解两个设备之间交换的数据或者对这些数据做任何处理。 信令服务器本质上就是一个中继器:两端连接的公共点,两端都知道它们的信令数据可以通过这个点来传输。 服务器不需要以任何方式对此信息做出反应。</p> </Callout>

信令过程

开启WebRTC会话时,回一次发生如下事件:

  • 每个Peer创建一个RTCPeerConnection对象,用来表示其 WebRTC 会话端
  • 每个Peer建立一个icecandidate事件的响应程序,用来在监测到该事件时将这些candidates通过信令通道发送给另一个Peer
  • 每个Peer建立一个track事件的响应程序,这个事件会在远程Peer添加一个track到其stream上时被触发。这个响应程序应将tracks和其消受者联系起来,例如video元素
  • 呼叫者创建并与接收者共享一个唯一的标识符或某种令牌,使得它们之间的呼叫可以由信令服务器上的代码来识别。此标识符的确切内容和形式取决于您。
  • 每个Peer连接到一个约定的信令服务器,如WebSocket 服务器,他们都知道如何与之交换消息。
  • 每个Peer告知信令服务器他们想加入同一WebRTC会话(由步骤 4 中建立的令牌标识)。

ice重连

在WbRTC会话生命周期内,网络田间发生变化时(例如从蜂窝网络转移到wifi),ICE代理可以选择执行ICE重连(与执行初始ICE协商完全相同)除了媒体继续流过原始网络连接直到新的开始运行。 然后媒体转移到新的网络连接,旧的关闭。

  • 全ICE重连:会导致会话中的所有媒体流重新协商。
  • 部分ICE重连: 允许ICE重新协商特定媒体流,而不是所有媒体流进行重新协商

设置ice重连的方式

js
// 更改为不同的ICE服务器群,设置新的RTCConfiguration字典然后重启ICE
RTCPeerConnection.setConfiguration(RTCConfiguration)
js
// 明确触发ICE重启,调用时会启动一个协商过程之后和平常处理连接一样
// 且当应答端检测到ICE ufrag和password发生改变时将自动重连

RTCPeerConnection.createOffer(
  {
    //...,
    iceRestart: true
  }
)

sinalling processing

客户端信令消息

  • join加入房间
  • leave离开房间
  • message端到端消息

端到端信令消息

  • offer消息
  • answer消息
  • candidate消息

服务器信令消息

  • joined加入房间
  • otherjoin其他用户加入房间
  • full房间人数已满
  • leaved已离开房间
  • bye对方离开房间

socketio

基于事件的websocket低延迟、双向通信库。优点有:

  • 基于TCP连接能确保信令的完整传输
  • 有房间的概念
  • 跨平台、跨终端、跨语言

<Callout type="info"> 具体使用参考官方文档:<a target="_blank" href="content/posts/2022/q2/29-webrtc-sinalling">Socket.IO</a> </Callout>

单个发送消息

js
socket.emit("hello", "world");

发送广播消息

js
io.emit("hello", "world");

房间消息客户端集

js
// 给指定房间内所有人发送消息
io.in("room").emit()

// 给room1、room2除了room3发送消息
io.to(["room1", "room2"]).except("room3").emit(/* ... */);

// 给指定房间名的所有人发送消息
io.of("room").emit()

// 给在本服务上所有人发送消息(当使用多个socket服务)
io.local.emit()

// 给指定房间内除了发送者以外所有人发消息
socket.to("room").emit()
socket.to(["room1", "room2"]).emit();

// 给除了发送者以外所有发送消息
socket.broadcast.emit("hello", "world");

action

js
// Server:
socket.emit("hello", "para1", "para2")

// Client
socket.on('heelo', (para1, para2) => {
	// ...
})

// Server:
socket.emit("hello", data, (para1, para2) => {
	// ...
})

// Client
socket.on('heelo', (data, fn) => {
	fn('para1', 'para2')
})

信令服务器

使用nodejs作为信令服务器

js
const socketIo = require('socket.io');
const socket_io = socketIo.listen(http_server);

const log4js = require('log4js');

log4js.configure({
    appenders: {
        file: {
            type: 'file',
            filename: 'app.log',
            layout: {
                type: 'pattern',
                pattern: '%r %p - %m',
            }
        }
    },
    categories: {
       default: {
          appenders: ['file'],
          level: 'debug'
       }
    }
});
// 生成日志文件
const logger = log4js.getLogger();

// 建立连接
socket_io.sockets.on('connection', (socket)=>{
	// 监听message事件并通知房间内所有人
	socket.on('message', (room, data)=>{
		socket.to(room).emit('message', room, socket.id, data)
	});

	// 加入房价
	socket.on('join', (room)=> {
		socket.join(room);
		var myRoom = sockio.sockets.adapter.rooms[room];
		var users = Object.keys(myRoom.sockets).length;
		logger.log('the number of user in room is: ' + users);
	 	socket.emit('joined', room, socket.id);

		//除自己之外
		//socket.to(room).emit('joined', room, socket.id);

		//房间内所有人
		//io.in(room).emit('joined', room, socket.id)

		//除自己,全部站点
		//socket.broadcast.emit('joined', room, socket.id);
	});

	// 离开房间
	socket.on('leave', (room)=> {
		var myRoom = sockio.sockets.adapter.rooms[room];
		var users = Object.keys(myRoom.sockets).length;
		//users - 1;

		logger.log('the number of user in room is: ' + (users-1));

		socket.leave(room);
	 	socket.emit('leaved', room, socket.id);

		 //除自己之外
	 	//socket.to(room).emit('leaved', room, socket.id);

		//房间内所有人
		//io.in(room).emit('leaved', room, socket.id)

		//除自己,全部站点
	 	//socket.broadcast.emit('leaved', room, socket.id);
	});
});

客户端逻辑

<Callout type="info"> socketIo客户端脚本及文档: <a target="_blank" href="https://github.com/socketio/socket.io-client">socket.io-client</a>、 <a target="_blank" href="content/posts/2022/q2/29-webrtc-sinalling">socket.io-client使用</a> </Callout>

html
<script src="/socket.io/socket.io.js"></script>
js
let room;
let socket;

function init() {
	// 建立连接
	socket = io.connect();

	// 监听加入
	socket.on('joined', (room) => {
		// ...
	});

	// 监听message事件
	socket.on('message', (data) => {
		// manipulate data
	});

	// 监听离开/
	socket.on('leaved', (room) => {
		// 断开连接
		socket.disconnect();
	});

	// 加入房间
	room = getUUID();
	socket.emit('join', room);
}

function sendMessage(data) {
	socket.emit('message', data)
}

function leaveRoom() {
	socket.emit('leaved', room)
}