基于云开发 CloudBase 搭建在线视频会议应用

在线视频会议应用是基于浏览器的能力 WebRTC 以及 腾讯云开发 CloudBase 能力构建而成的应用. 在云开发的助力下, 一个复杂的在线会议应用, 一个人一两天即可完成.

云开发CloudBase开通,参加:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite

在线体验 Demo

应用体验地址: Online Meeting Powered by Tencent Cloudbase

项目源码地址: Github

注: 应用仅供演示之用, 目前仅支持两人视频会议, 功能还不够完善, 还有许多可完善之处.

创建会议后可将会议地址发给他人, 或者在本机另起一浏览器窗口(未避免数据混乱, 可开隐私模式窗口, 或使用另一个浏览器)打开会议地址 来体验

在自己的云开发环境中部署

可以在线一键部署或通过本地部署的方式,来独立部署一个自己的在线视频会议应用

在线一键部署

只需要点击下方按钮,跳转到腾讯云控制台,即可在云端一键安装一个在线视频会议应用

本地部署

  1. 修改 .env 文件中的 ENV_ID 的值 tcb-demo-10cf5b 修改为自己的环境 ID
  2. 命令行 cd 到本目录中, 执行 npm run deploy 即可

技术解析

本应用用到的能力、工具、框架有:

  1. CloudBase Framework 用于项目基础目录结构生成, 一键部署
  2. Simple Peer 流行的 WebRTC 库
  3. 云开发-云函数, 包括云函数的定时调用
  4. 云开发-数据库
  5. 云开发-静态网站托管
  6. React
  7. Ant design

如果你不清楚项目开发的基本命令, 可阅读本项目使用的模版的 readme.md

背景知识

Web RTC

  1. WebRTC 即 Web 实时通信技术, 由一系列浏览器 API 组成, 包括 navigator.getUserMedia**, MediaStream, RTC相关的全局对象
  2. WebRTC 是一种 P2P 的通信技术, 浏览器之间可以对等连接. 但浏览器之间需要通过一个信令服务器来交换信令后方可建立连接
  3. 浏览器的信令信息的获取需要一个 ICE 服务器, 一般默认会使用谷歌的公共服务器



云开发

云开发(CloudBase)是云端一体化的后端云服务 ,采用 serverless 架构,免去了应用构建中繁琐的服务器搭建和运维。同时云开发提供的静态托管、命令行工具(CLI)、Flutter SDK 等能力降低了应用开发的门槛。使用云开发可以构建完整的小程序/小游戏、H5、Web、移动 App 等应用。

CloudBase Framework

CloudBase Framework 是云开发官方出品的开源前后端一体化部署工具,无需改动代码,实现前后端一键托管部署,支持常见的框架和语言,支持自动识别并部署。不仅可以部署应用前后端到 Serverless,还可以扩展更多后端能力。



Github 地址: https://github.com/TencentCloudBase/cloudbase-framework

完整搭建步骤:从 0 到 1 实现一个在线会议应用

整个应用的构建, 从项目初始化到最终可以一键部署, 共分为 6 个部分. 为方便读者查阅,主要的代码实现分了 6 次提交, 下述说明中会列出每一步对应的提交 commit.

第 1 步 初始化项目和视频页面

注意要点:

  1. 在进行操作之前, 请确保已经注册腾讯云账户
  2. WebRTC 需要浏览器支持, 只有现代浏览器才支持, 建议使用 Chrome、Firefox 来体验, 具体兼容性可查看 caniuse
  3. 由于安全策略限制, WebRTC 仅支持 https 协议网站; 其为方便本地开发, 也支持 http 的 localhost127.0.0.1 (不限端口), 不支持其他自定义的本机域名、IP
  4. WebRTC 并不具备穿透内网功能, 测试体验时, 确保双方机器都处于公网之中并能访问云开发相关域名

操作步骤

  1. 初始化项目结构

首先需要全局安装 Cloudbase CLI

npm i @cloudbase/cli@latest -g

使用以下命令来使用云开发的 react 应用模版创建一个 React 云开发项目

cloudbase init --template react-starter
  1. 引入 UI 库 ant-design
npm i ant-d @ant-design/icons -S
  1. 增加 landing 页, 用于检测 WebRTC 能力以及判断用户是否授予摄像头及麦克风访问权限

landing 页面核心代码 meeting-simple/src/landing/index.js

import { LoadingOutlined, WarningOutlined } from "@ant-design/icons";
import React, { useEffect, useState } from "react";
import * as utils from "../utils";
// import * as api from './meeting/api' export default function Landing(props) {
// 检测 RTC 支持
return !utils.isSupportRTC() ? (
<NotSupport />
) : (
<NotReady setReady={props.setReady} />
);
} // 不支持时的显示
function NotSupport() {
// ...
} // 支持 RTC 时的显示
function NotReady(props) {
const [permissionState, setPermissionState] = useState("prompt");
const [timeCount, setTimeCount] = useState(0);
const [loadingState, setLoadingState] = useState("init"); const retry = () => {
setTimeCount(timeCount + 1);
}; // 不同状态时的提示信息,prompt、granted、denied
const permissionStr = {
prompt: (
<p>
Please allow camera and microphone access to continue, you can turn off
camera or microphone later in meeting
</p>
),
denied: (
<p>
You should granted camera microphone permissions,{" "}
<a onClick={retry}>click to retry</a>
</p>
),
granted: <p>Loading meeting info...</p>,
}; useEffect(() => {
(async () => {
// 检测权限
const status = await utils.checkMediaPermission();
// 设置授权信息
setPermissionState(status ? "granted" : "denied");
if (!status) return;
try {
// 从浏览器参数拿到会话信息
const sessID = location.hash.slice(1);
// if (sessID) {
// await api.getSessionInfo(sessID)
// }
props.setReady("landing");
} catch (error) {
console.warn("failed to get session info", error);
setLoadingState("Failed to get meeting info: " + JSON.stringify(error));
}
})();
}, [timeCount]);
const tip =
permissionStr[permissionState] ||
(loadingState === "init" ? "loading..." : loadingState);
return <div className="landing-mask"><!--loading 信息--></div>;
}
  1. 增加 Video-window 页, 用于支持视频画面显示

Video-window 核心代码 meeting-simple/src/meeting/video-window/index.js

import React, { useRef, useEffect } from "react";
import * as utils from "../../utils"; export default function VideoWindow(props) {
const videoRef = useRef(null); useEffect(() => {
const updateStream = (stream) => {
// video 对象对应的dom
const dom = videoRef.current;
if (!dom) return;
// 自己则 mute 静音
dom.muted = !props.peer;
if ("srcObject" in dom) {
dom.srcObject = stream;
dom.onloadedmetadata = function () {
dom.play();
};
return;
}
// 设置实时视频的 stream 地址
dom.src = URL.createObjectURL(stream);
dom.play();
}; if (props.peer) {
props.peer.on("stream", updateStream);
return;
}
// 获得 mediaStream
utils.getMediaStream().then(updateStream); return () => {
if (!props.peer) return;
props.peer.off("stream", updateStream);
};
}, [props.peer]); return (
<video
ref={videoRef}
controls={!!props.peer}
width="640"
height="480"
></video>
);
}

工具方法的核心实现meeting-simple/src/utils.js,检测是否支持 WebRTC、

/** 检查是否支持 WebRTC */
export function isSupportRTC() {
return !!navigator.mediaDevices;
}
// 检测是否有media权限
export async function checkMediaPermission() {
// 请求获得媒体流输入(包含声音和视频)
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
}); // 判断是否有视频和声音轨道输入
const result =
stream.getAudioTracks().length && stream.getVideoTracks().length; // 终止媒体流输入
revokeMediaStream(stream); return result;
} // 终止媒体流
export function revokeMediaStream(stream) {
if (!stream) return;
const tracks = stream.getTracks(); tracks.forEach(function (track) {
track.stop();
});
} let cachedMediaStream = null;
export async function getMediaStream() {
if (cachedMediaStream) {
return Promise.resolve(cachedMediaStream);
}
// 请求媒体流输入
const stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: true,
}); revokeMediaStream(cachedMediaStream);
cachedMediaStream = stream; return cachedMediaStream;
}

代码提交记录

本步骤对应的 git commit

第 2 步 支持创建会议

注意要点:

  1. 浏览器端调用云开发能力需要借助官方 npm 包 tcb-js-sdk, 官方文档
  2. 因为视频会议应用无需注册, 即需要匿名使用云开发能力, 调用能力前, 需要在云开发 登录授权 中开启 「匿名登录」
  3. 使用云开发能力(不论是在浏览器端、Node 端或其他端)调用数据库时, 操作端 collection 必须存在, 否则会报错. 所以在本步骤应当提前进入云开发数据库控制台 创建视频会议使用的 collection meeting-simple
  4. 使用 JS sdk 调用云开发能力时, 需保证调用的域名已加入云开发WEB 安全域名中, 以避免调用时出现跨域问题. 即本地开发使用的域名应增加进 WEB 安全域名 中.

操作步骤

  1. 增加 「创建会议」界面
  2. 增加云开发能力调用模块 「api.js」, 添加 创建会议方法(通过云开发 js sdk 连接数据库创建记录)

创建会议的前端 API 核心代码 meeting-simple/src/meeting/api.js

import tcb from "tcb-js-sdk";

// 初始化云开发 JSSDK
const app = tcb.init({
env: "tcb-demo-10cf5b",
}); // 初始化 auth
const auth = app.auth({
persistence: "local",
}); const db = app.database(); // 会议表名称
const MEETING_COLLECTION = "meeting-simple"; // 匿名登录
async function signIn() {
if (auth.hasLoginState()) return true;
await auth.signInAnonymously();
return true;
} // 创建会议
export async function createMeeting(meeting) {
await signIn();
meeting.createdAt = Date.now();
// 添加一条会议的记录
const result = await db.collection(MEETING_COLLECTION).add(meeting);
return result;
}

代码提交记录

本步骤对应的 git commit

第 3 步 实现加入会议功能

操作步骤

  1. 增加 「加入会议」界面
  2. 在 「api.js」中增加方法(直接调用云开发数据库能力)获取会议信息、加入会议

获取会议信息和加入会议的前端 API 的核心代码 meeting-simple/src/meeting/api.js

// 获取会议信息
export async function getMeeting(meetingId) {
await signIn();
// 调用 db 查询数据
const result = await db.collection(MEETING_COLLECTION).doc(meetingId).get();
if (!result.data || !result.data.length) return;
const meeting = result.data[0]; meeting.hasPass = !!meeting.pass;
delete meeting.pass;
return meeting;
} // 加入会议
export async function joinMeeting(data) {
await signIn();
// 查询会议信息
const result = await db.collection(MEETING_COLLECTION).doc(data.id).get();
if (!result.data || !result.data.length)
throw new Error("meeting not exists"); const meeting = result.data[0];
// 前端对比会议 pass 码来验证,安全性较低,会在第 5 步进行优化
if (meeting.pass && meeting.pass === data.pass)
throw new Error("passcode not match");
return true;
}

注:

  1. 数据库需要设置成公开访问, 否则匿名用户无法查询到相关信息: 进入数据库找到对应 collection, 切换至 「权限设置」, 选择 「所有用户可读,仅创建者及管理员可写」并保存

代码提交记录

本步骤对应的 git commit

第 4 步 实现实时加入会议

操作步骤

  1. 增加 simple-peer 来管理 WebRTC 客户端
import Peer from "simple-peer";
import * as utils from "./utils";
import * as api from "./api"; export async function createPeer(initiator, meetingId) {
const peer = new Peer({ initiator });
const stream = await utils.getMediaStream();
peer.addStream(stream); // peer 接收到 signal 事件时,调用 peer.signal(data) 来建立连接,那么如何拿到 data 信息呢
peer.on("signal", (e) => {
console.log("[peer event]signal", e);
// 调用更新写入数据库
updateTicket(e, initiator, meetingId);
});
peer.on("connect", (e) => {
console.log("[peer event]connect", e);
});
peer.on("data", (e) => {
console.log("[peer event]data", e);
});
peer.on("stream", (e) => {
console.log("[peer event]stream", e);
});
peer.on("track", (e) => {
console.log("[peer event]track", e);
});
peer.on("close", () => {
console.log("[peer event]close");
});
peer.on("error", (e) => {
console.log("[peer event]error", e);
});
return peer;
} let cachedTickets = [];
let tid = 0; function updateTicket(signal, isInitiator, meetingId) {
cachedTickets.push(signal);
clearTimeout(tid);
tid = setTimeout(async () => {
const tickets = cachedTickets.splice(0);
try {
// 写入数据库
const result = await api.updateTicket({
meetingId,
tickets,
type: isInitiator ? "offer" : "answer",
});
console.warn("[updateTicket] success", result);
} catch (error) {
console.warn("[updateTicket] failed", error);
}
}, 100);
} export function signalTickets(peer, tickets) {
tickets.forEach((item) => {
peer.signal(item);
});
}
  1. 增加云函数 「更新 ticket」(用于更新 WebRTC 客户端的连接信息)并手动部署云函数, 增加对会议记录对监听(即使用数据库的实时推送能力)

用于更新 WebRTC 客户端的连接信息的云函数的核心代码 meeting-simple/cloudfunctions/update-ticket-meeting-simple/index.js

const cloud = require("@cloudbase/node-sdk");

   const MEETING_COLLECTION = "meeting-simple";

   exports.main = async (data) => {
const app = cloud.init({
env: cloud.SYMBOL_CURRENT_ENV,
}); const collection = app.database().collection(MEETING_COLLECTION); try {
// 查询会议信息
const result = await collection.doc(data.meetingId).get();
if (!result.data || !result.data.length)
throw new Error("meeting not exists");
const meeting = result.data[0]; const changed = {};
changed[data.type] = meeting[data.type] || // 若新的tickets中包含 offer 或 answer, 则已经存储的tickets信息无效
if (data.tickets.some((tk) => ["offer", "answer"].includes(tk.type))) {
changed[data.type] = data.tickets;
} else {
changed[data.type].push(...data.tickets);
} // 另一方信息已经被接受使用, 已无效, 清空之, 避免 客户端 watch 时使用无效数据
changed[data.type === "offer" ? "answer" : "offer"] = null; // 更新会议信息
const res = await collection.doc(data.meetingId).update(changed);
return {
code: 0,
data: res,
};
} catch (error) {
return {
code: 1,
message: error.message,
};
}
};

更新票据和监听会议信息变更的前端 API 核心代码 meeting-simple/src/meeting/api.js

// 更新票据
export async function updateTicket(data) {
await signIn();
const res = await app.callFunction({
name: "update-ticket-meeting-simple",
data,
});
return res;
} let watcher = null;
export async function watchMeeting(meetingId, onChange) {
await signIn(); // 如果有监听,关闭监听
watcher && watcher.close(); // 新建数据库监听
watcher = db
.collection(MEETING_COLLECTION)
.doc(meetingId)
.watch({
onChange: (snapshot) => {
console.error(snapshot); if (
!snapshot.docChanges ||
!snapshot.docChanges.length ||
!snapshot.docChanges[0].doc
)
return; // 回调最新的数据库文档信息
onChange(snapshot.docChanges[0].doc);
},
onError: (err) => {
console.log("watch error", err);
},
});
}
  1. 优化会议信息的获取提升体验

注意

  1. 监听数据库变化亦需要将数据库设置为公开访问, 即上述第三步中的注意事项 2 所述
  2. 匿名用户无法修改其他匿名用户创建的记录. 根据数据库安全策略, 虽同为匿名用户, 但不同客户端的匿名用户标志不一样, 故不能操作他人的记录. 而云函数有用管理员级别的数据库操作权限, 故 「更新 ticket」功能采用了云函数来编写

代码提交记录

本步骤对应的 git commit

第 5 步 提升非公开会议访问的安全性, 优化数据库使用

操作步骤

  1. 将会议密码分表存储

修改“加入会议”的前端 API 核心代码 meeting-simple/src/meeting/api.js

// 加入会议
export async function joinMeeting(data) {
await signIn();
// 加入会议改为使用调用云函数校验,保证密码安全
const result = await app.callFunction({
name: "join-meeting-meeting-simple",
data,
});
if (result.result.code) {
throw new Error(result.result.message);
} return true;
}

负责加入会议时进行密码校验的云函数的核心代码 meeting-simple/cloudfunctions/join-meeting-meeting-simple/index.js

const tcb = require("@cloudbase/node-sdk");
const MEETING_COLLECTION = "meeting-simple";
const MEETING_PASS_COLLECTION = "meeting-simple-pass";
const app = tcb.init({
env: tcb.SYMBOL_CURRENT_ENV,
});
const db = app.database(); exports.main = async function (evt) {
try {
const result = await db.collection(MEETING_COLLECTION).doc(evt.id).get();
if (!result.data || !result.data.length)
return { code: 1, message: "meeting not exists" };
const meeting = result.data[0]; if (meeting.hasPass) {
// 查询会议密码
const passResult = await db
.collection(MEETING_PASS_COLLECTION)
.where({ meetingId: evt.id })
.get();
if (!passResult.data || !passResult.data.length)
return { code: 2, message: "passcode not found" };
const passInfo = passResult.data[0];
// 对比会议密码
if (passInfo.pass !== evt.pass)
return {
code: 3,
message: "passcode not match",
};
}
return { code: 0 };
} catch (error) {
return {
code: 3,
message: error.message,
};
}
};
  1. 数据库 collection 定期清理旧的无用记录

清理数据的云函数的核心实现meeting-simple/cloudfunctions/autoclear-meeting-meeting-simple/index.js

const tcb = require("@cloudbase/node-sdk");
const MEETING_COLLECTION = "meeting-simple";
const MEETING_PASS_COLLECTION = "meeting-simple-pass";
const app = tcb.init({
env: tcb.SYMBOL_CURRENT_ENV,
});
const db = app.database();
/**
* 定时触发, 清理两天前的会议记录
*
{
"triggers": [
{
"name": "clear-time-trigger",
"type": "timer",
"config": "0 0 2 * * * *"
}
]
}
*/ exports.main = async function () {
const now = Date.now();
// 2天前
const threshold = now - 2 * 24 * 60 * 60 * 1000;
const _ = db.command;
try {
// 查询创建时间两天前的会议记录,进行删除
await db
.collection(MEETING_COLLECTION)
.where({
createdAt: _.lte(threshold),
})
.remove(); // 查询创建时间两天前的密码记录,进行删除
await db
.collection(MEETING_PASS_COLLECTION)
.where({
createdAt: _.lte(threshold),
})
.remove();
} catch (error) {
console.log("failed to batch remove", error);
}
};

注意:

  1. 定期清理数据库使用了云函数的定时触发器

代码提交记录

本步骤对应的 git commit

第 6 步 使用 cloudbase framework 一键部署

  1. 增加静态部署功能, 使用了 website 插件
  2. 增加部署云函数功能, 包括云函数定时调用的设置, 使用了function 插件
  3. 增加数据 collection 的创建, 包括 collection 访问权限的设置, 使用了 database 插件

meeting-simple/.env 文件中声明环境变量信息

PUBLIC_URL=./
ENV_ID=tcb-demo-10cf5b

meeting-simple/cloudbaserc.json 文件中声明静态资源、云函数和数据库等各个资源的构建和部署信息

{
"envId": "{{env.ENV_ID}}",
"$schema": "https://framework-1258016615.tcloudbaseapp.com/schema/latest.json",
"version": "2.0",
"functionRoot": "cloudfunctions",
"framework": {
"plugins": {
"client": {
"use": "@cloudbase/framework-plugin-website",
"inputs": {
"buildCommand": "npm run build",
"outputPath": "build",
"cloudPath": "/meeting-simple",
"envVariables": {
"REACT_APP_ENV_ID": "{{env.ENV_ID}}"
}
}
},
"db": {
"use": "@cloudbase/framework-plugin-database",
"inputs": {
"collections": [
{
"collectionName": "meeting-simple",
"aclTag": "READONLY"
},
{
"collectionName": "meeting-simple-pass"
}
]
}
},
"server": {
"use": "@cloudbase/framework-plugin-function",
"inputs": {
"functionRootPath": "cloudfunctions",
"functions": [
{
"name": "autoclear-meeting-meeting-simple",
"triggers": [
{
"name": "clear-time-trigger",
"type": "timer",
"config": "0 0 2 * * * *"
}
]
},
{ "name": "join-meeting-meeting-simple" },
{ "name": "create-meeting-meeting-simple" },
{ "name": "update-ticket-meeting-simple" }
]
}
}
}
}
}

执行 ClouBase Framework 的一键部署命令

cloudbase framework deploy



更多 CloudBase Framework 插件可阅读CloudBase Framework 官方文档

代码提交记录

本步骤对应的 git commit

总结

在本次实战案例里面,我们通过了解了 WebRTC 的基本使用,通过在线会议系统的实战了解了基于云开发开发一个应用的完整流程,学会使用了数据库实时推送能力的使用、匿名用户使用数据库的安全策略问题及云函数定时调用功能,掌握了使用 CloudBase Framework 一键部署前后端应用这一工具来快速交付。


CloudBase Framework 开源项目介绍

CloudBase Framework 是云开发开源的云原生前后端一体化部署工具,支持主流前后端框架,前后端一键托管部署在云端一体化平台,支持支持小程序、Web、Flutter、后端服务等多个平台。

Github 开源地址:https://github.com/TencentCloudBase/cloudbase-framework

欢迎给 CloudBase Framework 一个 star

CloudBase Framework 核心贡献者计划

欢迎大家参与 CloudBase Framework 的开发工作,成为我们的贡献者,我们将会在云开发社区展示贡献者的作品和信息,同时也会有惊喜奖励。

您可以选择如下的贡献方式:

CloudBase Framework 的发展离不开社区的积极贡献,这是我们的核心贡献者列表,再次感谢大家的贡献:https://github.com/TencentCloudBase/cloudbase-framework#contributors-

产品介绍

云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等serverless化能力,可用于云端一体化开发多种端应用(小程序,公众号,Web 应用,Flutter 客户端等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。

开通云开发:https://console.cloud.tencent.com/tcb?tdl_anchor=techsite

产品文档:https://cloud.tencent.com/product/tcb?from=12763

技术文档:https://cloudbase.net?from=10004

技术交流加Q群:601134960

最新资讯关注微信公众号【腾讯云云开发】

基于云开发 CloudBase 搭建在线视频会议应用教程的更多相关文章

  1. 微信小程序--简约风博客小程序(基于云开发 - 全开源)

    微信小程序--简约风博客小程序(基于云开发 - 全开源) 项目启动纯属突发奇想,想看看博客小程序,例如wehalo博客小程序,但是感觉自建平台还要浪费自己的服务器算力,还没有访问量,省省吧. 本着白嫖 ...

  2. 基于云开发开发 Web 应用(三):云开发相关数据调用

    介绍 在完成了 UI 界面的实现后,接下来可以开始进行和云开发相关的数据对接.完成数据对接后,应用基础就打好了,接下来的就是发布上线以及一些小的 feature 的加入. 配置 在进行相关的配置调用的 ...

  3. 用云开发Cloudbase,实现小程序多图片内容安全监测

    前言 相比于文本的安全检测,图片的安全检测要稍微略复杂一些,当您读完本篇,将get到 图片安全检测的应用场景 解决图片的安全校验的方式 使用云调用方式对图片进行检测 如何对上传图片大小进行限制 如何解 ...

  4. 基于云开发开发 Web 应用(一):项目介绍 & 初始化

    基于云开发开发 Web 应用(一):项目介绍 & 初始化 背景描述 Linux 中国曾在过去的 1 - 2 年内长期运行了一个 TL;DR 的中国版.不过当时做的版本是小程序的版本,一直以来, ...

  5. Android开发环境搭建篇详尽的教程实例汇

    原文链接:http://android.eoe.cn/topic/android_sdk 一.android开发环境搭建图文教程整理篇: 1.Android开发环境搭建全程演示(jdk+eclip+a ...

  6. Linux开发环境搭建与使用系列教程

    00.Linux开发环境搭建与使用1——Linux简史 01.Linux开发环境搭建与使用2——Linux系统(ubuntu)安装方案 02.Linux开发环境搭建与使用3——通过虚拟机安装系统(ub ...

  7. Hadoop伪分布配置与基于Eclipse开发环境搭建

    国内私募机构九鼎控股打造APP,来就送 20元现金领取地址:http://jdb.jiudingcapital.com/phone.html内部邀请码:C8E245J (不写邀请码,没有现金送)国内私 ...

  8. 【阿里云开发】- 搭建和卸载svn服务器

    Subversion(SVN) 是一个开源的版本控制系統, 也就是说 Subversion 管理着随时间改变的数据. 这些数据放置在一个中央资料档案库(repository) 中.这个档案库很像一个普 ...

  9. Python基于VS2013 开发环境搭建 Hello World 10分钟搞定

    1.先下载Python 安装 Next ->安装完成 2.以前安装过VS2013 打开VS2013 文件->新建项目 (此时如果没有Python Application,请点击里面的安装插 ...

随机推荐

  1. 数据结构与算法:AVL树

    AVL树 在计算机科学中,AVL树是最先发明的自平衡二叉查找树.在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树.增加和删除可能需要通过一次或多次树旋转来重新平衡这个树.AV ...

  2. monolog 日志

    1 安装 composer require monolog/monolog 2 使用 // 创建日志服务 $logger = new Logger('my_logger'); // 定义一个handl ...

  3. navicate premium黄色版本破解下载

    百度网盘下载 提取码: tsua 按照电脑安装32位或者64位 安装完成后点击最后一个进行破解汉化

  4. 【转】time 模块详解(时间获取和转换)

    转自鱼C论坛--https://fishc.com.cn/forum.php?mod=viewthread&tid=51326&highlight=time time 模块 -- 时间 ...

  5. npm 注册淘宝镜像

    临时使用 npm --registry https://registry.npm.taobao.org install express 1 2.持久使用 npm config set registry ...

  6. pyqt5屏幕坐标系

    我们直接用代码去理解屏幕坐标系 import sys from PyQt5.QtWidgets import QHBoxLayout,QMainWindow,QApplication,QPushBut ...

  7. HBase进阶

    date: 2020-10-26 15:43:00 updated: 2020-10-26 18:45:00 HBase进阶 1. 架构 master负责管理多个region server,一个reg ...

  8. vue中iframe加载慢,给它加loading效果

    js框架:vue ui框架:element 因为iframe加载慢,所以在它加载完成前添加loading效果,loading用的是element家的加载效果 <template> < ...

  9. 隐马尔科夫模型 HMM(Hidden Markov Model)

    本科阶段学了三四遍的HMM,机器学习课,自然语言处理课,中文信息处理课:如今学研究生的自然语言处理,又碰见了这个老熟人: 虽多次碰到,但总觉得一知半解,对其了解不够全面,借着这次的机会,我想要直接搞定 ...

  10. Redis 数据结构与编码技术 (Object Encoding)

    数据结构实现 相信大家对 redis 的数据结构都比较熟悉: string:字符串(可以表示字符串.整数.位图) list:列表(可以表示线性表.栈.双端队列.阻塞队列) hash:哈希表 set:集 ...