WebRTC技术简介及应用场景

写在前面

本文是参考稀土掘金的文章,整理得出,版权归原作者所有!

参考链接:https://juejin.cn/book/7168418382318927880/section/7171376753263247396

WebRTC(Web Real-Time Communication) 是一项开源技术,允许浏览器和移动应用直接进行实时音视频通信数据传输,无需安装插件或第三方软件。它由 Google 发起,现已成为 W3C 和 IETF 的标准。

核心特点:

  1. 点对点(P2P)连接

    • 设备间直接通信,降低延迟,提升效率。

    • 但需通过 ICE/STUN/TURN 服务器解决 NAT 穿越问题。

  2. 无需插件

    • 原生支持主流浏览器(Chrome、Firefox、Safari 等)。

  3. 关键组件

    • MediaStream(getUserMedia):获取摄像头/麦克风数据。

    • RTCPeerConnection:建立音视频传输连接。

    • RTCDataChannel:支持任意数据(如文件、游戏指令)传输。

  4. 安全加密

    • 强制使用 SRTP(音视频加密)和 DTLS(数据加密)。

  5. 适应网络变化

    • 自动调整码率、抗丢包,适应不同网络条件。

常见应用场景:

  • 视频会议(如 Google Meet、Zoom 的网页版)

  • 在线教育、远程医疗

  • 文件共享、屏幕共享

  • 物联网设备控制

摄像头和麦克风属于用户的隐私设备,WebRTC既然成为了浏览器中音视频即时通信的W3C标准,因此必然会提供API,让有一定代码开发能力的人去调用;

注意敲黑板: 使用这些API是有前提条件的哦,首先在安全源访问,调用API才没有任何阻碍的。那什么是安全源呢?看下面思维导图(更详细的看:chrome官方文档),且记住这句话:安全源 是至少匹配以下( Scheme 、 Host 、 Port )模式之一的源

举个简单的例子:你本地开发用HTTP请求地址获取摄像头API没有问题,但是你的同事用他的电脑访问你电脑IP对应的项目地址时,摄像头调用失败,为什么呢?

因为在他的浏览器中,你的项目访问地址非HTTPS,在非HTTPS的情况下,如果IP不是localhost127.0.0.1,都不属于安全源

当然事非绝对,在特定情况下必须使用非HTTPS访问也是可以的,Chrome提供了对应的取消限制但是不太建议用(安全为上),因此我在这里就不再多余阐述。

所以经常有人问,为什么我的代码在自己浏览器中可以获取到摄像头,但是在区域网下别的电脑的浏览器中获取不到?同样的浏览器、同样的操作系统,为什么获取不到呢?原因就是上面的安全源限制。

getUserMedia 

以前的版本中我们经常使用 navigator.getUserMedia 来获取计算机的摄像头或者麦克风,但是现在这个接口废弃,变更为 navigator.mediaDevices.getUserMedia,因此后面我们均使用新的API来完成代码编写。

getUserMedia可以干什么?

意如其名,那就是获取用户层面的媒体,当你的计算机通过 USB 或者其他网络形式接入了 N 多个摄像头或虚拟设备时,都是可以通过这个 API 获取到的。 当然不仅仅是视频设备,还包括音频设备和虚拟音频设备。 获取媒体设备是最简单的操作,它还可以控制获取到媒体的分辨率,以及其他的以一些可选项。

PS:在很多云会议中,我们开会只能选择一个摄像头,这并不是只能使用一个摄像头,而是厂商针对“大多数场景中只会用到一个摄像头”而设计的;但在有些业务中,我们可能需要自己设备上的N 个摄像头(带USB摄像头)同时使用,那么如何办到呢(这个场景其实蛮多的,后面留个课后题)。因此熟知这个 API 对于解决基本的会议和其他复杂场景问题很有用。

如何使用 getUserMedia

有简单的用法,有复杂的用法。一般简易场景下,大多数 API 用默认参数就可以实现对应功能,getUserMedia也一样,直接调用不使用任何参数,则获取的就是 PC 的默认摄像头和麦克风。

但是,当我们遇到复杂一点的应用场景,比如你的电脑上自带麦克风,同时你连接了蓝牙耳机和有线耳机,那么在视频通话过程中,你如何主动选择使用哪个呢?也就是说, 在用摄像头或者麦克风之前,我们先要解决如何从 N 个摄像头或者麦克风中选择我们想要的。

要解决这个问题,我们必须先有个大体的思路(当然这个思路并不是凭空想象出来的,而是在一定的技术储备下才有的。如果你开始前没有任何思路也没关系,可以参考他人的经验),如下:

  1. 获取当前设备所有的摄像头和麦克风信息;

  1. 从所有的设备信息中遍历筛选出我们想要使用的设备;

  1. 将我们想要使用的设备以某种参数的形式传递给浏览器 API

  1. 浏览器API去执行获取的任务。

上面提到的设备以某种参数的形式传递给 API,那么这个设备必然是以参数存在的,因此这里有几个概念需要提前知道,如下:

设备分成了图中的三个大类型,每个类型都有固定的字段,比如 ID、kind、label ,而其中用于区分它们的就是kind字段中的固定值最核心的字段就是 ID,后面我们经常用的就是这个 ID。

那么,在前端如何使用 JavaScript获取到这些信息?

大家先看下面这段代码,大体上过一遍,并留意 initInnerLocalDevice函数内部执行顺序。

function handleError(error) {alert("摄像头无法正常使用,请检查是否占用或缺失")console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name);
}
/*** @author suke* device list init */
function initInnerLocalDevice(){const that  = thisvar localDevice = {audioIn:[],videoIn: [],audioOut: []}let constraints = {video:true, audio: true}if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) {console.log("浏览器不支持获取媒体设备");return;}navigator.mediaDevices.getUserMedia(constraints).then(function(stream) {stream.getTracks().forEach(trick => {trick.stop()})// List cameras and microphones.navigator.mediaDevices.enumerateDevices().then(function(devices) {devices.forEach(function(device) {let obj = {id:device.deviceId, kind:device.kind, label:device.label}if(device.kind === 'audioinput'){if(localDevice.audioIn.filter(e=>e.id === device.deviceId).length === 0){localDevice.audioIn.push(obj)}}if(device.kind === 'audiooutput'){if(localDevice.audioOut.filter(e=>e.id === device.deviceId).length === 0){localDevice.audioOut.push(obj)}}else if(device.kind === 'videoinput' ){if(localDevice.videoIn.filter(e=>e.id === device.deviceId).length === 0){localDevice.videoIn.push(obj)}}});}).catch(handleError);}).catch(handleError);}

这个代码片段的主要作用就是获取用户设备上所有的摄像头和麦克风信息,起关键作用的是enumerateDevices函数,但是在调用这个关键函数之前,getUserMedia函数出现在了这里,它的出现是用户在访问服务时直接调用用户摄像头,此时如果用户授权且同意使用设备摄像头、麦克风,那么enumerateDevices函数就能获取设备信息了,在这里getUserMedia函数可以理解为获取摄像头或者麦克风权限集合的探路函数

看下图,我将我电脑上使用enumerateDevices函数加载到的信息,根据前面提到的字段kind,将其分三类并打印到控制台。

千万不要小看现在获取到的这些信息哦,在后面视频通话或会议过程中,我们需要抉择摄像头用前置还是后置,麦克风是用蓝牙还是有线,都是离不开这些信息的。

在拿到所有的摄像头麦克风信息之后,我们需选出最终要参与视频通话的那个信息体,看上图中 VideoIn数组里面label:"eseSoft Vcam"  这个摄像头就是我想要参会的摄像头,那么我怎样指定让代码去选择这个摄像头呢?这里就涉及到了getUserMedia的约束参数constraints 。

媒体约束 constraints

在具体讲解约束参数 constraints 之前,大家先看下面这段示例代码。

let constraints = {video:true, audio: true} function handleError(error) {console.error('navigator.MediaDevices.getUserMedia error: ', error.message, error.name);}/*** 获取设备 stream* @param constraints* @returns {Promise<MediaStream>}*/async function getLocalUserMedia(constraints){return await navigator.mediaDevices.getUserMedia(constraints)}let stream = await this.getLocalUserMedia(constraints).catch(handleError);
console.log(stream)

上面的代码片段为JavaScript获取计算机摄像头和麦克风的媒体流(视频和音频流我们统称为媒体流)的一种方式,大多数情况下都是这么用的,如果电脑有摄像头、麦克风,这样获取没有任何问题,但就担心你用的时候,你的电脑上没有配摄像头或麦克风,或者有多个摄像头而你想指定其中某一个。 为了兼容更多情况,我们需要知道constraints这个参数的详细用法。

接下来我们看下这个参数在几种常见场景下的具体配置,以及为什么这样配置。

1.同时获取视频和音频输入

使用下面约束, 如果遇到计算机没有摄像头的话,你调用上述代码的过程中就会报错,因此我们在调用之前可以通过enumerateDevices返回结果主动判断有无视频输入源,没有的话,可以动态将这个参数中的 video设置为false

{ audio: true, video: true }

2.获取指定分辨率

在会议宽带足够且流媒体传输合理的情况下,无需考虑服务端压力,而需考虑客户端用户摄像头的分辨率范围,通常我们会设置一个分辨率区间。

下面展示的①约束是请求一个 1920×1080 分辨率的视频,但是还提到 min 参数,将 320×240 作为最小分辨率,因为并不是所有的网络摄像头都可以支持 1920×1080 。当请求包含一个 ideal(应用最理想的)值时,这个值有着更高的权重,意味着浏览器会先尝试找到最接近指定理想值的设定或者摄像头(如果设备拥有不止一个摄像头)。

但是,在多人会议简单架构场景中,在不改变会议稳定性的情况下,为了让更多的客户端加入,我们通常会把高分辨率主动降低到低分辨率,约束特定摄像头获取指定分辨率如下面②配置。

--------------------①:1--------------------------{audio: true,video: {width: { min: 320, ideal: 1280, max: 1920 },height: { min: 240, ideal: 720, max: 1080 }}}--------------------②:2--------------------------{audio: true,video: { width: 720, height: 480}
}

3.指定视频轨道约束:获取移动设备的前置或者后置摄像头

facingMode属性。可接受的值有:user(前置摄像头)、environment(后置摄像头);需要注意的是,这个属性在移动端可用,当我们的会议项目通过 h5 在移动端打开时,我们可以动态设置这个属性从而达到切换前后摄像头的场景。

{ audio: true, video: { facingMode: "user" } }
{ audio: true, video: { facingMode: { exact: "environment" } } }

 4.定帧速率frameRate

帧速率(你可以理解为FPS)不仅对视频质量,还对带宽有着影响,所以在我们通话过程中,如果判定网络状况不好,那么可以限制帧速率。

我们都知道,视频是通过一定速率的连续多张图像形成的,比如每秒 24 张图片才会形成一个基础流畅的视频,因此帧速率对于实时通话的质量也有影响,你可以想象成和你的游戏的FPS一个道理。

const constraints = {audio: true,video: {width:1920,height:1080,frameRate: { ideal: 10, max: 15 }}
};

实际上,通过FPS我们可以引申出来一些场合,在特定场合选择特定的FPS搭配前面的分辨率配置,以提高我们会议系统的质量,比如:

  • 屏幕分享过程中,我们应当很重视高分辨率而不是帧速率,稍微卡点也没关系;
  • 在普通会议过程中,我们应当重视的是画面的流畅,即帧速率而不是高分辨率;
  • 在开会人数多但宽带又受限的情况下,我们重视的同样是会议的流程性,同样低分辨率更适合宽带受限的多人会议;
  • ……

5.使用特定的网络摄像头或者麦克风

重点哦,我们最前面enumerateDevices函数获取到的设备集合可以派上用场了。

/*** 获取指定媒体设备id对应的媒体流* @author suke* @param videoId* @param audioId* @returns {Promise<void>}*/
async function getTargetIdStream(videoId,audioId){const constraints = {audio: {deviceId: audioId ? {exact: audioId} : undefined},video: {deviceId: videoId ? {exact: videoId} : undefined,width:1920,height:1080,frameRate: { ideal: 10, max: 15 }}};if (window.stream) {window.stream.getTracks().forEach(track => {track.stop();});}//被调用方法前面有,此处不再重复let stream = await this.getLocalUserMedia(constraints).catch(handleError);}

getDisplayMedia

我们日常开会,多数需要通过会议 App 来分享自己的屏幕,或者仅分享桌面上固定的应用程序那么在浏览器中实现视频通话,能否实现分享屏幕呢?答案是肯定的, W3C的 Screen Capture 标准中有说明,就是使用getDisplayMedia

var promise = navigator.mediaDevices.getDisplayMedia(constraints);## 获取屏幕分享
navigator.mediaDevices.getDisplayMedia(constraints).then((stream) => {/* use the stream */}).catch((err) => {/* handle the error */});

参数 Constraints

同上一个函数一样,同样需要配置constraints约束,当然这个也是可选的, 如果选择传参的话,那么参数设置如下:

getDisplayMedia({audio: true,video: true
})

但是这里的constraints配置和前面getUserMedia的约束配置是有差别的。又一个重点来了,在屏幕分享的约束中,video 是不能设置为false 的,但是可以设置指定的分辨率,如下:

getDisplayMedia({audio: true,video: {width:1920,height:1080}
})
  1. audiotrue

  2. audiofalse

 请留意上面两图的对比,当去掉音频后,第二张图少了个勾选系统音频的 radio 框。

完整案例

/*** 获取屏幕分享的媒体流* @author suke* @returns {Promise<void>}*/
async function getShareMedia(){const constraints = {video:{width:1920,height:1080},audio:false};if (window.stream) {window.stream.getTracks().forEach(track => {track.stop();});}return await navigator.mediaDevices.getDisplayMedia(constraints).catch(handleError);
}

小提示

  • 在前面的案例代码中,我们在获取系统的音频或者视频的stream之前,一般会调用以下代码,目的是清除当前标签页中没有销毁的媒体流。   
       if (window.stream) {window.stream.getTracks().forEach(track => {track.stop();});}
    

    如果不销毁,你可以看到在标签页旁边一直有个小红圈闪烁,鼠标按上去提示正在使用当前设备的摄像头,因此在后面的开发中保持好习惯:结束自己会议后或页面用完摄像头后,一般除了强制刷新,也可以调用上面代码清除正在使用的stream调用。

    好了,这节课我们我们掌握了两个最重要的 API,下节课我们开始搭建一个信令服务器,同时完成 P2P (单人对单人)的视频通话(跑代码的时候一定要记得前面提到的安全源哦)。

检测函数

githup上检测webRtc链接:Select audio and video sources

静默基础检测

function isSupportWebRtcFlag() {// 获取用户代理字符串,用于检测浏览器类型const userAgent = navigator.userAgent,isIphone = userAgent.indexOf('iPhone') > -1,isUcBrowser = userAgent.indexOf('UCBrowser') > -1,isIphoneUC = isIphone && isUcBrowser;let canIUseDataChannel = true,canIUseRTCPeer = true,canIUseGetUserMedia = false,canIUseRealTime = false;// 检测是否支持 getUserMedia(获取设备列表)if (navigator.mediaDevices&& navigator.mediaDevices.getUserMedia|| navigator.getUserMedia|| navigator.mozGetUserMedia|| navigator.mozGetUserMedia) {canIUseGetUserMedia = true;}// 检测是否支持 RTCPeerConnection (数据通道)canIUseRTCPeer = Boolean(window.RTCPeerConnection)|| Boolean(window.webkitRTCPeerConnection)|| Boolean(window.mozRTCPeerConnection)|| Boolean(window.msRTCPeerConnection)|| Boolean(window.oRTCPeerConnection);try {const o = new (window.RTCPeerConnection || window.msRTCPeerConnection || window.mozRTCPeerConnection || window.webkitRTCPeerConnection)(null);// eslint-disable-next-line no-restricted-syntaxcanIUseDataChannel = 'createDataChannel' in o;} catch (e) {console.error('尝试创建 RTCPeerConnection 对象,以检测是否支持数据通道错误,error:', e);canIUseDataChannel = false;}// 综合判断是否支持所有 WebRTC 功能canIUseRealTime = canIUseGetUserMedia && canIUseRTCPeer && canIUseDataChannel && !isIphoneUC;if (!canIUseGetUserMedia) {console.warn('webRtcUtils[isSupportWebRtcFlag] --> 不支持getUserMedia');}if (!canIUseRTCPeer) {console.warn('webRtcUtils[isSupportWebRtcFlag] --> 不支持RTCPeerConnection');}if (!canIUseDataChannel) {console.warn('webRtcUtils[isSupportWebRtcFlag] --> 不支持createDataChannel');}if (canIUseRealTime) {console.info('webRtcUtils[isSupportWebRtcFlag] --> 支持炫彩api');} else {console.warn('webRtcUtils[isSupportWebRtcFlag] --> 不支持炫彩api');}return {canIUseGetUserMedia,canIUseRTCPeer,canIUseDataChannel,canIUseRealTime};
}isSupportWebRtcFlag();

静默黑名单检测

function isSupportWebRtcSilently() {const ua = navigator.userAgent;const isMobile = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i).test(ua);// 1. 检测关键 API 是否存在const hasGetUserMedia = Boolean(navigator.mediaDevices?.getUserMedia|| navigator.getUserMedia|| navigator.webkitGetUserMedia|| navigator.mozGetUserMedia);const hasRTCPeerConnection = Boolean(window.RTCPeerConnection|| window.webkitRTCPeerConnection|| window.mozRTCPeerConnection);// 2. 检测 DataChannel 支持let hasDataChannel = false;if (hasRTCPeerConnection) {try {const pc = new (window.RTCPeerConnection || window.webkitRTCPeerConnection)({iceServers: []});// eslint-disable-next-line no-restricted-syntaxhasDataChannel = 'createDataChannel' in pc;pc.close();} catch (e) {console.error('检测 DataChannel 支持', e);hasDataChannel = false;}}// 3. 排除已知有问题的浏览器或场景const isBlockedBrowser// 排除 UC 浏览器、QQ 浏览器、MIUI 浏览器等= (/UCBrowser|QQBrowser|MiuiBrowser|Quark|baiduboxapp/i).test(ua)// iOS 第三方浏览器(如 Firefox Focus)可能限制 WebRTC|| isMobile && (/Firefox/i).test(ua) && !(/FxiOS/).test(ua);// 4. 综合判断const isSupported= hasGetUserMedia&& hasRTCPeerConnection&& hasDataChannel&& !isBlockedBrowser;const result = {isSupported,details: {hasGetUserMedia,hasRTCPeerConnection,hasDataChannel,isBlockedBrowser}};	console.info('--result--', result);return result;
}
isSupportWebRtcSilently();

精准检测 (需用户授权)

async function preciseWebRTCSupportCheck() {const result = {supportsWebRTC: false,details: {hasRTCPeerConnection: false,hasDataChannel: false,hasGetUserMedia: false,hasIceSupport: false,hasCodecSupport: { video: [], audio: [] },errors: []}};try {// 1. 检测 RTCPeerConnection 和 DataChannelconst RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;if (!RTCPeerConnection) {result.details.errors.push('RTCPeerConnection API missing');return result;}result.details.hasRTCPeerConnection = true;const pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] });result.details.hasDataChannel = 'createDataChannel' in pc;// 2. 检测 ICE 支持(网络穿透)let hasIce = false;pc.onicecandidate = (e) => {if (e.candidate && e.candidate.candidate) {hasIce = true;result.details.hasIceSupport = true;}};// 3. 检测编解码器支持(H.264/VP8/Opus)const sender = pc.addTransceiver('video');const capabilities = sender.sender.getCapabilities();result.details.hasCodecSupport.video = capabilities.codecs.filter(c => c.mimeType.includes('video'));result.details.hasCodecSupport.audio = capabilities.codecs.filter(c => c.mimeType.includes('audio'));// 4. 实际创建 Offer 以触发 ICE 收集const offer = await pc.createOffer();await pc.setLocalDescription(offer);// 等待 ICE 收集完成(最多 2 秒)await new Promise(resolve => setTimeout(resolve, 2000));pc.close();// 5. 检测 getUserMedia(需用户授权)if (navigator.mediaDevices?.getUserMedia) {try {const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });stream.getTracks().forEach(track => track.stop());result.details.hasGetUserMedia = true;} catch (e) {result.details.errors.push(`getUserMedia failed: ${e.name}`);}}// 综合判定result.supportsWebRTC = (result.details.hasRTCPeerConnection &&result.details.hasDataChannel &&result.details.hasIceSupport &&result.details.hasGetUserMedia &&result.details.hasCodecSupport.video.length > 0);} catch (e) {result.details.errors.push(`Critical error: ${e.message}`);}return result;
}

 

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

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

相关文章

Windows 图形显示驱动开发-WDDM 2.1 功能(四)

驱动程序版本控制 图形适配器或芯片集的驱动程序 DLL 和 SYS 文件必须具有正确格式的文件版本。 驱动程序信息文件 (.inf)、内核模式驱动程序 (.sys) 和用户模式驱动程序 (.dll) 文件的版本信息必须一致。 此外&#xff0c;.inf 的 [SignatureAttributes] 部分中标识为 PETru…

什么是 StarRocks?核心优势与适用场景解析

在数据量持续爆发的时代&#xff0c;企业对实时分析的需求日益迫切。例如&#xff0c;电商大促期间的交易监控、广告投放效果的即时反馈等场景&#xff0c;均要求毫秒级的响应速度。然而&#xff0c;传统工具如 Hadoop、Hive 等存在明显短板&#xff1a;复杂查询性能不足、资源…

Java基础 4.3

1.对象机制练习 public class Object03 {public static void main(String[] args) {Person a new Person();a.age 10;a.name "小明";Person b;b a;System.out.println(b.name);//小明b.age 200;b null;System.out.println(a.age);//200System.out.println(b.a…

视频设备轨迹回放平台EasyCVR综合智能化,搭建运动场体育赛事直播方案

一、背景 随着5G技术的发展&#xff0c;体育赛事直播迎来了新的高峰。无论是NBA、西甲、英超、德甲、意甲、中超还是CBA等热门赛事&#xff0c;都是值得记录和回放的精彩瞬间。对于体育迷来说&#xff0c;选择观看的平台众多&#xff0c;但是作为运营者&#xff0c;搭建一套体…

搬砖--贪心+排序的背包

a在上面b在下面->a.v-M-b.m>b.v-M-a.m->剩余率大 所以我先遍历a&#xff0c;让a在上面 这就是要考虑贪心排序的01背包 因为它有放的限制条件 #include<bits/stdc.h> using namespace std; #define N 100011 typedef long long ll; typedef pair<ll,int>…

《2024年全球DDoS攻击态势分析》

从攻击态势来看&#xff0c;2024年DDoS攻击频次继续呈增长趋势&#xff0c;2024年同步增加1.3倍&#xff1b;超大规模攻击激增&#xff0c;超800Gbps同比增长3.1倍&#xff0c;累计高达771次&#xff0c;且互联网史上最大带宽和最大包速率攻击均被刷新&#xff1b;瞬时泛洪攻击…

数据分析参考架构详解

1.数仓方法论 2. 数仓建模参考架构 3.大数据参考架构 4.数据分析参考架构

领驭科技:以微软Azure Speech技术为核心,驱动翻译耳机新时代

在全球化的今天&#xff0c;语言不再是沟通的障碍。领驭科技&#xff0c;作为微软的核心合作伙伴&#xff0c;正引领翻译耳机行业进入一个全新的发展阶段。以时空壶与贸人为例&#xff0c;这两家公司的翻译耳机产品凭借其内置的微软Azure Speech人工智能语音技术&#xff0c;为…

seaweedfs分布式文件系统

seaweedfs https://github.com/seaweedfs/seaweedfs.git go mod tidy go -o bin ./… seaweed占不支持smb服务&#xff0c;只能用fuse的方式mount到本地文件系统 weed master 默认端口&#xff1a;9333&#xff0c;支持浏览器访问 weed volume 默认端口&#xff1a;8080 weed …

说清楚单元测试

在团队中推行单元测试的时候,总是会被成员问一些问题: 这种测试无法测试数据库的SQL(或者是ORM)是否执行正确?这种测试好像没什么作用?关联的对象要怎么处理呢?…借由本篇,来全面看一看单元测试。 单元测试是软件开发中一种重要的测试方法,其核心目的是验证代码的最小…

服务器磁盘io性能监控和优化

服务器磁盘io性能监控和优化 全文-服务器磁盘io性能监控和优化 全文大纲 磁盘IO性能评价指标 IOPS&#xff1a;每秒IO请求次数&#xff0c;包括读和写吞吐量&#xff1a;每秒IO流量&#xff0c;包括读和写 磁盘IO性能监控工具 iostat&#xff1a;监控各磁盘IO性能&#xff0c…

办公设备管理系统(springboot+ssm+jsp+maven)

基于springboot的办公设备管理系统(springbootssmjspmaven) 系统功能主要有&#xff1a; 欢迎页账号管理 管理员账号管理系统账号添加密码修改 普通管理员管理 用户管理用户添加用户查询 资产类型管理资产信息管理资产档案管理资产报表

【STM32设计】基于STM32的智能门禁管理系统(指纹+密码+刷卡+蜂鸣器报警)(代码+资料+论文)

本课题为基于单片机的智能门禁系统&#xff0c;整个系统由AS608指纹识别模块&#xff0c;矩阵键盘&#xff0c;STM32F103单片机&#xff0c;OLED液晶&#xff0c;RFID识别模块&#xff0c;继电器&#xff0c;蜂鸣器等构成&#xff0c;在使用时&#xff0c;用户可以录入新的指纹…

Java学习总结-io流-字节流

io的体系&#xff1a; FlieInputStream(文件字节输入流) 是什么&#xff1a;磁盘以字节的形式输入到内存中。 由于不同格式编码长度&#xff0c;每次读取一个或几个字节&#xff0c;都有可能出现乱码。 所以官方提供了&#xff0c;一次性读入全部字节&#xff0c;以数组的形式…

玩转JUC - 如何优雅的异步处理任务

1、概述 前面我们学习了并发包中的一些核心的基础类&#xff0c;包括原子类、Lock 、以及线程间通信的一些工具类&#xff0c;相信你已经能够正确的处理线程同步的问题了&#xff0c;今天我们继续学习并发包下的工具类&#xff0c;我们本次主要学习线程池和异步计算框架相关的内…

MINIQMT学习课程Day2

如何和聚宽进行绑定交易 目前市场上的方式主要为以下三种方案&#xff1a; 1.聚宽和一创直接绑定&#xff08;现在已经被废除&#xff09; 2.通过蒋老师所提出的redis方案&#xff0c;进行交易 3.李兴果的&#xff0c;网页发送到服务器数据库&#xff0c;然后本地读取数据进行…

【AI视频】度加视频测试

目标 前边&#xff0c;大藏经用AI翻译成功了&#xff0c;语音也生成了&#xff0c;就想着生成视频了&#xff0c;然后就发现了这个宝藏工具。 先说结果&#xff1a;速度不是很快&#xff0c;出错了&#xff0c;提示也不是很清晰&#xff0c;虽然不顺利&#xff0c;但过程还是…

SAP CEO引领云端与AI转型

在现任首席执行官克里斯蒂安克莱因&#xff08;Christian Klein&#xff09;的领导下&#xff0c;德国软件巨头 SAP 正在经历一场深刻的数字化转型&#xff0c;重点是向云计算和人工智能方向发展。他提出的战略核心是“RISE with SAP”计划&#xff0c;旨在帮助客户从传统本地部…

《系统分析师-基础篇-1-6章总结》

第1章 绪论 系统分析师角色 职责&#xff1a;需求分析、系统设计、项目管理、技术协调。 能力要求&#xff1a;技术深度&#xff08;架构设计、开发方法&#xff09; 业务理解&#xff08;企业流程、行业知识&#xff09; 沟通能力。 系统开发生命周期 传统模型&#xf…

HCIP-12 中间系统到中间系统基础

HCIP-12 中间系统到中间系统基础 一、ISIS的区域 1.管理区域&#xff1a;Area ID&#xff08;基于路由器的管理区域&#xff09; 2.算法区域 骨干区域&#xff1a;由连续的L2或者L1/2路由器组成的逻辑区域 非骨干区域&#xff1a;是由连续的L1或者L1/2路由器组成的逻辑区域…