webRTC实时通信demo

news2024/11/26 23:48:56

参考文档:

https://www.jianshu.com/p/f439ce5cc0be
https://www.w3cschool.cn/socket

demo流程示意图(用户A向用户B推送视频):

网页A 服务端 网页B 获取本地视频 同步网页A的信令及ice信息 同步网页B的信令及ice信息 返回网页B的信令及ice信息 返回网页A的信令及ice信息 推送网页A的视频流给网页B(视频流不经过服务端) 结束推送 网页A 服务端 网页B

demo运行效果

由于CSDN限制了上传gif文件的大小,故整个操作流程拆分成以下几个步骤:

打开网页A获取本地视频:
在这里插入图片描述
点击呼叫交换网页的信令和ice信息并开始视频流推送:
在这里插入图片描述
点击挂断退出视频流推送:
在这里插入图片描述

为了方便展示完整的交互流程,网页A和网页B都是在同一台PC上打开,实际上演示效果和局域网内用两台PC分开打开网页A和网页B是一样的。

准备条件

  1. 网页A所在PC需要准备好外接USB摄像头;
  2. 启动https server所需的私钥和证书(可以用openSSL工具生成,如启动的是http server,则不需要)。

demo源码

创建前端工程

新建一个文件夹,然后在文件夹内执行以下命令创建前端工程:

npm init

下载依赖

参考以下package.json内容下载依赖库(参考文档中使用的socket.io为2.x版本,demo中的部分代码针对4.x版本有做适配调整,想要在本地一次运行成功,所有的依赖库版本务必与demo保持一致):

{
  "name": "webrtc",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "liqing",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.3",
    "socket.io": "^4.4.1"
  }
}

服务端

在项目根目录创建index.js(本地运行时注意修改私钥、证书的地址以及server IP),代码如下:

/*
 * @Author: liqing
 * @Date: 2022-03-29 11:18:39
 * @LastEditors: liqing
 * @LastEditTime: 2023-08-02 14:51:25
 * @Description: description
 * DEMO参考文档:https://www.jianshu.com/p/f439ce5cc0be
 * 注意socket.io的版本:
 * 如果使用4.x版本,io.sockets.adapter.rooms[room]无法获得房间信息(3.x版本以后rooms返回的是Set,不再是对象了)
 * 如果使用2.0.3版本,navigator.mediaDevices.getUserMedia不可用(未验证需要如何修改)
 */
'use strict'

var express = require('express');
var fs = require('fs');
var app = express();

const options = {
    key: fs.readFileSync('D:/my/cakey.pem'),
    cert: fs.readFileSync('D:/my/cacert.pem')
};

var http = require('https').createServer(options, app);
// var http = require('http').createServer(app);
// socket.io API地址:https://www.w3cschool.cn/socket
var io = require('socket.io')(http);

// 静态资源代理
app.use('/css', express.static('css'));
app.use('/js', express.static('js'));
app.use('/img', express.static('img'));
app.use('/module', express.static('module'));

// 路由配置
// app.get('/', function (request, response) {
//     response.sendFile(__dirname + '/index.html');
// });

app.get('/userA', function (request, response) {
    response.sendFile(__dirname + "/userA.html")
});

app.get('/userB', function (request, response) {
    response.sendFile(__dirname + "/userB.html")
});

app.get('/userC', function (request, response) {
    response.sendFile(__dirname + "/userC.html")
});

app.get('/rtsp', function (request, response) {
    response.sendFile(__dirname + "/index.html")
});

io.on('connection', function (socket) {
    console.log(`有用户加入进来 and socket.id is ${socket.id}`);
    socket.on('signal', function (message) {
        socket.to('room').emit('signal', message);
    });

    socket.on('ice', function (message) {
        socket.to('room').emit('ice', message);
    });

    socket.on('create or join', function (room) {
        // 当前使用的socket.io版本为4.4.1,原代码中io.sockets.adapter.rooms返回的已经不是对象,而是一个Set,因此原来的io.sockets.adapter.rooms[room]必定返回undefined
        var clientsInRoom = io.sockets.adapter.rooms.get(room);
        if (typeof clientsInRoom === "undefined") {
            socket.join(room);
            socket.emit('create', room, socket.id);
            console.log('caller joined');
        } else {
            socket.join(room);
            socket.to(room).emit('call');
            console.log('callee joined');
        }
    });
});

/**
 * 注意:如果定义的是http server,则在访问页面时会禁止页面调用摄像头/麦克风设备
 * 规避方案:访问chrome://flags/,找到Insecure origins treated as secure配置项,把http://192.168.0.106:8080加入例外清单
 * 本地运行时需要把下面的IP替换成本地的IP
 */
var server = http.listen(8080, '192.168.0.106', function () {
    var host = server.address().address;
    var port = server.address().port;
    console.log(`listening on:http://${host}:${port}`);
});

/**
 * 服务端代码不涉及任何webRTC内容,socket.io同步的消息不涉及音视频流
 */

https server启动所需的私钥(cakey.pem),可以保存在本地任意路径,但要注意同步修改index.js中的引用地址:

-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCt4/3uQFLgyOGa0lFD8Y6QiVALVOwj1dV0ScMwtXskw0YvBqDk
tvW/xHFftmcqHj0/J8rBTcBXnQKPW/mAedE1jObkpUdv5h0VPI/dJ/uuFm/CoZr0
cKFwzY3hOPfNxXj/1wu7RA+eEbZXy1QaGETAb4reIp94gwc500Uvf0yzSwIDAQAB
AoGBAI9RrRW0AFryVjdjhsUoD2eDNOzSBnqWoIJi1TSNLzyikXLq1KsNPMjcYNER
JkApgjNOWacurQvJBbYgiShhvpI2bvnm12cq06Yh7NeWGwlejNXUV7PpvOptPUXD
An1hCyxdBp0eKDkh+ygbnPPsJQPes8sQvhJZ0TokgivEDKtRAkEA5KllwmzABQ8C
PlCQpEcU/Ukp4WNGsd5dBzMgxV5yHqvS4oSOgr4mwl78kLFRb4aS0KqHl7q3ztmp
qOmlQHJjWQJBAMKuOdt4Aec7N6eVD6MGfjfbRW5RVjN/5ScByvKzIkc/UC/nVRMT
kCS/JQQPpVcrD8mKzohiwTARizptb04660MCQBGEvOwZYtjAXp6hk4NSgtQo79F5
xqfH7n6ntyIH61xYM67xEu4HXXbUyirXuvJ9b/AWsI66Wmy5llr/k46NdPkCQBdj
GL49x3TAz2nJZWx/PjB1nfyntsRPC/dIptnLHUYT3A01LCozgnB3qfm363PyT141
16PYwT6GDQTC2sk6GMMCQERslIy4tmWDq4P+Nf5GYV8h3ZaD0OA6GhbdfrozxhyI
KC7GI/hF8XaTAWM8U0Lw/VFVNS3C2WzuAfPFbmoAUI0=
-----END RSA PRIVATE KEY-----

https server启动所需的证书(cacert.pem),可以保存在本地任意路径,但要注意同步修改index.js中的引用地址:

-----BEGIN CERTIFICATE-----
MIICZjCCAc+gAwIBAgIUCn88IxDVmvZKqgVaCVCPioC7DccwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA3MDUwOTIyNTBaFw0yMjA4
MDQwOTIyNTBaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwgZ8wDQYJKoZIhvcNAQEB
BQADgY0AMIGJAoGBAK3j/e5AUuDI4ZrSUUPxjpCJUAtU7CPV1XRJwzC1eyTDRi8G
oOS29b/EcV+2ZyoePT8nysFNwFedAo9b+YB50TWM5uSlR2/mHRU8j90n+64Wb8Kh
mvRwoXDNjeE4983FeP/XC7tED54RtlfLVBoYRMBvit4in3iDBznTRS9/TLNLAgMB
AAGjUzBRMB0GA1UdDgQWBBTzVCK2pDw/w/OfmtAQQHXCvv9NxDAfBgNVHSMEGDAW
gBTzVCK2pDw/w/OfmtAQQHXCvv9NxDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3
DQEBCwUAA4GBAJ7jf6ZTGXy5UWgN4nsfg3R/MA/FWbacatUwLrHH5U/vP6oxFY5a
4q7Cth4ayRagU7jF2kz6zZeEL0M+6b9Ysio9DquEbYnhUAnJBRm8l51wHkH5/fwQ
GYoKQlUx8R2vM84lHn/FPZazKOuIoaxSLGwwubn5BnW6N4W+HMbtRNa8
-----END CERTIFICATE-----

客户端

在项目根目录创建userA.html,代码如下:

<!--
 * @Author: liqing
 * @Date: 2022-03-29 11:13:04
 * @LastEditors: liqing
 * @LastEditTime: 2023-06-08 10:42:57
 * @Description: description
-->
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>userA</title>
</head>

<body>
    <div class="container">
        <h1>userA</h1>
        <hr>
        <div class="video_container" align="center">
            <video id="local_video" controls="controls" autoplay muted></video>
        </div>
        <hr>
        <button id="startButton">获取本地视频</button>
        <button id="callButton">呼叫</button>
        <button id="hangupButton">挂断</button>
        <!-- <span id="data"></span> -->
        <script src="/socket.io/socket.io.js"></script>
        <!-- <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script> -->
        <script src="js/userA.js"></script>
    </div>
</body>

</html>

在项目根目录创建js文件夹,在文件夹内创建userA.js,代码如下:

/*
 * @Author: liqing
 * @Date: 2022-03-29 11:17:08
 * @LastEditors: liqing
 * @LastEditTime: 2023-06-08 10:21:12
 * @Description: description
 */
'use strict'

var localVideo = document.getElementById('local_video');

var startButton = document.getElementById('startButton');
var callButton = document.getElementById('callButton');
var hangupButton = document.getElementById('hangupButton');

var pc;
var localStream;
var socket = io.connect();

var config = {
    'iceServers': [{
        'urls': 'stun:stun.l.google.com:19302'
    }]
};

const offerOptions = {
    offerToReceiveVideo: 1,
    offerToReceiveAudio: 1
};


callButton.disabled = false;
hangupButton.disabled = true;

startButton.addEventListener('click', startAction);
callButton.addEventListener('click', callAction);
hangupButton.addEventListener('click', hangupAction);

function gotDevices(infos) {
    // document.getElementById("data").innerHTML = JSON.stringify(infos);
}

function startAction() {
    try {

        // 测试获取设备后置摄像头
        navigator.mediaDevices.enumerateDevices().then(gotDevices);
        // 关于navigator.mediaDevices.getUserMedia的定义可以参考https://developer.mozilla.org/zh-CN/docs/Web/API/MediaDevices/getUserMedia
        navigator.mediaDevices.getUserMedia({ video: true, audio: false }).then(function (mediastream) {
            // 把音视频流放入变量localStream以及localVideo这个dom中
            localStream = mediastream;
            localVideo.srcObject = mediastream;
            startButton.disabled = true;
        }).catch(function (e) {
            // 如果获取本地音视频时无外接设备(摄像头/麦克风)则会提示exception is NotFoundError: Requested device not found
            console.log(`exception is ${e}`);
            alert(`exception is ${e}`);
        });
    } catch (e) {
        alert(`startAction exception is ${e}`);
    }
}

function callAction() {
    callButton.disabled = true;
    hangupButton.disabled = false;
    // pc = new RTCPeerConnection(config);
    // 创建一个本地到远端的webRTC对象
    pc = new RTCPeerConnection();
    // 获取媒体流中的轨道信息
    let tracks = localStream.getTracks();
    // 向上面生成的webRTC对象注入轨道信息
    tracks.forEach(track => pc.addTrack(track, localStream));
    // 作为源端创建offer对象(包含源端的媒体信息和编解码信息)
    pc.createOffer(offerOptions).then(function (offer) {
        // 在webRTC对象中记录offer信息(注意记录本端信息调用的是setLocalDescription,记录对端信息调用的是setRemoteDescription)
        console.log(`offer is ${JSON.stringify(offer)}`);
        pc.setLocalDescription(offer);
        // 同步offer信息给目的端(offer对象中的SDP参数含义可以参考https://blog.csdn.net/m370809968/article/details/88195181,SDP即为信令)
        socket.emit('signal', offer);
    });
    // 当webRTC对象调用setLocalDescription方法时会抛出icecandidate事件(即触发以下监听的回调)
    // 问题:为什么调用setLocalDescription方法会抛出icecandidate事件两次(两次信息不完全相同,如端口)
    pc.addEventListener('icecandidate', function (event) {
        var iceCandidate = event.candidate;
        console.log(`iceCandidate is ${JSON.stringify(iceCandidate)}`);
        if (iceCandidate) {
            // 同步补充描述信息给目的端(通过SDP协商结果进行信息交换),描述信息包括协议、IP、端口、优先级等等信息
            // 问题:为什么这些描述信息不可以放在信令中
            socket.emit('ice', iceCandidate);
        }
    });
    // 当信令和补充信息双方同步完成后即可开始会商
}

function hangupAction() {
    localStream.getTracks().forEach(track => track.stop());
    pc.close();
    pc = null;
    hangupButton.disabled = true;
    callButton.disabled = true;
    startButton.disabled = false;
}

socket.on('create', function (room, id) {
    console.log('userA创建聊天房间');
    console.log(room + id);
});

socket.on('call', function () {
    console.log('enter call');
    callButton.disabled = false;
});

// 监听目的端同步的offer信息
socket.on('signal', function (message) {
    if (pc !== 'undefined') {
        // 在webRTC对象中记录目的端offer信息(注意记录本端信息调用的是setLocalDescription,记录对端信息调用的是setRemoteDescription)
        pc.setRemoteDescription(new RTCSessionDescription(message));
        setTimeout(function () {
            console.log(`remote answer is ${JSON.stringify(pc.remoteDescription)}`);
        }, 1000);
    }
});

socket.on('ice', function (message) {
    if (pc !== 'undefined') {
        pc.addIceCandidate(new RTCIceCandidate(message));
        console.log('become candidate');
    }
});

socket.emit('create or join', 'room');

在项目根目录创建userB.html,代码如下:

<!--
 * @Author: liqing
 * @Date: 2022-03-29 11:17:35
 * @LastEditors: liqing
 * @LastEditTime: 2022-07-29 11:45:19
 * @Description: description
-->
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>对方的视频</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
    <div class="container">
        <h1>对方的视频</h1>
        <hr>
        <div class="video_container" align="center">
            <video id="remote_video" controls autoplay></video>
        </div>
        <hr>
        <script src="/socket.io/socket.io.js"></script>
        <!-- <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script> -->
        <script src="js/userB.js"></script>
    </div>
</body>

</html>

在js文件夹内创建userB.js,代码如下:

/*
 * @Author: liqing
 * @Date: 2022-03-29 11:17:53
 * @LastEditors: liqing
 * @LastEditTime: 2022-07-06 15:45:34
 * @Description: description
 */
'use strict'

var remoteVideo = document.getElementById('remote_video');

var socket = io.connect();

var config = {
    'iceServers': [{
        'urls': 'stun:stun.l.google.com:19302'
    }]
};

var pc;

socket.emit('create or join', 'room');

socket.on('join', function (room, id) {
    console.log('userB加入房间');
});

// 监听源端同步的offer信息
socket.on('signal', function (message) {
    console.log(`enter signal userB`);
    // pc = new RTCPeerConnection(config);
    // 创建一个本地到远端的webRTC对象,因为目的端是被动接收方,故在源端同步消息后才创建
    pc = new RTCPeerConnection();
    // 在webRTC对象中记录源端offer信息(注意记录本端信息调用的是setLocalDescription,记录对端信息调用的是setRemoteDescription)
    pc.setRemoteDescription(new RTCSessionDescription(message));
    // 作为目的端创建offer对象(包含目的端的媒体信息和编解码信息)
    pc.createAnswer().then(function (answer) {
        // 在webRTC对象中记录目的端offer信息(注意记录本端信息调用的是setLocalDescription,记录对端信息调用的是setRemoteDescription)
        pc.setLocalDescription(answer);
        // 同步offer信息给源端
        socket.emit('signal', answer);
    });

    pc.addEventListener('icecandidate', function (event) {
        var iceCandidate = event.candidate;
        if (iceCandidate) {
            console.log(`iceCandidate is ${JSON.stringify(iceCandidate)}`);
            socket.emit('ice', iceCandidate);
        }
    });

    pc.addEventListener('addstream', function (event) {
        remoteVideo.srcObject = event.stream;
    });
});

socket.on('ice', function (message) {
    console.log(`get ice message`);
    pc.addIceCandidate(new RTCIceCandidate(message));
});

运行项目

在根目录下执行以下命令启动服务端:

node index.js

服务端运行成功如下图:
服务端运行成功截图
在浏览器(chrome)中分别打开以下两个地址模拟用户A访问和用户B访问(注意本地运行时需要切换为本机IP):

https://192.168.0.106:8080/userA
https://192.168.0.106:8080/userB

在userA页面点击获取本地视频按钮,此时如果浏览器是初次调用摄像头设备,则会有如下安全提示:
在这里插入图片描述

点击允许后userA页面就可以在网页中获取到自己的视频:
在这里插入图片描述
然后在userA页面点击呼叫,在userB页面就可以播放userA的视频:
在这里插入图片描述
在userA页面点击挂断即可终止视频推送,此时userB页面会停留在userA页面推送视频的最后一帧。

注意点

  1. 服务端存在的意义仅仅是帮助两个客户端完成信令及ice信息的交换:当网页A和网页B开始视频流推送时即使停掉nodejs服务,也不会影响视频通信;
  2. demo只能让局域网内的两台PC完成视频通信,如果希望在公网的两台PC可以视频通信,则需要配置iceServers(demo中有相关代码,但在构造RTCPeerConnection对象时未使用相应的配置)。

备注

启动服务端成功,页面访问时服务端报错:
服务端报错
原因:这是由于node版本过低导致的,出现问题的node版本是8.11.1,切换为20.10.0后问题修复。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1349528.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaWeb——前端之HTMLCSS

学习视频链接&#xff1a;https://www.bilibili.com/video/BV1m84y1w7Tb/?spm_id_from333.999.0.0 一、Web开发 1. 概述 能通过浏览器访问的网站 2. Web网站的开发模式——主流是前后端分离 二、前端Web开发 1. 初识 前端编写的代码通过浏览器进行解析和渲染得到我们看到…

elasticsearch+Kibana

什么是es(elasticsearch) Elasticsearch是一个开源的分布式搜索和分析引擎&#xff0c;它构建在Apache Lucene搜索引擎库之上。它提供了一个分布式多用户能力的实时搜索和分析引擎&#xff0c;能够处理大规模的数据。Elasticsearch被广泛用于构建全文搜索、日志分析、实时应用…

灸哥问答:软件架构在软件研发中的作用

软件架构在软件开发中扮演着至关重要的角色。我们在软件研发的过程中&#xff0c;类比于建造一座公寓楼&#xff0c;而软件架构就像是盖楼之前的设计图纸&#xff0c;如果没有设计图纸就直接盖楼&#xff0c;可想而知带来的后果是什么。我对软件架构的作用表现总结如下&#xf…

iOS问题记录 - iOS 17通过NSUserDefaults设置UserAgent无效(续)

文章目录 前言开发环境问题描述问题分析1. 准备源码2. 定位源码3. 对比源码4. 分析总结 解决方案补充内容1. UserAgent的组成2. UserAgent的设置优先级 最后 前言 在上篇文章中对该问题做了一些判断和猜测&#xff0c;并给出了解决方案。不过&#xff0c;美中不足的是没有进一…

十四:爬虫-Redis基础

1、背景 随着互联网大数据时代的来临&#xff0c;传统的关系型数据库已经不能满足中大型网站日益增长的访问量和数据量。这个时候就需要一种能够快速存取数据的组件来缓解数据库服务I/O的压力&#xff0c;来解决系统性能上的瓶颈。 2、redis是什么 Redis 全称 Remote Dictio…

C/C++面向对象(OOP)编程-回调函数详解(回调函数、C/C++异步回调、函数指针)

本文主要介绍回调函数的使用&#xff0c;包括函数指针、异步回调编程、主要通过详细的例子来指导在异步编程和事件编程中如何使用回调函数来实现。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;C/C精进之路 &…

【Spring实战】16 Profile

文章目录 1. 定义2. 使用2.1 定义 Profile2.2 激活 Profile 3. 演示3.1 properties文件3.2 打印日志3.3 启动服务&验证3.4 修改 active3.5 重启服务&验证 4. 应用场景4.1 数据库配置4.2 日志配置 5. 代码详细总结 Spring 框架提供了一种强大的机制&#xff0c;允许在不…

图像分割实战-系列教程9:U2NET显著性检测实战1

&#x1f341;&#x1f341;&#x1f341;图像分割实战-系列教程 总目录 有任何问题欢迎在下面留言 本篇文章的代码运行界面均在Pycharm中进行 本篇文章配套的代码资源已经上传 U2NET显著性检测实战1 1、任务概述

第7课 利用FFmpeg将摄像头画面与麦克风数据合成后推送到rtmp服务器

上节课我们已经拿到了摄像头数据和麦克风数据&#xff0c;这节课我们来看一下如何将二者合并起来推送到rtmp服务器。推送音视频合成流到rtmp服务器地址的流程如下&#xff1a; 1.创建输出流 //初始化输出流上下文 avformat_alloc_output_context2(&outFormatCtx, NULL, &…

Java EE Servlet之Cookie 和 Session

文章目录 1. Cookie 和 Session1.1 Cookie1.2 理解会话机制 (Session)1.2.1 核心方法 2. 用户登录2.1 准备工作2.2 登录页面2.3 写一个 Servlet 处理上述登录请求2.4 实现登录后的主页 3. 总结 1. Cookie 和 Session 1.1 Cookie cookie 是 http 请求 header 中的一个属性 浏…

AI 工具探索(二)

我参加了 奇想星球 与 Datawhale 举办的 【AI办公 X 财务】第一期&#xff0c;现在这是第二次打卡&#xff0c;也即自由探索&#xff0c;我选择 Modelscope 的 Agent 探索&#xff0c;并用gpts创作助理对比&#xff01; 最近想学学小红书的运营方法&#xff0c;选择了 小红书I…

【微服务】1.虚拟机配置

创建虚拟机选经典&#xff0c;其他配置同其他讲解文档 特殊注意 如果要自己设置IP地址&#xff0c;修改/etc/sysconfig/network-scripts/ 编辑ifcfg-ens33需改ip地址 #开机加载网络配置启动网络服务 ONBOOT"yes" #分配ip的协议 none static :不自动分配&#xff0c…

axios的使用及说明

目录 1.说明 2.直接使用 3.封装使用 4.注意 1.说明 官网&#xff1a;Axios 实例 | Axios中文文档 | Axios中文网 Axios 是一个基于 promise 网络请求库&#xff0c;作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使…

FL Studio 21最新版本for mac 21.2.2.3740中文解锁版2024最新图文安装教程

FL Studio 21最新版本for mac 21.2.0.3740中文解锁版是最新强大的音乐制作工具。它可以与所有类型的音乐一起创作出令人惊叹的音乐。它提供了一个非常简单且用户友好的集成开发环境&#xff08;IDE&#xff09;来工作。这个完整的音乐工作站是由比利时公司 Image-Line 开发的。…

redis容灾的方案设计

背景 今年各个大厂的机房事故频繁&#xff0c;其中关键组件Redis是重灾区&#xff0c;本文就来看下怎么做Redis的多机房容灾 Redis多机房容灾方案 1.首先最最直观的是直接利用Redis内部的主从数据同步来进行灾备&#xff0c;但是由于Redis内部的主从实现对机房间的网络延迟等…

2024 React 后台系统 搭建学习看这一篇就够了(1)

年初&#xff0c;自己想写一篇关于 React 实战后台项目的 课程文章&#xff0c;也算是对自己 2023的前端学习做一个系统性总结&#xff0c;方便后续查阅&#xff0c;也方便自己浏览&#xff0c;还能增加自己的文笔 网上很多平台都不太稳定&#xff0c;所以用了阿里的语雀&…

声明式导航传参详情

1 动态路由传参 路由规则path ->/article/:aid 导航链接 <router-link to"/article/1">查看第一篇文章</router-link> 组件获取参数: this.$route.params.aid 如果想要所有的值&#xff0c;就用this. $route. params 注意&#xff1a;这两个必须匹配…

实战入门 K8s剩下三个模块

1.Label Label是kubernetes系统中的一个重要概念。它的作用就是在资源上添加标识&#xff0c;用来对它们进行区分和选择。 Label的特点&#xff1a; 一个Label会以key/value键值对的形式附加到各种对象上&#xff0c;如Node、Pod、Service等等 一个资源对象可以定义任意数量…

信创之国产浪潮电脑+统信UOS Linux操作系统体验10:visual studio code中调试C++程序

☞ ░ 前往老猿Python博客 ░ https://blog.csdn.net/LaoYuanPython 一、引言 老猿在CSDN的《信创之国产浪潮电脑统信UOS操作系统体验2&#xff1a;安装visual studio code和cmake搭建C开发环镜》介绍了在国产浪潮电脑统信UOS操作系统中安装visual studio code和cmake搭建C开…

2.3物理层下面的传输媒体

目录 2.3物理层下面的传输媒体2.3.1导引型传输媒体1.双绞线2.同轴电缆3.光纤 2.3.2非导引型传输媒体无线电微波通信 2.3物理层下面的传输媒体 传输媒体是数据传输系统中在发送器和接收器之间的物理通路 两大类&#xff1a; 导引型传输媒体&#xff1a;电磁波被导引沿着固体媒体…