从小白到入门webrtc音视频通话

0. 写在前面

先会骑车,再研究为什么这么骑,才是我认为学习技术的思路,底部付了demo例子,根据例子上面的介绍即可运行。

1. 音视频通话要用到的技术简介

  1. websocket
    • 介绍:1. 服务器可以向浏览器推送信息;2. 一次握手成功,可持续互相发送信息
    • 在音视频通话钟的作用:1. 作为音视频两个通话终端的桥梁,传递彼此上下线、网络环境等消息,因此他们都叫websocket为“信令服务器”
  2. coturn
    • 介绍:1. 包含stun服务和turn服务,stun可实现两个终端点对点语音通话;turn服务在无法点对点通话时,用作中转音视频流。
  3. webrtc
    • 介绍:1. 开源项目;2. 用于音视频实时互动、游戏、即时通讯、文件传输。

2. webrtc音视频通话开发思路

2.1. webrtc调用时序图

  1. 下图简化了B客户端创建PeerConnection,具体流程要看下面“调用时序图介绍”
    webrtc调用时序图

2.2. 调用时序图介绍

  1. 上图名词介绍
    1. client A:客户端A
    2. Stun Server:穿透服务器,也就是coturn服务器中的Stun
    3. Signal Server:信令服务器,也就是web socket搭建的服务器
    4. client B:客户端B
    5. PeerConnection(WebRtc的接口)
  2. 流程介绍
    1. A客户端先发送信息到信令服务器,信令服务器存储A客户端信息,等待其他客户端加入。
    2. B客户端再发送信息到信令服务器,信令服务器存储B客户端信息,并告知A已有新客户端加入。
    3. A客户端创建 PeerConnection(WebRtc的接口),用于获取本地的网络信息、以及保存对方的网络信息、传递音视频流、监听通话过程状态。(B客户端后面也需要创建PeerConnection)
    4. AddStreams:A客户端添加本地音视频流到PeerConnection
    5. CreateOffer:A客户端创建Offer并发送给信令服务器,由信令服务器转给B客户端。Offer中包含本地的网络信息(SDP)。
    6. CreateAnswer:B客户端收到Offer后,创建放到自己的PeerConnection中,并获取自己的网络信息(SDP),通过发送给信令服务器,由信令服务器转发给A客户端。
    7. 上面步骤进行完毕后,开始通过信令服务器(coturn),双方客户端获取自己的地址作为候选人“candidate”,然后通过websocket发送给对方。彼此拿到候选地址后,互相进行访问测试,建立链接。
    8. OnAddStream:获取对方音视频流,PeerConnection有ontrack监听器能拿到对方视频流数据。

2. 搭建WebSocket服务器

看例子中代码,使用nodejs启动

3. 搭建Coturn音视频穿透服务器

公司内网虚拟机中穿透服务器Coturn的搭建

4. 遇到的问题

后面再慢慢补吧,问题有点多

5. 例子

  1. 客户端代码使用html+js编写
  2. WebSocket代码使用js编写使用nodejs运行
  3. android端代码请下载:WebRtcAndroidDemo

5.1 客户端代码

  1. 引入adapter-latest.js文件,此文件如果过期了就自己百度找找吧。
  2. 将ws://192.168.1.60:9001/ws改为自己websocket服务所在电脑的ip地址,在本地启动则是本机地址
  3. 将iceServers中的ip改为coturn服务器所在ip地址
<html><head><title>Voice WebRTC demo</title></head><h1>WebRTC demo 1v1</h1><div id="buttons"><input id="zero-roomId" type="text" placeholder="请输入房间ID" maxlength="40"/><button id="joinBtn" type="button">加入</button><button id="leaveBtn" type="button">离开</button>    </div><div id="videos"><video id="localVideo" autoplay muted playsinline>本地窗口</video><video id="remoteVideo" autoplay playsinline>远端窗口</video></div><script src="js/main.js"></script><!-- 可直接引入在线js:https://webrtc.github.io/adapter/adapter-latest.js  --><script src="js/adapter-latest.js"></script>
</html>
'use strict';// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";var localUserId = Math.random().toString(36).substr(2); // 本地uid
var remoteUserId = -1;      // 对端
var roomId = 0;var localVideo = document.querySelector('#localVideo');
var remoteVideo = document.querySelector('#remoteVideo');
var localStream = null;
var remoteStream = null;
var pc = null;var zeroRTCEngine;function handleIceCandidate(event) {console.info("handleIceCandidate");if (event.candidate) {var candidateJson = {'label': event.candidate.sdpMLineIndex,'id': event.candidate.sdpMid,'candidate': event.candidate.candidate};var jsonMsg = {'cmd': SIGNAL_TYPE_CANDIDATE,'roomId': roomId,'uid': localUserId,'remoteUid':remoteUserId,'msg': JSON.stringify(candidateJson) };var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);console.info("handleIceCandidate message: " + message);console.info("send candidate message");} else {console.warn("End of candidates");}
}function handleRemoteStreamAdd(event) {console.info("handleRemoteStreamAdd");remoteStream = event.streams[0];// 视频轨道// let videoTracks = remoteStream.getVideoTracks()// 音频轨道// let audioTracks = remoteStream.getAudioTracks()remoteVideo.srcObject = remoteStream;
}function handleConnectionStateChange() {if(pc != null) {console.info("ConnectionState -> " + pc.connectionState);}
}function handleIceConnectionStateChange() {if(pc != null) {console.info("IceConnectionState -> " + pc.iceConnectionState);}
}function createPeerConnection() {var defaultConfiguration = {  bundlePolicy: "max-bundle",rtcpMuxPolicy: "require",iceTransportPolicy:"all",//relay 或者 all// 修改ice数组测试效果,需要进行封装iceServers: [{"urls": ["turn:192.168.1.173:3478?transport=udp","turn:192.168.1.173:3478?transport=tcp"       // 可以插入多个进行备选],"username": "lqf","credential": "123456"},{"urls": ["stun:192.168.1.173:3478"]}]};pc = new RTCPeerConnection(defaultConfiguration); // 音视频通话的核心类pc.onicecandidate = handleIceCandidate;pc.ontrack = handleRemoteStreamAdd;pc.onconnectionstatechange = handleConnectionStateChange;pc.oniceconnectionstatechange = handleIceConnectionStateChangelocalStream.getTracks().forEach((track) => pc.addTrack(track, localStream)); // 把本地流设置给RTCPeerConnection
}function createOfferAndSendMessage(session) {pc.setLocalDescription(session).then(function () {var jsonMsg = {'cmd': 'offer','roomId': roomId,'uid': localUserId,'remoteUid': remoteUserId,'msg': JSON.stringify(session)};var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);// console.info("send offer message: " + message);console.info("send offer message");}).catch(function (error) {console.error("offer setLocalDescription failed: " + error);});}function handleCreateOfferError(error) {console.error("handleCreateOfferError: " + error);
}function createAnswerAndSendMessage(session) {pc.setLocalDescription(session).then(function () {var jsonMsg = {'cmd': 'answer','roomId': roomId,'uid': localUserId,'remoteUid': remoteUserId,'msg': JSON.stringify(session)};var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);// console.info("send answer message: " + message);console.info("send answer message");}).catch(function (error) {console.error("answer setLocalDescription failed: " + error);});}function handleCreateAnswerError(error) {console.error("handleCreateAnswerError: " + error);
}var ZeroRTCEngine = function (wsUrl) {this.init(wsUrl);zeroRTCEngine = this;return this;
}ZeroRTCEngine.prototype.init = function (wsUrl) {// 设置websocket  urlthis.wsUrl = wsUrl;/** websocket对象 */this.signaling = null;
}ZeroRTCEngine.prototype.createWebsocket = function () {zeroRTCEngine = this;zeroRTCEngine.signaling = new WebSocket(this.wsUrl);zeroRTCEngine.signaling.onopen = function () {zeroRTCEngine.onOpen();}zeroRTCEngine.signaling.onmessage = function (ev) {zeroRTCEngine.onMessage(ev);}zeroRTCEngine.signaling.onerror = function (ev) {zeroRTCEngine.onError(ev);}zeroRTCEngine.signaling.onclose = function (ev) {zeroRTCEngine.onClose(ev);}
}ZeroRTCEngine.prototype.onOpen = function () {console.log("websocket打开");
}
ZeroRTCEngine.prototype.onMessage = function (event) {console.log("websocket收到信息: " + event.data);var jsonMsg = null;try {jsonMsg = JSON.parse(event.data);} catch(e) {console.warn("onMessage parse Json failed:" + e);return;}switch (jsonMsg.cmd) {case SIGNAL_TYPE_NEW_PEER:handleRemoteNewPeer(jsonMsg);break;case SIGNAL_TYPE_RESP_JOIN:handleResponseJoin(jsonMsg);break;case SIGNAL_TYPE_PEER_LEAVE:handleRemotePeerLeave(jsonMsg);break;case SIGNAL_TYPE_OFFER:handleRemoteOffer(jsonMsg);break;case SIGNAL_TYPE_ANSWER:handleRemoteAnswer(jsonMsg);break;case SIGNAL_TYPE_CANDIDATE:handleRemoteCandidate(jsonMsg);break;}
}ZeroRTCEngine.prototype.onError = function (event) {console.log("onError: " + event.data);
}ZeroRTCEngine.prototype.onClose = function (event) {console.log("onClose -> code: " + event.code + ", reason:" + EventTarget.reason);
}ZeroRTCEngine.prototype.sendMessage = function (message) {this.signaling.send(message);
}function handleResponseJoin(message) {console.info("handleResponseJoin, remoteUid: " + message.remoteUid);remoteUserId = message.remoteUid;// doOffer();
}function handleRemotePeerLeave(message) {console.info("handleRemotePeerLeave, remoteUid: " + message.remoteUid);remoteVideo.srcObject = null;if(pc != null) {pc.close();pc = null;}
}function handleRemoteNewPeer(message) {console.info("处理远端新加入链接,并发送offer, remoteUid: " + message.remoteUid);remoteUserId = message.remoteUid;doOffer();
}function handleRemoteOffer(message) {console.info("handleRemoteOffer");if(pc == null) {createPeerConnection();}var desc = JSON.parse(message.msg);pc.setRemoteDescription(desc);doAnswer();
}function handleRemoteAnswer(message) {console.info("handleRemoteAnswer");var desc = JSON.parse(message.msg);pc.setRemoteDescription(desc);
}function handleRemoteCandidate(message) {console.info("handleRemoteCandidate");var jsonMsg = message.msg;if(typeof message.msg === "string"){jsonMsg = JSON.parse(message.msg);}var candidateMsg = {'sdpMLineIndex': jsonMsg.label,'sdpMid': jsonMsg.id,'candidate': jsonMsg.candidate};var candidate = new RTCIceCandidate(candidateMsg);pc.addIceCandidate(candidate).catch(e => {console.error("addIceCandidate failed:" + e.name);});
}function doOffer() {// 创建RTCPeerConnectionif (pc == null) {createPeerConnection();}// let options = {offerToReceiveVideo:true}// pc.createOffer(options).then(createOfferAndSendMessage).catch(handleCreateOfferError);pc.createOffer().then(createOfferAndSendMessage).catch(handleCreateOfferError);
}function doAnswer() {pc.createAnswer().then(createAnswerAndSendMessage).catch(handleCreateAnswerError);
}function doJoin(roomId) {var jsonMsg = {'cmd': 'join','roomId': roomId,'uid': localUserId,};var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);console.info("doJoin message: " + message);
}function doLeave() {var jsonMsg = {'cmd': 'leave','roomId': roomId,'uid': localUserId,};var message = JSON.stringify(jsonMsg);zeroRTCEngine.sendMessage(message);console.info("doLeave message: " + message);hangup();
}function hangup() {localVideo.srcObject = null; // 0.关闭自己的本地显示remoteVideo.srcObject = null; // 1.不显示对方closeLocalStream(); // 2. 关闭本地流if(pc != null) {pc.close(); // 3.关闭RTCPeerConnectionpc = null;}
}function closeLocalStream() {if(localStream != null) {localStream.getTracks().forEach((track) => {track.stop();});}
}function openLocalStream(stream) {console.log('Open local stream');doJoin(roomId);localVideo.srcObject = stream;      // 显示画面localStream = stream;   // 保存本地流的句柄
}function initLocalStream() {navigator.mediaDevices.getUserMedia({audio: true,video: true}).then(openLocalStream).catch(function (e) {alert("getUserMedia() error: " + e.name);});
}
// zeroRTCEngine = new ZeroRTCEngine("wss://192.168.1.60:80/ws");
zeroRTCEngine = new ZeroRTCEngine("ws://192.168.1.60:9001/ws");
zeroRTCEngine.createWebsocket();document.getElementById('joinBtn').onclick = function () {roomId = document.getElementById('zero-roomId').value;if (roomId == "" || roomId == "请输入房间ID") {alert("请输入房间ID");return;}console.log("第一步:加入按钮被点击, roomId: " + roomId);// 初始化本地码流initLocalStream();
}document.getElementById('leaveBtn').onclick = function () {console.log("离开按钮被点击");doLeave();
}

5.2. 编写websocket服务

  1. 使用nodejs启动
var ws = require("nodejs-websocket")
var prort = 9001;// join 主动加入房间
// leave 主动离开房间
// new-peer 有人加入房间,通知已经在房间的人
// peer-leave 有人离开房间,通知已经在房间的人
// offer 发送offer给对端peer
// answer发送offer给对端peer
// candidate 发送candidate给对端peer
const SIGNAL_TYPE_JOIN = "join";
const SIGNAL_TYPE_RESP_JOIN = "resp-join";  // 告知加入者对方是谁
const SIGNAL_TYPE_LEAVE = "leave";
const SIGNAL_TYPE_NEW_PEER = "new-peer";
const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";
const SIGNAL_TYPE_OFFER = "offer";
const SIGNAL_TYPE_ANSWER = "answer";
const SIGNAL_TYPE_CANDIDATE = "candidate";/** ----- ZeroRTCMap ----- */
var ZeroRTCMap = function () {this._entrys = new Array();this.put = function (key, value) {if (key == null || key == undefined) {return;}var index = this._getIndex(key);if (index == -1) {var entry = new Object();entry.key = key;entry.value = value;this._entrys[this._entrys.length] = entry;} else {this._entrys[index].value = value;}};this.get = function (key) {var index = this._getIndex(key);return (index != -1) ? this._entrys[index].value : null;};this.remove = function (key) {var index = this._getIndex(key);if (index != -1) {this._entrys.splice(index, 1);}};this.clear = function () {this._entrys.length = 0;};this.contains = function (key) {var index = this._getIndex(key);return (index != -1) ? true : false;};this.size = function () {return this._entrys.length;};this.getEntrys = function () {return this._entrys;};this._getIndex = function (key) {if (key == null || key == undefined) {return -1;}var _length = this._entrys.length;for (var i = 0; i < _length; i++) {var entry = this._entrys[i];if (entry == null || entry == undefined) {continue;}if (entry.key === key) {// equalreturn i;}}return -1;};
}var roomTableMap = new ZeroRTCMap();function Client(uid, conn, roomId) {this.uid = uid;     // 用户所属的idthis.conn = conn;   // uid对应的websocket连接this.roomId = roomId;
}function handleJoin(message, conn) {var roomId = message.roomId;var uid = message.uid;console.info("uid: " + uid + "try to join room " + roomId);var roomMap = roomTableMap.get(roomId);if (roomMap == null) {roomMap = new  ZeroRTCMap();        // 如果房间没有创建,则新创建一个房间roomTableMap.put(roomId, roomMap);}if(roomMap.size() >= 2) {console.error("roomId:" + roomId + " 已经有两人存在,请使用其他房间");// 加信令通知客户端,房间已满return null;}var client = new Client(uid, conn, roomId);roomMap.put(uid, client);if(roomMap.size() > 1) {// 房间里面已经有人了,加上新进来的人,那就是>=2了,所以要通知对方var clients = roomMap.getEntrys();for(var i in clients) {var remoteUid = clients[i].key;if (remoteUid != uid) {var jsonMsg = {'cmd': SIGNAL_TYPE_NEW_PEER,'remoteUid': uid};var msg = JSON.stringify(jsonMsg);var remoteClient =roomMap.get(remoteUid);console.info("new-peer: " + msg);remoteClient.conn.sendText(msg);jsonMsg = {'cmd':SIGNAL_TYPE_RESP_JOIN,'remoteUid': remoteUid};msg = JSON.stringify(jsonMsg);console.info("resp-join: " + msg);conn.sendText(msg);}}}return client;
}function handleLeave(message) {var roomId = message.roomId;var uid = message.uid;var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.error("handleLeave can't find then roomId " + roomId);return;}if (!roomMap.contains(uid)) {console.info("uid: " + uid +" have leave roomId " + roomId);return;}console.info("uid: " + uid + " leave room " + roomId);roomMap.remove(uid);        // 删除发送者if(roomMap.size() >= 1) {var clients = roomMap.getEntrys();for(var i in clients) {var jsonMsg = {'cmd': 'peer-leave','remoteUid': uid // 谁离开就填写谁};var msg = JSON.stringify(jsonMsg);var remoteUid = clients[i].key;var remoteClient = roomMap.get(remoteUid);if(remoteClient) {console.info("notify peer:" + remoteClient.uid + ", uid:" + uid + " leave");remoteClient.conn.sendText(msg);}}}
}function handleForceLeave(client) {var roomId = client.roomId;var uid = client.uid;// 1. 先查找房间号var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.warn("handleForceLeave can't find then roomId " + roomId);return;}// 2. 判别uid是否在房间if (!roomMap.contains(uid)) {console.info("uid: " + uid +" have leave roomId " + roomId);return;}// 3.走到这一步,说明客户端没有正常离开,所以我们要执行离开程序console.info("uid: " + uid + " force leave room " + roomId);roomMap.remove(uid);        // 删除发送者if(roomMap.size() >= 1) {var clients = roomMap.getEntrys();for(var i in clients) {var jsonMsg = {'cmd': 'peer-leave','remoteUid': uid // 谁离开就填写谁};var msg = JSON.stringify(jsonMsg);var remoteUid = clients[i].key;var remoteClient = roomMap.get(remoteUid);if(remoteClient) {console.info("notify peer:" + remoteClient.uid + ", uid:" + uid + " leave");remoteClient.conn.sendText(msg);}}}
}function handleOffer(message) {var roomId = message.roomId;var uid = message.uid;var remoteUid = message.remoteUid;console.info("handleOffer uid: " + uid + "transfer  offer  to remoteUid" + remoteUid);var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.error("handleOffer can't find then roomId " + roomId);return;}if(roomMap.get(uid) == null) {console.error("handleOffer can't find then uid " + uid);return;}var remoteClient = roomMap.get(remoteUid);if(remoteClient) {var msg = JSON.stringify(message);remoteClient.conn.sendText(msg);    //把数据发送给对方} else {console.error("can't find remoteUid: " + remoteUid);}
}function handleAnswer(message) {var roomId = message.roomId;var uid = message.uid;var remoteUid = message.remoteUid;console.info("handleAnswer uid: " + uid + "transfer answer  to remoteUid" + remoteUid);var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.error("handleAnswer can't find then roomId " + roomId);return;}if(roomMap.get(uid) == null) {console.error("handleAnswer can't find then uid " + uid);return;}var remoteClient = roomMap.get(remoteUid);if(remoteClient) {var msg = JSON.stringify(message);remoteClient.conn.sendText(msg);} else {console.error("can't find remoteUid: " + remoteUid);}
}function handleCandidate(message) {var roomId = message.roomId;var uid = message.uid;var remoteUid = message.remoteUid;console.info("处理Candidate uid: " + uid + "transfer candidate  to remoteUid" + remoteUid);var roomMap = roomTableMap.get(roomId);if (roomMap == null) {console.error("handleCandidate can't find then roomId " + roomId);return;}if(roomMap.get(uid) == null) {console.error("handleCandidate can't find then uid " + uid);return;}var remoteClient = roomMap.get(remoteUid);if(remoteClient) {var msg = JSON.stringify(message);remoteClient.conn.sendText(msg);} else {console.error("can't find remoteUid: " + remoteUid);}
}
// 创建监听9001端口webSocket服务
var server = ws.createServer(function(conn){console.log("创建一个新的连接--------")conn.client = null; // 对应的客户端信息// conn.sendText("我收到你的连接了....");conn.on("text", function(str) {// console.info("recv msg:" + str);var jsonMsg = JSON.parse(str);switch (jsonMsg.cmd) {case SIGNAL_TYPE_JOIN:conn.client = handleJoin(jsonMsg, conn);break;case SIGNAL_TYPE_LEAVE:handleLeave(jsonMsg);break;case SIGNAL_TYPE_OFFER:handleOffer(jsonMsg);break;   case SIGNAL_TYPE_ANSWER:handleAnswer(jsonMsg);break; case SIGNAL_TYPE_CANDIDATE:handleCandidate(jsonMsg);break;      }});conn.on("close", function(code, reason) {console.info("连接关闭 code: " + code + ", reason: " + reason);if(conn.client != null) {// 强制让客户端从房间退出handleForceLeave(conn.client);}});conn.on("error", function(err) {console.info("监听到错误:" + err);});
}).listen(prort);

6. 参考文档

  1. WebRtc接口参考
  2. WebRTC 传输协议详解
  3. WebRTC的学习(java版本信令服务)
  4. Android webrtc实战(一)录制本地视频并播放,附带详细的基础知识讲解
  5. webSocket(wss)出现连接失败的问题解决方法
  6. 最重要的是这个,完整听了课程:2023最新Webrtc基础教程合集,涵盖所有核心内容(Nodejs+vscode+coturn

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

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

相关文章

C#,河豚算法(Blowfish Algorithm)的加密、解密源代码

Bruce Schneier 1 河豚算法&#xff08;Blowfish Algorithm&#xff09; 河豚算法&#xff08;Blowfish Algorithm&#xff09;是1993年11月由Bruce Schneier设计的一个完全开源的算法。 Blowfish算法是一个分组长度为64位、密钥长度可变的对称分组密码算法。 Blowfish算法具…

学习Android的第五天

目录 Android ConstraintLayout 约束布局 简介 ConstraintLayout 约束布局分类 1、相对定位 (Relative positioning) 2、边距 ( Margins ) 3、居中定位和偏向 ( Centering positioning and bias ) 4、环形定位 ( Circular positioning ) 5、对可见性的处理 ( Visibilit…

万物皆可播时代,我们如何把握机遇

在万物皆可播的时代&#xff0c;我们可以通过以下方式来把握机遇&#xff1a; 了解市场需求&#xff1a;通过观察和了解消费者的需求和偏好&#xff0c;发现具有潜力的市场空白。关注时尚、美妆、美食、旅游等领域的发展趋势&#xff0c;掌握最新的流行趋势&#xff0c;结合自…

春运开始,北斗卫星助力盲区来车预警提示

春运开始&#xff0c;北斗卫星助力盲区来车预警提示 近期春运开始&#xff0c;高德地图启动了2024年的“温暖回家路”服务计划&#xff0c;通过数字化服务创新保障春运出行。除了具备自学习能力的新能源导航首发亮相外&#xff0c;还重点升级了盲区会车预警服务。在山区弯道、…

❤ React18 环境搭建项目与运行(地址已经放Gitee开源)

❤ React项目搭建与运行 环境介绍 node v20.11.0 react 18.2 react-dom 18.2.0一、React环境搭建 第一种普通cra搭建 1、检查本地环境 node版本 18.17.0 检查node和npm环境 node -v npm -v 2、安装yarn npm install -g yarn yarn --version 3、创建一个新的React项目…

ABAP 笔记--内表结构不一致,无法更新数据库MODIFY和UPDATE

目录 ABAP 笔记内表结构不一致&#xff0c;无法更新数据库MODIFY和UPDATE ABAP 笔记 内表结构不一致&#xff0c;无法更新数据库 MODIFY和UPDATE 如果是使用MODIFY或者UPDATE

Live800:从客户反馈中学习与改进,塑造卓越的企业客户服务

在当今的商业环境中&#xff0c;客户反馈已经成为企业改进产品和服务&#xff0c;提升客户满意度&#xff0c;增强品牌形象的重要工具。今天将以企业客户服务为例&#xff0c;探讨如何从客户反馈中学习和改进&#xff0c;包括收集客户反馈、分析客户反馈、实施改进措施等方面。…

远程主机可能不符合 glibc 和 libstdc++ Vs Code 服务器的先决条件

vscode连接远程主机报错&#xff0c;原因官方已经公布过了&#xff0c;需要远程主机 glibc>2.28&#xff0c;所以Ubuntu18及以下版本没法再远程连接了&#xff0c;其他Linux系统执行ldd --version查看glibc版本自行判断。 解决方案建议&#xff1a; 不要再想升级glibc了 问题…

Golang GC 介绍

文章目录 0.前言1.发展史2.并发三色标记清除和混合写屏障2.1 三色标记2.2 并发标记问题2.3 屏障机制Dijkstra 插入写屏障Yuasa 删除写屏障混合写屏障 3.GC 过程4.GC 触发时机5.哪里记录了对象的三色状态&#xff1f;6.如何观察 GC&#xff1f;方式1&#xff1a;GODEBUGgctrace1…

机器学习 | 探索朴素贝叶斯算法的应用

朴素贝叶斯算法是一种基于贝叶斯定理和特征条件独立假设的分类算法。它被广泛应用于文本分类、垃圾邮件过滤、情感分析等领域&#xff0c;并且在实际应用中表现出色。 朴素贝叶斯法是基于贝叶斯定理与特征条件独立假设的分类方法&#xff1a; 1&#xff09;对于给定的待分类项r…

Architecture Lab:Part C【流水线通用原理/Y86-64的流水线实现/实现IIADDQ指令】

目录 任务描述 知识回顾 流水线通用原理 Y86-64流水线实现&#xff08;PIPE-与PIPE&#xff09; 开始实验 IIADDQ指令的添加 优化 ncopy.ys 仅用第四章知识&#xff0c;CEP11.55 8x1展开&#xff0c;CPE9.35 8x1展开2x1展开消除气泡&#xff0c;CPE8.10 流水线化通过…

算法每日一题: 使用循环数组所有元素相等的最少秒数 | 哈希

大家好&#xff0c;我是星恒&#xff0c;今天给大家带来的是一道需要感觉规律的题目&#xff0c;只要读懂题目中的规律&#xff0c;就可以做出来了 这道题用到了哈希&#xff0c;还有一个关键点比较类似循环队列 题目&#xff1a;leetcode 2808 给你一个下标从 0 开始长度为 n…

Postman发送带登录信息的请求

环境&#xff1a;win10Postman10.17.7 假设有个请求是这样的&#xff1a; RequiresPermissions("tool:add") PostMapping(value"/predict") ResponseBody /** * xxx * param seqOrderJson json格式的参数 * return */ public String predictSampleIds(Req…

svg基础(三)分组、渐变

上一篇文章简单介绍了svg常用标签及其属性,本篇主要介绍分组&#xff0c;渐变 1 分组<g> 分组容器 添加到g元素上的变换会应用到其所有的子元素上添加到g元素的属性会被其所有的子元素继承定义复杂对象&#xff0c;可通过<use>元素引用 1.1 分组 <svg>&…

EasyExcel分页上传数据

EasyExcel分页上传数据 一、实例 controller上传入口 PostMapping("/upload")ResponseBodyLog(title "导入工单", businessType BusinessType.IMPORT)public AjaxResult uploadFile(HttpServletRequest request, MultipartFile files) throws Exceptio…

Node.js+Express+Mysql服务添加环境变量

1、使用dotenv插件 1&#xff09;安装插件&#xff1a;npm install dotenv-cli --save-dev 2&#xff09;在项目根目录下添加对应的 .env 配置文件&#xff1b; // .env配置文件内容 MODEdevelopment, BASE_URLhttp://127.0.0.1:80813) 在启动命令中设置对应的加载文件&#…

springboot159基于springboot框架开发的景区民宿预约系统的设计与实现

简介 【毕设源码推荐 javaweb 项目】基于springbootvue 的 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负。 看运行截图看 第五章 第四章 获取资料方式 **项…

STM32内部Flash

目录 一、内部Flash简介 二、内部Flash构成 1. 主存储器 2. 系统存储区 3. 选项字节 三、内部Flash写入过程 1. 解锁 2. 页擦除 3. 写入数据 四、工程空间分布 某工程的ROM存储器分布映像&#xff1a; 1. 程序ROM的加载与执行空间 2. ROM空间分布表 一、内部Flash…

2024.3.28-29日ICVS-AI智能汽车产业峰会(杭州)

本次安策将携手泰雷兹一起&#xff0c;参展ICVS2024第四届AI智能汽车产业峰会(杭州)&#xff0c;2024年3月28日-29日&#xff0c;欢迎新老朋友参加和莅临27号展台交流。 随着自动驾驶汽车政策密集落地。从我国四部门联合发布《关于开展智能网联汽车准入和上路通行试点工作的通知…

【头歌·计组·自己动手画CPU】一、计算机数据表示(讲解版) 【计算机硬件系统设计】

&#x1f57a;作者&#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux &#x1f618;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 &#x1f3c7;码字不易&#xff0c;你的&#x1f44d;点赞&#x1f64c;收藏❤️关注对我真的很重要&…