webRTC实时通信demo

参考文档:

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]必定返回undefinedvar 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/getUserMedianavigator.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 foundconsole.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.rhkb.cn/news/230777.html

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

相关文章

SpringBoot从配置文件中获取属性的方法

方式一&#xff1a;Value 基本类型属性注入&#xff0c;直接在字段上添加Value("\${xxx.xxx}")即可&#xff0e;注意这里用的是$&#xff0c;而不是&#xff03;&#xff0c;Value注入的属性&#xff0c;一般其他属性没有关联关系。 配置文件 user:name: Manaphya…

Docker介绍、常用命令、项目部署

什么是Docker 简单说&#xff1a;Docker就是一个虚拟机&#xff0c;专业说&#xff1a;它是一个开源的容器平台。它和我们常用的VMware有很多相似的地方。 名词解释 镜像/images 由本体打包出来的文件。并不是文件本身&#xff0c;但是具有该文件的功能。举个不太贴切的例子&…

保姆级教程:从0到1搭建web自动化测试环境

之前都是在linux上安装&#xff0c;第一次在windows上配置环境&#xff0c;加上距离上次配置环境有点久了&#xff0c;竟也花了点时间。特此记录下保姆级教程&#xff0c;给初学者一个有效的参考&#xff01; 一. 环境搭建 工具清单 工具工具名版本Java开发工具包JDK1.8浏览…

2024美赛数学建模思路A题B题C题D题E题F题思路汇总 选题分析

文章目录 1 赛题思路2 美赛比赛日期和时间3 赛题类型4 美赛常见数模问题5 建模资料 1 赛题思路 (赛题出来以后第一时间在CSDN分享) https://blog.csdn.net/dc_sinor?typeblog 2 美赛比赛日期和时间 比赛开始时间&#xff1a;北京时间2024年2月2日&#xff08;周五&#xff…

Spring AOP的环境搭建、切入点表达式、通知注解

Spring AOP的实现 Spring AOP环境搭建AOP坐标依赖引入添加xml配置实现三层架构 定义切入点Pointcut("匹配规则")切入点表达式1. 执行所有的公共方法2.执行任意的set方法3.设置指定包下的任意类的任意方法 (指定包: com.svt.service)4.设置指定包及于包下的任意类的任…

SpringBoot整合ElasticSearch实现CRUD操作

本文来说下SpringBoot整合ES实现CRUD操作 文章目录 概述项目搭建ES简单的crud操作保存数据修改数据查看数据删除数据 本文小结 概述 SpringBoot支持两种技术和es交互。一种的jest&#xff0c;还有一种就是SpringData-ElasticSearch。根据引入的依赖不同而选择不同的技术。反正作…

【如何选择Mysql服务器的CPU核数及内存大小】

文章目录 &#x1f50a;博主介绍&#x1f964;本文内容&#x1f4e2;文章总结&#x1f4e5;博主目标 &#x1f50a;博主介绍 &#x1f31f;我是廖志伟&#xff0c;一名Java开发工程师、Java领域优质创作者、CSDN博客专家、51CTO专家博主、阿里云专家博主、清华大学出版社签约作…

HackTheBox - Medium - Linux - Interface

Interface Interface 是一种中等难度的 Linux 机器&#xff0c;具有“DomPDF”API 端点&#xff0c;该端点通过将“CSS”注入处理后的数据而容易受到远程命令执行的影响。“DomPDF”可以被诱骗在其字体缓存中存储带有“PHP”文件扩展名的恶意字体&#xff0c;然后可以通过从其…

LC 2397. 被列覆盖的最多行数

2397. 被列覆盖的最多行数 2397. 被列覆盖的最多行数 文章目录 2397. 被列覆盖的最多行数二进制枚举代码实现&#xff1a; 递归回溯实现代码实现 Gospers Hack代码实现 难度&#xff1a; 中等 题目大意&#xff1a; 给你一个下标从 0 开始、大小为 m x n 的二进制矩阵 matri…

Midjourney表情包制作及变现最全教程

盘点Midijourney&#xff08;AIGF&#xff09;热门赚米方法&#xff0c;总有一种适合你之AI绘画操作技巧及变现渠道剖析 【表情包制作】 首先我们对表情包制作进行详细的讲解&#xff1a; 当使用 Midjourney&#xff08;AIGF&#xff09; 绘画来制作表情包时&#xff0c;你可以…

python学完之后可以做什么,python学完可以做什么

大家好&#xff0c;小编来为大家解答以下问题&#xff0c;python学完可以做哪些工作&#xff0c;python学完之后可以做什么&#xff0c;今天让我们一起来看看吧&#xff01; Python是一种全栈的开发语言&#xff0c;你如果能学好Python&#xff0c;前端&#xff0c;后端&#x…

【Python机器学习】k近邻——k近邻分类

k-NN算法最简单的版本是只考虑一个最近邻&#xff0c;也就是想要预测的数据点最近的训练数据点&#xff0c;预测结果就是这个训练数据点的已知输出。 除了仅考虑最近邻&#xff0c;还可以考虑任意&#xff08;k个&#xff09;邻居&#xff0c;这也是k近邻算法名字的由来。在考…

Windows 安装配置 Anaconda、CUDA、cuDNN、pytorch-cuda全流程

Windows 安装配置 Anaconda、CUDA、cuDNN、pytorch-cuda全流程 1. 安装Anaconda 网址&#xff1a;https://repo.anaconda.com/archive/ 选择第一个下载即可 双击exe文件&#xff0c;按安装向导安装即可&#xff08;除安装路径自己选择外&#xff0c;其余均可按默认选项&#x…

kubeadm来快速搭建一个K8S集群

二进制搭建适合大集群&#xff0c;50台以下的主机 kubeadm更适合中下企业的业务集群 我们采用了二进制包搭建出的k8s集群&#xff0c;本次我们采用更为简单的kubeadm的方式来搭建k8s集群。 二进制的搭建更适合50台主机以上的大集群&#xff0c;kubeadm更适合中小型企业的集群…

爬虫工作量由小到大的思维转变---<第三十四章 Scrapy 的部署scrapyd+Gerapy>

前言: scrapy-redis没被部署,感觉讲起来很无力;因为实在编不出一个能让scrapy-redis发挥用武之地的案子;所以,索性直接先把分布式爬虫的部署问题给讲清楚!! 然后,曲线救国式地再在部署的服务器上,讲scrapy redis我感觉这样才好! 正文: 现在还有不少人在用scrapy web进行爬虫管…

Axure医疗-住院板块,住院患者原型预览,新增医护人员原型预览,新增病房原型预览,选择床位原型预览,主治医生原型预览,主治医生医嘱原型预览

目录 一.医疗项目原型图-----住院板块 1.1 住院板块原型预览 1.2 新增住院患者原型预览 1.3 新增医护人员原型预览 1.4 新增病房原型预览 1.5 选择床位原型预览 1.6 主治医生原型预览 1.7 主治医生医嘱原型预览 1.8 主治医生查看患者报告原型预览 1.9 护士原型预…

Ubuntu 22.04/20.04 安装 SSH

OpenSSH 是安全远程通信的重要工具&#xff0c;提供了一种安全的方式来访问和管理服务器。对于那些计划在 Ubuntu 22.04 Jammy Jellyfish 或其较旧的稳定版本的 Ubuntu 20.04 Focal Fossa 上安装 SSH 并启用它的人来说&#xff0c;了解其功能和优势至关重要。 OpenSSH的主要特…

node加速镜像源 管理工具nrm安装使用

我们在开发node.js的时候,经常会遇到某些包无法下载, 或者下载太慢, 还有需要加载我们自己是有源中的包的问题, 今天推荐给大家的这款 nrm 镜像源管理工具就是解决这类问题的. 安装 方法也很简单, 执行 npm install nrm -g 就可以安装 # 安装nrm npm install nrm -g# 添加…

普通用户用哪款电脑杀毒软件最好?

前言 各位小伙伴接触到电脑的时候&#xff0c;都一定有听过“电脑一定要安装杀毒软件”这句话。 毕竟在电脑诞生之初到今天&#xff0c;电脑木马和病毒依旧存在。 中了木马或病毒的电脑会出现什么现象&#xff1f;具体得看中了什么样的病毒。 但轻则资料泄漏、电脑瘫痪&…

CRYPTO现代密码学学习

CRYPTO现代密码学学习 RC4 加密算法RSA加密解密DES加密解密详解密钥的生成密文的生成 RC4 加密算法 简单介绍&#xff1a;RC4加密算法是一种对称加密算法&#xff0c;加密和解密使用同一个函数 初始化分为以下几个步骤 初始化存储0-255字节的Sbox(其实就是一个数组)填充key到…