当前位置: 首页 > article >正文

OPENSSL-2023/10/31学习记录(单向散列函数)

简介

  • OpenSSL是一个用于TLS/SSL协议的工具包。它也是一个通用密码库。·15-5-2020 OpensSL 3.0 Alpha2 Release(支持国密sm2 sm3 sm4)
  • 包含对称加密,非对称加密,单项散列,伪随机,签名,密码交换,证书等一系列算法库
  • mysql,python,libevent

环境

安装好vs2019社区版本
下载 http://www.openssl.vip/download

安装好ubuntu 18.04系统

目标

·在windows上使用vs2019编译OpenSSL

·使用vs2019编写第一个openssl项目. ubuntu下编译OpenSSL3.0
·编写第一个linux下OpenSSL项目

Windows上编译OpenSSL3.0
.openssl.vip安装过程和工具下载

·安装vs2019 perl nasm
·生成项目文件

perl Configure VC-WIN32
perl Configure VC-WIN64A --prefix=%cd %lout

·以管理员运行控制台x64 Native Tools Command Prompt for VS 2019

· nmake
. nmake install

Linux编译openssl3.0

wget --no-check-certificate https://www.openssl.org/sourcelopenssl-3.0.0-alpha2.tar.gz

tar -xvf openssl-3.0.0-alpha2.tar.gz

cd openssl-3.0.0-alpha2

./config

·#三十二线程编译·make -j32
·#安装so库,头文件和说明文档

make install
openssl命令行/usr/local/bin

配置安装在/usr/local/ssl
头文件/usr/local/include/openssl

so库文件/usr/local/lib

(特别注意的是在linux环境下,项目引用的时候是先去环境变量里面去找的,还有的项目用到多个openssl版本,一定要指定版本)

Base64概述和应用场景

·概述
·二进制转字符串·应用场景
·邮件编码(base64)
· xml或者json存储二进制内容。网页传递数据URL
·数据库中以文本形式存放二进制数据
·可打印的比特币钱包地址base58Check(hash校验,后面再讲)

·比特币地址 bech32 (base32)

·基本学习目标:
·从0编写base16编解码算法

·理解base64原理
	·使用OpensSL BIO接口完成base64编解码

·高级目标
·理解比特币钱包地址base58原理并读懂源码

抽取比特币base58代码并测试

Openssl 接口

BIO包含了多种接口,用于控制在BIO_METHOD中的不同实现函数,包括6种filter型和8种source/sink型。

应用场景
BlO_new创建一个BIO对象
数据源:source/sink类型的BIO是数据源BIO_new(BIO_s_mem())

过滤: filter BIO就是把数据从一个BIO转换到另外一个BIO或应用接口
BIO_new(BIO_f_base64())
BIO链: 一个BIO链通常包括一个source BIO和一个或多个filter BIO
·BIO_push(b64__bio, mem_bio);

写编码,读解码 BIO_write BlO_read_ex

时间为种子的伪随机数
#include <iostream>
#include <openssl/rand.h>
#include <time.h>
using namespace std;


int test(int argc, char* argv[])
{
	cout << "First opensll code" << endl;
	time_t t = time(0);
	unsigned char buf[16] = { 0 };
	int re = RAND_bytes(buf, sizeof(buf));
	for (int i = 0; i <= sizeof(buf); i++)
	{
		cout << "["<<(int)buf[i]<<"]"<<endl;
	}
	getchar();
	return 0;
}

关于base加解密系列的笔记

base16
#include <iostream>
#include <string.h>
using namespace std;
static const  char BASE_16_ENCODE[] = "0123456789ABCDEF";
// '0'~~'9' => 48~~57       'A'~~'F' => 65~~70
static const  char BASE_16_DECODE[] = {
	-1,										//0
	-1,-1,-1,-1,-1,  -1,-1,-1,-1,-1,		//1-10
	-1,-1,-1,-1,-1,  -1,-1,-1,-1,-1,		//11-20
	-1,-1,-1,-1,-1,  -1,-1,-1,-1,-1,		//21-30
	-1,-1,-1,-1,-1,  -1,-1,-1,-1,-1,		//31-40
	-1,-1,-1,-1,-1,  -1,-1, 0, 1, 2,		//41-50
	 3, 4, 5, 6, 7,   8, 9,-1,-1,-1,		//51-60
	-1,-1,-1,-1,10,  11,12,13,14,15			//61-70
};

int Base16Encode(const unsigned char* in, int size, char* out1)
{
	for (int i = 0; i < size; i++)
	{
		char h = in[i] >> 4;	//1000 0001 >> 4 == 0000 1000
		char l = in[i] & 0x0F;	//1000 0001&0000 FFFF == 0000 0001
		out1[i * 2] = BASE_16_ENCODE[h];
		out1[i * 2 + 1] = BASE_16_ENCODE[l];
	}

	return size * 2;
}

int Base16Decode(const string& in,unsigned char* out2)
{
	//将两个字节拼成一个字节
	for (size_t i = 0; i < in.size(); i += 2)
	{
		unsigned char ch = in[i];		//高位转换的字符 ‘B'=>66->10
		unsigned char cl = in[i + 1];	//低位转换的字符 ‘B'=>50->2
		unsigned char h = BASE_16_DECODE[ch]; //转换为原来的值
		unsigned char l = BASE_16_DECODE[cl];
		//两个4位拼接成一个字节(8位)
		out2[i / 2] = (int)(h << 4 | l);
	}
	return in.size() / 2;
}

int main1(int argc, int* argv[])
{
	cout << "Test Base16" << endl;
	const unsigned  char data[] = "测试Base16";
	cout << data << endl;
	int len = sizeof(data);
	char out1[1024] = { 0 };
	unsigned char out2[1024] = { 0 };
	int re = Base16Encode(data, len, out1);
	cout << "编码后长度:"<< re <<endl<< out1 << endl;
	int Result = Base16Decode(out1, out2);
	cout << "解码:" << out2 << endl;
	getchar();
	return 0;
}
base64
#include <iostream>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
using namespace std;

int Base64Encode(const unsigned char* in, int len, char* out)
{
	if (!in || len <= 0 || !out) return 0;
	//内存源
	auto mem_bio = BIO_new(BIO_s_mem());
	if (!mem_bio) return 0;

	//Base64 filter
	auto b64_bio = BIO_new(BIO_f_base64());
	if (!b64_bio)
	{
		BIO_free(mem_bio);
		return 0;
	}
	//形成BIO链表
	//b64-------mem
	BIO_push(b64_bio, mem_bio);
	//设置属性超过64字节不添加换行符号
	BIO_set_flags(b64_bio, BIO_FLAGS_BASE64_NO_NL);

	//写入到base64 filter 进行编码,结果会传递到链表的下一个节点
	//到mem中读取结果(链表头部代表了整个链表)
	//write是编码 三字节转化位4个字节,不足就补0和=
	//编码数据每64字节会 加\n  换行 
	int re = BIO_write(b64_bio, in, len);
	if (re <= 0)
	{
		//清理整个链表结点
		BIO_free_all(b64_bio);
		return 0;
	}
	//刷新缓存,写入链表的mem
	BIO_flush(b64_bio);
	int outsize = 0;
	//从链表源内存读取
	BUF_MEM* p_data = 0;
	BIO_get_mem_ptr(b64_bio, &p_data);
	if (p_data)
	{
		memcpy(out, p_data->data,p_data->length);
		outsize = p_data->length;
	}
	BIO_free_all(b64_bio);
	return outsize;
}

int Base64Decode(const char* in, int len, unsigned char* out_data)
{
	if (!in || len <= 0 || !out_data) return 0;
	//内存源(密文)
	auto mem_bio = BIO_new_mem_buf(in , len);
	if (!mem_bio) return 0;
	//base64过滤器
	auto b64_bio = BIO_new(BIO_f_base64());
	if (!b64_bio)
	{
		BIO_free(mem_bio);
		return 0;
	}
	//形成BIO链
	BIO_push(b64_bio, mem_bio);

	//默认读取换行符做结束  注意编解码的一致性,否则不成功
	BIO_set_flags(b64_bio, BIO_FLAGS_BASE64_NO_NL);

	//读取进行解码 四字节转三字节
	size_t size = 0;
	BIO_read_ex(b64_bio, out_data,len, &size);
	BIO_free_all(b64_bio);
	return size;
}


int main2(int argc, int* argv[])
{
	cout << "Test openssl BIO base64!" << endl;
	const unsigned char data[] = "测试base64数据1111gdsfdsfas11sfgserghwrthsdfhsdfhsdfgsdf1111111335555555554867gjuihtshrtstfhsdfhsdfg";
	int len = sizeof(data);
	char out[1024] = { 0 };
	unsigned char out2[1024] = { 0 };
	cout <<"Source:" << data << endl;
	int re = Base64Encode(data, len, out);
	if (len > 0)
	{
		out[re] = '\0';
		cout << "Encode:" << out << endl;
	}
	int Result = Base64Decode(out, re, out2);
	cout <<"Decode:" << out2 << endl;
	getchar();
	return 0;
}
base58转换

编码集不同,Base58的编码集在 Base64的字符集的基础上去掉了比较容易混淆的字符。
Base58不含Base64中的0(数字0).o(大写字母o)、I(小写字母L)、l(大写字母i),以及“+”和“/”两个字符。

辗转相除法

·也就是字符1代表0,字符2代表1,字符3代表2…字符z代表57。然后回一下辗转相除法。
·如要将1234转换为58进制;
·第一步:1234除于58,商21,余数为16,查表得H·第二步:21除于58,商0,余数为21,查表得N·所以得到base58编码为:NH
·如果待转换的数前面有0怎么办?直接附加编码1来代表,有多少个就附加多少个(编码表中1代表0)

base58输出字节字数

·在编码后字符串中,是从58个字符中当中选择,需要表示的位数是

l o g 2 58 log_2 58 log258

,每一个字母代表的信息量是

l o g 2 58 log_2 58 log258

·输入的字节: (length * 8)bit
·预留的字符数量就是

( l e n g t h ∗ 8 ) / l o g 2 58 (length * 8)/ log_2 58 length8)/log258

l e n g t h ∗ ( l o g 2 256 / l o g 2 58 ) length *( log_2 256/log_2 58 ) length(log2256/log258)

. length * 1.38

#include <iostream>
#include <vector>
#include <assert.h>
using namespace std;

static const char* pszBase58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
static const int8_t mapBase58[256] = {
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1, 0, 1, 2, 3, 4, 5, 6,  7, 8,-1,-1,-1,-1,-1,-1,
    -1, 9,10,11,12,13,14,15, 16,-1,17,18,19,20,21,-1,
    22,23,24,25,26,27,28,29, 30,31,32,-1,-1,-1,-1,-1,
    -1,33,34,35,36,37,38,39, 40,41,42,43,-1,44,45,46,
    47,48,49,50,51,52,53,54, 55,56,57,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
    -1,-1,-1,-1,-1,-1,-1,-1, -1,-1,-1,-1,-1,-1,-1,-1,
};
constexpr inline bool IsSpace(char c) noexcept {
    return c == ' ' || c == '\f' || c == '\n' || c == '\r' || c == '\t' || c == '\v';
}

bool DecodeBase58(const char* psz, std::vector<unsigned char>& vch, int max_ret_len)
{
    // Skip leading spaces.
    while (*psz && IsSpace(*psz))
        psz++;
    // Skip and count leading '1's.
    int zeroes = 0;
    int length = 0;
    while (*psz == '1') {
        zeroes++;
        if (zeroes > max_ret_len) return false;
        psz++;
    }
    // Allocate enough space in big-endian base256 representation.
    int size = strlen(psz) * 733 / 1000 + 1; // log(58) / log(256), rounded up.
    std::vector<unsigned char> b256(size);
    // Process the characters.
    static_assert(std::size(mapBase58) == 256, "mapBase58.size() should be 256"); // guarantee not out of range
    while (*psz && !IsSpace(*psz)) {
        // Decode base58 character
        int carry = mapBase58[(uint8_t)*psz];
        if (carry == -1)  // Invalid b58 character
            return false;
        int i = 0;
        for (std::vector<unsigned char>::reverse_iterator it = b256.rbegin(); (carry != 0 || i < length) && (it != b256.rend()); ++it, ++i) {
            carry += 58 * (*it);
            *it = carry % 256;
            carry /= 256;
        }
        assert(carry == 0);
        length = i;
        if (length + zeroes > max_ret_len) return false;
        psz++;
    }
    // Skip trailing spaces.
    while (IsSpace(*psz))
        psz++;
    if (*psz != 0)
        return false;
    // Skip leading zeroes in b256.
    std::vector<unsigned char>::iterator it = b256.begin() + (size - length);
    // Copy result into output vector.
    vch.reserve(zeroes + (b256.end() - it));
    vch.assign(zeroes, 0x00);
    while (it != b256.end())
        vch.push_back(*(it++));
    return true;
}
std::string EncodeBase58(const unsigned char* pbegin,const unsigned char* pend)
{
    // Skip & count leading zeroes.
    int zeroes = 0;
    int length = 0;
    while (pbegin != pend && *pbegin == 0) {
        pbegin++;
        zeroes++;
    }
    // Allocate enough space in big-endian base58 representation.
    int size = (pend-pbegin) * 138 / 100 + 1; // log(256) / log(58), rounded up.
    std::vector<unsigned char> b58(size);
    // Process the bytes.
    while (pbegin != pend) {
        int carry = *pbegin;
        int i = 0;
        // Apply "b58 = b58 * 256 + ch".
        for (std::vector<unsigned char>::reverse_iterator it = b58.rbegin(); (carry != 0 || i < length) && (it != b58.rend()); it++, i++) {
            carry += 256 * (*it);
            *it = carry % 58;
            carry /= 58;
        }

        assert(carry == 0);
        length = i;
        pbegin++;
    }
    // Skip leading zeroes in base58 result.
    std::vector<unsigned char>::iterator it = b58.begin() + (size - length);
    while (it != b58.end() && *it == 0)
        it++;
    // Translate the result into a string.
    std::string str;
    str.reserve(zeroes + (b58.end() - it));
    str.assign(zeroes, '1');
    while (it != b58.end())
        str += pszBase58[*(it++)];
    return str;
}



int main3(int argc,int* argv[])
{
    unsigned char data[] = "测试base58数据";
    int len = sizeof(data);
    cout <<"Source:" << data << endl;
    std::string re = EncodeBase58(data, data + len);
    cout << "Encode:" << re << endl;
    std::vector<unsigned char > vsh;
    DecodeBase58(re.data(), vsh, 4096);
    cout <<"Decode:" << vsh.data() << endl;
    return 0;

}

单项散列函数

MD5 、SHA-1已经被攻破可以找到相同散列值的不同消息,强碰撞

文件完整(下载的软件是否被篡改)代码演示
口令加密(不可逆,随机数防字典,口令+随机数salt同密码不同hash)代码演示消息认证码(确保不被篡改)代码演示
发送者和接受者Hash(共享秘钥+消息),防错误、篡改、伪装

HMAC
	SSl安全套接字通信

伪随机数
配合非对称加密做数字签名比特币工作量证明(代码演示)

哈希列表(Hash List )验证文件完整性

哈希列表(Hash List )
读取文件,分块生成hash值

合并所有hash值再生成hash值

hash(hash(f1)…hash(f100))

Merkle Tree可信树

每个块都会有一个 Merkle树,它从叶子节点(树的底部)开始,一个叶子节点就是一个交易哈希(比特币使用双SHA256哈希)

Merkle树的好处就是一个节点可以在不下载整个块的情况下,验证是否包含某笔交易

SHA-2算法(目前相对安全的主流)

·1消息填充模512与448同余补充消息长度
·2初始化链接变量缓冲区用8个32位的寄存器(SHA256)
·取自前8个素数(2、3、5、7、11、13、17、19)的平方根的小数部分其二进制表示的前32位8*32 = 256
.SHA512是用64位寄存器
·以512位(64)分组为单位处理,进行64步循环,SHA512以1024 (128)位为一个分组
.SHA-384和SHA-512也都有6个迭代函数

模拟比特币挖矿

. block的版本version
·上一个block的hash值: prev_hash
·需要写入的交易记录的hash树的值:merkle_root

·更新时间: ntime
·当前难度:nbits
·挖矿的过程就是找到nonce使得
SHA256(SHA256(version + prev_hash + merkle_root + ntime + nbits + nonce ))

.<TARGET
TARGET可以根据当前难度求出的 0000FFFF

SHA3海绵结构

国密SM3认识

HMAC(应用在SSL) 消息认证码

image.png

image.png

image.png

#include <iostream>
#include <openssl/md5.h>
#include <fstream>
#include <thread>
#include <vector>
#include <openssl/sha.h>
#include <openssl/hmac.h>
#include <string>
using namespace std;

string GetFileListHash(string filepath)
{
	string hash;
	ifstream ifs(filepath, ios::binary);//以二进制方式打开
	if (!ifs) return hash;
	//一次读取多少字节的文件
	int block_size = 128;
	//文件buf
	unsigned char buf[1024] = { 0 };
	//hash输出
	unsigned char out[1024] = { 0 };
	while (!ifs.eof())
	{
		ifs.read((char*)buf, block_size);
		int read_size = ifs.gcount();
		if (read_size <= 0)break;
		MD5(buf, read_size, out);
		hash.insert(hash.end(), out, out + 16);
	}
	ifs.close();
	MD5((unsigned char*)hash.data(), hash.size(), out);

	return string(out,out+16);
}
void PrintHex(string data)
{
	for (auto c : data)
	{
		cout << hex << (int)(unsigned char)c;
	}
	cout << endl;
}
/*
					A               A
				  /  \            /   \
				B     C         B       C
			   / \    |        / \     / \
			  D   E   F       D   E   F   F
			 / \ / \ / \     / \ / \ / \ / \
			 1 2 3 4 5 6     1 2 3 4 5 6 5 6
*/
//文件可信树hash
string GetFileMerkleHash(string filepath)
{
	string hash;
	vector<string> hashs;
	ifstream ifs(filepath, ios::binary);
	if (!ifs) return hash;
	unsigned char buf[1024] = { 0 };
	unsigned char out[1024] = { 0 };
	int block_size = 128;
	while (!ifs.eof())
	{
		ifs.read((char*)buf, block_size);
		int read_size = ifs.gcount();
		if (read_size <= 0) break;
		SHA1(buf, read_size, out);

		//写入叶子结点的hash值
		hashs.push_back(string(out, out + 16));
	}
	while (hash.size() > 1)// ==1  表示已经计算到root节点
	{
		//不是二的倍数就要补节点(二叉树)
		if (hash.size() & 1)
		{
			hashs.push_back(hashs.back());
		}
		//把hash结果还写入hashs中
		for (size_t i = 0; i < hashs.size() / 2; i++)
		{
			//两个节点拼起来
			string tmp_hash = hashs[i * 2];
			tmp_hash += hashs[i * 2 + 1];
			SHA1((unsigned char *)tmp_hash.data(), tmp_hash.size(), out);
			//写入结果
			hashs[i] = string(out, out + 20);

		}
		if (hash.size() == 0) return hash;
		//hash列表删除上一次多余的hash值
		hashs.resize(hashs.size() / 2);
	}
	if (hashs.size() == 0) return hash;
	return hashs[0];
}

void TestBit()
{
	unsigned char data[128] = "测试比特币挖矿,模拟交易链";
	int data_size = strlen((char*)data);
	unsigned int nonce = 0;//找到nonce
	unsigned char md1[1024] = { 0 };
	unsigned char md2[1024] = { 0 };
	for (;;)
	{
		nonce++;
		memcpy(data + data_size, &nonce, sizeof(nonce));
		SHA256(data, data_size + sizeof(nonce), md1);
		SHA256(md1, 64, md2);
		//工作量,难度
		if (md2[0] == 0 && md2[1] == 0 && md2[2] == 0) break;
	}
	cout << "nonce = " << nonce << endl;
}

#define TESTA_KEY "123456"
#define HASH_SIZE 32
string GetHMACI()
{
	unsigned char data[1024] = "HMCAI";
	int data_size = strlen((char*)data);
	unsigned char mac[1024] = { 0 };
	unsigned int mac_size = 0;
	char key[1024] = TESTA_KEY;
	HMAC(EVP_sha256(), //选用的hash算法
		key, strlen(key),//共享密钥
		data, data_size, //MSG
		mac, &mac_size	//mac 消息认证码
	);
	string msg(mac, mac + mac_size);
	msg.append(data, data + data_size);
	return msg;
}

void TestHMAC()
{
	unsigned char out[1024];
	unsigned int out_size = 0;
	string msg1 = GetHMACI();
	const char* data = msg1.data() + HASH_SIZE;
	int data_size = msg1.size() - HASH_SIZE;//去掉头部

	//获取收到的消息内部的消息认证码
	string hmac(msg1.begin(), msg1.begin() + HASH_SIZE);
	//验证消息完整性和认证
	HMAC(EVP_sha256(), 
		TESTA_KEY, strlen(TESTA_KEY),
		(unsigned char*)data, data_size,
		out, &out_size
	);
	//服务端生成的消息认证码
	string smac(out, out + out_size);
	if (hmac == smac)
	{
		cout << "hmac success!no change!" << endl;
	}
	else
	{
		cout << "hmac failed!msg changed!" << endl;
	}

	//--------------------篡改消息-------------------------
	msg1[33] = 'B';
	//验证消息完整性和认证
	HMAC(EVP_sha256(),
		TESTA_KEY, strlen(TESTA_KEY),
		(unsigned char*)data, data_size,
		out, &out_size
	);
	//服务端生成的消息认证码
	string smac1(out, out + out_size);
	if (hmac == smac1)
	{
		cout << "hmac success!no change!" << endl;
	}
	else
	{
		cout << "hmac failed!msg changed!" << endl;
	}
}


int main(int argc, char* argv[])
{
	TestHMAC();
	TestBit();

	getchar();

	cout << "Test Hash!" << endl;
	unsigned char data[] = "测试md5数据";
	unsigned char out[1024] = { 0 };
	int len = sizeof(data);
	MD5_CTX c;
	MD5_Init(&c);
	MD5_Update(&c, data, len);
	MD5_Final(out, &c);
	for (int i = 0; i < 16; i++)
		cout << hex << (int)out[i];
	cout << endl;

	MD5(data, len, out);//简化版
	for (int i = 0; i < 16; i++)
		cout << hex << (int)out[i];
	cout << endl;

	string filepath = "../../Resource/first_openssl/hash.cpp";
	auto hash1 = GetFileListHash("../../Resource/first_openssl/hash.cpp");
	PrintHex(hash1);

	//校验文件完整性
	for (;;)
	{
		auto hash = GetFileListHash(filepath);
		auto thash = GetFileMerkleHash(filepath);
		cout << "HashList: ";
		PrintHex(hash);
		cout << "MerkleTree: ";
		PrintHex(thash);
		if (hash != hash1)
		{
			cout << "文件被修改";
			PrintHex(hash);
		}
		this_thread::sleep_for(2s);
	} 
	getchar();
	return 0;
}

缺失

3-11OpensSL EVP接口调用国密SM2和SHA3


http://www.kler.cn/news/354542.html

相关文章:

  • 【网络安全】-web安全-基础知识梳理
  • Junit单元测试时提示:Method should have no parameters
  • qiankun 应用之间数据传递
  • linux 开发机与测试机建立 ssh 隧道
  • Vue3的Composition组合式API(computed计算属性、watch监视属性、watchEffect函数)
  • TDengine 3.3.3.0 发布:新增 MySQL 函数与 MongoDB 数据源支持
  • 鸿蒙网络编程系列7-TLS安全数据传输单向认证示例
  • c# FrozenDictionary
  • 基于php的网上购物商场的设计
  • Java第二阶段---09类和对象---第一节 类和对象
  • 【c++ 并发编程】
  • 问题:uniApp 开发测试中的页面回弹效果的问题
  • python基于图片内容识别的微信自动发送信息(对其中逻辑修改一些可以改为自动化回复)
  • 智能伺服,精准控制:匠芯创科技M6800系列方案助力工业升级
  • Redis——事务
  • OpenAI研究揭示ChatGPT的性别和种族偏见
  • web网页---新浪网页面
  • 12月17-19日 | 2024南京软博会,持续升级中!
  • Spring 概念汇总
  • FPGA中的亚稳态