同态加密和SEAL库的介绍(四)CKKS 方案

news2024/9/24 7:23:23

写在前面:

        上篇介绍了 BFV 的 Batch Encoder,其虽然充分利用了槽空间,但是每个槽只包含一个模 plain_modulus 的整数,除非 plain_modulus 非常大,否则我们可能会很快遇到数据类型溢出并在需要进行整数计算时得到意外的结果。CKKS 方案(及其编码器 CKKSEncoder)解决了数据类型溢出问题,但代价是只能得到近似结果。

一、CKKS方案介绍

下面展示 Cheon-Kim-Kim-Song (CKKS) 方案用于加密实数或复数的计算。
我们首先为 CKKS 方案创建加密参数。与 BFV 方案相比,有两个重要区别:

  1. CKKS 不使用 plain_modulus 加密参数;
  2. 在使用 CKKS 方案时,以特定方式选择 coeff_modulus 可能非常重要。示例文件 `ckks_basics.cpp` 中进一步解释这一点。在这个示例中,我们使用 CoeffModulus::Create 生成 5 个 40 位的素数。

1.1 参数设置和实例创建

EncryptionParameters parms(scheme_type::ckks);

size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, { 40, 40, 40, 40, 40 }));

SEALContext context(parms);

示例这里也对SEALContext进行输出:

密钥的创建和其他类的设置跟之前相同:

KeyGenerator keygen(context);
auto secret_key = keygen.secret_key();
PublicKey public_key;
keygen.create_public_key(public_key);
RelinKeys relin_keys;
keygen.create_relin_keys(relin_keys);

Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);

1.2 CKKS的编码器

        首先要注意,CKKS用的编码器和前面的不同:[CKKSEncoder] (仅用于 CKKS 方案)。
        BatchEncoder 不能用于 CKKS 方案。CKKSEncoder 将实数或复数的向量编码为 Plaintext 对象,这些对象随后可以被加密。
        在高层次上,这看起来与 BatchEncoder 对 BFV 方案所做的非常相似,但其背后的理论完全不同

CKKSEncoder encoder(context);
size_t slot_count = encoder.slot_count();

        有一点要格外注意:在 CKKS 中,槽的数量是 poly_modulus_degree / 2,每个槽编码一个实数或复数。这应该与 BFV 方案中的 BatchEncoder 形成对比,BFV 方案中的槽数量等于 poly_modulus_degree,并排列成一个两行的矩阵。
        即能用的槽只有 poly_modulus_degree 的一半!并且如果输入的数量小于一半CKKSEncoder 在编码时会隐式地用零填充它以达到完整大小 (poly_modulus_degree / 2)。

二、编解码和运算

2.1 对输入编码和加密

先准备一个数组,并且结合上面说的(编码器会自动补全,故不需要存满):

vector<double> input{ 0.0, 1.1, 2.2, 3.3 };

        现在我们用 CKKSEncoder 对其进行编码。`input` 的浮点系数将被参数 `scale` 放大。这是必要的,因为即使在 CKKS 方案中,明文元素基本上也是具有整数系数的多项式。将 `scale` 视为决定编码精度的比特数是有启发性的;自然地,它会影响结果的精度。
        在 CKKS 中,消息是模 coeff_modulus 存储的 (在 BFV 中它是模 plain_modulus 存储),因此放大的消息不能太接近 coeff_modulus 的总大小。在这种情况下,我们的 coeff_modulus 相当大 (200 位),所以在这方面我们没什么好担心的。对于这个简单的示例,30 位的 scale 已经绰绰有余。

Plaintext plain;
double scale = pow(2.0, 30);
encoder.encode(input, scale, plain);
encryptor.encrypt(plain, encrypted);

        解密解码输出验证下正确性,可以发现其他位置的确进行了自动补全。但是仔细点会发现有 -0.00,其实就是因为近似性,可能产生了很小的误差(偏小了一点点,这里的位数看不出来)。

2.2 运算

密文上的基本操作仍然很容易进行。在这里,我们对密文进行平方运算:

evaluator.square_inplace(encrypted);
evaluator.relinearize_inplace(encrypted, relin_keys);

cout << "Scale in squared input: " << encrypted.scale() << " (" << log2(encrypted.scale()) << " bits)";

decryptor.decrypt(encrypted, plain);
encoder.decode(plain, output);

解密,解码并打印结果,(同时计算下平方后的 scale):

可以注意到:乘法结果中的 scale 增加了。实际上,它现在是原始 scale 的平方:2^{60}CKKS 方案允许在加密计算之间缩小 scale。这是一个基本且关键的特性,使 CKKS 非常强大和灵活,下面进行介绍。


三、更复杂的运算

        在本示例中,演示如何在加密的浮点输入数据 x 上评估多项式:

\pi *x^3 + 0.4*x + 1

        输入是在区间 [0, 1] 内的 4096 个等距点上,这个示例展示了 CKKS 方案的许多主要功能,但也展示了使用它的挑战。
        上文提到了,CKKS 中的乘法会导致密文中的尺度增长。任何密文的尺度都不能太接近 coeff_modulus 的总大小,否则密文将无法存储放大后的明文。CKKS 方案提供了 `rescale` 功能,可以减少尺度,并稳定尺度扩展
        Rescaling 是一种模数切换操作。像模数切换一样,它移除了 coeff_modulus 中的最后一个素数,但作为副作用,它通过移除的素数缩小了密文的尺度。(这里难理解可以看下篇关于级别的具体介绍后,再回来看)

3.1 参数设置

        通常我们希望完全控制尺度的变化,这就是为什么在 CKKS 方案中更常用精心选择的素数作为coeff_modulus。更准确地说,假设 CKKS 密文中的尺度是 S,当前 coeff_modulus 中的最后一个素数是 P。Rescaling 到下一个级别将尺度改为 S/P,并像往常一样移除 coeff_modulus 中的素数 P。素数的数量限制了可以进行的 rescaling 次数,从而限制了计算的乘法深度

        可以自由选择初始尺度。一种好的策略是将初始尺度 S 和 coeff_modulus 中的素数 P_i
设为非常接近。若密文在乘法前的尺度为 S,乘法后的尺度为 S^2,rescaling 后的尺度为 S^2/P_i。如果所有 P_i 都接近 S,则 S^2/P_i 再次接近 S。这样我们就可以在计算过程中将尺度稳定在 S 附近。
        一般来说,对于深度为 D 的电路,我们需要 rescaling D 次,即我们需要从系数模数中移除 D 个素数。一旦 coeff_modulus 中只剩下一个素数,剩余的素数必须比 S 大几个比特,以保持明文的小数点前的值。

因此,一般好的策略是为 CKKS 方案选择参数如下:

  1. 选择一个 60 位的素数作为 coeff_modulus 中的第一个素数。这将在解密时提供最高的精度;
  2. 选择另一个 60 位的素数作为 coeff_modulus 的最后一个元素,因为它将用作特殊素数,应该和其他最大的素数一样大;
  3. 选择中间的素数,使它们彼此接近。

        我们使用 CoeffModulus::Create 来生成合适大小的素数。注意,我们的 coeff_modulus 总共是 200 位,这低于我们 poly_modulus_degree 的限制:CoeffModulus::MaxBitCount(8192) 返回 218。

size_t poly_modulus_degree = 8192;
parms.set_poly_modulus_degree(poly_modulus_degree);
parms.set_coeff_modulus(CoeffModulus::Create(poly_modulus_degree, { 60, 40, 40, 60 }));

        我们选择初始尺度为 2^{40}。在最后一级时,这让我们在小数点前有 60-40=20 位的精度,并且有足够(大约 10-20 位)的精度在小数点后。由于我们的中间素数是 40 位(实际上,它们非常接近 2^{40}),我们可以实现上述的尺度稳定。

double scale = pow(2.0, 40);

SEALContext context(parms);

输出参数:

其他类的实例与之前相同:

KeyGenerator keygen(context);
auto secret_key = keygen.secret_key();
PublicKey public_key;
keygen.create_public_key(public_key);
RelinKeys relin_keys;
keygen.create_relin_keys(relin_keys);
GaloisKeys gal_keys;
keygen.create_galois_keys(gal_keys);
Encryptor encryptor(context, public_key);
Evaluator evaluator(context);
Decryptor decryptor(context, secret_key);

CKKSEncoder encoder(context);
size_t slot_count = encoder.slot_count();

这里再次强调,能用的槽空间只有poly_modulus_degree 的一半,这与BFV不同。

3.2 输入编码与加密

vector<double> input;
input.reserve(slot_count);

double curr_point = 0;
double step_size = 1.0 / (static_cast<double>(slot_count) - 1);
for (size_t i = 0; i < slot_count; i++)
{
    input.push_back(curr_point);
    curr_point += step_size;
}

Plaintext x_plain;
encoder.encode(input, scale, x_plain);
Ciphertext x1_encrypted;
encryptor.encrypt(x_plain, x1_encrypted);

输入为区间 [0, 1] 内的 4096 个等距点,打印出来能看到:

        因为要计算的多项式是:\pi *x^3 + 0.4*x + 1,这里回想一下,槽里面的运算是对应位置相乘相加。所以这里我们为  \pi、0.4 和 1 创建明文,使用 CKKSEncoder::encode 的重载函数,将给定的浮点值编码到向量的 每个槽中

Plaintext plain_coeff3, plain_coeff1, plain_coeff0;
encoder.encode(3.14159265, scale, plain_coeff3);
encoder.encode(0.4, scale, plain_coeff1);
encoder.encode(1.0, scale, plain_coeff0);

3.3 重新线性化

在进行乘法之前,有必要再补充介绍一下重线性化的概念,之前BFV那里说的比较简单:

        在同态加密中,密文的加法和乘法是支持的。加法相对简单,不会增加密文的大小。然而,密文的乘法会增加密文的大小和复杂度。例如,两个密文 c_1​ 和 c_2​ 相乘,结果密文 c_3 = c_1 \times c_2的大小和复杂度会增加。

作用:

        重线性化的主要作用是减少乘法后的密文大小,使其恢复到和原始密文相同的大小和复杂度。具体步骤如下:

  • 乘法后的密文:乘法后的密文通常会变得更大、更复杂。
  • 应用重线性化密钥:使用重线性化密钥,将复杂的密文重新线性化,降低其大小和复杂度。

何时需要

  • 密文与密文相乘:每次密文乘法后通常需要进行重线性化。(明文乘密文不用)
  • 深度运算:在多次连续乘法运算后,重线性化有助于控制密文的增长。

3.4 深度乘法运算

        为了计算 x^3,我们首先计算 x^2 并重新线性化。

Ciphertext x3_encrypted;
print_line(__LINE__);
cout << "Compute x^2 and relinearize:" << endl;
evaluator.square(x1_encrypted, x3_encrypted);
evaluator.relinearize_inplace(x3_encrypted, relin_keys);
cout << "    + Scale of x^2 before rescale: " << log2(x3_encrypted.scale()) << " bits" << endl;

 

        这里能看到,此时尺度已经增长到 2^{80}(即初始尺度的平方)。区分重新线性化,那个改变的是密文大小和复杂度,不会改变尺度,这里就需要 rescale。
        进行 rescale 除了模数切换,尺度还减少了一个等于移除的素数(40 位素数)的因子。因此,新的尺度应接近 2^{40}。然而,注意尺度并不等于 2^{40}:这是因为 40 位素数仅接近 2^{40}

evaluator.rescale_to_next_inplace(x3_encrypted);

        现在 x3_encrypted 和 x1_encrypted 处于不同的级别,这使得我们无法将它们相乘以计算 x^3。我们可以简单地将 x1_encrypted 切换到模数切换链中的下一个参数。 然而,由于我们仍然需要将 x^3 项与 \pi(plain_coeff3)相乘,我们首先计算 \pi *x 并将其与 x^2 相乘以获得 \pi *x^3。为此,我们首先计算 \pi *x 并将其从尺度 2^{80} rescale 到接近 2^{40}

Ciphertext x1_encrypted_coeff3;
evaluator.multiply_plain(x1_encrypted, plain_coeff3, x1_encrypted_coeff3);
cout << "    + Scale of PI*x before rescale: " << log2(x1_encrypted_coeff3.scale()) << " bits" << endl;
evaluator.rescale_to_next_inplace(x1_encrypted_coeff3);
cout << "    + Scale of PI*x after rescale: " << log2(x1_encrypted_coeff3.scale()) << " bits" << endl;


        由于 x3_encrypted 和 x1_encrypted_coeff3 具有相同的确切尺度并使用相同的加密参数,我们可以将它们相乘 \pi *x^3 。我们将结果写入 x3_encrypted,重新线性化并重新缩放。请注意,尽管尺度再次接近 2^{40},但由于再次被一个素数缩放,所以不完全是 2^{40}。我们已经降到了模数切换链的最后一级。

cout << "Compute, relinearize, and rescale (PI*x)*x^2." << endl;
evaluator.multiply_inplace(x3_encrypted, x1_encrypted_coeff3);
evaluator.relinearize_inplace(x3_encrypted, relin_keys);
cout << "    + Scale of PI*x^3 before rescale: " << log2(x3_encrypted.scale()) << " bits" << endl;
evaluator.rescale_to_next_inplace(x3_encrypted);
cout << "    + Scale of PI*x^3 after rescale: " << log2(x3_encrypted.scale()) << " bits" << endl;


        接下来我们计算一次项 0.4*x 。这只需要与 plain_coeff1 进行一次 multiply_plain。我们用结果覆盖 x1_encrypted。

evaluator.multiply_plain_inplace(x1_encrypted, plain_coeff1);
evaluator.rescale_to_next_inplace(x1_encrypted);

3.5 加密参数调整

        现在我们希望计算三个项的总和。然而,有一个严重的问题:由于重新缩放时的模数切换,所有三个项使用的加密参数不同
        加密的加法和减法要求输入的尺度相同,并且加密参数(parms_id)匹配。如果不匹配,Evaluator 会抛出异常。


先看加密参数的不匹配

cout << "Parameters used by all three terms are different." << endl;
cout << "+ Modulus chain index for x3_encrypted: "
     << context.get_context_data(x3_encrypted.parms_id())->chain_index() << endl;
cout << "+ Modulus chain index for x1_encrypted: "
     << context.get_context_data(x1_encrypted.parms_id())->chain_index() << endl;
cout << "+ Modulus chain index for plain_coeff0: "
     << context.get_context_data(plain_coeff0.parms_id())->chain_index() << endl;

这里的打印的是 parms_id,由于乘法后的 rescale,这个会自动切换,所以这三个不同。

        这很容易通过使用传统的模数切换(无重新缩放)来解决。CKKS 支持像 BFV 方案一样的模数切换,允许我们在不需要时移除系数模数的部分。

parms_id_type last_parms_id = x3_encrypted.parms_id();
evaluator.mod_switch_to_inplace(x1_encrypted, last_parms_id);
evaluator.mod_switch_to_inplace(plain_coeff0, last_parms_id);

再看 尺度的不相同

        具体展开解释,先将 coeff_modulus 中的素数表示为 P_0, P_1, P_2, P_3,依次排列。P_3 用作特殊模数,不参与重新缩放。计算后的密文尺度为:

  1. x^2 的乘积的尺度为 2^{80},位于级别 2;
  2. \pi *x 的乘积的尺度为 2^{80},位于级别 2;
  3. 我们将两者重新缩放到尺度 2^{80}P_2,位于级别 1;
  4. \pi *x^3 的乘积的尺度为 \left ( 2^{80} / P_{2}\right )^{2}
  5. 我们将其重新缩放到尺度\left ( 2^{80} / P_{2}\right )^{2} / P_1,位于级别 0;
  6. 0.4*x 的乘积的尺度为 2^{80}
  7. 我们将其重新缩放到尺度 2^{80} / P_2,位于级别 1;
  8. 常数项 1 的尺度为 2^{40},位于级别 2。

虽然所有三个项的尺度大约为 2^{40}但它们的确切值不同,因此它们不能相加。
这里输出一下,供大家直观的查看:

cout << "The exact scales of all three terms are different:" << endl;
ios old_fmt(nullptr);
old_fmt.copyfmt(cout);
cout << fixed << setprecision(10);
cout << "+ Exact scale in PI*x^3: " << x3_encrypted.scale() << endl;
cout << "+ Exact scale in  0.4*x: " << x1_encrypted.scale() << endl;
cout << "+ Exact scale in      1: " << plain_coeff0.scale() << endl;
cout.copyfmt(old_fmt);

        即确切值是有一定差距的。第一种解决方法是:由于 P_2P_1 非常接近 2^{40},我们可以简单地“欺骗” Microsoft SEAL,将尺度设置为相同。例如,将 \pi *x^3 的尺度改为 2^{40} 意味着我们将 \pi *x^3 的值缩放为 2^{120} / \left ( P_2 ^ 2 * P_1 \right ),这非常接近 1。这不应该导致明显的误差
        另一种方法是将 1 编码为尺度 2^{80}/P_2,与 0.4*x 进行 multiply_plain,然后重新缩放。在这种情况下,我们需要确保以适当的加密参数(parms_id)编码 1。

这里用第一种解决:

x3_encrypted.scale() = pow(2.0, 40);
x1_encrypted.scale() = pow(2.0, 40);

3.6 相加与解密

现在所有三个密文都兼容,可以相加了。

 Ciphertext encrypted_result;
 evaluator.add(x3_encrypted, x1_encrypted, encrypted_result);
 evaluator.add_plain_inplace(encrypted_result, plain_coeff0);

下面对其进行解密解码,并输出。同时打印真实结果作为验证:

vector<double> true_result;
for (size_t i = 0; i < input.size(); i++)
{
    double x = input[i];
    true_result.push_back((3.14159265 * x * x + 0.4) * x + 1);
}
print_vector(true_result, 3, 7);

decryptor.decrypt(encrypted_result, plain_result);
vector<double> result;
encoder.decode(plain_result, result);
print_vector(result, 3, 7);

通过输出可以发现,结果基本正确(近似,有一定误差)。

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

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

相关文章

【Docker安装】Ubuntu系统下离线部署Docker环境教程

【Docker安装】Ubuntu系统下离线部署Docker环境教程 前言一、本次实践介绍1.1 本次实践规划1.2 本次实践简介二、检查本地环境2.1 检查操作系统版本2.2 检查内核版本2.3 更新软件源三、卸载Docker四、下载安装包4.1 创建目录4.2 官网下载五、部署Docker环境5.1 解压安装包5.2 复…

北京青蓝智慧科技:160个项目通过“数据要素×”大赛湖北分赛初赛

近日&#xff0c;2024年“数据要素”大赛的湖北分赛在武汉热烈开幕。 八个赛道的参赛队伍齐聚一堂&#xff0c;共同争夺数据创新先锋的殊荣。 经过激烈的角逐&#xff0c;初赛评审专家团最终评选出了160个入围项目&#xff0c;每个赛道分别有20个项目脱颖而出&#xff0c;其中…

Unity物理模块 之 2D效应器

本文仅作笔记学习和分享&#xff0c;不用做任何商业用途 本文包括但不限于unity官方手册&#xff0c;unity唐老狮等教程知识&#xff0c;如有不足还请斧正​ 1.什么是效应器 2D 效应器 - Unity 手册 2D 效应器是与 2D 碰撞器一起使用的组件&#xff0c;相当于预先编写好的插…

8月8日笔记

8月8日笔记 msf常见命令 启动MSF控制台 msfconsole: 启动MSF控制台。msfconsole -h: 显示帮助菜单。msfconsole -q: 启动MSF控制台并立即退出。 导航和管理 back: 返回上一级菜单。exit: 退出MSF控制台。banner: 显示MSF的横幅。cd: 更改工作目录。color: 开启或关闭彩色输…

深度学习任务中的 Zero-shot、One-shot 和 Few-shot 是什么?

深度学习任务中的 Zero-shot、One-shot 和 Few-shot 是什么&#xff1f; 在深度学习的任务中&#xff0c;Zero-shot、One-shot 和 Few-shot 学习是处理有限数据的三种重要方法。这些方法尤其在计算机视觉领域表现得非常突出。接下来&#xff0c;我们将详细探讨这三种学习方式&…

【ML】self-supervised Learning for speech and Image

【ML】self-supervised Learning for speech and Image 1. self-supervised Learning for speech and Image1.1 自监督学习在语音处理领域的方法及其特点1.2 自监督学习在图像处理领域的方法及其特点 2. Predictive Approach2.1 特点2.2 适用场景 3. contrastive Learning4. 语…

鸿蒙开发5.0【Debug调试】运维

Debug 介绍 本示例通过使用ohos.hidebug接口来获取当前应用内存使用情况。 )效果预览 使用说明 1.点击界面右上方Debug图标&#xff0c;弹出Debug信息界面&#xff0c;显示当前应用内存使用情况&#xff1b; 2.再次点击右上方Debug图标&#xff0c;Debug信息界面消失。 具…

Zed : 一款不容错过的编码神器

简介 “Code at the speed of thought“ — 用过之后都说好&#xff0c;名副其实&#xff5e; 不过&#xff0c;请记住它的定位是“编辑器、编辑器、编辑器”&#xff08;重要的事情说三遍&#xff09; 一切皆可配置 步入正题&#xff0c;开始配置&#xff5e;&#xff01; 配…

嵌入式学习之文件IO和标准IO

IO概述 I/O是Input/Output的缩写&#xff0c;指的是输入/输出。在计算机科学和工程领域&#xff0c;I/O是指计算机系统与外部环境或内部组件之间进行数据交换的过程和机制。 用户I/O 用户通过输入设备与计算机交互。例如&#xff0c;通过键盘输入文字、通过鼠标点击界面等。…

2007-2023年上市公司金融化程度测算数据(含原始数据+计算代码+计算结果)

2007-2023年上市公司金融化程度测算数据&#xff08;含原始数据计算代码计算结果&#xff09; 1、时间&#xff1a;2007-2023年 2、指标&#xff1a;行业代码、行业名称、stkcd、year、证券简称、是否发生ST或ST或PT、是否发生暂停上市、股票简称、成立日期、交易性金融资产、…

西门子s7通信协议

目录 西门子s7通信协议 S7协议帧结构 s7协议的使用 连接 COTP连接&#xff08;第一次握手&#xff09;报文 S7连接&#xff08;第二次握手&#xff09;报文 使用tcp五次握手进行连接 读取和写入报文格式 数据的读取 接收数据的响应 数据的写入 完整代码 西门子s7通信协议 S7C…

精通C++ STL(四):vector的模拟实现

目录 vector各函数接口总览 vector当中的成员变量介绍 默认成员函数 构造函数1 构造函数2 构造函数3 拷贝构造函数 赋值运算符重载函数 析构函数 迭代器相关函数 begin和end 容量和大小相关函数 size和capacity reserve resize empty 修改容器内容相关函数 push_back po…

pyttsx3自动化脚本经典案例

pyttsx 是一个 Python 库&#xff0c;可以将文本转换为语音。它支持多个语音引擎&#xff0c;并且可以在 Windows、Linux 和 macOS 等不同平台上运行。 pyttsx 可以用来做什么&#xff1f; 将文本转换成语音输出&#xff0c;例如将电子书朗读出来。 在语音助手或者聊天机器人中…

Linux 快速构建LAMP环境

目录 部署方式&#xff1a; 基础环境准备&#xff1a; 1.安装Apache服务 &#xff08;1&#xff09;安装Apache &#xff08;2&#xff09;安装一些Apache的扩展包 2.安装PHP语言 &#xff08;1&#xff09;下载php软件仓库 &#xff08;2&#xff09;指定php安装版本…

Linux-vim编辑器以及权限-04

我们为什么要把这两个单独拎出来讲呢&#xff1f;大家应该需要知道权限是什么,我们的linux是多用户多任务的,所以可能有许多用户可以操作,万一他们把重要的文件删了呢,所以要给他设置权限,而我们的vim编辑器也是非常重要的,用来编辑我们的文本信息,第二章我们讲到了vi,他们两个…

并行训练技术概述

继续开一个新专栏&#xff0c;这里主要收集一些并行训练的相关内容。 文章目录 并行/分布式训练概述为什么需要&#xff1f;如何实施&#xff1f; 并行/分布式训练概述 首先想要说明的是&#xff0c;并行训练和分布式训练的概念其实都能讲&#xff0c;但前者可能更侧重于技术实…

李晨晨的嵌入式学习 DAY21

今天主要也是对昨天学习的进行了补充 一&#xff0c;时间函数 1.time函数 函数原型&#xff1a;time_t time(time_t *tloc); 功能&#xff1a;获取当前时间&#xff08;自1970年1月1日&#xff08;称为Unix纪元或Epoch&#xff09;以来的秒数&#xff0c;即Unix时间戳&#x…

群晖NAS安装Video Station结合内网穿透实现远程访问本地存储的影音文件

文章目录 前言1.使用环境要求&#xff1a;2.下载群晖video station&#xff1a;3.公网访问本地群晖video station&#xff1a;4.公网条件下访问本地群晖video station5.公网条件下使用移动端&#xff08;安卓&#xff0c;ios等系统&#xff09;访问本地群晖video station 前言 …

使用 Arduino 串行绘图仪可视化实时数据

使用 Arduino 串行绘图仪可视化实时数据 Using The Arduino Serial Plotter To Visualize Real Time Data 参考&#xff1a; Arduino Docs: Using the Serial Plotter Tool (IDE v2) Arduino Docs&#xff1a;使用串行绘图仪工具 &#xff08;IDE v2&#xff09; The ADC-10-…

8.1 迭代器的概念与使用:走进 Python 的迭代世界

欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;欢迎订阅相关专栏&#xff1a; 工&#x1f497;重&#x1f497;hao&#x1f497;&#xff1a;野老杂谈 ⭐️ 全网最全IT互联网公司面试宝典&#xff1a;收集整理全网各大IT互联网公司技术、项目、HR面试真题.…