SRS流媒体服务器(5)源码分析之RTMP握手
1.概述
学习 RTMP 握手逻辑前,需明确两个核心问题:
- rtmp协议连接流程阶段
- rtmp简单握手和复杂握手区别
具体可以学习往期博客:
RTMP协议分析_rtmp与264的关系-CSDN博客
2.rtmp握手源码分析
2.1 握手入口
根据SRS流媒体服务器(4)可知,服务启动SrsServer → 初始化 SrsBufferListener → 每个 SrsBufferListener 管理一个 SrsTcpListener → SrsTcpListener 通过协程循环接受新连接 → on_tcp_client 回调到上层SrsServer
→ SrsServer::accept_client 接收新 TCP 连接 → 创建SrsRtmpConn
连接对象→SrsRtmpConn::do_cycle()协程驱动cycle()
主循环→完成握手、应用连接、媒体流传输→连接断开清理。
srs_error_t SrsRtmpConn::do_cycle()
{srs_error_t err = srs_success;// 打印RTMP客户端的IP地址和端口srs_trace("RTMP client ip=%s:%d, fd=%d", ip.c_str(), port, srs_netfd_fileno(stfd));// 设置RTMP的接收和发送超时时间rtmp->set_recv_timeout(SRS_CONSTS_RTMP_TIMEOUT);rtmp->set_send_timeout(SRS_CONSTS_RTMP_TIMEOUT);// 执行RTMP握手if ((err = rtmp->handshake()) != srs_success) {return srs_error_wrap(err, "rtmp handshake");}// 获取RTMP代理的真实客户端IP地址uint32_t rip = rtmp->proxy_real_ip();// 获取请求信息SrsRequest* req = info->req;if ((err = rtmp->connect_app(req)) != srs_success) {return srs_error_wrap(err, "rtmp connect tcUrl");}// 执行服务循环if ((err = service_cycle()) != srs_success) {err = srs_error_wrap(err, "service cycle");}srs_error_t r0 = srs_success;if ((r0 = on_disconnect()) != srs_success) {err = srs_error_wrap(err, "on disconnect %s", srs_error_desc(r0).c_str());srs_freep(r0);}// 如果客户端被重定向到其他服务器,则已经记录了该事件// If client is redirect to other servers, we already logged the event.if (srs_error_code(err) == ERROR_CONTROL_REDIRECT) {srs_error_reset(err);}return err;
}
2.2 简单和复杂握手
主要是优先尝试复杂握手,随后解析客户端发来的C0C1(并解析是否是代理,Schema1模式等)并返回S0S1S2给客户端,最后再接收C2。
Schema0是一种特殊的握手验证方式,主要为了兼容Adobe Flash Player。在 Schema0 中,Digest 固定位于 C1/S1 的第 8-71 字节(共 64 字节),剩余的 1464 字节为随机数据。这种固定位置的设计简化了验证逻辑,但安全性较低。
Schema1是更安全的握手验证方式,主要用于现代客户端(如 OBS、FFmpeg)Schema1 中,Digest 的位置由 C1 的前 4 字节(时间戳)计算得出,这种方式使得 Digest 位置不固定,提高了安全性。公式为:
digest_offset = (timestamp[0] + timestamp[1] + timestamp[2] + timestamp[3]) % 728 + 12
2.2.1 复杂握手代码示例
SrsRtmpServer::handshake() 复杂握手或简单握手
SrsComplexHandshake::handshake_with_client 读取客户端发送的c0c1数据,解析c1,
生成并发送s0s1s2数据,然后接收客户端发送的c2数据。
c1s1::parse(char* _c1s1, int size, srs_schema_type schema) 根据握手消息的schema类型,解析c1s1握手消息
c1s1_strategy_schema1::parse(char* _c1s1, int size) Schema1解密
/*** @brief 与客户端进行 RTMP 握手** 此函数用于与 RTMP 客户端进行握手,以建立连接。首先尝试复杂握手,如果失败且错误码为 ERROR_RTMP_TRY_SIMPLE_HS,则尝试简单握手。** @return srs_error_t 握手结果,成功返回 srs_success,失败返回错误码并附加中文注释。*/
srs_error_t SrsRtmpServer::handshake()
{srs_error_t err = srs_success;srs_assert(hs_bytes); SrsComplexHandshake complex_hs;// 尝试与客户端进行复杂握手,如果握手失败 则尝试简单握手 //SrsRtmpConn(xxx) -> skt = new SrsTcpConnection(c); -> io = skt;if ((err = complex_hs.handshake_with_client(hs_bytes, io)) != srs_success) {if (srs_error_code(err) == ERROR_RTMP_TRY_SIMPLE_HS) {srs_freep(err); SrsSimpleHandshake simple_hs;if ((err = simple_hs.handshake_with_client(hs_bytes, io)) != srs_success) {// 如果简单握手失败,返回错误并添加中文注释return srs_error_wrap(err, "simple handshake");}} else {// 如果复杂握手失败且错误码不是 ERROR_RTMP_TRY_SIMPLE_HS,返回错误并添加中文注释return srs_error_wrap(err, "complex handshake");}}hs_bytes->dispose(); // 释放 hs_bytes 占用的资源return err; // 返回错误码
}/*** @brief 与客户端进行复杂握手** 该函数用于与客户端进行复杂握手协议。握手过程包括读取客户端发送的c0c1数据,解析c1,* 生成并发送s0s1s2数据,然后接收客户端发送的c2数据。** @param hs_bytes 存储握手字节数据的对象指针* @param io 读写接口指针** @return 错误码,成功时返回srs_success*/
srs_error_t SrsComplexHandshake::handshake_with_client(SrsHandshakeBytes* hs_bytes, ISrsProtocolReadWriter* io)
{srs_error_t err = srs_success;ssize_t nsize;// 读取客户端发送的c0c1数据if ((err = hs_bytes->read_c0c1(io)) != srs_success) {return srs_error_wrap(err, "read c0c1");}// decode c1c1s1 c1;// 尝试使用schema0进行解析// @remark, 使用schema0是为了让Flash播放器满意if ((err = c1.parse(hs_bytes->c0c1 + 1, 1536, srs_schema0)) != srs_success) {return srs_error_wrap(err, "parse c1, schema=%d", srs_schema0);}// 尝试使用schema1进行解析if ((err = c1.c1_validate_digest(is_valid)) != srs_success || !is_valid) {}// encode s1c1s1 s1;if ((err = s1.s1_create(&c1)) != srs_success) {return srs_error_wrap(err, "create s1 from c1");}// 验证s1if ((err = s1.s1_validate_digest(is_valid)) != srs_success || !is_valid) {srs_freep(err);return srs_error_new(ERROR_RTMP_TRY_SIMPLE_HS, "verify s1 failed, try simple handshake");}c2s2 s2;if ((err = s2.s2_create(&c1)) != srs_success) {return srs_error_wrap(err, "create s2 from c1");}// 验证s2if ((err = s2.s2_validate(&c1, is_valid)) != srs_success || !is_valid) {srs_freep(err);return srs_error_new(ERROR_RTMP_TRY_SIMPLE_HS, "verify s2 failed, try simple handshake");}// 发送s0s1s2数据if ((err = hs_bytes->create_s0s1s2()) != srs_success) {return srs_error_wrap(err, "create s0s1s2");}if ((err = s1.dump(hs_bytes->s0s1s2 + 1, 1536)) != srs_success) {return srs_error_wrap(err, "dump s1");}if ((err = s2.dump(hs_bytes->s0s1s2 + 1537, 1536)) != srs_success) {return srs_error_wrap(err, "dump s2");}if ((err = io->write(hs_bytes->s0s1s2, 3073, &nsize)) != srs_success) {return srs_error_wrap(err, "write s0s1s2");}// 接收客户端发送的c2数据if ((err = hs_bytes->read_c2(io)) != srs_success) {return srs_error_wrap(err, "read c2");}c2s2 c2;if ((err = c2.parse(hs_bytes->c2, 1536)) != srs_success) {return srs_error_wrap(err, "parse c2");}// verify c2// 不验证c2,因为ffmpeg会失败// Flash播放器可以正常工作srs_trace("complex handshake success");return err;
}
/*** @brief 读取RTMP握手过程中的C0C1包* * 该函数负责从给定的协议读取器中读取C0C1包数据,并进行rtmp代理处理。*/
srs_error_t SrsHandshakeBytes::read_c0c1(ISrsProtocolReader* io)
{c0c1 = new char[1537];if ((err = io->read_fully(c0c1, 1537, &nsize)) != srs_success) {return srs_error_wrap(err, "read c0c1");}// Whether RTMP proxy, @see https://github.com/ossrs/go-oryx/wiki/RtmpProxy//如果是一个通过 RTMP 代理传输的数据包。if (uint8_t(c0c1[0]) == 0xF3) {//表示代理数据头部之后额外数据的长度。uint16_t nn = uint16_t(c0c1[1])<<8 | uint16_t(c0c1[2]);ssize_t nn_consumed = 3 + nn;// 4B client real IP.if (nn >= 4) {//提取出客户端的真实 IP 地址。proxy_real_ip = uint32_t(c0c1[3])<<24 | uint32_t(c0c1[4])<<16 | uint32_t(c0c1[5])<<8 | uint32_t(c0c1[6]);nn -= 4;}// 移除代理头部,确保后续处理时只考虑原始的 RTMP 数据。memmove(c0c1, c0c1 + nn_consumed, 1537 - nn_consumed);//从 io 中读取被移除部分的数据,填补到 c0c1 缓冲区的末尾,确保总长度仍为 1537 字节。if ((err = io->read_fully(c0c1 + 1537 - nn_consumed, nn_consumed, &nsize)) != srs_success) {return srs_error_wrap(err, "read c0c1");}}return err;
}
/*** @brief 解析c1s1握手消息** 该函数用于解析c1s1握手消息,并根据指定的schema类型选择相应的解析策略。** @param _c1s1 指向握手消息的指针* @param size 握手消息的大小,应为1536字节* @param schema 握手消息的schema类型,应为srs_schema0或srs_schema1** @return 如果解析成功,返回srs_success;否则返回相应的错误码和错误信息*/
srs_error_t c1s1::parse(char* _c1s1, int size, srs_schema_type schema)
{srs_assert(size == 1536);// 检查schema类型是否有效if (schema != srs_schema0 && schema != srs_schema1) {return srs_error_new(ERROR_RTMP_CH_SCHEMA, "parse c1 failed. invalid schema=%d", schema);}// 创建SrsBuffer对象,用于读取数据SrsBuffer stream(_c1s1, size);// 读取时间戳time = stream.read_4bytes();// 读取版本号version = stream.read_4bytes(); // client c1 version// 释放旧的payload指针srs_freep(payload);// 根据schema类型选择不同的解析策略if (schema == srs_schema0) {//schema0 是一种特定的解析方式,它针对旧版 Flash 播放器的特性进行了优化。payload = new c1s1_strategy_schema0();} else {//Schema1是更安全的握手验证方式,主要用于现代客户端(如 OBS、FFmpeg)payload = new c1s1_strategy_schema1();}// 复杂握手解析明文和密文 传入原始数据和解析后的数据大小return payload->parse(_c1s1, size);
}
/*** @brief 解析c1s1策略模式schema1** 该函数用于解析c1s1策略模式schema1的数据结构。** @param _c1s1 输入的c1s1数据指针* @param size 输入数据的大小,必须为1536字节** @return srs_error_t 类型的错误码。成功时返回 srs_success,失败时返回相应的错误码。*/
srs_error_t c1s1_strategy_schema1::parse(char* _c1s1, int size)
{srs_error_t err = srs_success;srs_assert(size == 1536);if (true) {SrsBuffer stream(_c1s1 + 8, 764);//密文if ((err = digest.parse(&stream)) != srs_success) {return srs_error_wrap(err, "parse c1 digest");}}if (true) {SrsBuffer stream(_c1s1 + 8 + 764, 764);//明文if ((err = key.parse(&stream)) != srs_success) {return srs_error_wrap(err, "parse c1 key");}}return err;
}
2.2.2 简单握手代码示例
简单握手中C1和S1从第9个字节开始都是随机数。S2是C1的复制。C2是S1的复制。S0是空包,S012回复包组成是参考C1和S2独立数据包。
/*** @brief 与客户端进行简单握手** 该函数用于与RTMP客户端进行简单握手。** @param hs_bytes 握手字节数据* @param io 读写接口** @return 返回握手结果的状态码,如果成功则返回srs_success,否则返回相应的错误状态码。*/
srs_error_t SrsSimpleHandshake::handshake_with_client(SrsHandshakeBytes* hs_bytes, ISrsProtocolReadWriter* io)
{srs_error_t err = srs_success;ssize_t nsize;// 读取客户端的C0C1if ((err = hs_bytes->read_c0c1(io)) != srs_success) {return srs_error_wrap(err, "read c0c1");}// 检查版本号,if (hs_bytes->c0c1[0] != 0x03) {return srs_error_new(ERROR_RTMP_PLAIN_REQUIRED, "only support rtmp plain text, version=%X", (uint8_t)hs_bytes->c0c1[0]);}// 创建S0S1S2if ((err = hs_bytes->create_s0s1s2(hs_bytes->c0c1 + 1)) != srs_success) {return srs_error_wrap(err, "create s0s1s2");}// 向客户端发送S0S1S2if ((err = io->write(hs_bytes->s0s1s2, 3073, &nsize)) != srs_success) {return srs_error_wrap(err, "write s0s1s2");}// 读取客户端的C2if ((err = hs_bytes->read_c2(io)) != srs_success) {return srs_error_wrap(err, "read c2");}// 打印握手成功日志srs_trace("simple handshake success.");return err;
}/*** @brief 创建S0S1S2握手字节** 该函数创建一个长度为3073字节的握手字节数组,并将其赋值给成员变量s0s1s2。** @param c1 用于生成S2部分的输入字符串* @return srs_error_t 成功时返回srs_success,失败时返回相应的错误码*/
srs_error_t SrsHandshakeBytes::create_s0s1s2(const char* c1)
{srs_error_t err = srs_success;// 如果s0s1s2已经存在,则直接返回成功if (s0s1s2) {return err;}// 为s0s1s2分配内存s0s1s2 = new char[3073];srs_random_generate(s0s1s2, 3073);// 创建一个缓冲区,用于写入s0s1s2的前9个字节// plain text required.SrsBuffer stream(s0s1s2, 9);// 向缓冲区写入第一个字节stream.write_1bytes(0x03);// 向缓冲区写入当前时间戳(4个字节)stream.write_4bytes((int32_t)::time(NULL));// 如果c0c1存在,则将c0c1的后4个字节写入缓冲区// s1 time2 copy from c1if (c0c1) {stream.write_bytes(c0c1 + 1, 4);}// 如果c1存在,则将c1复制到s0s1s2的1537到3072字节位置// if c1 specified, copy c1 to s2.// @see: https://github.com/ossrs/srs/issues/46if (c1) {memcpy(s0s1s2 + 1537, c1, 1536);}return err;
}
学习资料分享
0voice · GitHub
相关文章:
SRS流媒体服务器(5)源码分析之RTMP握手
1.概述 学习 RTMP 握手逻辑前,需明确两个核心问题: rtmp协议连接流程阶段rtmp简单握手和复杂握手区别 具体可以学习往期博客: RTMP协议分析_rtmp与264的关系-CSDN博客 2.rtmp握手源码分析 2.1 握手入口 根据SRS流媒体服务器(4)可知&am…...
内核性能测试(60s不丢包性能)
以xGAP-200-SE7K-L(双口10G)在飞腾D2000上为例(单通道最高性能约2.8Gbps) 单口测试 0口: tcp: taskset -c 4 iperf -c 1.1.1.1 -i 1 -t 60 -p 60001 taskset -c 4 iperf -s -i 1 -p 60001 udp: taskse…...

RabbitMQ高级篇-MQ的可靠性
目录 MQ的可靠性 1.如何设置数据持久化 1.1.交换机持久化 1.2.队列持久化 1.3.消息持久化 2.消息持久化 队列持久化: 消息持久化: 3.非消息持久化 非持久化队列: 非持久化消息: 4.消息的存储机制 4.1持久化消息&…...
MySQL 数据库集群部署、性能优化及高可用架构设计
MySQL 数据库集群部署、性能优化及高可用架构设计 集群部署方案 1. 主从复制架构 传统主从复制:配置一个主库(Master)和多个从库(Slave)GTID复制:基于全局事务标识符的复制,简化故障转移半同步复制:确保至少一个从库接收到数据…...

fpga系列 HDL : Microchip FPGA开发软件 Libero Soc 项目仿真示例
新建项目 项目初始界面中创建或导入设计文件: 新建HDL文件 module test (input [3:0] a,input [3:0] b,output reg [3:0] sum,output reg carry_out );always (*) begin{carry_out, sum} a b; endendmodule点击此按钮可进行项目信息的重新…...
将单链表反转【数据结构练习题】
- 第 98 篇 - Date: 2025 - 05 - 16 Author: 郑龙浩/仟墨 反转单链表(出现频率非常的高) 文章目录 反转单链表(出现频率非常的高)题目:反转一个链表思路:代码实现(第3种思路): 题目:反转一个链表 将 1->2->3->4->5->NULL反转…...

DeepSearch:WebThinker开启AI搜索研究新纪元!
1,项目简介 WebThinker 是一个深度研究智能体,使 LRMs 能够在推理过程中自主搜索网络、导航网页,并撰写研究报告。这种技术的目标是革命性的:让用户通过简单的查询就能在互联网的海量信息中进行深度搜索、挖掘和整合,从…...

springCloud/Alibaba常用中间件之Setinel实现熔断降级
文章目录 SpringCloud Alibaba:依赖版本补充Sentinel:1、下载-运行:Sentinel(1.8.6)下载sentinel:运行:Sentinel <br> 2、流控规则① 公共的测试代码以及需要使用的测试Jmeter①、流控模式1. 直接:2. 并联:3. 链路: ②、流控效果1. 快速…...
从裸机开发到实时操作系统:FreeRTOS详解与实战指南
从裸机开发到实时操作系统:FreeRTOS详解与实战指南 本文将带你从零开始,深入理解嵌入式系统中的裸机开发与实时操作系统,以FreeRTOS为例,全面剖析其核心概念、工作原理及应用场景。无论你是嵌入式新手还是希望提升技能的开发者&am…...

Deeper and Wider Siamese Networks for Real-Time Visual Tracking
现象: the backbone networks used in Siamese trackers are relatively shallow, such as AlexNet , which does not fully take advantage of the capability of modern deep neural networks. direct replacement of backbones with existing powerful archite…...
简单介绍C++中线性代数运算库Eigen
Eigen 是一个高性能的 C 模板库,专注于线性代数、矩阵和向量运算,广泛应用于科学计算、机器学习和计算机视觉等领域。以下是对 Eigen 库的详细介绍: 1. 概述 核心功能:支持矩阵、向量运算,包括基本算术、矩阵分解&…...
Python爬虫实战:研究decrypt()方法解密
1. 引言 1.1 研究背景与意义 在当今数字化时代,网络数据蕴含着巨大的价值。然而,许多网站为了保护其数据安全和商业利益,会采用各种加密手段对传输的数据进行处理。这些加密措施给数据采集工作带来了巨大挑战。网络爬虫逆向解密技术应运而生,它通过分析和破解网站的加密机…...

黑马程序员C++2024版笔记 第0章 C++入门
1.C代码的基础结构 以hello_world代码为例: 预处理指令 #include<iostream> using namespace std; 代码前2行是预处理指令,即代码编译前的准备工作。(编译是将源代码转化为可执行程序.exe文件的过程) 主函数 主函数是…...
c#定义占用固定字节长度的结构体字段
在c中,经常类似这样定义结构体: struct DEMO_STRUCT {int a;int b;char c[128]; }; 定义这个结构体,占用了136个字节的内存空间,关键的是,它的内存块是连续的,其中c占用了128个字节 然后如果想在c#中定义…...

foxmail - foxmail 启用超大附件提示密码与帐号不匹配
foxmail 启用超大附件提示密码与帐号不匹配 问题描述 在 foxmail 客户端中,启用超大附件功能,输入了正确的账号(邮箱)与密码,但是提示密码与帐号不匹配 处理策略 找到 foxmail 客户端目录/Global 目录下的 domain.i…...

Crowdfund Insider聚焦:CertiK联创顾荣辉解析Web3.0创新与安全平衡之术
近日,权威金融科技媒体Crowdfund Insider发布报道,聚焦CertiK联合创始人兼CEO顾荣辉教授在Unchained Summit的主题演讲。报道指出,顾教授的观点揭示了Web3.0生态当前面临的挑战,以及合规与技术在推动行业可持续发展中的关键作用。…...
EDR与XDR如何选择适合您的网络安全解决方案
1. 什么是EDR? 端点检测与响应(EDR) 专注于保护端点设备(如电脑、服务器、移动设备)。通过在端点安装代理软件,EDR实时监控设备活动,检测威胁并快速响应。 EDR核心功能 实时监控:…...

PowerBI链接EXCEL实现自动化报表
PowerBI链接EXCEL实现自动化报表 曾经我将工作中一天的工作缩短至2个小时,其中最关键的一步就是使用PowerBI链接Excel做成一个自动化报表,PowerBI更新源数据,Excel更新报表并且保留报表格式。 以制作一个超市销售报表为例,简单叙…...

腾讯云MCP数据智能处理:简化数据探索与分析的全流程指南
引言 在当今数据驱动的商业环境中,企业面临着海量数据处理和分析的挑战。腾讯云MCP(Managed Cloud Platform)提供的数据智能处理解决方案,为数据科学家和分析师提供了强大的工具集,能够显著简化数据探索、分析流程,并增强数据科学…...

Android framework 中间件开发(一)
在Android开发中,经常会调用到一些系统服务,这些系统服务简化了上层应用的开发,这便是中间件的作用,中间件是介于系统和应用之间的桥梁,将复杂的底层逻辑进行一层封装,供上层APP直接调用,或者将一些APP没有权限一些操作放到中间件里面来实施. 假设一个需求,通过中间件调节系统亮…...
Lua中使用module时踩过的坑
在lua中设置某个全局对象(假如对象名为LDataUser)为nil时, LDataUser并不会变成nil, 但在有些情况下设置LDataUser nil时却真变成了nil,然后会导致后续再使用LDataUser时会抛nil异常, 后来发现是使用module搞的鬼,下面看看豆包AI给的解释,还…...

MATLAB中的概率分布生成:从理论到实践
MATLAB中的概率分布生成:从理论到实践 引言 MATLAB作为一款强大的科学计算软件,在统计分析、数据模拟和概率建模方面提供了丰富的功能。本文将介绍如何使用MATLAB生成各种常见的概率分布,包括均匀分布、正态分布、泊松分布等,并…...

C# 面向对象 构造函数带参无参细节解析
继承类构造时会先调用基类构造函数,不显式调用基类构造函数时,默认调用基类无参构造函数,但如果基类没有写无参构造函数,会无法调用从而报错;此时,要么显式的调用基类构造函数,并按其格式带上参…...
轨迹误差评估完整流程总结(使用 evo 工具)
roslaunch .launch rosbag play your_dataset.bag -r 2.0 ✅ 第二步:录制估计轨迹 bash 复制编辑 rosbag record -O traj_only.bag /aft_mapped_to_init 运行一段时间后 CtrlC 停止,生成 traj_only.bag 第三步:提取估计轨迹和真值轨迹为…...
Spring Boot 跨域问题全解:原理、解决方案与最佳实践
精心整理了最新的面试资料和简历模板,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 一、跨域问题的本质 1.1 什么是跨域? 跨域(Cross-Origin)问题源于浏览器的同源策略(Same-Origin Policy&…...
vhca_id 简介,以及同 pf, vf 的关系
vhca_id 指的是 Virtual Host Channel Adapter ID(虚拟主机通道适配器编号),它是 NVIDIA(Mellanox)网络设备虚拟化架构中的一个核心概念。 它与 PF(物理功能)、VF(虚拟功能ÿ…...
LlamaIndex 第九篇 Indexing索引
索引概述 数据加载完成后,您将获得一个文档对象(Document)列表(或节点(Node)列表)。接下来需要为这些对象构建索引(Index),以便开始执行查询。 索引(Index) 是一种数据结构,能够让我们快速检索…...
微信小程序原生swiper高度自适应图片,不同屏幕适配,正方形1:1等比例图片轮播
🤵 作者:coderYYY 🧑 个人简介:前端程序媛,目前主攻web前端,后端辅助,其他技术知识也会偶尔分享🍀欢迎和我一起交流!🚀(评论和私信一般会回!!) 👉 个人专栏推荐:《前端项目教程以及代码》 ✨一、前言分析 一开始只设了图片的mode="widthFix" st…...

在 C# 中将 DataGridView 数据导出为 CSV
在此代码示例中,我们将学习如何使用 C# 代码将 DataGridView 数据导出到 CSV 文件并将其保存在文件夹中。 在这个程序中,首先,我们必须连接到数据库并从中获取数据。然后,我们将在数据网格视图中显示该数据,…...
解锁 CPU 性能天花板:多维优化策略深度剖析
在数字世界的底层战场,CPU 如同指挥千军万马的将军,掌控着程序运行的节奏与效率。无论是大型服务器应用,还是手机端的轻量化程序,CPU 性能的优化都如同解锁隐藏力量的密码,能让程序在执行效率上实现质的飞跃。本文将深…...