从小白到入门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.rhkb.cn/news/252015.html

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

相关文章

Java SWT Composite 绘画

Java SWT Composite 绘画 1 Java SWT2 Java 图形框架 AWT、Swing、SWT、JavaFX2.1 Java AWT (Abstract Window Toolkit)2.2 Java Swing2.3 Java SWT (Standard Widget Toolkit)2.4 Java JavaFX 3 比较和总结 1 Java SWT Java SWT&#xff08;Standard Widget Toolkit&#xff…

Yarn常见问题处理

任务出现container OOM异常导致作业失败 原因 Container内存不足或者作业数据倾斜 解决方案 检查Container相关参数,判断是否设置过小(低于4GB)。如果Container小于4GB,优先考虑调大当前作业container大小,如果是Tez任务,还需要同步调整以下参数 # tez container size…

STM32--SPI通信协议(1)SPI基础知识总结

前言 I2C (Inter-Integrated Circuit)和SPI (Serial Peripheral Interface)是两种常见的串行通信协议&#xff0c;用于连接集成电路芯片之间的通信&#xff0c;选择I2C或SPI取决于具体的应用需求。如果需要较高的传输速度和简单的接口&#xff0c;可以选择SPI。如果需要连接多…

探索设计模式的魅力:从单一继承到组合模式-软件设计的演变与未来

设计模式专栏&#xff1a;http://t.csdnimg.cn/nolNS 在面对层次结构和树状数据结构的软件设计任务时&#xff0c;我们如何优雅地处理单个对象与组合对象的一致性问题&#xff1f;组合模式&#xff08;Composite Pattern&#xff09;为此提供了一种简洁高效的解决方案。通过本…

记录下ibus-libpinyin输入法的重新安装

目前的版本为: 首先把现在的ibus-libpinyin卸了 sudo apt-get --purge remove ibus-libpinyin sudo apt-get autoremove 安装教程请参考 Installation libpinyin/ibus-libpinyin Wiki GitHub yilai sudo apt install pkg-config sudo apt-get install libglib2.0-de…

CSS要点总结

一、CSS 快速入门 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>css 快速入门</title><!-- 解读1. 在 head 标签内&#xff0c;出现了 <style type"text/css"></style…

初探unity中的ECS

ECS是一种软件架构模式&#xff0c;就像MVC一样。ECS最早在游戏《守望先锋》中提及到的相关链接。ECS具体是指实体&#xff08;entity&#xff09;、 组件&#xff08;component&#xff09;和系统&#xff08;system&#xff09;&#xff1a; 实体&#xff1a;实体是一个ID&a…

从源代码看Chrome 版本号

一直以来都是用Chrome 浏览器&#xff0c;但是看到Chrome 点分4 组数据的表达方式&#xff0c;总是感觉怪怪的&#xff0c;遂深入源代码了解她的版本号具体表示的内容 chrome 浏览器中显示的版本号 源代码中的版本号标识 版本号文件位于 chrome/VERSION &#xff0c; 看到源代…

SpringBoot实战(二十六)集成SFTP

目录 一、SFTP简介二、SpringBoot 集成2.1 Maven 依赖2.2 application.yml 配置2.3 DemoController.java 接口2.4 SftpService.java2.5 DemoServiceImpl.java 实现类2.6 SftpUtils.java 工具类2.7 执行结果1&#xff09;上传文件2&#xff09;下载文件3&#xff09;重命名文件&…

全链游戏的未来趋势与Bridge Champ的创新之路

为了充分探索全链游戏的特点和趋势&#xff0c;以及Bridge Champ如何作为一个创新案例融入这一发展脉络&#xff0c;我们需要深入了解这两者之间的互动和相互影响。全链游戏&#xff0c;或完全基于区块链的游戏&#xff0c;代表了游戏行业的一个重要转型&#xff0c;它们利用区…

nginx反向代理----->微服务网关----->具体微服务

今天&#xff0c;做项目的时候做项目的时候配路由出现bug&#xff0c;特此理顺一下从nginx到微服务网关再到微服务这一过程。 nginx配置 upstream admin-gateway{server localhost:21217; }server {listen 8803;location / {root F:/develop/admin-web/;index index.html;}…

LeetCode--代码详解 2.两数相加

2.两数相加 题目 难度&#xff1a;中等 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数…

【django】建立python虚拟环境-20240205

1.确保已经安装pip3 install venv 2.新建虚拟环境 python -m venv myenv 3.安装虚拟环境的依赖包 pip install … 4.激活虚拟环境 cd myenv cd Scripts activate 激活activate.bat并进入虚拟环境 进入虚拟环境后&#xff0c;命令行前面显示&#xff08;myenv&#xff0…

AspNet web api 和mvc 过滤器差异

最近在维护老项目。定义个拦截器记录接口日志。但是发现不生效 最后发现因为继承的 ApiController不是Controller 只能用 System.Web.Http下的拦截器生效。所以现在总结归纳一下 Web Api: System.Web.Http.Filters.ActionFilterAttribute 继承该类 Mvc: System.Web.Mvc.Ac…

【Linux系统 01】Vim工具

目录 一、Vim概述 1. 文件打开方式 2. 模式切换 二、命令模式 1. 移动与跳转 2. 复制与粘贴 3. 剪切与撤销 三、编辑模式 1. 插入 2. 替换 四、末行模式 1. 保存与退出 2. 查找与替换 3. 分屏显示 4. 命令执行 一、Vim概述 1. 文件打开方式 vim 文件路径&#…

Android学习之路(29) Gradle初探

前言: 大家回想一下自己第一次接触Gradle是什么时候&#xff1f; 相信大家也都是和我一样&#xff0c;在我们打开第一个AS项目的时候&#xff0c; 发现有很多带gradle字样的文件&#xff1a;setting.gradle, build.gradle,gradle.warpper,以及在gradle文件中各种配置&#xff…

设计模式1-访问者模式

访问者模式是一种行为设计模式&#xff0c;它允许你定义在对象结构中的元素上进行操作的新操作&#xff0c;而无需修改这些元素的类。这种模式的主要思想是将算法与元素的结构分离开&#xff0c;使得可以在不修改元素结构的情况下定义新的操作。 所谓算法与元素结构分离&#x…

###C语言程序设计-----C语言学习(9)#函数基础

前言&#xff1a;感谢您的关注哦&#xff0c;我会持续更新编程相关知识&#xff0c;愿您在这里有所收获。如果有任何问题&#xff0c;欢迎沟通交流&#xff01;期待与您在学习编程的道路上共同进步。 一. 基础知识的学习 1.函数的定义 函数是一个完成特定工作的独立程序模块&…

JavaWeb之HTML-CSS --黑马笔记

什么是HTML ? 标记语言&#xff1a;由标签构成的语言。 注意&#xff1a;HTML标签都是预定义好的&#xff0c;HTML代码直接在浏览器中运行&#xff0c;HTML标签由浏览器解析。 什么是CSS ? 开发工具 VS Code --安装文档和安装包都在网盘中 链接&#xff1a;https://p…

2024年混合云:趋势和预测

混合云环境对于 DevOps 团队变得越来越重要&#xff0c;主要是因为它们能够弥合公共云资源的快速部署与私有云基础设施的安全和控制之间的差距。这种环境的混合为 DevOps 团队提供了灵活性和可扩展性&#xff0c;这对于大型企业中的持续集成和持续部署 (CI/CD) 至关重要。 在混…