P2P视频聊天技术分析
整个P2P视频过程需要知道双方的媒体类型、流和候选者,所以这里就会用到一下技术:
信令服务器socket.io
状态机
ICE服务器
WebRTC框架
媒体协商
信令服务器Socket.io
信令服务器说白了作用就是发消息的中转站,A把msg发到信令服务器,然后信令服务器把msg发给B
Socket.IO 是一个库,可在客户端和服务器之间实现低延迟、双向和基于事件的通信。
它建立在 WebSocket 协议之上,并提供额外的保证,例如回退到 HTTP 长轮询或自动重新连接。
WebSocket 是一种通信协议,它在服务器和浏览器之间提供全双工和低延迟通道。更多信息可以在这里找到。
有几种可用的 Socket.IO 服务器实现:
- JavaScript (可以在本网站上找到其文档)
- 安装步骤
- API
- 源代码
- Java: https://github.com/mrniko/netty-socketio
- Java: https://github.com/trinopoty/socket.io-server-java
- Python: https://github.com/miguelgrinberg/python-socketio
大多数主要语言的客户端实现:
- JavaScript (可以在浏览器、Node.js 或 React Native 中运行)
- 安装步骤
- API
- 源代码
- Java: https://github.com/socketio/socket.io-client-java
- C++: https://github.com/socketio/socket.io-client-cpp
- Swift: https://github.com/socketio/socket.io-client-swift
- Dart: https://github.com/rikulo/socket.io-client-dart
- Python: https://github.com/miguelgrinberg/python-socketio
- .Net: https://github.com/doghappy/socket.io-client-csharp
- Golang: https://github.com/googollee/go-socket.io
- Rust: https://github.com/1c3t3a/rust-socketio
- Kotlin: https://github.com/icerockdev/moko-socket-io
这是一个使用普通 WebSocket 的基本示例:
服务器 (基于 ws)
import { WebSocketServer } from "ws";const server = new WebSocketServer({ port: 3000 });server.on("connection", (socket) => {// 向客户端发送消息socket.send(JSON.stringify({type: "hello from server",content: [ 1, "2" ]}));// 从客户端接收消息socket.on("message", (data) => {const packet = JSON.parse(data);switch (packet.type) {case "hello from server":// ...break;}});
});
客户端
const socket = new WebSocket("ws://localhost:3000");socket.addEventListener("open", () => {// 向服务器发送消息socket.send(JSON.stringify({type: "hello from server",content: [ 3, "4" ]}));
});// 从服务器接收消息
socket.addEventListener("message", ({ data }) => {const packet = JSON.parse(data);switch (packet.type) {case "hello from server":// ...break;}
});
这是与 Socket.IO 相同的示例:
服务器
import { Server } from "socket.io";const io = new Server(3000);io.on("connection", (socket) => {// 向客户端发送消息socket.emit("hello from server", 1, "2", { 3: Buffer.from([4]) });// 从客户端接收消息socket.on("hello from server", (...args) => {// ...});
});
客户端
import { io } from "socket.io-client";const socket = io("ws://localhost:3000");// 向服务器发送消息
socket.emit("hello from server", 5, "6", { 7: Uint8Array.from([8]) });// 从服务器接收消息
socket.on("hello from server", (...args) => {// ...
});
这两个示例看起来非常相似,但实际上 Socket.IO 提供了附加功能,这些功能隐藏了在生产环境中运行基于 WebSockets 的应用程序的复杂性。 下面列出了这些功能。
但首先,让我们明确 Socket.IO 不是什么。
Socket.IO 不是什么:
Socket.IO 不是 WebSocket实现。
尽管 Socket.IO 确实在可能的情况下使用 WebSocket 进行传输,但它为每个数据包添加了额外的元数据。这就是为什么 WebSocket 客户端将无法成功连接到 Socket.IO 服务器,而 Socket.IO 客户端也将无法连接到普通 WebSocket 服务器。
// 警告:客户端将无法连接!
const socket = io("ws://echo.websocket.org");
如果您正在寻找一个普通的 WebSocket 服务器,请查看 ws 或 µWebSockets.js.
还有关于在 Node.js 核心中包含 WebSocket 服务器的讨论。在客户端,您可能对robust-websocket感兴趣。
Socket.IO 并不打算在移动应用程序的后台服务中使用。
Socket.IO 库保持与服务器的开放 TCP 连接,这可能会导致用户消耗大量电池。请为此用例使用请为此用例使用FCM等专用消息传递平台。
特点
以下是 Socket.IO 在普通 WebSockets 上提供的功能:
HTTP 长轮询回退
如果无法建立 WebSocket 连接,连接将回退到 HTTP 长轮询。
这个特性是人们在十多年前创建项目时使用 Socket.IO 的原因(!),因为浏览器对 WebSockets 的支持仍处于起步阶段。
即使现在大多数浏览器都支持 WebSockets(超过97%),它仍然是一个很棒的功能,因为我们仍然会收到来自用户的报告,这些用户无法建立 WebSocket 连接,因为他们使用了一些错误配置的代理。
自动重新连接
在某些特定情况下,服务器和客户端之间的 WebSocket 连接可能会中断,而双方都不知道链接的断开状态。
这就是为什么 Socket.IO 包含一个心跳机制,它会定期检查连接的状态。
当客户端最终断开连接时,它会以指数回退延迟自动重新连接,以免使服务器不堪重负。
数据包缓冲
当客户端断开连接时,数据包会自动缓冲,并在重新连接时发送。
更多信息在这里.
收到后的回调
Socket.IO 提供了一种方便的方式来发送事件和接收响应:
发件人
socket.emit("hello", "world", (response) => {console.log(response); // "got it"
});
接收者
socket.on("hello", (arg, callback) => {console.log(arg); // "world"callback("got it!");
});
您还可以添加超时:
socket.timeout(5000).emit("hello", "world", (err, response) => {if (err) {// 另一方未在给定延迟内确认事件} else {console.log(response); // "got it"}
});
广播
在服务器端,您可以向所有连接的客户端或客户端的子集发送事件:
// 到所有连接的客户端
io.emit("hello");// 致“news”房间中的所有连接客户端
io.to("news").emit("hello");
这在扩展到多个节点时也有效。
多路复用
命名空间允许您在单个共享连接上拆分应用程序的逻辑。例如,如果您想创建一个只有授权用户才能加入的“管理员”频道,这可能很有用。
io.on("connection", (socket) => {// 普通用户
});io.of("/admin").on("connection", (socket) => {// 管理员用户
});
常见问题
现在还需要 Socket.IO 吗?
这是一个很好的问题,因为现在几乎所有地方 都支持 WebSocket 。
话虽如此,我们相信,如果您在应用程序中使用普通的 WebSocket,您最终将需要实现 Socket.IO 中已经包含(并经过实战测试)的大部分功能,例如重新连接,确认或广播.
Socket.IO 协议的数据表大小?
socket.emit("hello", "world")
将作为单个 WebSocket 帧发送,其中包含42["hello","world"]
:
4
是 Engine.IO “消息”数据包类型2
是 Socket.IO “消息”数据包类型["hello","world"]
是JSON.stringify()
参数数组的 -ed 版本
因此,每条消息都会增加几个字节,可以通过使用自定义解析器进一步减少。
&浏览器包本身的大小是10.4 kB
(缩小和压缩)。
开始
声明:socket.io的版本不同,用法不同,这里用最新用法
第一步:使用express框架搭建服务器路由
const express = require('express'); //引入express模块
const app = express();
app.get('/', (req, res) => {res.sendFile(__dirname + '/login.html');
});
/http/
const http = require('http');//此处引用的是http,但是我们的目的是创建P2P视频,所以要开启https,所以需要拥有ssl证书,这个可以在各大云服务商免费申请(推荐华为云和阿里云)
const httpServer = http.createServer(app).listen(8888, "0.0.0.0");
https
const https = require('https');
var fs = require("fs");
var opt = {key:fs.readFileSync("./cert/ssl_server.key"),cert:fs.readFileSync("./cert/ssl_server.pem")
}//引入ssl证书
const httpsServer = http.createServer(app,opt).listen(8008, "0.0.0.0");
//这样一个服务器就搭好了,直接node xx.js就可以启动
第二步:使用Socket.io服务端
服务端直接npm install socket.io就可以
老版本///
var io = socketIo.listen(httpsServer);
/新版本///
const io = new Server(httpsServer);io.sockets.on("connection", (socket)=>{/操作内容///
})
第三步:使用Socket.io客户端
客户端可以使用CDN引入也可以下载相关的js库
<script src="/socket.io/socket.io.js"></script>
<script>var url = "120.78.xxx.xx:8008"var socket = io(url);});
</script>
这样一个客户端就已经连上一个服务端了,接下来就是相关操作
第四步:相关操作(略)
P2P加入房间流程
下图有三个客户端ABC,它们同时连接信令服务器,首先是A发起加入房间信号join,然后信令服务器就把A加入房间然后回答joined信号给A;然后B又来发起加入房间信号join,然后信令服务器就把B加入房间而且回答joined信号给B,同时给A其他人加入房间信号otherjoin。然后C就来发起加入房间的信号join,但是信令服务器会对房间人数进行控制,当房间人数等于2就给新请求加入的客户端回答full信号表示房间已满,没有把你加入。这时候A和B就在一个房间里面,接下来它们就可以在房间里面通讯。
状态机
利用状态机进行状态变换和判断
为什么要有状态机:
先考虑一个客户端在一个聊天室会有几种状态:
未加入房间前或者离开房间后(Init/Leave)
加入房间后(joined)
第二个聊天者加入后(joined_conn)
第二个聊天者离开 后(joined_unbind)
通过上面的状态可以发现,用户状态除非就是加入和离开,但是用户进进出出房间会出什么情况呢。这里就要思考用户进入聊天室和离开聊天室,会影响什么?一个聊天室至少会有一个人存在,那么这个人就是发起人,那么怎么知道当前用户是发起人呢。就是通过用户状态来确定。接下来看下面这张图,开始当前用户还未加入房间也就是处于离开状态,但加入房间后就变成joined状态,当第二个人加入就变成joined_conn状态,而相对于第二个人是不会出现joined_conn状态,所以就可以判断当前用户是不是第一个用户,也就是发起者(发起者的作用涉及媒体协商)。最后就是当第二个用户离开就会变为joined_unbind状态。
ICE框架
首先来了解一下两个客户端是怎么点对点通讯的。
第一种:自己知道host ip
第二种:用到一个STUN server,A和B都访问这个STUN server就可以拿到对方的公网ip,然后再利用信令服务器访问,任何客户端都加入了信令服务器,通过信令服务器交换信息,就可以达到NAT穿透的效果
第三种:用到一个中继服务器Relay server(TURN server)
这三种方式就是三个候选者,为什么要三种通讯方式呢,因为信令服务器想尽量不参与通讯,或者说信令服务器只想做一些简单的信息通讯。所以可知道我们音视频通讯利用的大概率就是Relay server和STUN server
现在host IP、Relay server和STUN server都集中到一个ICE服务器项目中,现在只要搭建这个服务器就可以了。
搭建stun/turn服务器步骤:
先安装依赖库:
ubuntu:
apt(-get) install build-essential
apt(-get) install openssl libssl-dev
centos:
yum install libevent-devel openssl-devel
下载4.5版本源码
wget https://github.com/coturn/coturn/archive/4.5.1.1.tar.ge
连不上github的查下资料改hosts
解压
tar -zxvf 4.5.1.1.tar.gz
进入到项目目录
cd coturn-4.5.1.1
源码安装3连
./configure
make
make install
复制配置文件
cp examoles/etc/turnserver.conf bin/turnserver.conf
修改配置文件
#服务器监听端口,3478为默认端口,可修改为自己想要的端口号
listening-port=3478
#想要监听的ip地址,云服务器则是云服务器的内网ip
listening-ip=xxx.xxx.xxx.xxx
#扩展ip地址,云服务器则是云服务器的外网ip
extenal-ip=xxx.xxx.xxx.xxx
#可以设置访问的用户及密码
user=demon:123
启动服务
turnserver -v -r 外网ip:监听端口 -a -o
验证:
https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/
host 本地连接
srflx nat映射后的结果
relay 中继服务器
WebRTC框架
RTCPeerConnection相关原理
pc = new RTCPeerConnection([config])
pc的相关能力:
媒体协商
流和轨的添加与停止
传输相关功能
统计相关功能
媒体协商
每个客户机都有自己支持的媒体格式信息,所以为了统一双方媒体支持格式,所以就想要媒体协商,这是pc的功能。
过程如下:
首先A是一个发起者(通过状态机知道),A首先Get一个offer,这个Get过程收集A自己的媒体信息和候选者(通过ICE就知道)两个信息,然后setLocalDescription这个信息,然后把这个媒体信息发给信令服务器,信令服务器发给房间内的另一个客户机B。然后B收到这个消息后就把这个消息setRemoteDescription,同时B也要收集自己的媒体信息和候选者信息两个信息setLocalDescription后,把answer发给信令服务器,信令服务器转发给A,A收到answer后也setRemoteDescription。这样双方都知道对方支持的媒体信息和候选者。
代码如下:
function mediaNegociate() {if (status === "joined_conn") {//joined_conn代表我是连接的发起人if (pc) {var options = {//要协商的内容,如音频、视频...offerToReceiveVideo:true,offerToReceiveAudio:true}pc.createOffer(options).then(getOffer).catch(handleErr);}}
}socked.on("vgetdata", (room, data)=>{console.log("vgetdata:", data);if (!data) {return ;}if (data.type === "candidata") {//拿到对方传过来的候选者信息//} else if(data.type === "offer") {//媒体协商默认有type值为offerconsole.log("get offer");pc.setRemoteDescription(new RTCSessionDescription(data));//把对方的媒体格式设置进来//查询自己的媒体格式信息并且应答给对方pc.createAnswer().then(getAnswer).catch(handleErr);} else if(data.type === "answer") {//媒体协商默认回应有type值为answerconsole.log("get answer");pc.setRemoteDescription(new RTCSessionDescription(data));//我把offer发给对方,对方回的answer。offer和answer都是有媒体格式信息。所以offer和answer不会同时存在一个客户端,第一个进来的会发offer,第二个进来的会发answer。把对方的媒体格式设置进来}});
P2P代码
html代码:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>视频聊天</title><link rel="icon" href="./2.jpg" type="image/x-icon">
</head>
<body><div align="center"><table><tr><td colspan="2"><h1 id="welcom">欢迎来到1v1的聊天室</h1><input id = "room"><button id = "enterRoom">进入房间</button><button id="leaveRoom" >离开房间</button></td></tr><tr><td><lable>本地视频</lable></td><td><label>远端视频</label></td></tr><tr><td><video id="localVideo" autoplay playsinline></video></td><td><video id="remoteVideo" autoplay playsinline></video></td></tr></table></div><script src="js/socket.io.js"></script><script src="js/videoRoom.js"></script></body>
</html>
信令服务器代码:
"use strict";var http = require("http");
var https = require("https");
var fs = require("fs");//自己安装的模块
var express = require("express");
var serveIndex = require("serve-index");//文件目录
var sqlite3 = require("sqlite3");
var log4js = require("log4js");
var socketIo = require("socket.io");var logger = log4js.getLogger();
logger.level = "info";var app=express();
app.use(serveIndex("./zhangyangsong"));
app.use(express.static("./zhangyangsong"));var opt = {key:fs.readFileSync("./cert/ssl_server.key"),cert:fs.readFileSync("./cert/ssl_server.pem")
}// var httpServer=http.createServer(app)
// .listen(8888, "0.0.0.0");
var httpsServer=https.createServer(opt, app).listen(8008, "0.0.0.0");var db = null;
var sql = "";
// var io = socketIo.listen(httpServer);
var io = socketIo.listen(httpsServer);
io.sockets.on("connection", (socket)=>{logger.info("connection:", socket.id);//处理1v1聊天室的消息socket.on("vjoin", (room, uname)=>{logger.info("vjoin", room, uname);socket.join(room);var myRoom = io.sockets.adapter.rooms[room];var users = Object.keys(myRoom.sockets).length;logger.info(room + "user=" + users);if (users > 2) {socket.leave(room);socket.emit("vfull", room);} else {socket.emit("vjoined", room);if (users > 1) {socket.to(room).emit("votherjoined", room, uname);}}});socket.on("vdata", (room, data)=>{logger.info("vdata", data);socket.to(room).emit("vgetdata", room, data);});socket.on("vleave", (room, uname)=>{if (room === "") {logger.info("room is empty string");} else if (room === undefined) {logger.info("room is undefine");} else if (room === null) {logger.info("room is null");}var myRoom = io.sockets.adapter.rooms[room];var users = Object.keys(myRoom.sockets).length;logger.info("vleave users=" + (users - 1));socket.leave(room);socket.emit("vleft", room);socket.to(room).emit("votherleft", room, uname);});
});function handleErr(e) {logger.info(e);
}
具体操作js代码:
"use strict"
//整个P2P过程需要知道双方的媒体类型、流和候选者
var hWelcom = document.querySelector("h1#welcom");var url = location.href;
var uname = url.split("?")[1].split("=")[1];hWelcom.textContent = "欢迎来到1v1视频聊天室:" + uname;var iptRoom = document.querySelector("input#room");
var btnEnterRoom = document.querySelector("button#enterRoom");
var btnLeaveRoom = document.querySelector("button#leaveRoom");var videoLocal = document.querySelector("video#localVideo");
var videoRemote = document.querySelector("video#remoteVideo");var localStream = null;
var remoteStream = null;var socked = null;
var room = null;
var status = "init";
var pc = null;
var url = "120.78.130.50:8008"
function getMedia(stream) {localStream = stream;videoLocal.srcObject = stream;
}function start() {var constraints = {video:true,audio:true};//打开摄像头navigator.mediaDevices.getUserMedia(constraints).then(getMedia).catch(handleErr);conn();
}function conn() {socked = io.connect(url);//监听来自服务器的信号socked.on("vfull", (room)=>{status = "leaved";alert("房间已满:" + room);console.log("vfull", status);});socked.on("vjoined", (room)=>{//创建视频连接类alert("成功加入房间:" + room);createPeerConnection();status = "joined";console.log("vjoined:", status);});socked.on("votherjoined", (room, uname)=>{//建立视频连接alert("有人进来了:" + uname);if (status === "joined_unbind") {createPeerConnection();}status = "joined_conn";//当第二个人进来就要发起媒体协商了:媒体协商就是双方互相知道和设置对方的媒体格式mediaNegociate();console.log("votherjoined:", status);});socked.on("vgetdata", (room, data)=>{console.log("vgetdata:", data);if (!data) {return ;}if (data.type === "candidata") {//拿到对方传过来的候选者信息console.log("get other candidata");//候选者信息var cddt = new RTCIceCandidate({sdpMLineIndex:data.label,candidate:data.candidate});pc.addIceCandidate(cddt);//把候选者对象加入pc} else if(data.type === "offer") {//媒体协商默认有type值为offerconsole.log("get offer");pc.setRemoteDescription(new RTCSessionDescription(data));//把对方的媒体格式设置进来//查询自己的媒体格式信息并且应答给对方pc.createAnswer().then(getAnswer).catch(handleErr);} else if(data.type === "answer") {//媒体协商默认回应有type值为answerconsole.log("get answer");pc.setRemoteDescription(new RTCSessionDescription(data));//我把offer发给对方,对方回的answer。offer和answer都是有媒体格式信息。所以offer和answer不会同时存在一个客户端,第一个进来的会发offer,第二个进来的会发answer。把对方的媒体格式设置进来}});socked.on("vleft", (room)=>{status = "leaved";console.log("vleft:", status);});socked.on("votherleft", (room, uname)=>{status = "joined_unbind";closePeerConnection();console.log("votherleft:", status);});
}function getAnswer(decs) {pc.setLocalDescription(decs);//设置一下本地的媒体格式信息sendMessage(decs);
}function mediaNegociate() {if (status === "joined_conn") {//joined_conn代表我是连接的发起人if (pc) {var options = {//要协商的内容,如音频、视频...offerToReceiveVideo:true,offerToReceiveAudio:true}pc.createOffer(options).then(getOffer).catch(handleErr);}}
}function getOffer(desc) {//收到的媒体格式pc.setLocalDescription(desc);sendMessage(desc);//把我需要的媒体格式发给对方
}function createPeerConnection() {if (!pc) {var pcConfig = {//ICE服务器"iceServers":[{"urls":"turn:120.78.130.xx:3478", //指定中继服务器turn"username":"zhangyangsong","credential":"123"}]}pc = new RTCPeerConnection(pcConfig); //pc作用:媒体协商,流和轨的添加和停止,传输相关功能,统计相关功能pc.onicecandidate = (e)=>{ //得到了ICE服务器选择的候选者返回的事件if (e.candidate) {//先判断是不是候选者事件回来的console.log("CANDIDATE", e.candidate);sendMessage({//把候选者信息发给对方(会发给信令服务器然后转发给对方)type:"candidata",label:e.candidate.sdpMLineIndex,//候选者标签id:e.candidate.sdpMid,//候选者idcandidate:e.candidate.candidate//候选者数据});}}//当媒体到达的时候,做什么pc.ontrack = (e)=>{//ontrack收到远程音视频轨e时//alert("连接成功")remoteStream = e.streams[0];videoRemote.srcObject = remoteStream;//把远程媒体流放到远程音频标签里面显示出来}}if (localStream) {localStream.getTracks().forEach((track)=>{pc.addTrack(track, localStream);//将本地的媒体流轨加到pc里面})}
}start();function sendMessage(data) {if (socked) {socked.emit("vdata", room, data);}
}function handleErr(e) {console.log(e);
}function enterRoom() {room = iptRoom.value.trim();if (room === "") {alert("请输入房间号");return;}socked.emit("vjoin", room, uname);
}function leaveRoom() {socked.emit("vleave", room, uname);closePeerConnection();
}function closePeerConnection() {console.log("close RTCPeerConnection");if (pc) {pc.close();pc = null;}
}btnEnterRoom.onclick = enterRoom;
btnLeaveRoom.onclick = leaveRoom;
到这里,整个WebRTCP2P聊天室就完成了,WebRTC可以开发的功能还有很多,但基本原理都是这几个内容。
注意:记得打开chrome://flags/这个网站搜索platform然后打开
相关文章:
![](https://img-blog.csdnimg.cn/08c10ad5eca54e6ba6cd060d64c5bebd.png)
P2P视频聊天技术分析
整个P2P视频过程需要知道双方的媒体类型、流和候选者,所以这里就会用到一下技术: 信令服务器socket.io 状态机 ICE服务器 WebRTC框架 媒体协商 信令服务器Socket.io 信令服务器说白了作用就是发消息的中转站,A把msg发到…...
![](https://img-blog.csdnimg.cn/8502bf7c408841d2a313919149491d8b.png)
MyBatis 的一级、二级缓存机制
目录标题缓存什么是缓存为什么使用缓存什么样的数据能使用缓存,什么样的数据不能使用适用于缓存不适用于缓存MyBatis 一级缓存、二级缓存关系1. 一级缓存1.1 什么是一级缓存mybatis1.2 一级缓存配置1.3 什么情况下会命中一级缓存mybatis清除一级缓存的几种方法1.4 内…...
![](https://www.ngui.cc/images/no-images.jpg)
剑指 Offer 65. 不用加减乘除做加法
摘要 剑指 Offer 65. 不用加减乘除做加法 一、位运算 有符号整数通常用补码来表示和存储,补码具有如下特征: 正整数的补码与原码相同;负整数的补码为其原码除符号位外的所有位取反后加 11。可以将减法运算转化为补码的加法运算来实现。符…...
![](https://www.ngui.cc/images/no-images.jpg)
5年软件测试年薪30w+,我的坎坷之路谁又知道
在深圳做了五年软件测试工作,从之前的一脸懵的点点点,到现在会自动化测试,说一点点非计算机专业人员从事软件测试的心得体会,仅供参考交流。 大部分测试在公司没啥地位,当然如果你懂技术就还行,单纯点点点…...
![](https://img-blog.csdnimg.cn/6dd11855af644d9c97dea427243e24d2.png)
【Opencv--自适应图像二值化】cv2.adaptiveThreshold()
【Opencv–adaptiveThreshold】自适应阈值图像二值化 文章目录【Opencv--adaptiveThreshold】自适应阈值图像二值化1. 介绍2. adaptiveThreshold函数2.1 函数调用2.2 补充说明3. 代码示例4. 效果4.1 原图(ori.img)4.2 处理后5. 参考1. 介绍 在这里 cv2.…...
![](https://img-blog.csdnimg.cn/img_convert/9b66939be6cf4625bb425f28edf9ca18.png)
洛谷P8601[蓝桥杯][2013年第四届真题]剪格子
题目描述如图 11 所示,33 的格子中填写了一些整数。我们沿着图中的红色线剪开,得到两个部分,每个部分的数字和都是 60。本题的要求就是请你编程判定:对给定的 mn 的格子中的整数,是否可以分割为两个部分,使…...
![](https://www.ngui.cc/images/no-images.jpg)
配置alias实现快速生成.gitignore文件
git工具:版本控制开发工具。 cscope工具:用于浏览C源码的工具,类似于ctags。在代码根目录下执行cscope -Rbq,然后产生三个索引文件(cscope.out、cscope.in.out和cscope.po.out三个文件)。 在Linux下使用vi…...
![](https://img-blog.csdnimg.cn/2aadb15bad8d4c4d83bf1e037e3c8f0f.png)
MySQL数据库调优————GROUP BY及DISTINCT优化
GROUP BY 三种处理GROUP BY的方式 松散索引扫描(Loose Index Scan)紧凑索引扫描(Tight Index Scan)临时表(Temporary table) 三种方式的性能一次递减 松散索引扫描 无需扫描满足条件的所有索引键即可返…...
![](https://www.ngui.cc/images/no-images.jpg)
LRU缓存算法
双向链表哈希表(非线程安全) https://leetcode.cn/problems/lru-cache/solutions/259678/lruhuan-cun-ji-zhi-by-leetcode-solution/ /*** LRU算法: 哈希表双向链表实现* 1. 双向链表按照被使用的顺序来存储, 靠近头部的节点是最近使用的, 靠近尾部的节…...
![](https://www.ngui.cc/images/no-images.jpg)
@Configuration注解
Configuration注解介绍 Configuration注解,用于标注一个类是一个spring的配置类(同时,也是一个bean),配置类中可以使用ComponentScan、Import、ImportResource 和 Bean等注解的方式定义beanDefinition。 Target(Elem…...
![](https://img-blog.csdnimg.cn/img_convert/03d930fe4fd42bda396e189aac5162eb.png)
基于springboot+vue的食疗系统
基于springbootvue的食疗系统 ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取项目下载方式🍅 一、项目背景介绍&…...
![](https://img-blog.csdnimg.cn/435b5be1432043e3956390bf766773c0.png)
sklearn学习-朴素贝叶斯
文章目录一、概述1、真正的概率分类器2、sklearn中的朴素贝叶斯二、不同分布下的贝叶斯1、高斯朴素贝叶斯GaussianNB2、探索贝叶斯:高斯朴素贝叶斯擅长的数据集3、探索贝叶斯:高斯朴素贝叶斯的拟合效果与运算速度总结一、概述 1、真正的概率分类器 算法…...
![](https://img-blog.csdnimg.cn/45bfe7b5eb0542f998160a4134cd907b.jpeg)
分享112个HTML艺术时尚模板,总有一款适合您
分享112个HTML艺术时尚模板,总有一款适合您 112个HTML艺术时尚模板下载链接:https://pan.baidu.com/s/1D3-mfPOud-f3vy9yLl-bmw?pwdfph2 提取码:fph2 Python采集代码下载链接:采集代码.zip - 蓝奏云 时尚平面模特网站模板 潮…...
![](https://www.ngui.cc/images/no-images.jpg)
用GDB远程调试运行于QEMU的程序
1. 前言 限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。 2. 测试环境 本文使用 Ubuntu 16.04.4 LTS QEMU 环境进行调试。 3. 用 GDB 调试 QEMU 内程序 3.1 编写用来调试的程序 我们用 ARM32 来进行调试…...
![](https://img-blog.csdnimg.cn/29bbf845390248bb827d571743d5c94f.png)
20 堆排序
文章目录1 堆排序的概念2 堆排序基本思想3 堆排序步骤图解说明4 堆排序的代码实现1 堆排序的概念 1) 堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏,最好,平均时间复杂度均为 O(nlogn)…...
![](https://img-blog.csdnimg.cn/6e6e185e352248529350b2b28a8b5e5c.png)
2023最新文件快递柜系统网站源码 | 匿名口令分享 | 临时文件分享
内容目录一、详细介绍二、效果展示1.部分代码2.效果图展示三、学习资料下载一、详细介绍 2023最新文件快递柜系统网站源码 | 匿名口令分享 | 临时文件分享 很多时候,我们都想将一些文件或文本传送给别人,或者跨端传递一些信息,但是我们又不…...
![](https://www.ngui.cc/images/no-images.jpg)
分片策略(二)
分片策略 基本概念 分片键 用于分片的字段,是将数据库或表拆分的字段,比如,我可以使用user_id作为分片键将用户数据分到不同的表中,这里的user_id就是分片键,除了这种单字段分片,ShardingSphere还支持多…...
![](https://img-blog.csdnimg.cn/867a269cfc8d477d96be9ae28aa312aa.png)
Qt之调色板类QPalette的使用
文章目录QPalette调色板类前言代码知识点讲解QPalette调色板类 前言 Qt提供的调色板类QPalette专门用于管理部件的外观显示,相当于部件或对话框的调色板,管理他们所有的颜色信息。每个部件都包含一个QPalette对象,在显示时,按照…...
![](https://img-blog.csdnimg.cn/7388fc9f642243ecaec1d89e48334179.png)
Kotlin 32. Kotlin 多语言支持
Kotlin 多语言支持 对于 Kotlin 来说,当我们新建一个项目时,会默认在 values/ 文件夹下,生成一个 strings.xml 文件。比如说, <resources><string name"app_name">exampleNewProject</string> <…...
![](https://img-blog.csdnimg.cn/img_convert/c7913e361b09e471aea477ea5122070a.png)
【Flutter入门到进阶】Dart进阶篇---DartVM单线程设计原理
1 虚拟机的指令执行设计 1.1 虚拟机的分类 基于栈的虚拟机,比如JVM虚拟机 基于寄存器的虚拟机,比如Dalvik虚拟机 1.2 虚拟机的概念 首先问一个基本的问题,作为一个虚拟机,它最基本的要实现哪些功能? 他应该能够模拟…...
![](https://www.ngui.cc/images/no-images.jpg)
Dem和NvM(NVRAM Manager)的交集
NVRAM(NvM)提供了在NVRAM中存储数据Block的机制。 NVRAM Block(最大大小取决于配置)被分配给Dem,并由Dem实现事件状态信息和相关数据的永久存储(例如通电复位)。 ECU 状态管理器(Ec…...
![](https://www.ngui.cc/images/no-images.jpg)
AI神经网络CNN/RNN/DNN/SNN的区别对比
@版权声明: 本文由 ChatGpt 创作; BiliBili: https://www.bilibili.com/video/BV17D4y1P7pM/?share_source=copy_web&vd_source=6d217e0ff6387a749dc570aba51d36fd 引言 随着人工智能技术的发展,神经网络作为人工智能的核心技术之一,被广泛应用于图像识别、语音识别、…...
![](https://img-blog.csdnimg.cn/74e994f9715a4b5bbb6290219eeab7c5.png#pic_center)
【JavaWeb】一文学会JPA
✅✅作者主页:🔗孙不坚1208的博客 🔥🔥精选专栏:🔗JavaWeb从入门到精通(持续更新中) 📋📋 本文摘要:本篇文章主要介绍JPA的概念、注解实现ORM规范…...
![](https://img-blog.csdnimg.cn/6bb48ab828a44ca1a4c59e7a67d65305.png)
【安卓逆向】APK修改与反编译回编译
【安卓逆向】反编译修改APK回编译使用工具流程步骤Apktool相关安装与使用常用命令备查APK签名命令备查实战练习反编译查看修改的地方使用Apktool反编译得到产物文件夹并进行修改回编APK实用场景在日常开发我们可能需要替换某些资源或者修改某些代码,但是我们没有源码…...
![](https://www.ngui.cc/images/no-images.jpg)
【计组笔记04】计算机组成原理之多模块存储器、Cache高速缓存存储器、Cache地址映射
这篇文章,主要介绍计算机组成原理之多模块存储器、Cache高速缓存存储器、Cache地址映射。 目录 一、双口RAM和多模块存储器 1.1、存取周期 1.2、双口RAM 1.3、多模块存储器...
![](https://www.ngui.cc/images/no-images.jpg)
英语基础-状语的应用
1. 非谓语动词作状语 1. 试着翻译下列句子 当他是一个小孩子的时候,他很喜欢玩电脑游戏。 When he was a child, he liked playing computer games. 如果他通过考试,他妈妈就会给他买一台新电脑。 If he passes the examination, his mother will b…...
![](https://www.ngui.cc/images/no-images.jpg)
发表论文需要注意的两点(建议收藏)
在学习人工智能的过程中,论文有着重要的作用,无论是深入学术科研,还是毕业找工作,都离不开发表论文这一步骤,所以今天就和大家分享一些关于论文发表的经验,希望对大家有所帮助。 为什么要早点发表论文&…...
![](https://www.ngui.cc/images/no-images.jpg)
ISTQB-TM-大纲
1. 测试过程 1.1 简介 在 ISTQB 软件测试基础级认证大纲中已描述了基本的测试过程包括以下活动: 计划和控制分析和设计实施和执行评估出口准则和报告测试结束活动 基础级大纲认同这些活动虽然有逻辑顺序,但过程中的某些活动可能重叠,或并行…...
![](https://img-blog.csdnimg.cn/89ab71e3fe3d41f497b92fe4737005ba.png)
Java SPI 机制详解
在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。 为了实现在模块装…...
![](https://www.ngui.cc/images/no-images.jpg)
腾讯前端经典react面试题(附答案)
React 性能优化在哪个生命周期?它优化的原理是什么? react的父级组件的render函数重新渲染会引起子组件的render方法的重新渲染。但是,有的时候子组件的接受父组件的数据没有变动。子组件render的执行会影响性能,这时就可以使用s…...
![](/images/no-images.jpg)
网站如何做搜索/西安网络seo公司
随着软件开发日趋国际化,对软件的质量要求和管理也随之增高。微软看到了应用程序生命周期管理在业界逐渐被接受认可的趋势。在微软VS2010(Visual Studio 2010 Ultimate)中,可以利用各种工具辅助每个关键环节进行管理(A…...
![](/images/no-images.jpg)
手机网站用什么软件做的/网络营销方案策划论文
A.TCP建立连接要进行"三次握手",也就是交换三个分组。大致流程如下: >客户端向服务器发送一个SYN J >服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J1 >客户端再向服务器发一个确认ACK K1 当客户端调用connect时&am…...
![](/images/no-images.jpg)
提供设计的的网站/山东最新消息今天
制造业税务成本100问-90(转载) 90。如何利用制造业税务成本系统数据empower 原有制造业系统?90。如何利用制造业税务成本系统数据empower 原有制造业系统?目前还有许多的工厂中的成本会计和前端的制造业系统并未整合。究其原因,除了成本会计计…...
国外的电商网站有哪些/搜seo
一般来说,STM32的型号是这样表示的 STM32F103XYxxx 这XY是什么意思呢?其实啊,这个X表示引脚数量,具体的值有如下形式: R64PIN V100PIN Z144PIN Y表示FLASH容量大小,具体有如下取值: 4 16K…...
![](/images/no-images.jpg)
移动网站视频主持人网/东莞公司网上推广
在某些情况下,or条件可以避免全表扫描的。 1 .where 语句里面如果带有or条件, myisam表能用到索引, innodb不行。 1)myisam表: CREATE TABLE IF NOT EXISTS a ( id int(1) NOT NULL AUTO_INCREMENT, uid int(11) NOT NULL, aNum char(2…...
![](https://img-blog.csdnimg.cn/20210424175440517.png)
班级网站中个人简介怎么做/电脑培训
PROXMOX6.3.6 更改机器名称 参考链接 https://www.baidu.com/link?urlljbh9wHn6YRho6nxxBdbydXNZ3vDERHVZF98SqkDNxJagu4NCtTfvnz5NilOiqqs-peS57O0v5z1t7r2-Zy2dhGQ7D5oIilOM6guFO9uTIa&wd&eqida61791420001d917000000066083e127 参考这个链接准备不停机修改主机名…...