最新文章
-
webrtc网页代码分析二
交互流程 上图是完整的处理流程。 获取URL参数 const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); const deviceId = urlParams.get('deviceId'); 这段代码先获取当前页面 URL 中的查询字符串部分(window.location.search),然后使用 URLSearchParams 对象解析查询字符串,尝试获取名为 deviceId 的参数值,后续很多操作会根据这个 deviceId 的有无来进行不同的逻辑处理。 页面加载时的初始化操作 window.onload = function () { if (deviceId == undefined) { document.getElementById('video').style.display = 'none'; document.getElementById('getting-started').style.display = 'block'; } const canvas = document.createElement('canvas') canvas.width = 640 canvas.height = 480 const ctx = canvas.getContext('2d') ctx.fillStyle = 'rgba(200, 200, 200, 100)' ctx.fillRect(0, 0, 640, 480) const img = new Image(640, 480) img.src = canvas.toDataURL() document.getElementById('imgStream').src = img.src; } window.onload是页面加载完成后的回调函数,上面使用的是匿名函数,即当页面所有内容加载完成后执行的逻辑。具体逻辑如下 这里判断如果deviceId未定义(也就是可能页面直接访问,没有带上对应的参数),则隐藏视频相关区域(id为video的div),显示初始引导内容区域(id 为 getting-started 的 div)。在函数内创建了一个 canvas 元素,绘制了一个半透明的矩形,将其转换为图片数据后设置给img id=\\\\"imgStream\\\\"元素作为初始显示内容。 MQTT操作 发起连接 var options = { keepalive: 600, username: 'xxx', password: 'xxxx', }; const client = mqtt.connect('wss://xxx.xxx.com:8084/mqtt', options); 定义了 MQTT连接的相关选项(如保活时间、用户名、密码等),然后尝试连接到指定的MQTT服务器地址(wss://xxx.xxx.xxx:8084/mqtt),后续通过client对象来处理 MQTT 的各种消息收发以及相关操作。 事件处理 connect事件 client.on('connect', function () { console.log("connected") client.subscribe('webrtc/' + deviceId + '/jsonrpc-reply', function (err) { if (!err) { } }); console.log('publish to webrtc/' + deviceId + '/jsonrpc'); client.publish('webrtc/' + deviceId + '/jsonrpc', JSON.stringify({ jsonrpc: '2.0', method: 'offer', id: offerId, }), { qos: 2 }); }) 结构为client.on(\\\\'connect\\\\', function () {... }); client.on这里是mqtt对象的事件回调,这里将function(){...}作为回调函数注册进去,funciton实现是匿名函数的方式。当前收到connect事件时,就会执行function的内容。具体逻辑如下。 当成功连接到MQTT服务器后,先在控制台输出 connected 表示连接成功,然后订阅一个特定主题(webrtc/\\\\' + deviceId + \\\\'/jsonrpc-reply)用于接收回复消息,接着向另一个特定主题(webrtc/\\\\' + deviceId + \\\\'/jsonrpc)发布一条包含offer信息的 JSON 格式消息(设置了消息质量等级 qos 为 2),用于发起某种交互(可能与 WebRTC 相关的会话建立等操作)。 (1)订阅函数subscribe client.subscribe(topic, options, callback); topic: 需要订阅的主题,可以是一个字符串或者一个主题数组。主题是 MQTT 中用于分类消息的方式。主题的命名规则通常是层级的,使用斜杠(/)分隔每一层级,例如 \\\\'home/livingroom/temperature\\\\'。 options: 可选参数,配置订阅的一些选项,如 QoS(服务质量)级别。常见的选项包括: -- qos: 服务质量(Quality of Service)级别,取值范围是: -- 0 - 至多一次:消息最多发送一次,不做确认。 -- 1 - 至少一次:消息至少发送一次,可能会重复。 -- 2 - 只有一次:消息只能传递一次,不会丢失,也不会重复。 callback: 可选的回调函数,当订阅成功时会被调用。回调函数接受一个错误对象作为参数,如果订阅成功,err 为 null;如果订阅失败,则返回相应的错误信息。 (2)发布函数publish client.publish(topic, message, options, callback); topic: 需要发布消息的主题,可以是一个字符串,表示主题名称。 message: 需要发布的消息内容,可以是字符串、Buffer、对象等。一般情况下,消息会被自动转换为字符串或 Buffer(如果传入对象,会自动通过 JSON.stringify() 转换为字符串)。 options: 可选参数,配置发布消息的选项,常见的选项包括: -- qos: 服务质量(Quality of Service)级别(如前述),默认是 0。 -- retain: 是否保留消息,若为 true,代理服务器会保存消息并在新订阅者连接时发送该消息。 callback: 可选的回调函数,发布消息成功后会被调用。回调函数接受一个错误对象作为参数,如果发布成功,err 为 null;如果发布失败,则返回相应的错误信息。 messange事件 MQTT消息接收处理(client.on(\\\\'message\\\\', function (topic, message) {... })),语法与上面类似,逻辑如下。 client.on('message', function (topic, message) { let msg = JSON.parse(message.toString()); if (msg.id == offerId) { let sdp = msg.result; let offer = { type: 'offer', sdp: sdp }; log(offer); pc.setRemoteDescription(offer); pc.createAnswer().then(d => pc.setLocalDescription(d)).catch(log); } else if (msg.id == answerId) { log('receive answer ok'); } }) 当接收到MQTT消息时,先将消息内容解析为 JSON 对象,然后根据消息中的 id 判断消息类型,如果 id 与之前定义的 offerId 相等,说明是对之前 offer 的回复,会提取其中的 sdp 信息设置为远程描述(setRemoteDescription),并创建一个本地回答(createAnswer)然后设置本地描述(setLocalDescription);如果 id 等于 answerId,则只是在控制台输出表示接收到了回答消息。 WebRTC操作 创建对象 var pc = new RTCPeerConnection({ iceServers: [ { urls: "turn:rtpeer.allwinnertech.com:3478", username: "pctest", credential: "Aa123456", }, ], }); 创建了一个 RTCPeerConnection 对象用于建立 WebRTC 连接,配置了 iceServers,这里指定了一个 TURN 服务器的相关信息(地址、用户名、密码),有助于在复杂网络环境(如存在 NAT 等情况)下建立稳定的点对点连接,确保音视频数据能顺利传输。 获取PC录音 navigator.mediaDevices.getUserMedia({ video: false, audio: true }) .then(stream => { stream.getTracks().forEach(track => pc.addTrack(track, stream)); }).catch(log); 通过 navigator.mediaDevices.getUserMedia 方法请求获取音频媒体流(这里明确只要音频,video: false, audio: true),如果获取成功,将音频轨道添加到 RTCPeerConnection 对象(pc)中,以便后续传输音频数据,如果出现错误则通过 log 函数(也就是在控制台输出错误信息)进行处理。 连接状态 pc.oniceconnectionstatechange = e => { log(pc.iceConnectionState); document.getElementById('status').innerHTML = pc.iceConnectionState; if (pc.iceConnectionState == 'connected') { // default to muted if (!isMuted) onMuted(); } } 当 WebRTC 连接的 ICE 连接状态发生变化时,先在控制台输出当前连接状态(通过 log 函数),然后更新页面上 id 为 status 的元素文本内容为当前连接状态,并且当连接状态变为 connected(已连接)时,如果当前不是静音状态(!isMuted),则调用 onMuted 函数将音频静音。 ICE候选者 pc.onicecandidate = event => { if (event.candidate === null) { console.log(pc.localDescription.sdp) let json = { jsonrpc: \\'2.0\\', method: \\'answer\\', params: pc.localDescription.sdp, id: answerId, } console.log(json) client.publish(\\'webrtc/\\' + deviceId + \\'/jsonrpc\\', JSON.stringify(json)) setInterval(() => {....}, 1000); } } 当有 ICE 候选者信息产生并且当候选者为 null 时(意味着 ICE 收集过程结束),执行一系列操作,包括打印 RTCPeerConnection 的本地描述中的 sdp 信息,向 MQTT 特定主题发布包含本地描述 sdp 信息的 JSON 消息,并且每隔 1 秒获取连接的统计信息(通过 pc.getStats 方法),整理后更新到页面中 id 为 stats-box 的元素内展示出来。 当收到对端的offer候选信息后,本地会进行与stun/turn服务交互,交互获取到ICE信息后,就会触发回调这个函数,将自己SDP信息组装成json通过mqtt publish发送给对端。 接收音视频 pc.ontrack = function (event) { if (event.track.kind == \\'video\\') { var el = document.getElementById(\\'videoStream\\'); var newStream = new MediaStream(); newStream.addTrack(event.track); el.srcObject = newStream; el.autoplay = true el.controls = false el.muted = true document.getElementById(\\'imgStream\\').style.display = \\'none\\'; document.getElementById(\\'videoStream\\').style.display = \\'block\\'; // 当视频流添加成功后,获取视频分辨率和码率信息并更新显示 el.addEventListener(\\'loadedmetadata\\', function () { updateVideoInfo(event.track); }); } else if (event.track.kind == \\'audio\\') { var el = document.getElementById(\\'audioStream\\'); var newStream = new MediaStream(); newStream.addTrack(event.track); el.srcObject = newStream; el.controls = false el.muted = false } } 当通过 RTCPeerConnection 接收到远端传来的媒体轨道(视频或音频轨道)时,根据轨道类型进行不同的处理。对于视频轨道,获取页面中的视频元素(id 为 videoStream),创建新的 MediaStream 对象添加轨道后设置为视频元素的播放源,同时设置自动播放、隐藏控制条、静音等属性,并在视频元数据加载完成后(通过 loadedmetadata 事件)调用 updateVideoInfo 函数更新视频分辨率和码率信息显示;对于音频轨道,类似地获取音频元素(id 为 audioStream),添加轨道到新的 MediaStream 对象并设置为音频元素播放源,设置不显示控制条且不静音。 数据通道 const datachannel = pc.createDataChannel(\\'pear\\') datachannel.onclose = () => console.log(\\'datachannel has closed\\'); datachannel.onopen = () => { console.log(\\'datachannel has opened\\'); console.log(\\'sending ping\\'); setInterval(() => { console.log(\\'sending ping\\'); datachannel.send(\\'ping\\'); }, 1000); } datachannel.onmessage = e => { if (e.data.byteLength === undefined) { console.log(e.data); } else { // is binary data. mjpeg stream // console.log(e.data.byteLength); var arrayBufferView = new Uint8Array(e.data); var blob = new Blob([arrayBufferView], { type: \\"image/jpeg\\" }); var urlCreator = window.URL || window.webkitURL; var imageUrl = urlCreator.createObjectURL(blob); var imageElement = document.getElementById(\\'imgStream\\'); imageElement.src = imageUrl; } } 创建了一个名为pear的数据通道(const datachannel = pc.createDataChannel(\\'pear\\')),并分别对数据通道的打开、关闭、接收消息等事件绑定了相应的回调函数。例如在数据通道打开时(datachannel.onopen),会定期(每隔 1 秒)发送 ping 消息用于保持连接或检测连接状态等;在接收到消息时(datachannel.onmessage),根据消息数据类型判断,如果是文本消息直接在控制台输出,如果是二进制数据(判断为 MJPEG 流),则将其转换为图片的 URL 并设置给页面中的图片元素(id 为 imgStream)用于显示。 所以这里支持mjpeg的方式,但是是通过数据通道来实现的。 -
webrtc网页代码分析一
文档结构 <!doctype html> <html lang="en"> <head> ... </head> <body> ... </body> </html> 文档类型声明():声明文档使用HTML5标准。 html标签:设置文档的语言为英语(lang=\"en\")。 head标签:包含元数据、样式、外部链接等,定义网页的头部信息。 body标签:页面的主体部分,包含所有网页内容。 head标签 字符集和窗口设置 <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> meta标签:用于定义页面的字符集、视口设置、作者信息等。 charset=\\"utf-8\\":指定字符编码为UTF-8,确保网页能够正确显示多种语言的字符。 viewport 设置:用于响应式设计,确保页面在不同设备上显示良好。width=device-width 表示页面宽度等于设备的屏幕宽度,initial-scale=1 表示初始缩放比例为 1。 外部资源引入 <script src="https://unpkg.com/mqtt@4.1.0/dist/mqtt.min.js"></script> <script src="https://kit.fontawesome.com/8c8bbe3334.js" crossorigin="anonymous"></script> MQTT库:引入了 MQTT JavaScript 客户端库(版本 4.1.0)。MQTT 是一种轻量级的消息发布/订阅协议,通常用于 IoT(物联网)应用。这表明页面可能涉及到实时消息传递或设备间通信。 FontAwesome图标库:引入了 FontAwesome 的 JavaScript 库,用于提供图标支持。crossorigin=\"anonymous\" 允许浏览器在不发送凭据的情况下访问该资源。 CSS样式 style标签包含了许多样式规则,以下是主要部分的分析。 全局设置 html { font-family:"Calibri", sans-serif; box-sizing: border-box; } *, *:before, *:after { box-sizing: inherit; } font-family: \"Calibri\", sans-serif;:设置了页面字体为 \"Calibri\",如果不支持则使用 sans-serif。 box-sizing: border-box;:使得所有元素的 padding 和 border 都包含在 width 和 height 的计算中,避免了布局问题。 响应式设计 @media screen and (max-width: 650px) { .column { width: 100%; display: block; } } 使用 @media 媒体查询来调整页面布局。在屏幕宽度小于或等于 650px 时,将 .column 类的元素的宽度设置为 100%,并显示为块级元素。这样有助于在小屏幕设备(如手机)上优化布局。 卡片样式和容器 .card { box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2); } .container { position: relative; text-align: center; color: white; } .card:为卡片元素设置了一个阴影效果,使其在页面上看起来更立体。 .container:设置容器为相对定位,并使文本居中。文本颜色为白色。 按钮样式 .title { color: grey; } .bottom-left, .top-left, .top-right, .bottom-right { position: absolute; padding: 10px; } .button { border: none; outline: 0; display: inline-block; padding: 8px; color: white; background-color: #000; text-align: center; cursor: pointer; } .button:hover { background-color: #555; } .title:为标题设置灰色字体颜色。 .bottom-left, .top-left, .top-right, .bottom-right:为定位在四个角的元素提供了绝对定位样式。通过定位,它们可以被放置在容器的四个角。 .button:定义了按钮的基本样式,包括无边框、白色文字、黑色背景以及光标指针。hover 样式将按钮背景色更改为灰色(#555)。 导航栏样式 .topnav { overflow: hidden; background-color: #333; } .topnav a { float: left; display: block; color: #f2f2f2; text-align: center; padding: 14px 16px; text-decoration: none; font-size: 17px; } .topnav a:hover { background-color: #ddd; color: black; } .topnav a.active { background-color: #04AA6D; color: white; } .topnav:定义了一个水平的导航栏,背景色为深灰色,且隐藏溢出的内容。 .topnav a:定义了导航栏中每个链接的样式,包括文本颜色、对齐方式、内边距等。 .topnav a:hover:设置链接在悬停时背景色变为浅灰色,文字颜色变为黑色。 .topnav a.active:当链接处于 \"活动\" 状态时,背景色变为绿色(#04AA6D),文字变为白色。 按钮样式 .btn-group button { margin-top: -4px; background-color: white; border: 1px solid white; color: black; padding: 10px 24px; cursor: pointer; } .btn-group button:hover { background-color: #f2f2f2; } .btn-group button:定义了按钮组中的按钮样式,包括背景色为白色,边框为白色,文本为黑色,且在悬停时背景色变为浅灰色。 普通按钮样式 .btn { background-color: DodgerBlue; border: none; color: white; padding: 12px 16px; font-size: 16px; cursor: pointer; } .btn:hover { background-color: RoyalBlue; } .btn:定义了一个蓝色的按钮,包含内边距、字体颜色等样式。在悬停时,按钮的背景色会变成更深的蓝色(RoyalBlue)。 body标签 body包含页面部分显示部分和script部分。 页面内容 导航栏区域 <div class="topnav" id="myTopnav"> <a href="#">Peer Project</a> <a href="https://ko-fi.com/sepfy95" style="float: right">Sponsor</a> <a href="https://github.com/sepfy/pear" style="float: right">Github</a> </div> 这是一个顶部导航栏(topnav)元素,包含三个链接: Peer Project:这是主页面的链接,指向当前页面(#)。可以是应用的标题或者主页面链接。 ko-fi.com:链接到 ko-fi.com,可能是用于捐赠或支持项目。 github: GitHub 项目的链接,用户可以访问该链接查看源代码或参与项目。 这些链接使用了 float: right 样式,使得“Sponsor”和“Github”链接都被浮动到右侧,而“Peer Project”链接则停留在左侧。 音视频展示区域 //这是包含视频内容的容器 <div class="column" id="video"> //用来包裹所有显示视频、音频及空间的内容,创建一个卡片式布局 <div class="card"> //容器内放置了视频和音频元素,包含一些展示信息的<p>标签 <div class="container"> // 媒体元素信息 //图片元素,在没有视频流可用时显示,他的id是imgStream <img id="imgStream" style="width: 100%"> //video元素,用于显示视频流,display样式为none,默认视频流不可见。通过js控制 <video id="videoStream" playsinline style="width:100%; display: none"></video> //audio元素,用于播放音频流的,默认被隐藏,通过js控制。 <audio id="audioStream" style="display: none"></audio> //用于设置设备信息 //段落元素用于显示设备信息,这里是device id <p class="top-left" id="device-id"></p> //段落元素,用于显示设备当的状态。 <p class="top-right" id="status">waiting</p> </div> //控制按钮,包括音量、录音、旋转、暂停 <div class="btn-group" style="width:100%"> //控制音量的按钮,调用onVolume函数,图标为fa-solid fa-volume-xmark <button style="width:25%" class="btn" onclick="onVolume()"><i id="volume-icon" class="fa-solid fa-volume-xmark"></i></button> //控制是否静音,调用onMuted函数,图标为fa-solid fa-microphone-slash <button style="width:25%" class="btn" onclick="onMuted()"><i id="mute-icon" class="fa-solid fa-microphone-slash"></i></button> //用于旋转视频,点击调用onRotate函数,图标为solid fa-arrows-rotate <button style="width:25%" class="btn" onclick="onRotate()"><i class="fa-solid fa-arrows-rotate"></i></button> //用于停止视频或音频流,点击调用onStop函数,图标为fa-solid fa-circle-stop <button style="width:25%" class="btn" onclick="onStop()"><i id="stop-icon" class="fa-solid fa-circle-stop"></i></button> </div> </div> </div> -
javascript之dom
什么是dom DOM(Document Object Model)是一个编程接口,它将 HTML 或 XML 文档呈现为一个由节点和对象(这些节点和对象其实是文档的各种元素、属性和文本内容等)组成的树形结构。这个树形结构允许开发者使用编程语言(如 JavaScript)来访问、修改和操作文档的内容、结构和样式。对于以下简单的 HTML 文档: <!DOCTYPE html> <html> <head> <title>My Page</title> </head> <body> <h1>Hello, World!</h1> <p id=\\\"myParagraph\\\">This is a paragraph.</p> </body> </html> 它的 DOM 树结构大致如下: - html 节点是根节点,它包含两个子节点:head 和 body。 - head 节点包含一个子节点 title,title 节点的文本内容是 \\\\\\\"My Page\\\\\\\"。 - body 节点包含两个子节点:h1 和 p。h1 节点的文本内容是 \\\\\\\"Hello, World!\\\\\\\",p 节点有一个属性 id 为 \\\\\\\"myParagraph\\\\\\\",其文本内容是 \\\\\\\"This is a paragraph.\\\\\\\"。 获取dom元素 getElementById 这是最常用的方法之一,用于通过元素的id属性获取单个元素。id在HTML文档中应该是唯一的,语法如下。 document.getElementById(\\\"elementId\\\"); 在上面的 HTML 文档中,如果要获取p元素,可以使用以下 JavaScript 代码: var paragraph = document.getElementById(\\\"myParagraph\\\"); console.log(paragraph.textContent); // 输出:This is a paragraph. getElementsByTagName 根据标签名获取元素集合。它返回一个类似数组的对象(HTMLCollection),包含所有匹配标签名的元素。语法如下: document.getElementsByTagName(\\\"tagName\\\"); 要获取文档中的所有 h1 元素: var h1Elements = document.getElementsByTagName(\\\"h1\\\"); for (var i = 0; i < h1Elements.length; i++) { console.log(h1Elements[i].textContent); // 输出:Hello, World! } getElementsByClassName 通过类名获取元素集合。同样返回一个 HTMLCollection,包含所有具有指定类名的元素。 document.getElementsByClassName(\\\"className\\\"); 假设HTML中有多个元素有highlight类,如下: <p class=\\\"highlight\\\">This is a highlighted paragraph.</p> <span class=\\\"highlight\\\">This is a highlighted span.</span> 可以使用以下代码获取这些元素: var highlightedElements = document.getElementsByClassName(\\\"highlight\\\"); for (var i = 0; i < highlightedElements.length; i++) { console.log(highlightedElements[i].textContent); // 输出两个元素的文本内容 } 修改元素 修改元素内容 textContent属性 用于获取或设置元素的文本内容,如改变前面获取的p元素的内容 var paragraph = document.getElementById(\\\"myParagraph\\\"); paragraph.textContent = \\\"This is a new paragraph content.\\\"; innerHTML属性 用于获取或设置元素内部的HTML内容。可以用于添加或修改元素内部的标签和文本。如在前面p元素内部添加一个strong标签: var paragraph = document.getElementById(\\\"myParagraph\\\"); paragraph.innerHTML = \\\"This is a <strong>modified</strong> paragraph.\\\"; 修改元素属性 获取和设置属性 可以通过元素对象的属性来获取和设置大多数 HTML 属性。例如,对于一个 元素的 src 属性。假设 HTML 中有一个img元素,可以这样修改其src属性: <img id=\\\"myImage\\\" src=\\\"original.jpg\\\"> var image = document.getElementById(\\\"myImage\\\"); image.src = \\\"new.jpg\\\"; 使用setAttribute和getAttribute setAttribute 用于设置元素的属性,getAttribute 用于获取元素的属性。如对于上面的img元素,也可以这样操作: var image = document.getElementById(\\\"myImage\\\"); image.setAttribute(\\\"src\\\", \\\"new.jpg\\\"); var currentSrc = image.getAttribute(\\\"src\\\"); console.log(currentSrc); // 输出:new.jpg 创建与添加DOM元素 创建元素 使用createElement方法。具体为先使用 document.createElement(\\\\\\\"tagName\\\\\\\") 创建一个新的 HTML元素。如创建一个新的div元素并设置其 id 和内容,如下。 var newDiv = document.createElement(\\\"div\\\"); newDiv.id = \\\"newDiv\\\"; newDiv.textContent = \\\"This is a new div.\\\"; 添加元素 appendChild 使用appendChild方法,将一个元素添加为另一个元素的子元素。例如,将新创建的div元素添加到 body元素中。 var body = document.getElementsByTagName(\\\"body\\\")[0]; var newDiv = document.createElement(\\\"div\\\"); newDiv.id = \\\"newDiv\\\"; newDiv.textContent = \\\"This is a new div.\\\"; body.appendChild(newDiv); insertBefore 在指定的子元素之前插入一个新元素。它需要两个参数:要插入的新元素和参考元素(新元素将插入在参考元素之前)。如假设有两个 p 元素在 body 中,在第一个 p 元素之前插入新的 div 元素。 <body> <p>First Paragraph</p> <p>Second Paragraph</p> </body> var body = document.getElementsByTagName(\\\"body\\\")[0]; var newDiv = document.createElement(\\\"div\\\"); newDiv.id = \\\"newDiv\\\"; newDiv.textContent = \\\"This is a new div.\\\"; var firstParagraph = document.getElementsByTagName(\\\"p\\\")[0]; body.insertBefore(newDiv, firstParagraph); 删除DOM元素 用于从父元素中删除指定的子元素,语法如下。 parentElement.removeChild(childElement); 如假设要删除前面添加的div元素(假设它有 id 为 \\\\\\\"newDiv\\\\\\\")。 var divToRemove = document.getElementById(\\\"newDiv\\\"); var body = document.getElementsByTagName(\\\"body\\\")[0]; body.removeChild(divToRemove); -
快速搭建一个可访问的网页
简介 搭建网站需要以下几个组件, Web服务器(如Apache、Nginx):它是网页能够被访问的核心组件。当用户在浏览器中输入网站的域名或 IP 地址并请求访问网页时,Web 服务器软件会接收这些 HTTP 请求。负责从服务器的存储设备(如硬盘)中找到对应的网页文件(如 HTML、CSS、JavaScript 文件等),并将这些文件发送回用户的浏览器。 编程语言支持(如PHP、Python、Node.js):实现网页的动态生成和业务逻辑,如果只是静态网页或不需要服务器端脚本处理PHP也不需要安装。 SSL证书(可选):用于加密HTTP通信,提升安全性。如果不需要安全链接,也不需要安装。 数据库(如MySQL、PostgreSQL)(可选):如果网页需要存储和管理大量的数据(如用户信息、文章内容、产品信息等),数据库就非常重要。 安装 安装nginx apache和nginx可以二选一 软件安装步骤 步骤1: 执行命令安装nginx sudo yum install nginx 步骤2: 启动nginx sudo systemctl start nginx 步骤3:查看启动状态 sudo systemctl status nginx 配置证书支持HTTPS 步骤1: 打开nginx.conf文件 /etc/nginx/nginx.conf 步骤2: 配置证书路径 步骤3: 重启nginx sudo systemctl restart nginx 根页面 在nginx.conf文件中,可以配置根页面,如上图的root /usr/share/nginx/html。 安装Apache 软件安装步骤 步骤1:执行命令安装Apache及其扩展包。 sudo yum -y install httpd httpd-manual httpd-devel mod_ssl mod_perl php-mysqli 如果回显信息显示Complete!,则表示Apache安装成功。 步骤2:依次执行命令启动Apache并设置自启动。 sudo systemctl start httpd sudo systemctl enable httpd 步骤3:执行命令查看Apache运行状态。如果回显信息显示active(running)时,表示Apache已启动。 systemctl status httpd 4.在本地电脑浏览器地址栏中输入http://<ECS服务器的公网IP>,测试Apache服务是否安装成功。如果显示如图所示的测试页面,表示Apache服务已安装成功。ECS服务器的弹性公网IP可以从ECS实例页面获取。 至此,按照上面流程,就可以访问一个页面了。页面的位置在;/usr/share/httpd/noindex/index.html 证书更新步骤 步骤1:找到ssl.conf文件 cd /etc/httpd/conf.d 步骤2: 编辑ssl.conf更新SSLCertificateFile、SSLCertificateKeyFile、SSLCertificateChainFile。 步骤3: 重启Apache。 sudo systemctl restart httpd 通过以上的步骤,把你的网页放到/var/www/html下面,远端就可以访问了。 数据库 暂时用不到,后续再补充。 PHP 暂时用不到,后续再补充。 其他 关于开机自启动 设置应用开机自启动 systemctl enable nginx 查询应用是否开机 systemctl list-unit-files | grep nginx 或者systemctl is-enabled nginx 关闭应用自启动 systemctl disable httpd -
openwrt规则编译cmake软件包
软件包中有完整CMakelists.txt include $(TOPDIR)/rules.mk PKG_NAME:=usrsctp PKG_VERSION:=0.9.5.0 PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:= https://codeload.github.com/sctplab/usrsctp/tar.gz/v$(PKG_VERSION)? PKG_HASH:=260107caf318650a57a8caa593550e39bca6943e93f970c80d6c17e59d62cd92 PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE PKG_BUILD_PARALLEL:=1 include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/cmake.mk define Package/usrsctp SECTION:=libs CATEGORY:=Libraries TITLE:=sctp stack URL:=https://github.com/sctplab/usrsctp endef define Package/cJSON/description this is a userland SCTP stack endef CMAKE_OPTIONS += -DBUILD_SHARED_LIBS=on #Provide compilation dependencies for other modules to call define Build/InstallDev $(INSTALL_DIR) $(1)/usr/include $(CP) $(PKG_INSTALL_DIR)/usr/include/usrsctp.h $(1)/usr/include $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/libusrsctp.so* $(1)/usr/lib/ $(INSTALL_DIR) $(1)/usr/lib/pkgconfig $(CP) $(PKG_INSTALL_DIR)/usr/lib/pkgconfig/usrsctp.pc $(1)/usr/lib/pkgconfig $(SED) 's,/usr,$(STAGING_DIR)/usr,g' $(1)/usr/lib/pkgconfig/usrsctp.pc endef #Installed to the root file system for call at runtime define Package/usrsctp/install $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/libusrsctp.so* $(1)/usr/lib/ endef $(eval $(call BuildPackage,usrsctp)) 如上是示例,这种类型比较简单,包含了cmake.mk后,openwrt会自动生成编译,只需要安装需要被其他代码依赖的库和头文件,以及运行时需要的库即可。 软件包中有完整CMakelists.txt但需要创建build目录 include $(TOPDIR)/rules.mk PKG_NAME:=libsrtp PKG_VERSION:=2.6.0 PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/cisco/libsrtp/tar.gz/v$(PKG_VERSION)? PKG_HASH:=bf641aa654861be10570bfc137d1441283822418e9757dc71ebb69a6cf84ea6b PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE PKG_BUILD_PARALLEL:=1 include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/cmake.mk define Package/libsrtp SECTION:=libs CATEGORY:=Libraries TITLE:=libsrtp Client Library URL:=https://github.com/cisco/libsrtp endef define Package/libsrtp/description srtp Library endef CMAKE_BINARY_SUBDIR=build CMAKE_OPTIONS += -DBUILD_SHARED_LIBS=on -DTEST_APPS=off define Build/InstallDev $(INSTALL_DIR) $(1)/usr/include/srtp2 $(CP) $(PKG_INSTALL_DIR)/usr/include/srtp2/*.h $(1)/usr/include/srtp2 $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/lib*.so* $(1)/usr/lib/ endef define Package/libsrtp/install $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/lib*.so* $(1)/usr/lib/ endef $(eval $(call BuildPackage,libsrtp)) 这种相对前面的方式类似,只是这种软件包CMakelist.text要求需要创建build目录进行编译,因此在Makefile中添加CMAKE_BINARY_SUBDIR=build进行声明。因此在编译输出目录,可以看到openwrt会先创建build目录,而第一种方式是没有创建build目录。 软件包中只有部分CMakelist.text 这种方式就是软件包中没有提供完整的CMakelist.txt,但是提供了部分,需要用户包含提供部分的Cmakelist.txt实现,这种方式目前是通过打patches的方式生成主要CMakelist.txt。 Makefile文件 include $(TOPDIR)/rules.mk PKG_NAME:=coreMQTT PKG_VERSION:=2.3.1 PKG_RELEASE:=3 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:= https://codeload.github.com/FreeRTOS/coreMQTT/tar.gz/v$(PKG_VERSION)? PKG_HASH:=b8e95044e6ef8381610949b7fe546c5ddf4e52989ce9938209d5dd5f3371b5d7 PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE PKG_BUILD_PARALLEL:=1 include $(INCLUDE_DIR)/package.mk include $(INCLUDE_DIR)/cmake.mk define Package/coreMQTT SECTION:=libs CATEGORY:=Libraries TITLE:=coreMQTT Client Library URL:=https://github.com/FreeRTOS/coreMQTT endef define Package/coreMQTT/description coreMQTT Client Library endef define Build/InstallDev $(INSTALL_DIR) $(1)/usr/include $(CP) $(PKG_INSTALL_DIR)/*.h $(1)/usr/include $(CP) $(PKG_INSTALL_DIR)/include/*.h $(1)/usr/include $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/lib*.so* $(1)/usr/lib/ endef define Package/coreMQTT/install $(INSTALL_DIR) $(1)/usr/lib $(CP) $(PKG_INSTALL_DIR)/usr/lib/libcoreMQTT.so* $(1)/usr/lib/ endef $(eval $(call BuildPackage,coreMQTT)) patches diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..754a6a2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.10) + +project(CoreMQTT) + +include(${CMAKE_CURRENT_LIST_DIR}/mqttFilePaths.cmake) + +add_definitions("-DMQTT_DO_NOT_USE_CUSTOM_CONFIG") + +include_directories ( + ${MQTT_INCLUDE_PUBLIC_DIRS} +) + +add_library(coreMQTT SHARED ${MQTT_SOURCES} ${MQTT_SERIALIZER_SOURCES}) + +install(TARGETS coreMQTT + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +install(DIRECTORY ${MQTT_INCLUDE_PUBLIC_DIRS}/ + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/ + FILES_MATCHING PATTERN "*.h" +) -- 2.25.1 以上是通过打patches生成主CMakelist.txt,可以看到包含了mqttFilePaths.cmake文件。 -
Cmake构建模版
基础示例 # 最低 CMake 版本要求 cmake_minimum_required(VERSION 3.16) # 定义工程名称和语言 project(mlink_device VERSION 1.0.0 LANGUAGES C) # ============================================ # 收集源码文件 # ============================================ # 只收集 src/ 目录下的实现文件 file(GLOB_RECURSE MLINK_DEVICE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c ) # ============================================ # 创建库目标(可选静态 / 可选共享) # ============================================ option(MLINK_DEVICE_BUILD_SHARED "Build mlink_device as shared library" ON) if(MLINK_DEVICE_BUILD_SHARED) add_library(mlink_device SHARED ${MLINK_DEVICE_SOURCES}) else() add_library(mlink_device STATIC ${MLINK_DEVICE_SOURCES}) endif() # 给库取一个 namespace 别名,让别人可以用 components::mlink_device add_library(components::mlink_device ALIAS mlink_device) # ============================================ # 设置 C 标准 # ============================================ target_compile_features(mlink_device PUBLIC c_std_99) # ============================================ # PRIVATE 宏定义(不会影响其他 target) # ============================================ target_compile_definitions(mlink_device PRIVATE _GNU_SOURCE ) # ============================================ # 头文件路径设置(现代写法) # ============================================ # PUBLIC:构建时 + 安装后供使用者使用 # PRIVATE:仅本库自己使用,不导出 target_include_directories(mlink_device PUBLIC # 本地构建时使用的 include 路径 $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> # 安装后,头文件会被安装到 ${CMAKE_INSTALL_PREFIX}/include # 所以使用相对路径 include(自动加上 install prefix) $<INSTALL_INTERFACE:include> PRIVATE # 库内部使用的私有头文件,不暴露给外部项目 ${CMAKE_CURRENT_SOURCE_DIR}/private ) # ============================================ # 链接依赖(示例) # ============================================ # target_link_libraries(mlink_device # PUBLIC some_other_lib # PRIVATE pthread # ) # ============================================ # 安装规则(核心) # ============================================ # 安装库文件 install( TARGETS mlink_device EXPORT mlink_deviceTargets # 导出 target 列表 ARCHIVE DESTINATION lib # 静态库 .a 安装位置 LIBRARY DESTINATION lib # 共享库 .so 安装位置 RUNTIME DESTINATION bin # 可执行文件(如果是 EXE) ) # 安装公共头文件到 include/ install( DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/ DESTINATION include ) # ============================================ # 导出 CMake package(下游使用 find_package) # ============================================ # 生成 mlink_deviceTargets.cmake install( EXPORT mlink_deviceTargets NAMESPACE components:: # 别名前缀 DESTINATION lib/cmake/mlink_device # 包的安装位置 ) # 生成 mlink_deviceConfig.cmake & version 文件 include(CMakePackageConfigHelpers) # 自动生成版本文件,包含 version 检查逻辑 write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/mlink_deviceConfigVersion.cmake VERSION ${PROJECT_VERSION} COMPATIBILITY ExactVersion ) # 生成 Config 文件(下游项目 find_package 用) configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/mlink_deviceConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/mlink_deviceConfig.cmake INSTALL_DESTINATION lib/cmake/mlink_device ) # 安装 config 与 version 文件 install( FILES ${CMAKE_CURRENT_BINARY_DIR}/mlink_deviceConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/mlink_deviceConfigVersion.cmake DESTINATION lib/cmake/mlink_device ) 收集源代码 file(GLOB_RECURSE MLINK_DEVICE_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/core/*.c" ) 自动递归扫描 core/ 目录下所有 .c 文件,并把结果放到变量 MLINK_DEVICE_SOURCES 中。 创建库目标 option(MLINK_DEVICE_BUILD_SHARED "Build mlink_device as shared library" ON) if(MLINK_DEVICE_BUILD_SHARED) add_library(mlink_device SHARED ${MLINK_DEVICE_SOURCES}) else() add_library(mlink_device STATIC ${MLINK_DEVICE_SOURCES}) endif() # 给库取一个 namespace 别名,让别人可以用 components::mlink_device add_library(components::mlink_device ALIAS mlink_device) option:添加一个Bool的开关,决定是编译动态库还是静态库。 add_library:创建一个库,语法add_library(name [STATIC | SHARED | MODULE] [sources...]) ALIAS是给库创建一个新的别名,方便其他模块调用。 编译参数 target_compile_features(mlink_device PUBLIC c_std_99) # ======== # PRIVATE 宏定义(不会影响其他 target) # ======== target_compile_definitions(mlink_device PRIVATE _GNU_SOURCE ) target_compile_features:如上示例,gcc -std=c99 target_compile_definitions:添加编译宏,-Dxxx,如上-D_GNU_SOURCE,但PRIVATE设置了作用于只对taget自己游侠,下游不继承。 作用域 设置方式 A 本身用 A 的用户(B)用 PRIVATE ✔ ✘ PUBLIC ✔ ✔(传播) INTERFACE ✘ ✔(只传播) 头文件搜索 target_include_directories(mlink_device PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include> PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include/mcp ${CMAKE_CURRENT_SOURCE_DIR}/include/transport ${CMAKE_CURRENT_SOURCE_DIR}/include/utils ) target_include_directories:给目标mlink_device配置编译时要搜索的头文件目录,类似加上-I....。 PRIVATE:在编译mlink_device模块编译自己这个库时寻找的头文件路径,不会传给它的依赖目标。 PUBLIC:这是mlink_device对外提供API的头文件目录,其他模块链接mlink_device库时也会自动继承这些include目录。 PRIVATE搜索的头文件路径不会传播,只会影响当前的mlink_device模块这个编译目标。而PUBLIC会传播给他一来的target,然后这个传播又分为两类一个是构建期和安装后。 PUBLIC + BUILD_INTERFACE:构建阶段,适用于同一个构建Cmake,提供的头文件路径为源码路径,${CMAKE_CURRENT_SOURCE_DIR}/include就是srobot/components/mlink/include。 PUBLIC + INSTALL_INTERFACE:安装阶段,适用于不同的构建Cmake,给其他模块find_package() 后使用的 include 路径。:include就会变成 ${PREFIX}/include,如srobot/outpu/sdk/include。 库搜索 find_package+target_link_libraries组合。 find_package(OpenSSL REQUIRED) Cmake就会自动搜索 /usr/lib/cmake/openssl/... /usr/local/lib/cmake/openssl/... /opt/sdk/lib/cmake/openssl/... 然后只需要: target_link_libraries(myapp PRIVATE OpenSSL::SSL OpenSSL::Crypto) 库和头文件导出 #导出编译产物如库、可执行文件 install( TARGETS mlink_device EXPORT ComponentsTargets ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin ) #导出普通文件,如头文件 install( FILES ${CMAKE_CURRENT_SOURCE_DIR}/include/mlink.h DESTINATION include ) EXPORT:mlink_device 这个 target 加入一个“targets 导出列表”,后面install(EXPORT...) 把它写入 CMake 模块文件,让外部项目 find_package 时导入 components::mlink_device。 ARCHIVE DESTINATION lib:静态库(.a)安装到哪里?——> prefix/lib。编译cmake指定的-DCMAKE_INSTALL_PREFIX LIBRARY DESTINATION lib:共享库(.so / .dylib)安装到哪里?——>prefix/lib。 RUNTIME DESTINATION bin:可执行文件安装到哪里?——>prefix/bin。 FILES:导出头文件到——>prefix/include -
部署MQTT云服务
Centos安装 配置emqx的源 curl -s https://assets.emqx.com/scripts/install-emqx-rpm.sh | sudo bash 安装emqx sudo yum install emqx -y 启动emqx sudo systemctl start emqx 如果要卸载 sudo yum remove emqx 后台配置 访问前,服务端的端口18083权限打开。 截屏2024-12-01 08.53.43 xxx.xxx.xxx:18083 默认登录账号:admin,public。首次登录会要求更新密码。 如果要支持tcp/ssl/ws/wss这几种监听,也需要把相应的端口打开以及服务器端口权限打开。 测试访问 使用mosquitto测试 这里使用的是ubuntu系统上的mosquito_sub/pub工具测试。先安装工具: sudo apt-get install mosquitto-clients 订阅 mosquitto_sub -h www.xxx.xxx -t \"test\" -v 发布 mosquitto_pub -h www.laumy.tech -t \"test\" -m \"hello world\" 使用MQTTX软件测试 下载地址:https://mqttx.app/zh/downloads 建立一个回话连接用于订阅消息 接着对填写订阅主题 再建立一个回话用于发布消息 连接上后进行发布主题 这样订阅的回话就收到订阅消息了 开启SSL/TLS连接 参考:https://docs.emqx.com/zh/emqx/v5.8/network/overview.html 启动异常排查 查找emqx.log cd / find -name \"emqx.log*\" 参考: https://docs.emqx.com/zh/cloud/latest/connect_to_deployments/react_sdk.html https://docs.emqx.com/zh/emqx/v5.8/connect-emqx/developer-guide.html -
coturn安装
安装 yum install coturn -y 配置 vim /etc/coturn/turnserver.conf listening-port=3478 #指定 coturn 监听的端口。默认的 TURN 协议端口是 3478,用于接收 TURN 客户端的连接请求。 tls-listening-port=5349 #指定用于 TLS (传输层安全协议) 加密连接的监听端口。TURN 支持通过 TLS 来增强通信的安全性,默认的端口是 5349。 relay-ip=172.20.65.5 #指定 TURN 服务器用于中继流量的 IP 地址。这个 IP 地址通常是内部的局域网地址,用于数据中继传输。 relay-ip=fe80::216:3eff:fe01:105b #指定 TURN 服务器的 IPv6 地址,通常用于支持 IPv6 网络的环境中。 external-ip=8.134.108.235 #指定TURN服务器的外部公网 IP 地址。当 TURN 服务器位于 NAT 后面时,external-ip 用于告知客户端如何访问该服务器。 relay-threads=50 #指定服务器处理中继流量时使用的线程数量。增加线程数有助于提高并发连接的处理能力。 min-port=40000 max-port=65535 #指定 coturn 使用的最小和最大端口范围。coturn 会在该范围内选择端口来建立临时的媒体连接。这个端口范围通常需要开放,供客户端进行通讯。 server-name=www.laumy.tech #指定 TURN 服务器的名称。这通常用于身份验证或显示给客户端的服务器标识。 lt-cred-mech # 打开这个表示要使用加密,默认是打开的,加密的话就需要验证username和密码,如下的user字段。 no-auth #如果打开这个,表示不需要加密,和lt-cred-mech是二选一。 user=laumy:A8xxxx22 #指定允许连接到 TURN 服务器的用户名和密码。格式为 username:password,用于身份验证。laumy 是用户名,A8xxxx22 是密码。 realm=www.laumy.tech #指定 TURN 服务器的“域”名,用于标识一个身份验证域。这个域通常与 user 结合使用,确保身份验证的正确性。 cert=/etc/xxx/cert/www.laumy.tech.pem pkey=/etc/nginx/cert/www.laumy.tech.key # 设置证书密钥 启停 启动: systemctl start coturn ps -ef |grep turnserver 关闭: systemctl stop coturn 调试 查看启动日志 /var/log/coturn/turnserver.log 测试 https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/ 参考: https://www.zhiboblog.com/2105.html -
STUN协议
NAT墙 完全锥形 IP限制型锥形 端口限制型锥形 对称型 NAT穿透 STUN协议简介 STUN(Session Traversal Utilities for NAT)是一个用于帮助客户端发现其所在NAT之后的公网IP地址和端口号。通信的双方都能知道所在的公网IP,并且能够让公网的NAT帮助转发,从而实现NAT穿透。STUN协议广泛应用在WebRTC实时通信系统中。STUN交互过程主要有以下几个步骤: 客户端向 STUN 服务器发送请求。 STUN 服务器处理并响应。 客户端接收响应并解析公共IP和端口。 客户端根据收到的公共 IP 和端口进行 NAT 穿越和路径选择。 绑定请求与响应 客户端发STUN请求 客户端发送STUN请求,包的类型为Binding Request。请求的目标是让STUN 服务器返回客户端的公网IP地址和端口。 STUN Message Header: - Message Type: 0x0001 (Binding Request) //类型 - Message Length: 0x0000 - Magic Cookie: 0x2112A442 //固定值,用于标识STUN - Transaction ID: 0x63f96d584f90a66d18b68202 //用于唯一标识该请求的ID,匹配响应 STUN Attributes (Optional): //以下是附加信息 - Username: <username> - Integrity: <HMAC-SHA1 signature> - Fingerprint: <CRC-32C of the message> STUN服务端响应 STUN服务器收到客户端的请求后,会放binding response回复,内容中携带了客户端的公网IP和端口号。 STUN Message Header: - Message Type: 0x0101 (Binding Response) //表明是一个 Binding Response - Message Length: 0x000C - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a66d18b68202 STUN Attributes: - Mapped Address: Family: IPv4 Address: 203.0.113.1 //公网的IP Port: 3478 //公网的端口 - Username: <username> - Integrity: <HMAC-SHA1 signature> - Fingerprint: <CRC-32C of the message> 连接检查 以webrtc举例,当需要通信的双方,通过信令交互SDP(在libpeer文章中,是通过mqtt)中交互的双方各自的公网IP地址信息后,可以使用STUN的binding request和binding response检查P2P的连通性,以确保P2P链路可达。 首先一般由一端率先发起binding request。 STUN Message Header: - Message Type: 0x0001 (Binding Request) - Message Length: 0x0008 - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a32d18b68202 STUN Attributes: - Username: <peer-username> - Message Integrity: <HMAC-SHA1 signature> 接着等待对端回复binding response STUN Message Header: - Message Type: 0x0101 (Binding Response) - Message Length: 0x0008 - Magic Cookie: 0x2112A442 - Transaction ID: 0x63f96d584f90a32d18b68202 STUN Attributes: - Mapped Address: Family: IPv4 Address: 203.0.113.10 Port: 3479 当发起方接受到binding response后,表明连通性正常,下面是代码的流程示例。 agent_connectivity_check() { /*1. 发送 binding request*/ agent_create_binding_request(agent, &msg) stun_msg_create(msg, STUN_CLASS_REQUEST | STUN_METHOD_BINDING); stun_msg_write_attr(msg, STUN_ATTR_TYPE_USERNAME, strlen(username), username); stun_msg_write_attr(msg, STUN_ATTR_TYPE_PRIORITY, 4, (char*)&agent->nominated_pair->priority); stun_msg_finish(msg, STUN_CREDENTIAL_SHORT_TERM, agent->remote_upwd, strlen(agent->remote_upwd)); agent_socket_send(agent, &agent->nominated_pair->remote->addr, msg.buf, msg.size);//发送,这里目标的ip和端口就是对端的ip地址和端口号。 /*2. 等待接受*/ agent_recv switch (stun_msg.stunclass) { /*收到对端发送的binding request,构建一个response发送回去。*/ case STUN_CLASS_REQUEST: agent_process_stun_request agent_create_binding_response stun_set_mapped_address agent_socket_send /*收到对端回复的binding response,将state设置为success*/ case STUN_CLASS_RESPONSE: agent_process_stun_response agent->nominated_pair->state = ICE_CANDIDATE_STATE_SUCCEEDED; } } 实践流程分析 本次实验使用的stun服务器是,有两个设备,设备A是一块开发板,设备B是电脑,分别连接到同一个路由器上。 Resolved stun.l.google.com -> 74.125.250.129, Resolved stun/turn server 74.125.250.129:19302 设备A(开发板)的IP地址信息是 inet addr:192.168.51.127 设备B(电脑)的IP地址信息是 192.168.51.227 设备发送向STUN服务端发送请求 开发板发送binding request 这里是设备向stun.l.google.com:74.125.250.129:19302服务发送binding request请求。 电脑发送binding request 设备的电脑一下发了3个binding request,其中一个是发送给google stun服务器的,但是另外两个实际上是发给设备A的本地IP和NAT墙的IP,可能是此前连接过,浏览器缓存了信息。 服务端回复 设备A收到回复 stun.l.google.com回复了binding success response。并表明了设备的公网IP为120.236.240.177,其端口为8139。 电脑B收到回复 通过message transaction ID可以看到电脑收到了Google STUN回复的binding success response,告知其NAT地址为120.236.240.177,端口号是8140。因为连接的是同一个路由器,所以NAT是一样的。同时板子A设备192.168.51.127也对齐回复了binding success response,告知其地址为192.168.51.227:59777。 板子A请求检查 板子A发送了一个binding request,对端回复了一个binding success response,则表示联通已建立。 TURN协议 TURN协议是在STUN的基础上增加了中继功能,解决的是对称型NAT无法穿透的问题。如下图的结构体,需要注意的是,使用TURN作为中继转发,传输的设备一方式TURN Client,另一方是Peer A。即TURN Client需要与TURN Server建立TURN协议交互,而Peer A与TURN Server只需要做STUN协议交互。此前调试了两个通信设备,两端都是使用TURN协议发起连接,导致一端checking失败,最后将一端修改为STUN协议交互即可。 Peer A Server-Reflexive +---------+ Transport Address | | 192.0.2.150:32102 | | | /| | TURN | / ^| Peer A | Client’s Server | / || | Host Transport Transport | // || | Address Address | // |+---------+ 10.1.1.2:49721 192.0.2.15:3478 |+-+ // Peer A | | ||N| / Host Transport | +-+ | ||A|/ Address | | | | v|T| 192.168.100.2:49582 | | | | /+-+ +---------+| | | |+---------+ / +---------+ | || |N| || | // | | | TURN |v | | v| TURN |/ | | | Client |----|A|----------| Server |------------------| Peer B | | | | |^ | |^ ^| | | | |T|| | || || | +---------+ | || +---------+| |+---------+ | || | | | || | | +-+| | | | | | | | | Client’s | Peer B Server-Reflexive Relayed Transport Transport Address Transport Address Address 192.0.2.1:7000 192.0.2.15:50000 192.0.2.210:49191 Allocate 请求服务器,分配一个中继服务。中继建立连接后,客户端需要进行保活,定期发送refresh request给服务器端。 TURN TURN Peer Peer client server A B |-- Allocate request --------------->| | | | | | | |<--------------- Allocate failure --| | | | (401 Unauthorized) | | | | | | | |-- Allocate request --------------->| | | | | | | |<---------- Allocate success resp --| | | | (192.0.2.15:50000) | | | / / / / | | | | |-- Refresh request ---------------->| | | | | | | |<----------- Refresh success resp --| | | | | | | 如上示例,客户端先发送不带验证信息的Allocate请求,此时STUN服务器会返回error response,客户端收到错误后加上验证信息再次请求。以IPC通信为例,摄像头端是Peer A(资源有限,不支持证书请求等协议),发起的是STUN协议,而手机预览端是TURN Client,发起的是TURN交互协议。 Send机制 客户端和peer直接通信可以通过TURN server进行转发信息,主要有两种方式,第一种是使用Send/data方式,第二种是使用channels方式。其主要目的是通过某种方式告知服务器,从client接收到的数据应该发给那个peer。下面是Send/data机制。 TURN Server在进行转发前,需要client先安装一个到对等端的许可(permission),许可证可以通过CreatePermission request/resp来进行交互。 TURN TURN Peer Peer client server A B | | | | |-- CreatePermission req (Peer A) -->| | | |<-- CreatePermission success resp --| | | | | | | |--- Send ind (Peer A)-------------->| | | | |=== data ===>| | | | | | | |<== data ====| | |<-------------- Data ind (Peer A) --| | | | | | | | | | | |--- Send ind (Peer B)-------------->| | | | | dropped | | | | | | | |<== data ==================| | dropped | | | | | | | Chanel机制 等同于send/data机制。 TURN TURN Peer Peer client server A B | | | | |-- ChannelBind req ---------------->| | | | (Peer A to 0x4001) | | | | | | | |<---------- ChannelBind succ resp --| | | | | | | |-- [0x4001] data ------------------>| | | | |=== data ===>| | | | | | | |<== data ====| | |<------------------ [0x4001] data --| | | | | | | |--- Send ind (Peer A)-------------->| | | | |=== data ===>| | | | | | | |<== data ====| | |<------------------ [0x4001] data --| | | | | | | 参考: https://mp.weixin.qq.com/s/XMhSDABc74dpALIHrPPt7w https://www.ctyun.cn/developer/article/586260405821509 https://www.cnblogs.com/pannengzhi/p/5048965.html -
libpeer分析
关键数据结构 PeerConfiguration typedef struct PeerConfiguration { IceServer ice_servers[5]; MediaCodec audio_codec; MediaCodec video_codec; DataChannelType datachannel; void (*onaudiotrack)(uint8_t* data, size_t size, void* userdata); void (*onvideotrack)(uint8_t* data, size_t size, void* userdata); void (*on_request_keyframe)(void* userdata); void* user_data; } PeerConfiguration; 实例: PeerConfiguration config = { .ice_servers = { {.urls = "stun:stun.l.google.com:19302"}, }, .datachannel = DATA_CHANNEL_STRING, .video_codec = CODEC_H264, .audio_codec = CODEC_PCMA}; Agent Agent 结构体,主要用于实现 WebRTC 或类似协议中的 ICE (Interactive Connectivity Establishment) 机制。ICE 是一种 NAT 穿越协议,用于在点对点通信中建立连接并选择最佳的网络路径。Agent 结构体存储了与 ICE 协议操作相关的各种数据,用于管理连接的候选地址、协商过程以及状态。 struct Agent { char remote_ufrag[ICE_UFRAG_LENGTH + 1]; char remote_upwd[ICE_UPWD_LENGTH + 1]; char local_ufrag[ICE_UFRAG_LENGTH + 1]; char local_upwd[ICE_UPWD_LENGTH + 1]; IceCandidate local_candidates[AGENT_MAX_CANDIDATES]; IceCandidate remote_candidates[AGENT_MAX_CANDIDATES]; int local_candidates_count; int remote_candidates_count; UdpSocket udp_sockets[2]; Address host_addr; int b_host_addr; uint64_t binding_request_time; AgentState state; AgentMode mode; IceCandidatePair candidate_pairs[AGENT_MAX_CANDIDATE_PAIRS]; IceCandidatePair* selected_pair; IceCandidatePair* nominated_pair; int candidate_pairs_num; int use_candidate; uint32_t transaction_id[3]; }; ServiceConfiguration typedef struct ServiceConfiguration { const char* mqtt_url; int mqtt_port; const char* client_id; const char* http_url; int http_port; const char* username; const char* password; PeerConnection* pc; } ServiceConfiguration; 实例 #define SERVICE_CONFIG_DEFAULT() \ { \ .mqtt_url = "broker.emqx.io", \ //访问的MQTT服务器 .mqtt_port = 8883, \ //访问的MQTT端口 .client_id = "peer", \ //用于订阅的主题 webrtc/peer/jsonrpc //发布的主题 webrtc/peer/jsonrpc-reply .http_url = "", \ //信令使用的是MQTT,没有使用http, .http_port = 443, \ //因此HTTP的没有用。 .username = "", \ .password = "", \ .pc = NULL \ } PeerConnection struct PeerConnection { PeerConfiguration config; PeerConnectionState state; Agent agent; DtlsSrtp dtls_srtp; Sctp sctp; Sdp local_sdp; Sdp remote_sdp; void (*onicecandidate)(char* sdp, void* user_data); void (*oniceconnectionstatechange)(PeerConnectionState state, void* user_data); void (*on_connected)(void* userdata); void (*on_receiver_packet_loss)(float fraction_loss, uint32_t total_loss, void* user_data); uint8_t temp_buf[CONFIG_MTU]; uint8_t agent_buf[CONFIG_MTU]; int agent_ret; int b_local_description_created; Buffer* audio_rb; Buffer* video_rb; Buffer* data_rb; RtpEncoder artp_encoder; RtpEncoder vrtp_encoder; RtpDecoder vrtp_decoder; RtpDecoder artp_decoder; uint32_t remote_assrc; uint32_t remote_vssrc; }; WebRTC简介 概览 上图中一共有4中角色,分别是signaling server、STUN/RURN server、Client A,Client B; signaling server:信令指的是管理两个通信设备A和B建立和管理点对点连接过程中的控制消息交换机制。让通信双方能够交换各种信息,从而建立、维护和终止一个点对点的实时通信连接。 STUN/RURN server:用于设备A和设备B传过NAT。 Client A:通信设备A Client B:通信设备B 通信流程 WebRTC中客户端与信令服务器、STUN/TURN服务器的交互流程如下: ClientA首先创建PeerConnection对象,然后打开本地音视频设备,将音视频数据封装成MediaStream添加到PeerConnection中。 ClientA调用PeerConnection的CreateOffer方法创建一个用于offer的SDP对象,SDP对象中保存当前音视频的相关参数。ClientA通过PeerConnection的SetLocalDescription方法将该SDP对象保存起来,并通过Signal服务器发送给ClientB。 ClientB接收到ClientA发送过的offer SDP对象,通过PeerConnection的SetRemoteDescription方法将其保存起来,并调用PeerConnection的CreateAnswer方法创建一个应答的SDP对象,通过PeerConnection的SetLocalDescription的方法保存该应答SDP对象并将它通过Signal服务器发送给ClientA。 ClientA接收到ClientB发送过来的应答SDP对象,将其通过PeerConnection的SetRemoteDescription方法保存起来。 在SDP信息的offer/answer流程中,ClientA和ClientB已经根据SDP信息创建好相应的音频Channel和视频Channel并开启Candidate数据的收集,Candidate数据可以简单地理解成Client端的IP地址信息(本地IP地址、公网IP地址、Relay服务端分配的地址)。 当ClientA收集到Candidate信息后,PeerConnection会通过OnIceCandidate接口给ClientA发送通知,ClientA将收到的Candidate信息通过Signal服务器发送给ClientB,ClientB通过PeerConnection的AddIceCandidate方法保存起来。同样的操作ClientB对ClientA再来一次。 这样ClientA和ClientB就已经建立了音视频传输的P2P通道,ClientB接收到ClientA传送过来的音视频流,会通过PeerConnection的OnAddStream回调接口返回一个标识ClientA端音视频流的MediaStream对象,在ClientB端渲染出来即可。同样操作也适应ClientB到ClientA的音视频流的传输。 上述序列中,WebRTC并不提供Stun服务器和Signal服务器,服务器端需要自己实现。Stun服务器可以用google提供的实现stun协议的测试服务器(stun:stun.l.google.com:19302),Signal服务器则完全需要自己实现了,可以使用MQTT的broker.emqx.io来作为信令服务器。它需要在ClientA和ClientB之间传送彼此的SDP信息和candidate信息,ClientA和ClientB通过这些信息建立P2P连接来传送音视频数据。 信令 待补充 SDP SDP是一个比较老的协议,发布于2006年,以type=value的格式描述回话内容。WebRTC引入SDP来描述媒体信息,用于媒体协商时决定双方是否可以进行通信。当对端设备anser的时候会携带一个SDP格式的内容,如下。 v=0 o=- 8646007345366799659 2 IN IP4 127.0.0.1 s=- t=0 0 a=group:BUNDLE video audio datachannel a=msid-semantic: WMS 5a162afd-e195-4207-a699-e977bb510327 m=video 10773 UDP/TLS/RTP/SAVPF 96 102 c=IN IP4 14.29.67.186 a=rtcp:9 IN IP4 0.0.0.0 a=candidate:1969734079 1 udp 2122260223 192.168.31.123 65492 typ host generation 0 network-id 1 network-cost 10 a=candidate:2345473323 1 tcp 1518280447 192.168.31.123 9 typ host tcptype active generation 0 network-id 1 network-cost 10 a=candidate:3819939686 1 udp 1686052607 14.29.67.186 10773 typ srflx raddr 192.168.31.123 rport 65492 generation 0 network-id 1 network-cost 10 a=ice-ufrag:JgBs a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 a=setup:active a=mid:video a=recvonly a=rtcp-mux a=rtpmap:96 H264/90000 a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f a=rtpmap:102 H264/90000 a=rtcp-fb:102 nack a=rtcp-fb:102 nack pli a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f m=audio 9 UDP/TLS/RTP/SAVP 8 c=IN IP4 0.0.0.0 a=rtcp:9 IN IP4 0.0.0.0 a=ice-ufrag:JgBs //对端用户名 a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v //对端的密码 a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 //指纹信息 a=setup:active a=mid:audio a=sendrecv a=msid:5a162afd-e195-4207-a699-e977bb510327 693effe8-ba8d-469d-ba6a-f58e2b5f73df a=rtcp-mux a=rtpmap:8 PCMA/8000 a=ssrc:1668486723 cname:o+PtcUckwEIRvBPp m=application 9 UDP/DTLS/SCTP webrtc-datachannel c=IN IP4 0.0.0.0 a=ice-ufrag:JgBs a=ice-pwd:1USyOciE2u1Uzq97tWIhcx7v a=ice-options:trickle a=fingerprint:sha-256 C1:6F:4D:10:6E:99:AC:9C:5F:CD:24:C8:A5:83:75:AE:45:1A:D4:7D:E0:73:B6:0A:67:4E:ED:C3:88:C2:6C:42 a=setup:active a=mid:datachannel a=sctp-port:5000 a=max-message-size:262144 ICE candidates ICE (Interactive Connectivity Establishment 互动连接建立) candidates(候选人)简称ICE candidates,WebRTC中的ICE Candidate是用来描述可以连接的远端的基本信息,什么是candidate,两台设备的连接,需要知道设备的网络信息,如IP地址,端口号以及使用的协议,因此candidate至少包含{address,port,protocol}三元组信息集。通过SDP会话来交互candidate信息。 WebRTC将Candidate分成四种类型,且类型间存在优先级次序,从高到低分别为host、srflx、prflx和relay,本章节使用的是srflx类型,从STUN服务器获取的地址。 host:从本机网卡上获取到的地址,一般来说,一个网卡对应一个地址。 srflx(server reflexive):从STUN服务器获取到的地址。 relay:从TRUN服务器获取到的地址。 prflx(peer reflexive):在交互过程中从对端数据报文中获取到的地址。 其中,srflx和prflx地址可能是一样的,但获取的途径不一样,下面是描述ICE candidates的数据结构。 typedef enum IceCandidateType { ICE_CANDIDATE_TYPE_HOST, ICE_CANDIDATE_TYPE_SRFLX, ICE_CANDIDATE_TYPE_PRFLX, ICE_CANDIDATE_TYPE_RELAY, } IceCandidateType; typedef struct IceCandidate IceCandidate; struct IceCandidate { int foundation; int component; //1:RTP,2:RTCP uint32_t priority;//优先级 char transport[32 + 1]; //传输协议,基于UDP IceCandidateType type; //类型,如上。 IceCandidateState state; Address addr; Address raddr; }; main函数分析 /*1. 调用srtp_init,初始化srtp内核模块加密套件、debug等*/ peer_init(); /*2. 创建一个peer连接,其中创建了agent,打开了一个udp socket,初始化 了rtp 音频、视频编解码,主要是确定用什么编码格式。*/ g_pc = peer_connection_create(&config); /*3. 注册一个peer连接的状态切换回调函数,回调函数 onconnectionstatechange, 用于处理对等连接的 ICE 连接状态变更。当连接的状态发生变化(例如从new到connected, 或者 failed),这个回调函数将被调用。*/ peer_connection_oniceconnectionstatechange(g_pc, onconnectionstatechange); /*4. 设置数据通道(DataChannel)相关的回调函数。当数据通道打开时,会触发 onopen;接收到消息时,会触发 onmessage;当数据通道关闭时会触发onclose*/ peer_connection_ondatachannel(g_pc, onmessage, onopen, onclose); /*5. 设置信令服务器的配置,通常用于 WebRTC 会话中的信令部分。通过传入 service_config 配置,WebRTC 客户端可以向信令服务器注册或传递配置信息 (如 client_id 和对等连接句柄 pc) 这里的信令服务器用的是公用的mqtt服务器? broker.emqx.io 使用mqtt来作为信令通信,没有使用http,在里面会注册一个peer_connect的候选回调 */ /*什么是信令 在webrtc中,信令指的是建立和管理点对点连接过程中的控制消息交换机制。让通信双方 能够交换各种信息,从而建立、维护和终止一个点对点的实时通信连接。 WebRTC 的核心目标是实现浏览器之间的直接通信,支持音频、视频和数据传输, 但在建立这种连接之前,双方需要交换一些控制信息,包括: - 会话描述(如 SDP,Session Description Protocol):描述连接的媒体设置(如视频编码格式、音频参数等)。 - 网络候选(ICE候选,ICE candidates):用于寻找最佳的点对点网络路径,以确保即使在复杂的网络环境下也能建立连接。Interactive Connectivity Establishment(互动连接建立) - 连接状态:例如连接是否已建立、是否关闭等。 */ /*信令的实现 虽然 WebRTC 协议本身并没有指定信令的实现方式,但是它提供了用于交换数据的 API。 开发者可以选择适合自己的信令协议和传输方式,通常的实现方式包括: - WebSocket:一个常见的双向通信协议,可以实时地交换信令数据。 - HTTP 请求:通过轮询、长轮询等方式进行信令交换。 - SIP (Session Initiation Protocol):一些应用可能采用 SIP 作为信令协议。 - XMPP (Extensible Messaging and Presence Protocol):也是一种可以用来实现信令的协议,尤其适用于即时消息和通信应用。 - MQTT:通过订阅和发布的方式来进行传输,本文就是使用这种方式。 */ /*信令的工作流程 假设有两个用户 A 和 B 通过 WebRTC 建立视频通话,信令的过程大致如下: (1) 用户A生成offer:用 A创建一个 RTCPeerConnection 对象,并 生成一个offer。这个 offer 包含了 A 的媒体设置和网络候选信息(ICE 候选)。 A 将这个 offer 通过信令系统(例如 MQTT)发送给用户 B。 (2)用户B接收到 offer,并生成 answer:用户 B 收到 offer 后,使用它来创 建一个 RTCPeerConnection,并生成一个 answer(应答)。这个 answer 包含 B 的媒体配置和 ICE 候选信息。B 将 answer 通过信令系统发送给用户 A。 (3)交换 ICE 候选:在连接过程中,双方都会收集到 ICE 候选并通过信令进行交换, 直到双方都确定了最佳的网络路径。 (4)建立连接:当双方完成了 ICE 候选交换并且建立了连接时,通信就可以开始了。 (5)连接终止:当会话结束时,双方通过信令通知对方关闭连接。*/ service_config.client_id = argv[1]; service_config.pc = g_pc; peer_signaling_set_config(&service_config); /*6.初始化MQTT,连接broker.emqx.io, */ peer_signaling_join_channel(); /*7. 创建peer连接的task*/ pthread_create(&peer_connection_thread, NULL, peer_connection_task, NULL); /*8. 创建signal的task*/ pthread_create(&peer_singaling_thread, NULL, peer_singaling_task, NULL); /*9. 读取本地音视频初始化*/ reader_init(); /*10. 循环判断PEER的状态是否是连接态,如果是已经连接,则发送音视频数据*/ while (!g_interrupted) { if (g_state == PEER_CONNECTION_COMPLETED) { curr_time = get_timestamp(); // FPS 25 if (curr_time - video_time > 40) { video_time = curr_time; if (reader_get_video_frame(buf, &size) == 0) { peer_connection_send_video(g_pc, buf, size); } } if (curr_time - audio_time > 20) { if (reader_get_audio_frame(buf, &size) == 0) { peer_connection_send_audio(g_pc, buf, size); } audio_time = curr_time; } usleep(1000); } } 信令交互 int peer_signaling_join_channel() { /*1. 与信令服务器建立连接,实际上就是连接MQTT的broker*/ if (peer_signaling_mqtt_connect(g_ps.mqtt_host, g_ps.mqtt_port) < 0) { LOGW("Connect MQTT server failed"); return -1; } /*1. 发布信令的消息*/ peer_signaling_mqtt_subscribe(1); return 0; } 与信令服务器建立连接 因为与MQTT broker.emqx.io的交互是TLS加解密的,因此先使用ssl_transport_connect建立加解密通道,接着对MQTT进行初始化,将ssl的收发函数注册到MQTT中,这样MQTT后续的就可以用SSL的加解密通道进行通信,最后是发起MQTT连接。 static int peer_signaling_mqtt_connect(const char* hostname, int port) { MQTTStatus_t status; MQTTConnectInfo_t conn_info; bool session_present; /*1. 与mqtt的broker,其host="broker.emqx.io",port=8883建立TLS连接,MQTT的收发消息 是否通过加密的方式,因此需要先使用TLS进行连接*/ if (ssl_transport_connect(&g_ps.net_ctx, hostname, port, NULL) < 0) { LOGE("ssl transport connect failed"); return -1; } /*2. ssl建立连接之后,将发送和接收的函数通过MQTT_Init进行注册,后续其收发就会调 调用ssl_transport_recv和ssl_transport_send进行解加密收发。 MQTT_Init初始化时还注册回调函数, 对CONNACK/PUBLISH/SUBACK事件进处理 需要注意的时,当设备收到broker发过来的PUBLISH事件时,会回调这个函数进行 处理,这样的场景是当对端设备访问连接是,就会调用peer_signaling_mqtt_event_cb。*/ g_ps.transport.recv = ssl_transport_recv; g_ps.transport.send = ssl_transport_send; g_ps.transport.pNetworkContext = &g_ps.net_ctx; g_ps.mqtt_fixed_buf.pBuffer = g_ps.mqtt_buf; g_ps.mqtt_fixed_buf.size = sizeof(g_ps.mqtt_buf); status = MQTT_Init(&g_ps.mqtt_ctx, &g_ps.transport, ports_get_epoch_time, peer_signaling_mqtt_event_cb, &g_ps.mqtt_fixed_buf); memset(&conn_info, 0, sizeof(conn_info)); conn_info.cleanSession = false; if (strlen(g_ps.username) > 0) { conn_info.pUserName = g_ps.username; conn_info.userNameLength = strlen(g_ps.username); } if (strlen(g_ps.password) > 0) { conn_info.pPassword = g_ps.password; conn_info.passwordLength = strlen(g_ps.password); } if (strlen(g_ps.client_id) > 0) { conn_info.pClientIdentifier = g_ps.client_id; conn_info.clientIdentifierLength = strlen(g_ps.client_id); } conn_info.keepAliveSeconds = KEEP_ALIVE_TIMEOUT_SECONDS; /*3.建立MQTT的,MQTT协议与broker连接。前面步骤1是建立SSL连接,相当于是建立了加解密通道, 而这里是建立MQTT的协议通道。*/ status = MQTT_Connect(&g_ps.mqtt_ctx, &conn_info, NULL, CONNACK_RECV_TIMEOUT_MS, &session_present); if (status != MQTTSuccess) { LOGE("MQTT_Connect failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } LOGI("MQTT_Connect succeeded."); return 0; } 通知信令服务器已准备 通知服务器已经准备,主要是通过MQTT的订阅主题为\"webrtc/peer/jsonrpc\",等待接入设备接入。 static int peer_signaling_mqtt_subscribe(int subscribed) { MQTTStatus_t status = MQTTSuccess; MQTTSubscribeInfo_t sub_info; /*1. 获取MQTT的packet id*/ uint16_t packet_id = MQTT_GetPacketId(&g_ps.mqtt_ctx);、 /*2. MQTT QOS为0即只发一次,订阅的主题是"webrtc/peer/jsonrpc" */ memset(&sub_info, 0, sizeof(sub_info)); sub_info.qos = MQTTQoS0; sub_info.pTopicFilter = g_ps.subtopic; sub_info.topicFilterLength = strlen(g_ps.subtopic); /*3. 发送订阅消息*/ if (subscribed) { status = MQTT_Subscribe(&g_ps.mqtt_ctx, &sub_info, 1, packet_id); } else { status = MQTT_Unsubscribe(&g_ps.mqtt_ctx, &sub_info, 1, packet_id); } if (status != MQTTSuccess) { LOGE("MQTT_Subscribe failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } /*4. 等待MQTT 订阅消息发送成功*/ status = MQTT_ProcessLoop(&g_ps.mqtt_ctx); /* 调用该函数实时处理循环接收数据,并自定发送心跳包保持链接,当有数据到来时,会触发myEventCallback回调函数*/ if (status != MQTTSuccess) { LOGE("MQTT_ProcessLoop failed: Status=%s.", MQTT_Status_strerror(status)); return -1; } LOGD("MQTT Subscribe/Unsubscribe succeeded."); return 0; } 接收信令服务器事件处理 在peer_signaling_mqtt_connect函数中,调用MQTT_Init初始化时,注册了一个peer_signaling_mqtt_event_cb回调函数,其对接收的MQTT消息进行处理,包括收到了Broker发过来的PUBLISH消息,因此当设备接入时,调用的是peer_signaling_mqtt_event_cb函数。 static void peer_signaling_mqtt_event_cb(MQTTContext_t* mqtt_ctx, MQTTPacketInfo_t* packet_info, MQTTDeserializedInfo_t* deserialized_info) { switch (packet_info->type) { case MQTT_PACKET_TYPE_CONNACK: LOGI("MQTT_PACKET_TYPE_CONNACK"); break; case MQTT_PACKET_TYPE_PUBLISH: LOGI("MQTT_PACKET_TYPE_PUBLISH"); /*收到了broker发送过来的PUBLISH消息*/ peer_signaling_on_pub_event(deserialized_info->pPublishInfo->pPayload, deserialized_info->pPublishInfo->payloadLength); break; case MQTT_PACKET_TYPE_SUBACK: LOGD("MQTT_PACKET_TYPE_SUBACK"); break; default: break; } } static void peer_signaling_on_pub_event(const char* msg, size_t size) { cJSON *req, *res, *item, *result, *error; int id = -1; char* payload = NULL; PeerConnectionState state; req = res = item = result = error = NULL; /*1. 先获取peer connect的状态*/ state = peer_connection_get_state(g_ps.pc); printf("%s,%d, msg:%s\n",__func__,__LINE__,msg); do { /*2. 解析json文件*/ req = cJSON_Parse(msg); if (!req) { error = cJSON_CreateRaw(RPC_ERROR_PARSE_ERROR); LOGW("Parse json failed"); break; } /*3. 解析jsion中字段的id值*/ item = cJSON_GetObjectItem(req, "id"); if (!item && !cJSON_IsNumber(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_REQUEST); LOGW("Cannot find id"); break; } id = item->valueint; /*4. 解析jison中的method字段是是什么?*/ item = cJSON_GetObjectItem(req, "method"); if (!item && cJSON_IsString(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_REQUEST); LOGW("Cannot find method"); break; } /* 5. 解析json文件,发现metho为offer,broker发过来要求offer,表示有设备加入了。*/ /* {"jsonrpc":"2.0","method":"offer","id":89} */ if (strcmp(item->valuestring, RPC_METHOD_OFFER) == 0) { switch (state) { case PEER_CONNECTION_NEW: case PEER_CONNECTION_DISCONNECTED: case PEER_CONNECTION_FAILED: case PEER_CONNECTION_CLOSED: { g_ps.id = id; /*6. 将peer connect状态切换为PEER_CONNECTION_NEW 在peer_connection_loop中就会调用到peer_connection_state_new, offer一个SDP*/ peer_connection_create_offer(g_ps.pc); } break; default: { error = cJSON_CreateRaw(RPC_ERROR_INTERNAL_ERROR); } break; } /*7. broker发送过来的是anwer,表示对端设备的应答*/ } else if (strcmp(item->valuestring, RPC_METHOD_ANSWER) == 0) { item = cJSON_GetObjectItem(req, "params"); if (!item && !cJSON_IsString(item)) { error = cJSON_CreateRaw(RPC_ERROR_INVALID_PARAMS); LOGW("Cannot find params"); break; } /*8. 收到对端应答后,解析对端anser的SDP内容,设置远端的描述信息*/ if (state == PEER_CONNECTION_NEW) { peer_connection_set_remote_description(g_ps.pc, item->valuestring); result = cJSON_CreateString(""); } } else if (strcmp(item->valuestring, RPC_METHOD_STATE) == 0) { result = cJSON_CreateString(peer_connection_state_to_string(state)); } else if (strcmp(item->valuestring, RPC_METHOD_CLOSE) == 0) { peer_connection_close(g_ps.pc); result = cJSON_CreateString(""); } else { error = cJSON_CreateRaw(RPC_ERROR_METHOD_NOT_FOUND); LOGW("Unsupport method"); } } while (0); /*9. 发布消息,发布什么内容??*/ if (result || error) { res = cJSON_CreateObject(); cJSON_AddStringToObject(res, "jsonrpc", RPC_VERSION); cJSON_AddNumberToObject(res, "id", id); if (result) { cJSON_AddItemToObject(res, "result", result); } else if (error) { cJSON_AddItemToObject(res, "error", error); } payload = cJSON_PrintUnformatted(res); if (payload) { peer_signaling_mqtt_publish(&g_ps.mqtt_ctx, payload); free(payload); } cJSON_Delete(res); } if (req) { cJSON_Delete(req); } } Offer SDP 当对端Client B设备通过信令服务器发起连接时,信令服务器的MQTT broker发送publish给当前Client A设备,接收到MQTT 的publish后,解析到method为offer,即将peer的状态切换为PEER_CONNECTION_NEW,继而peer_connection_loop中状态切换为peer_connection_state_new。 static void peer_connection_state_new(PeerConnection* pc, DtlsSrtpRole role, int isOfferer) { char* description = (char*)pc->temp_buf; memset(pc->temp_buf, 0, sizeof(pc->temp_buf)); dtls_srtp_reset_session(&pc->dtls_srtp); /*dtls srtp 初始化,在后续的章节再进行描述。*/ dtls_srtp_init(&pc->dtls_srtp, role, pc); pc->dtls_srtp.udp_recv = peer_connection_dtls_srtp_recv; pc->dtls_srtp.udp_send = peer_connection_dtls_srtp_send; pc->sctp.connected = 0; if (isOfferer) { agent_clear_candidates(&pc->agent); pc->agent.mode = AGENT_MODE_CONTROLLING; } else { pc->agent.mode = AGENT_MODE_CONTROLLED; } /*1. 通过stun传输获取candidate信息*/ agent_gather_candidate(&pc->agent, NULL, NULL, NULL); // host address for (int i = 0; i < sizeof(pc->config.ice_servers) / sizeof(pc->config.ice_servers[0]); ++i) { if (pc->config.ice_servers[i].urls) { LOGI("ice server: %s", pc->config.ice_servers[i].urls); agent_gather_candidate(&pc->agent, pc->config.ice_servers[i].urls, pc->config.ice_servers[i].username, pc->config.ice_servers[i].credential); } } /*将candidate信息转化为SDP格式?*/ agent_get_local_description(&pc->agent, description, sizeof(pc->temp_buf)); memset(&pc->local_sdp, 0, sizeof(pc->local_sdp)); // TODO: check if we have video or audio codecs /*创建一个SDP会话*/ sdp_create(&pc->local_sdp, pc->config.video_codec != CODEC_NONE, pc->config.audio_codec != CODEC_NONE, pc->config.datachannel); /*填充其他的sdp信息*/ if (pc->config.video_codec == CODEC_H264) { sdp_append_h264(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); } switch (pc->config.audio_codec) { case CODEC_PCMA: sdp_append_pcma(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); break; case CODEC_PCMU: sdp_append_pcmu(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); break; case CODEC_OPUS: sdp_append_opus(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); default: break; } if (pc->config.datachannel) { sdp_append_datachannel(&pc->local_sdp); sdp_append(&pc->local_sdp, "a=fingerprint:sha-256 %s", pc->dtls_srtp.local_fingerprint); sdp_append(&pc->local_sdp, peer_connection_dtls_role_setup_value(role)); strcat(pc->local_sdp.content, description); } pc->b_local_description_created = 1; /*将SDP信息发送出去。*/ if (pc->onicecandidate) { pc->onicecandidate(pc->local_sdp.content, pc->config.user_data); } } anser SDP peer_signaling_on_pub_event strcmp(item->valuestring, RPC_METHOD_ANSWER peer_connection_set_remote_description(g_ps.pc, item->valuestring); 设备收到publish消息后,解析jsion判断出method是anser,调用peer_connection_set_remote_description进行处理。 void peer_connection_set_remote_description(PeerConnection* pc, const char* sdp_text) { char* start = (char*)sdp_text; char* line = NULL; char buf[256]; char* val_start = NULL; uint32_t* ssrc = NULL; DtlsSrtpRole role = DTLS_SRTP_ROLE_SERVER; int is_update = 0; Agent* agent = &pc->agent; /*1. 解析SDP会话内容*/ while ((line = strstr(start, "\n"))) { line = strstr(start, "\n"); strncpy(buf, start, line - start); buf[line - start] = '\0'; if (strstr(buf, "a=setup:passive")) { role = DTLS_SRTP_ROLE_CLIENT; } if (strstr(buf, "a=fingerprint")) { strncpy(pc->dtls_srtp.remote_fingerprint, buf + 22, DTLS_SRTP_FINGERPRINT_LENGTH); } if (strstr(buf, "a=ice-ufrag") && strlen(agent->remote_ufrag) != 0 && (strncmp(buf + strlen("a=ice-ufrag:"), agent->remote_ufrag, strlen(agent->remote_ufrag)) == 0)) { is_update = 1; } if (strstr(buf, "m=video")) { ssrc = &pc->remote_vssrc; } else if (strstr(buf, "m=audio")) { ssrc = &pc->remote_assrc; } if ((val_start = strstr(buf, "a=ssrc:")) && ssrc) { *ssrc = strtoul(val_start + 7, NULL, 10); LOGD("SSRC: %" PRIu32, *ssrc); } start = line + 2; } if (is_update) { return; } if (!pc->b_local_description_created) { peer_connection_state_new(pc, role, 0); } /*2. 设置远程的描述信息,主要是填充PeerConnection中agent的信息, 这个agent信息存储了本地和远端的icecandicate*/ agent_set_remote_description(&pc->agent, (char*)sdp_text); /*3. 将peer connect状态切换为checking*/ STATE_CHANGED(pc, PEER_CONNECTION_CHECKING); } 测试P2P通路 收到对端的anser SDP后,解析得到了ICE candidate,那么将peer connect切换为checking状态,先测试一下P2P的通路。 int agent_connectivity_check(Agent* agent) { char addr_string[ADDRSTRLEN]; uint8_t buf[1400]; StunMessage msg; if (agent->nominated_pair->state != ICE_CANDIDATE_STATE_INPROGRESS) { LOGI("nominated pair is not in progress"); return -1; } memset(&msg, 0, sizeof(msg)); /*1. 使用对端的ip地址发送数据*/ if (agent->nominated_pair->conncheck % AGENT_CONNCHECK_PERIOD == 0) { addr_to_string(&agent->nominated_pair->remote->addr, addr_string, sizeof(addr_string)); LOGD("send binding request to remote ip: %s, port: %d", addr_string, agent->nominated_pair->remote->addr.port); agent_create_binding_request(agent, &msg); agent_socket_send(agent, &agent->nominated_pair->remote->addr, msg.buf, msg.size); } /*2. 接收对端的反馈数据,如果返回正确,表示链路连通正常*/ agent_recv(agent, buf, sizeof(buf)); if (agent->nominated_pair->state == ICE_CANDIDATE_STATE_SUCCEEDED) { agent->selected_pair = agent->nominated_pair; return 0; } return -1; } 测试P2P链路正常后,就将Peer Connection状态切换为PEER_CONNECTION_CONNECTED,接下来就是正式的P2P链路创建连接了。 P2P通信 通过上面信令的建立,P2P链路已经建立了连接,接下来就是创建P2P的通信链路了,P2P的传输使用的是SRTP链路。 初始化 在offer SDP的时候,函数peer_connection_state_new中会调用dtls_srtp_init进行初始化。 int dtls_srtp_init(DtlsSrtp* dtls_srtp, DtlsSrtpRole role, void* user_data) { static const mbedtls_ssl_srtp_profile default_profiles[] = { MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_80, MBEDTLS_TLS_SRTP_AES128_CM_HMAC_SHA1_32, MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_80, MBEDTLS_TLS_SRTP_NULL_HMAC_SHA1_32, MBEDTLS_TLS_SRTP_UNSET}; /*1. 设置srtp的角色,目前是做server,并更新为INIT状态,并设置UDP收发的回调函数。*/ dtls_srtp->role = role; dtls_srtp->state = DTLS_SRTP_STATE_INIT; dtls_srtp->user_data = user_data; dtls_srtp->udp_send = dtls_srtp_udp_send; dtls_srtp->udp_recv = dtls_srtp_udp_recv; /*2.初始化 MBEDTLS 相关结构体*/ mbedtls_ssl_config_init(&dtls_srtp->conf); //初始化 ssl_config 结构体,这是 SSL 配置的核心,包含加密协议、证书、密钥等信息 mbedtls_ssl_init(&dtls_srtp->ssl); //初始化 ssl 结构体,表示一个 SSL 会话上下文。 mbedtls_x509_crt_init(&dtls_srtp->cert); //初始化 X.509 证书结构,用于存储公钥证书。 mbedtls_pk_init(&dtls_srtp->pkey); //初始化公钥对象结构,用于存储私钥。 mbedtls_entropy_init(&dtls_srtp->entropy); //初始化熵源,熵源是生成随机数所需的基础。 mbedtls_ctr_drbg_init(&dtls_srtp->ctr_drbg); //初始化 CTR_DRBG(Counter-based Deterministic Random Byte Generator), //它是用于生成随机数的伪随机数生成器。 /*3. 设置打印等级*/ #if CONFIG_MBEDTLS_DEBUG mbedtls_debug_set_threshold(3); mbedtls_ssl_conf_dbg(&dtls_srtp->conf, dtls_srtp_debug, NULL); #endif /*4.生成一个自签名证书。这个证书将在 DTLS 握手期间用于身份验证*/ dtls_srtp_selfsign_cert(dtls_srtp); /*5. 配置证书验证和认证模式*/ mbedtls_ssl_conf_verify(&dtls_srtp->conf, dtls_srtp_cert_verify, NULL); //配置证书验证回调函数 dtls_srtp_cert_verify,用来验证远端证书的有效性。 mbedtls_ssl_conf_authmode(&dtls_srtp->conf, MBEDTLS_SSL_VERIFY_REQUIRED); //设置身份验证模式为MBEDTLS_SSL_VERIFY_REQUIRED,即客户端必须提供有效证书进行验证(服务器验证客户端的证书)。 /*6. 配置证书链和私钥*/ mbedtls_ssl_conf_ca_chain(&dtls_srtp->conf, &dtls_srtp->cert, NULL); //配置服务器的根证书链,这里使用dtls_srtp->cert 作为证书链的起始证书。 mbedtls_ssl_conf_own_cert(&dtls_srtp->conf, &dtls_srtp->cert, &dtls_srtp->pkey); //配置本地证书和私钥。服务器和客户端都需要自己的证书和私钥来进行加密通信。 /*7. 配置随机数生成器和超时*/ mbedtls_ssl_conf_rng(&dtls_srtp->conf, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); //配置随机数生成器,使用 mbedtls_ctr_drbg_random 和 dtls_srtp->ctr_drbg 作为随机数源。 mbedtls_ssl_conf_read_timeout(&dtls_srtp->conf, 1000); //设置读取超时时间为 1000 毫秒,意味着在等待数据时,如果超过 1000 毫秒没有接收到数据,则超时。 /*8. 如果是服务器 (DTLS_SRTP_ROLE_SERVER),则配置为 DTLS 服务器模式,并设置用 于防止 DoS 攻击的 DTLS cookies(cookie 是用于防止重放攻击的)。 如果角色是客户端,则直接配置为 DTLS 客户端模式。这里是做服务端*/ if (dtls_srtp->role == DTLS_SRTP_ROLE_SERVER) { mbedtls_ssl_config_defaults(&dtls_srtp->conf, MBEDTLS_SSL_IS_SERVER, MBEDTLS_SSL_TRANSPORT_DATAGRAM, MBEDTLS_SSL_PRESET_DEFAULT); mbedtls_ssl_cookie_init(&dtls_srtp->cookie_ctx); mbedtls_ssl_cookie_setup(&dtls_srtp->cookie_ctx, mbedtls_ctr_drbg_random, &dtls_srtp->ctr_drbg); mbedtls_ssl_conf_dtls_cookies(&dtls_srtp->conf, mbedtls_ssl_cookie_write, mbedtls_ssl_cookie_check, &dtls_srtp->cookie_ctx); } else { mbedtls_ssl_config_defaults(&dtls_srtp->conf, MBEDTLS_SSL_IS_CLIENT, MBEDTLS_SSL_TRANSPORT_DATAGRAM, MBEDTLS_SSL_PRESET_DEFAULT); } /*10. 生成并打印本地指纹, 用于生成本地证书的指纹(一个哈希值),用于标识证书。*/ dtls_srtp_x509_digest(&dtls_srtp->cert, dtls_srtp->local_fingerprint); LOGD("local fingerprint: %s", dtls_srtp->local_fingerprint); /*11. 配置 DTLS-SRTP 的保护配置文件,default_profiles 是预定义的安全配置, 包含 SRTP 协议所支持的加密算法、密钥长度等信息。*/ mbedtls_ssl_conf_dtls_srtp_protection_profiles(&dtls_srtp->conf, default_profiles); /*12. 配置 SRTP MKI(主密钥标识符)的支持情况。在这里,设置为不支持 MKI。*/ mbedtls_ssl_conf_srtp_mki_value_supported(&dtls_srtp->conf, MBEDTLS_SSL_DTLS_SRTP_MKI_UNSUPPORTED); /*13. 禁用客户端证书请求中的 CA 列表。*/ mbedtls_ssl_conf_cert_req_ca_list(&dtls_srtp->conf, MBEDTLS_SSL_CERT_REQ_CA_LIST_DISABLED); /*14. 完成所有配置后,调用 mbedtls_ssl_setup 初始化 ssl 上下文并将配置应用于 当前的 DTLS 会话。*/ mbedtls_ssl_setup(&dtls_srtp->ssl, &dtls_srtp->conf); return 0; } 创建 case PEER_CONNECTION_CONNECTED: /*1. 建立DTLS SRTP链接*/ if (dtls_srtp_handshake(&pc->dtls_srtp, NULL) == 0) { LOGD("DTLS-SRTP handshake done"); /*2. 建立sctp 链接*/ if (pc->config.datachannel) { LOGI("SCTP create socket"); sctp_create_socket(&pc->sctp, &pc->dtls_srtp); pc->sctp.userdata = pc->config.user_data; } STATE_CHANGED(pc, PEER_CONNECTION_COMPLETED); } 主要是创建DTLS_SRTP和SCTP两条链路。 DTSL_SRTP dtls_srtp_handshake_server() { /* 循环发起*/ while(1) { mbedtls_ssl_session_reset(&dtls_srtp->ssl); mbedtls_ssl_set_client_transport_id(&dtls_srtp->ssl, client_ip, sizeof(client_ip)); /* 发起握手*/ ret = dtls_srtp_do_handshake(dtls_srtp); if (ret == MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED) { LOGD("DTLS hello verification requested"); } else if (ret != 0) { LOGE("failed! mbedtls_ssl_handshake returned -0x%.4x", (unsigned int)-ret); break; } else { break; } } } 判断返回的错误是MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED,会进行再次循环发起。 static int dtls_srtp_do_handshake(DtlsSrtp* dtls_srtp) { int ret; /* timer 是 mbedtls_timing_delay_context 类型的一个静态变量,用于管理 定时器(通常是控制超时的机制)。这个定时器在 DTLS 协议中用于处理超时等操作。*/ static mbedtls_timing_delay_context timer; /*定时器是 DTLS 握手中用来处理超时的一个重要机制,因为 DTLS 是基于UDP协议的, 而 UDP 本身不保证数据的可靠传输,因此需要在握手过程中手动管理超时。*/ mbedtls_ssl_set_timer_cb(&dtls_srtp->ssl, &timer, mbedtls_timing_set_delay, mbedtls_timing_get_delay); #if CONFIG_MBEDTLS_2_X /*回调函数会在 DTLS 握手完成后用于派发加密密钥。*/ mbedtls_ssl_conf_export_keys_ext_cb(&dtls_srtp->conf, dtls_srtp_key_derivation_cb, dtls_srtp); #else mbedtls_ssl_set_export_keys_cb(&dtls_srtp->ssl, dtls_srtp_key_derivation_cb, dtls_srtp); #endif /*用于设置 DTLS 上下文的 BIO(Basic Input/Output)回调函数,这些回调函数 定义了数据的发送和接收方式。回调函数在dtls_srtp_init进行初始化的。*/ mbedtls_ssl_set_bio(&dtls_srtp->ssl, dtls_srtp, dtls_srtp->udp_send, dtls_srtp->udp_recv, NULL); /*执行 DTLS 握手,直到握手成功或发生错误。*/ do { ret = mbedtls_ssl_handshake(&dtls_srtp->ssl); } while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE); return ret; } SSL的握手 /* Main handshake loop */ while (ssl->state != MBEDTLS_SSL_HANDSHAKE_OVER) { ret = mbedtls_ssl_handshake_step(ssl); if (ret != 0) { break; } } 主要就是调用mbedtls_ssl_handshake_step,下面只列出了服务端的代码示例。 int mbedtls_ssl_handshake_step(mbedtls_ssl_context *ssl) { int ret = MBEDTLS_ERR_ERROR_CORRUPTION_DETECTED; ret = ssl_prepare_handshake_step(ssl); if (ret != 0) { return ret; } ret = mbedtls_ssl_handle_pending_alert(ssl); if (ret != 0) { goto cleanup; } #if defined(MBEDTLS_SSL_SRV_C) if (ssl->conf->endpoint == MBEDTLS_SSL_IS_SERVER) { /*调用mbedtls_ssl_handshake_server_step进行握手管理*/ if (mbedtls_ssl_conf_is_tls12_only(ssl->conf)) { ret = mbedtls_ssl_handshake_server_step(ssl); } } #endif if (ret != 0) { /* handshake_step return error. And it is same * with alert_reason. */ if (ssl->send_alert) { ret = mbedtls_ssl_handle_pending_alert(ssl); goto cleanup; } } cleanup: return ret; } 主要是接着调用mbedtls_ssl_handshake_server_step进行处理。 int mbedtls_ssl_handshake_server_step(mbedtls_ssl_context *ssl) { int ret = 0; MBEDTLS_SSL_DEBUG_MSG(2, ("server state: %d", ssl->state)); switch (ssl->state) { case MBEDTLS_SSL_HELLO_REQUEST: ssl->state = MBEDTLS_SSL_CLIENT_HELLO; break; /* * <== ClientHello */ case MBEDTLS_SSL_CLIENT_HELLO: ret = ssl_parse_client_hello(ssl); break; #if defined(MBEDTLS_SSL_PROTO_DTLS) case MBEDTLS_SSL_SERVER_HELLO_VERIFY_REQUEST_SENT: return MBEDTLS_ERR_SSL_HELLO_VERIFY_REQUIRED; #endif /* * ==> ServerHello * Certificate * ( ServerKeyExchange ) * ( CertificateRequest ) * ServerHelloDone */ case MBEDTLS_SSL_SERVER_HELLO: ret = ssl_write_server_hello(ssl); break; case MBEDTLS_SSL_SERVER_CERTIFICATE: ret = mbedtls_ssl_write_certificate(ssl); break; case MBEDTLS_SSL_SERVER_KEY_EXCHANGE: ret = ssl_write_server_key_exchange(ssl); break; case MBEDTLS_SSL_CERTIFICATE_REQUEST: ret = ssl_write_certificate_request(ssl); break; case MBEDTLS_SSL_SERVER_HELLO_DONE: ret = ssl_write_server_hello_done(ssl); /* * <== ( Certificate/Alert ) * ClientKeyExchange * ( CertificateVerify ) * ChangeCipherSpec * Finished */ case MBEDTLS_SSL_CLIENT_CERTIFICATE: ret = mbedtls_ssl_parse_certificate(ssl); break; case MBEDTLS_SSL_CLIENT_KEY_EXCHANGE: ret = ssl_parse_client_key_exchange(ssl); break; case MBEDTLS_SSL_CERTIFICATE_VERIFY: ret = ssl_parse_certificate_verify(ssl); break; case MBEDTLS_SSL_CLIENT_CHANGE_CIPHER_SPEC: ret = mbedtls_ssl_parse_change_cipher_spec(ssl); break; case MBEDTLS_SSL_CLIENT_FINISHED: ret = mbedtls_ssl_parse_finished(ssl); break; /* * ==> ( NewSessionTicket ) * ChangeCipherSpec * Finished */ case MBEDTLS_SSL_SERVER_CHANGE_CIPHER_SPEC: #if defined(MBEDTLS_SSL_SESSION_TICKETS) if (ssl->handshake->new_session_ticket != 0) { ret = ssl_write_new_session_ticket(ssl); } else #endif ret = mbedtls_ssl_write_change_cipher_spec(ssl); break; case MBEDTLS_SSL_SERVER_FINISHED: ret = mbedtls_ssl_write_finished(ssl); break; case MBEDTLS_SSL_FLUSH_BUFFERS: MBEDTLS_SSL_DEBUG_MSG(2, ("handshake: done")); ssl->state = MBEDTLS_SSL_HANDSHAKE_WRAPUP; break; case MBEDTLS_SSL_HANDSHAKE_WRAPUP: mbedtls_ssl_handshake_wrapup(ssl); break; default: MBEDTLS_SSL_DEBUG_MSG(1, ("invalid state %d", ssl->state)); return MBEDTLS_ERR_SSL_BAD_INPUT_DATA; } return ret; } 主要就是针对握手的状态进行握手交互,具体这里就不阐述了,可以参考《SSL/TLS协议分析》文章。 SCTP SCTP主要使用的usrctp开源组件, 传输 while (!g_interrupted) { if (g_state == PEER_CONNECTION_COMPLETED) { curr_time = get_timestamp(); // FPS 25 if (curr_time - video_time > 40) { video_time = curr_time; if (reader_get_video_frame(buf, &size) == 0) { peer_connection_send_video(g_pc, buf, size); } } if (curr_time - audio_time > 20) { if (reader_get_audio_frame(buf, &size) == 0) { peer_connection_send_audio(g_pc, buf, size); } audio_time = curr_time; } usleep(1000); } }