-
线上判题器对时延的敏感性到底如何?10s的时间不改变成绩嘛?
-
答案不唯一时,长度误差限为多少 也是1e-4吗? 另: 任务书什么时候更新关于此情况的描述,看到很多回答都承诺了,但截至现在还未更新。
-
队伍解散后重写创队伍,会和之前的提交内容判重吗
-
如题,如果能使用 c++17/20 的特性会习惯很多
-
1、 使用Git实现revert的完整操作步骤【转载】cid:link_02、C++中new关键字用法示例详解【转载】cid:link_13、在C# WinForm项目中跨.cs文件传值的六种常用方案【转载】cid:link_24、 一文带你搞懂Java中Error和Exception的区别【转载】cid:link_35、 Java中实现Word和TXT之间互相转换的实用教程【转载】cid:link_46、MyBatis-Plus 默认不更新null的4种方法【转载】cid:link_57、SpringBoot接口防抖的5种高效方案【转载】cid:link_68、 Java中锁分类及在什么场景下使用【转载】cid:link_79、 Java中锁的全面解析之类型、使用场景、优缺点及实现方式(示例代码【转载】cid:link_810、 Caffeine结合Redis空值缓存实现多级缓存【转载】cid:link_911、在PostgreSQL中优雅高效地进行全文检索的完整过程【转载】cid:link_1012、MySQL CDC原理解析及实现方案【转载】cid:link_1113、 PostgreSQL优雅的进行递归查询的实战指南【转载】cid:link_1214、Redis 常用命令之基础、进阶与场景化实战案例【转载】https://bbs.huaweicloud.com/forum/thread-0212720487861500817-1-1.html15、Git中忽略文件机制的.gitignore与.git/info/exclude两种方式详解【转载】https://bbs.huaweicloud.com/forum/thread-0212720487688092711-1-1.html
-
一、引言你想知道在 C# WinForm 项目中,不同.cs 文件(如 Form1.cs、About.cs、SerialConfig.cs)之间传递数据 的通用处理方法,我会结合串口调试工具的场景,讲解 6 种常用方案(从简单到规范),并说明每种方案的适用场景、优缺点和具体实现,帮你解决跨文件传值的核心问题。二、跨.cs 文件传值的 6 种核心方案(按场景分类)方案 1:构造函数传值(最基础,单向传值)适用场景:创建子窗体(如 About.cs)时,从主窗体(Form1.cs)向子窗体传递初始化数据(如串口参数)。核心原理:子窗体定义带参数的构造函数,主窗体创建子窗体实例时传入数据。实现示例(串口参数传值):12345678910111213141516171819202122232425// 1. 子窗体(About.cs):定义带参数构造函数namespace SerialDebug{ public partial class About : Form { public string PortName { get; set; } public int BaudRate { get; set; } // 带参数构造函数接收数据 public About(string portName, int baudRate) { InitializeComponent(); PortName = portName; BaudRate = baudRate; label1.Text = $"串口:{PortName},波特率:{BaudRate}"; } }} // 2. 主窗体(Form1.cs):创建子窗体时传值private void MenuItem_About_Click(object sender, EventArgs e){ About about = new About(comboBox_Serial.Text, 9600); about.Show();}优点:简单直接,新手易理解;缺点:参数过多时构造函数冗长,仅支持 “创建时一次性传值”。方案 2:实体类封装传值(推荐,多变量传值)适用场景:需要传递多个关联变量(如串口的波特率、数据位、校验位等),避免构造函数参数混乱。核心原理:创建专门的实体类(如 SerialConfig.cs)封装所有需要传递的字段,仅传递一个实体对象。实现示例:12345678910111213141516171819202122232425262728293031323334353637// 1. 实体类(SerialConfig.cs):封装所有参数namespace SerialDebug{ public class SerialConfig { public string PortName { get; set; } public int BaudRate { get; set; } public string Parity { get; set; } public int SendCount { get; set; } }} // 2. 子窗体(About.cs):接收实体对象public partial class About : Form{ public SerialConfig Config { get; set; } public About(SerialConfig config) { InitializeComponent(); Config = config; label1.Text = $"串口:{Config.PortName},波特率:{Config.BaudRate}"; }} // 3. 主窗体(Form1.cs):传实体对象private void MenuItem_About_Click(object sender, EventArgs e){ SerialConfig config = new SerialConfig() { PortName = comboBox_Serial.Text, BaudRate = 9600, Parity = "None" }; About about = new About(config); about.Show();}优点:代码整洁,扩展方便(新增参数仅改实体类);缺点:需额外创建实体类文件(但符合工业级开发规范)。方案 3:公共属性 / 字段传值(灵活,动态传值)适用场景:需要在窗体创建后动态赋值 / 修改数据(如子窗体显示后,主窗体更新子窗体的参数)。核心原理:子窗体定义公共属性 / 字段,主窗体通过窗体实例直接赋值。实现示例:12345678910111213141516171819202122232425262728293031323334353637383940414243444546// 1. 子窗体(About.cs):定义公共属性namespace SerialDebug{ public partial class About : Form { // 公共属性(可读写) public string PortName { get; set; } public int BaudRate { get; set; } // 无参构造函数(必须) public About() { InitializeComponent(); } // 刷新显示方法 public void RefreshInfo() { label1.Text = $"串口:{PortName},波特率:{BaudRate}"; } }} // 2. 主窗体(Form1.cs):动态赋值private About _aboutForm; // 保存子窗体实例private void MenuItem_About_Click(object sender, EventArgs e){ // 创建子窗体(无参) _aboutForm = new About(); // 动态赋值(创建后可随时改) _aboutForm.PortName = comboBox_Serial.Text; _aboutForm.BaudRate = 115200; // 刷新显示 _aboutForm.RefreshInfo(); _aboutForm.Show();} // 按钮点击:动态更新子窗体数据private void button_UpdateAbout_Click(object sender, EventArgs e){ if (_aboutForm != null && !_aboutForm.IsDisposed) { _aboutForm.PortName = "COM3"; _aboutForm.RefreshInfo(); }}优点:灵活,支持创建后动态传值 / 修改;缺点:易遗漏赋值,需手动保证数据完整性。方案 4:静态类 / 静态变量(全局共享数据)适用场景:多个.cs 文件需要共享全局数据(如串口工具的收发计数、全局配置),无需创建窗体实例即可访问。核心原理:创建静态类,定义静态变量,所有.cs 文件可直接读写。实现示例:123456789101112131415161718192021222324252627282930313233// 1. 静态类(GlobalData.cs):存储全局数据namespace SerialDebug{ public static class GlobalData { // 静态变量:全局共享 public static string CurrentPortName { get; set; } = "COM1"; public static int SendCount { get; set; } = 0; public static int ReceCount { get; set; } = 0; // 静态方法:全局通用逻辑 public static void ResetCount() { SendCount = 0; ReceCount = 0; } }} // 2. 主窗体(Form1.cs):读写全局变量private void button_Send_Click(object sender, EventArgs e){ // 写全局变量 GlobalData.SendCount += 1; // 读全局变量 toolStripStatusLabel1.Text = $"发送计数:{GlobalData.SendCount}";} // 3. 子窗体(About.cs):直接访问全局变量private void About_Load(object sender, EventArgs e){ label1.Text = $"当前串口:{GlobalData.CurrentPortName},接收计数:{GlobalData.ReceCount}";}优点:全局可访问,无需传参,适合共享数据;缺点:静态变量生命周期和程序一致,易导致内存泄漏(需手动重置),过度使用会降低代码可维护性。方案 5:事件委托(反向传值:子窗体→主窗体)适用场景:子窗体需要向主窗体传递数据(如 About 窗体修改了串口参数,通知主窗体更新)。核心原理:子窗体定义事件,主窗体订阅事件,子窗体触发事件时传递数据。实现示例:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152// 1. 子窗体(About.cs):定义事件和参数namespace SerialDebug{ // 自定义事件参数(传递修改后的串口参数) public class PortChangedEventArgs : EventArgs { public string NewPortName { get; set; } public int NewBaudRate { get; set; } } public partial class About : Form { // 定义事件 public event EventHandler<PortChangedEventArgs> PortChanged; public About() { InitializeComponent(); } // 子窗体按钮:触发事件,传递数据给主窗体 private void button_SavePort_Click(object sender, EventArgs e) { // 构造事件参数 PortChangedEventArgs args = new PortChangedEventArgs() { NewPortName = textBox_Port.Text, NewBaudRate = 115200 }; // 触发事件(通知主窗体) PortChanged?.Invoke(this, args); this.Close(); } }} // 2. 主窗体(Form1.cs):订阅事件,接收子窗体数据private void MenuItem_About_Click(object sender, EventArgs e){ About about = new About(); // 订阅子窗体的事件 about.PortChanged += About_PortChanged; about.Show();} // 事件处理方法:接收子窗体传递的数据private void About_PortChanged(object sender, PortChangedEventArgs e){ // 更新主窗体的串口参数 comboBox_Serial.Text = e.NewPortName; MessageBox.Show($"主窗体收到新串口:{e.NewPortName},波特率:{e.NewBaudRate}");}优点:解耦子窗体和主窗体(子窗体无需知道主窗体存在),符合 “发布 - 订阅” 设计模式;缺点:代码稍复杂,适合反向传值场景。方案 6:接口传值(规范,多窗体统一传值)适用场景:多个窗体需要按统一规则传递数据(如所有窗体都需要传递串口配置),提升代码规范性。核心原理:定义接口,规定传值的属性 / 方法,所有需要传值的窗体实现该接口。实现示例:123456789101112131415161718192021222324252627282930313233343536373839// 1. 接口(ISerialConfig.cs):定义传值规则namespace SerialDebug{ public interface ISerialConfig { string PortName { get; set; } int BaudRate { get; set; } void SetConfig(SerialConfig config); }} // 2. 子窗体(About.cs):实现接口public partial class About : Form, ISerialConfig{ public string PortName { get; set; } public int BaudRate { get; set; } public About() { InitializeComponent(); } // 实现接口方法 public void SetConfig(SerialConfig config) { PortName = config.PortName; BaudRate = config.BaudRate; label1.Text = $"串口:{PortName},波特率:{BaudRate}"; }} // 3. 主窗体(Form1.cs):按接口传值private void MenuItem_About_Click(object sender, EventArgs e){ ISerialConfig aboutForm = new About(); // 按接口声明 SerialConfig config = new SerialConfig() { PortName = "COM2", BaudRate = 9600 }; aboutForm.SetConfig(config); // 统一调用接口方法 ((Form)aboutForm).Show(); // 转为Form显示}优点:规范统一,适合多窗体、复杂项目;缺点:入门门槛高,小型项目无需使用。三、方案选择指南(按场景匹配)传值场景推荐方案主窗体→子窗体,少量参数(1-3 个)方案 1(构造函数)主窗体→子窗体,多参数(3 个以上)方案 2(实体类封装)主窗体→子窗体,动态传值 / 修改方案 3(公共属性)多个.cs 文件共享全局数据方案 4(静态类)子窗体→主窗体,反向传值方案 5(事件委托)多窗体统一规则传值,大型项目方案 6(接口)四、关键注意事项(避坑)命名空间一致:所有.cs 文件的命名空间必须相同(如SerialDebug),否则无法识别类 / 属性;空值校验:传值时校验空值(如if (!string.IsNullOrEmpty(PortName))),避免空引用异常;类型转换容错:字符串转数字时用int.TryParse,避免非数字输入崩溃;窗体释放校验:动态传值时校验子窗体是否已关闭(if (aboutForm != null && !aboutForm.IsDisposed))。五、总结关键点回顾单向传值(主→子):优先用构造函数(少量参数) 或实体类(多参数),简单 / 规范;动态传值:用公共属性,支持创建后修改;反向传值(子→主):用事件委托,解耦窗体依赖;全局共享:用静态类,适合全局数据;大型项目:用接口,提升代码规范性。跨.cs 文件传值的核心是 “根据场景选择合适的封装方式”,小型串口工具优先用实体类 + 构造函数,既能满足需求,又保证代码整洁;复杂场景再考虑事件 / 接口方案。
-
一.什么是new(What)1.new是一个关键字,用于开辟空间,开辟的空间在堆上,而一般声明的变量存放在栈上;2.new得到的是一段空间的首地址。所以一般需要用指针来存放这段地址123456789new int(10);//返回new出来这块内存的地址 int *p=new int(10);//用一个指针去接受这个地址 cout << p << endl;//返回内存空间地址00995B08 cout << *p << endl;//返回初始值10 delete p;3.开辟的内存空间需要记得delete掉,否则会造成内存泄漏!delete p的时候:首先调用这个对象的析构函数,然后释放这个对象的空间。二.使用new的场景(When and Where)C++ 中的存储方法大致有:静态存储:声明变量的时候前面添加static关键字;自动存储: 这个举个例子,在一个函数A里面定义了一个变量并初始化, int a =10 ; 这个就是自动存储,a仅当A()函数活动时存在。当成许控制权回到main()时,a使用的内存将自动被释放,动态存储:数据的声明周期不完全受程序或函数的生命时间控制 ,所以C++ 中有new 来分配空间,不过由于内存不会自动释放,所以使用完之后还需要使用delete 来释放内存。所以new是为了动态内存分配而服务的。不适用的场合:频繁调用场合不适合用new,new会频繁申请和释放内存。常见的使用场景如下:1.为变量动态分配内存,包括基本数据类型变量int、double;一维数组;二维数组;2.为类对象动态分配内存,也可以为结构体分配内存(类和结构体类似);三.如何使用new(How)1.动态创建一个类对象获得一块堆内存空间;调用构造函数;返回正确的指针。有一个类class Car,构造函数是Car(),创建一个该类的对象,并开辟一块空间存储,并返回空间的首地址;123Car *Audi = new Car(); delete Audi;2. 动态创建一个基本数据类型变量获得一块堆内存空间;返回正确的指针。没有了构造函数,但是可以同样在括号内赋初值。123new int(10);//返回这个空间的首地址int *arr=new int(10);//开辟一个存放整数的存储空间,附上初值,返回一个指向该存储空间的地址(即指针)delete arr;3.动态创建一个一维数组12char* p = new char[10];//开辟一个存放字符数组(包括10个元素)的空间,返回首元素的地址delete[] p;4.动态创建一个二维数组12345678910111213141516171819202122232425//开辟一个存放二维整型数组(大小为3*2)的空间,返回首元素的地址 int** pc = new int*[3];//这边表示开辟行数为3//int*[3]表示的为开辟三个存放int*元素的数组,所以才有了下一步pc[i]中对列数的开辟 for (int i = 0; i < 3; i++) { pc[i] = new int[2];//这边表示开辟列数为2 for (int j = 0; j < 2; j++) { pc[i][j] = i + j; cout <<pc[i][j] << " "; } cout << endl; }//或者使用另一种方法int(*pc)[2] = new int[3][2];//创建数组指针pc,注:数组指针与二级指针不一样for (int i = 0; i < 3; i++) { for (int j = 0; j < 2; j++) { pc[i][j] = i + j; cout <<pc[i][j] << " "; } cout << endl; } delete[] pc;5.动态创建一个结构体对象1234567891011121314#include<iostream>using namespace std;struct MyStruct{ int a; MyStruct* b; MyStruct(int x): a(x), b(NULL) {}; //初始化列表构造函数 函数名与类名相同是构造函数};int main() { MyStruct* my = new MyStruct(); delete my; system("pause"); return 0;}总结
-
背景:首次使用MDC610开发,进行一款定制的红外相机接入AP,有几个方向性问题需要得到指导,感谢。1,对于华为定制的摄像头来说,数据是否是通过传感器->ETH->MCAL层->COM->CM层的?2,对于非标红外相机来说,需要客服自行开发轻量级解包模块,为适配MCAL层数据,以便COM能够正常解析给CM?3,有点不理解的是,在CM配置完毕后,会有server模块和client模块?server还需要手动填参。MMC 工具生成的服务端 / 客户端测试代码,并非 COM 到 CM 数据填充的业务逻辑本身,而是华为提供的「配置验证工具代码」—— 其核心作用是验证你在 MMC 中配置的 “COM 信号→CM 结构体” 映射规则是否正确,确保真实场景下 COM 能按预期将数据精准填充到 CM;测试代码仅用于调试 / 验证,不参与实际业务中摄像头数据到 CM 的真实填充流程。对于上面的理解对么?部署CM后,需要启动COM:# 重启COM通信服务systemctl restart mdc-com-service# 重启CM功能服务(以红外相机为例)systemctl restart mdc-cm-infrared-service这样才能真正从:传感器->ETH->MCAL层->COM->CM层
-
引入协程背景有大量的异步业务逻辑, 传统的回调代码割裂, 可读性差, 不可避免的回调地狱简化编码复杂度,希望底层能够支持协程, 简化跨线程或者实现rpc的能力项目Github: CrystalNetCrystalNet支持C++20, 包括requires, 协程等特性协程框架是CrystalNet的一个底层支持协程源码路径:kernel/include/kernel/comp/Coroutines测试case: TestCoroutine.h/TestCoroutine.cpp TestPoller.h/TestPoller.cpp细节介绍封装一个通用的协程类template<typename T> class CoTask<T>封装一个阻塞等待类CoWaiter,并提供阻塞等待接口:CoTask<> Waiting(), 用于等待条件满足时唤醒协程设计封装了一个Poller,主要用于处理事件循环, 协程的suspend时候会向Poller抛异步任务调度协程, 直到在CoWaiting时永久阻塞CoTask提供GetParam让用户在协程阻塞时获取到协程句柄,方便用户在条件满足时通过协程句柄唤醒协程Poller提供SendAsync接口实现跨线程的通信(协程方式提供)// 跨线程协程消息(otherPoller也可以是自己) // req暂时只能传指针,而且会在otherChannel(可能不同线程)释放 // req/res 必须实现Release, ToString接口 template<typename ResType, typename ReqType> requires requires(ReqType req, ResType res) { // req/res必须有Release接口 req.Release(); res.Release(); // req/res必须有ToString接口 req.ToString(); res.ToString(); } CoTask<KERNEL_NS::SmartPtr<ResType, AutoDelMethods::Release>> SendToAsync(Poller &otherPoller, ReqType *req) { // 1.ptr用来回传ResType KERNEL_NS::SmartPtr<ResType *, KERNEL_NS::AutoDelMethods::CustomDelete> ptr(KERNEL_NS::KernelCastTo<ResType *>( kernel::KernelAllocMemory<KERNEL_NS::_Build::TL>(sizeof(ResType **)))); ptr.SetClosureDelegate([](void *p) { // 释放packet auto castP = KERNEL_NS::KernelCastTo<ResType*>(p); if(*castP) (*castP)->Release(); KERNEL_NS::KernelFreeMemory<KERNEL_NS::_Build::TL>(castP); }); *ptr = NULL; // 设置stub => ResType的事件回调 UInt64 stub = ++_maxStub; KERNEL_NS::SmartPtr<KERNEL_NS::TaskParamRefWrapper, KERNEL_NS::AutoDelMethods::Release> params = KERNEL_NS::TaskParamRefWrapper::NewThreadLocal_TaskParamRefWrapper(); SubscribeStubEvent(stub, [ptr, params](KERNEL_NS::StubPollerEvent *ev) mutable { KERNEL_NS::ObjectPollerEvent<ResType> *finalEv = KernelCastTo<KERNEL_NS::ObjectPollerEvent<ResType>>(ev); // 将结果带出去 *ptr = finalEv->_obj; finalEv->_obj = NULL; // 唤醒Waiter auto &coParam = params->_params; if(coParam && coParam->_handle) coParam->_handle->ForceAwake(); }); // 发送对象事件 ObjectPollerEvent到 other auto iterChannel = _targetPollerRefChannel.find(&otherPoller); if(LIKELY(iterChannel != _targetPollerRefChannel.end())) { auto objEvent = ObjectPollerEvent<ReqType>::New_ObjectPollerEvent(stub, false, this, iterChannel->second); objEvent->_obj = req; iterChannel->second->Send(objEvent); } else { auto objEvent = ObjectPollerEvent<ReqType>::New_ObjectPollerEvent(stub, false, this, nullptr); objEvent->_obj = req; otherPoller.Push(objEvent); } // 等待 ObjectPollerEvent 的返回消息唤醒 auto poller = this; // 外部如果协程销毁兜底销毁资源 auto releaseFun = [stub, poller]() { poller->UnSubscribeStubEvent(stub); }; auto delg = KERNEL_CREATE_CLOSURE_DELEGATE(releaseFun, void); co_await KERNEL_NS::Waiting().SetDisableSuspend().GetParam(params).SetRelease(delg); if(LIKELY(params->_params)) { auto &pa = params->_params; if(pa->_errCode != Status::Success) { g_Log->Warn(LOGFMT_OBJ_TAG("waiting err:%d, stub:%llu, req:%p") , pa->_errCode, stub, req); UnSubscribeStubEvent(stub); } // 销毁waiting协程 if(pa->_handle) pa->_handle->DestroyHandle(pa->_errCode); } // 3.将消息回调中的ResType引用设置成空 auto res = *ptr; *ptr = NULL; co_return KERNEL_NS::SmartPtr<ResType, KERNEL_NS::AutoDelMethods::Release>(res); } 提供异步化工具函数: PostCaller异步编码举例代码在测试用例:TestPoller, 示例中实现了co_await 请求一个req,并返回一个resclass TestTimeoutStartup : public KERNEL_NS::IThreadStartUp { POOL_CREATE_OBJ_DEFAULT_P1(IThreadStartUp, TestTimeoutStartup); public: TestTimeoutStartup(KERNEL_NS::LibEventLoopThread * target) : _target(target) { } virtual void Run() override { KERNEL_NS::PostCaller([this]() mutable -> KERNEL_NS::CoTask<> { auto targetPoller = co_await _target->GetPoller(); auto req = HelloWorldReq::New_HelloWorldReq(); auto res = co_await targetPoller->template SendAsync<HelloWorldRes, HelloWorldReq>(req).SetTimeout(KERNEL_NS::TimeSlice::FromSeconds(5)); g_Log->Info(LOGFMT_NON_OBJ_TAG(TestTimeoutStartup, "res return")); }); } virtual void Release() override { TestTimeoutStartup::Delete_TestTimeoutStartup(this); } KERNEL_NS::LibEventLoopThread * _target; };
-
1. if语句1.1 if表达式成立(真)则语句执行,表达式不成立(假)则语句不执行。在C语言中,0为假,非0为真。if(表达式) 语句AI写代码cpp运行练习:判断输入的数字是否为奇数#include <stdio.h>int main(){ int num=0; scanf ("%d",&num); if(num %2==1) printf("输入的%d是一个奇数\n",num); return 0;}AI写代码cpp运行1.2 else 在不符合 if 中条件时,有其他执行方案,就用到 if ... else...语句if(判断条件) 语句else 语句AI写代码cpp运行接上面的例子,判断输入的数字是偶数还是奇数并打印#include <stdio.h>int main(){ int num=0; scanf("%d",&num); if(num %2==1) printf("输入的%d是奇数\n",num); else printf("输入的%d是偶数\n",num); return 0;}AI写代码cpp运行练习2,判断输入的年纪是否成年#include <stdio.h>int main(){ int age=0; scanf("%d",&age); if(age>=18) printf("输入的年纪已成年"); else printf("输入的年纪未成年"); return 0;}AI写代码cpp运行1.3 分支中包含多条语句if 和 else默认只控制一条语句,如果要控制多条语句,则要用 { } 将其括起来。这个块也叫程序块或者复合语句。接上面成年和未成年的例子#include <stdio.h>int main(){ int age=0; scanf("%d",&age); if(age>=18) { printf("输入的年纪已成年"); printf("开启下一阶段"); } else { printf("输入的年纪未成年"); printf("继续努力吧"); } return 0;}AI写代码cpp运行1.4嵌套if在 if...else... 语句中,else可以和另一套if 连用,构成多重判断。例如 判断输入的年纪属于哪个层次#include <stdio.h>int main(){ int age=0; scanf("%d",&age); if(age<18) printf("少年"); else if(age<40) printf("青年"); else if(age<=59) printf("中年"); else if(age<=89) printf("老年"); else printf("寿星"); return 0;}AI写代码cpp运行1.5悬空else如果有多个 if 和 else ,那么 else 和最近的 if 匹配用以下例子分析#include <stdio.h>int main(){ int a=0; int b=2; if(a==1) if(b==2) printf("Hello\n"); else printf("World\n"); return 0;}AI写代码cpp运行这段代码的结果是什么都不输出。因为最后一个else 是和第二个 if 匹配的。第一个 if 语句不成立无法执行,那么嵌套的 if 和else 也无法执行,最后什么都不打印。那么如果想 else和第一个 if 匹配,可以添加大括号修改#include <stdio.h>int main(){ int a=0; int b=2; if(a==2) { if(b==2) printf("Hello\n"); } else printf("World\n"); return 0;}AI写代码cpp运行2.关系操作符关系表达式通常返回 0(表示假) 或 1(所有非0值都表示真)比如 20>12 返回1, 12>20 返回0关系表达式常用于 if 或 while 结构注意:不要混淆 相等运算符 == 和 赋值运算符 =另外,多个关系运算符不宜连用例如i<j<kAI写代码cpp运行以上连续使用两个小于运算符。这是合法表达式,不会报错,但不会得到理想效果。由于关系运算符是从左到右计算,所以实际执行的是下面的表达式(i<j)<kAI写代码cpp运行i < j 返回的是0 或1,最后是 0 或 1 和变量 k 进行比较。如果想要判断 j 是否在 i 和 k 中间,应该做如下修改i<j&&j<kAI写代码cpp运行3.条件操作符也被称为三目运算符,需要接受三个操作数exp1 ? exp2 : exp3AI写代码cpp运行该操作符的运算逻辑:如果 exp1 为真,exp2 计算,计算的结果是整个表达式的结果;如果 exp1 为假,exp3 计算,计算的结果是整个表达式#include <stdio.h>int main(){ int a=0; int b=0; scanf("%d\n",&a); if(a>5) printf("b=3"); else printf("b=-3"); return 0;} //改造后#include <stdio.h>int main(){ int a=0; int b=0; scanf("%d\n",&a); b=a>5?3:-3; printf("%d\n",b); return 0;}AI写代码cpp运行4.逻辑操作符!:逻辑取反运算符,改变单个表达式的真假&&:逻辑与运算符,两侧的表达式都为真,才为真,否则为假| |:逻辑或运算符,两侧至少有一个表达式为真,才为真,否则为假4.1逻辑取反运算符 !如果现有一变量为假,要用该变量实现某代码,如下有一例子#include <stdio.h>int main(){ int num=0;//C语言中0为假 if(!num) printf("Hello world\n"); return 0;}AI写代码cpp运行4.2逻辑与运算符需要两边表达式均为真,整个表达式才为真运用举例 现要一变量既要大于等于a,又小于等于b,必须同时满足#include <stdio.h>int main(){ int num=0; scanf("%d",&num); if(num>=6 && num<=9) { printf("Hello World\n"); } return 0;}AI写代码cpp运行4.3逻辑或运算符该运算符要求两边都为假时,整个表达式才为假;如果至少有一个为真,那么整个表达式都为真举个例子,若输入的num满足1、3、6的任何一个,均输出Hello World#include <stdio.h>int main(){ int num=0; scanf("%d",&num); if(num==1||num==3||num==6) { printf("Hello World\n"); } return 0;}AI写代码cpp运行4.4练习:判断闰年闰年判断规则:能被4整除但不能被100整除的是闰年;能被400整除的是闰年#include <stdio.h>int main(){ int year=0; scanf("%d",&year); if(year%400==0) printf("输入的年份是闰年\n"); else if(year%4==0 && year%100!=0) printf("输入的年份是闰年\n"); return 0;}//又或者可以用以下代码#include <stdio.h>int main(){ int year=0; scanf("%d",&year); if(year%4==0 && year%100!=0 ||year%400==0) printf("输入的年份是闰年\n"); return 0;}AI写代码cpp运行4.5短路在C语言中逻辑运算符总是先对左侧的表达式求值,再对右边的表达式求值。那么如果左边的表达式满足逻辑运算符的条件,就不再对右边的表达式求值,这种情况被称为短路练习:计算代码结果#include <stdio.h>int main(){ int i=0,a=2,b=4,c=5,d=6; i=a++ && ++b && d++; printf("i=%d\n a=%d\n b=%d\n c=%d\n d=%d\n",i,a,b,c,d); return 0;}AI写代码cpp运行5.switch语句switch 是一种特殊的if else 结构,用于判断条件有多个结果的情况。它将多个 else if 改成可读性更好的形式。switch(expression){ case value1:statement case value2:statement default:statement}AI写代码cpp运行上述代码中,根据表达式expression 不同的值,执行相应的case分支。如果找不到对应的值,就执行 default 分支。注意:switch 后的expression 和case后的值都必须是整型表达式5.1 if 语句和 switch语句的对比练习:输入任意一个整数,计算除3之后的余数//用if语句#include <stdio.h>int main(){ int num=0; scanf("%d",&num); if(num%3==0) printf("余数为0\n"); else if(num%3==1) printf("余数为1\n"); else printf("余数为2\n"); return 0;}//用switch#include <stdio.h>int main(){ int num=0; scanf("%d",&num); switch(num%3) { case 0: printf("余数是0\n"); break; case 1: printf("余数是1\n"); break; case 2: printf("余数是2\n"); break; } return 0;}AI写代码cpp运行在switch语句中,需要注意:case 和后面的数字之间必须有空格;每一个case 语句的代码执行完成之后,需要加上 break ,才能跳出这个switch语句如果删去 case 中的break,代码会继续往下执行,有可能执行其他 case 的代码,直到碰见下一个 break 或者switch语句结束。但 break 也不是每个 case 语句都有,需要根据实际情况来看。练习:输入数字1-7并打印相应的星期数#include <stdio.h>int main(){ int num=0; scanf("%d",&num); switch(num) { case 1: printf("星期一"); break; case 2: printf("星期二"); break; case 3: printf("星期三"); break; case 4: printf("星期四"); break; case 5: printf("星期五"); break; case 6: printf("星期六"); break; case 7: printf("星期日"); break; } return 0;}AI写代码cpp运行也可以用工作日和休息日的表达方式#include <stdio.h>int main(){ int num=0; scanf("%d",&num); switch(num) { case 1: case 2: case 3: case 4: case 5: printf("工作日"); break; case 6: case 7: printf("休息日"); break; } return 0;}AI写代码cpp运行5.2switch语句中的default当 switch 后表达式中无法匹配代码中的 case 语句时,要么不做处理,要么在 switch 语句中加入 default子句。switch(expression){ case value1:statement case value2:statement default:statement}AI写代码cpp运行例如上面输入星期数的练习,入如果输入的不是1-7,则要提示输入错误#include <stdio.h>int main(){ int num=0; scanf("%d",&num); switch(num) { case 1: case 2: case 3: case 4: case 5: printf("工作日"); break; case 6: case 7: printf("休息日"); break; default: printf("输入错误\n"); break; } return 0;}AI写代码cpp运行另外 switch 中 case 和 default 语句顺序没有要求,只是习惯把default 放在最后处理。6.while循环和if 语句相比,两者的语句结构很相似//if语句if(表达式) 语句;//while语句while(表达式) 语句;//循环体如果想包含更多语句,可以加大括号AI写代码cpp运行对于 while 来讲,首先上来是执行表达式,表达式的值为0,循环直接结束;表达式的值不为0,则执行循环语句,语句执行完后再继续判断,是否进行下一次判断举个例子:在屏幕上打印1-10的值#include <stdio.h>int main(){ int i=1; while(i<=10) { printf("%d",i); i++; } return 0;}AI写代码cpp运行练习:逆序打印输入数值的顺序#include <stdio.h>int main(){ int num=0; scanf("%d",&num); while(num) { printf("%d",num%10); num/=10;//相当于num=num/10,意义在于不断剥离最后一位数字 } return 0;}AI写代码cpp运行7.for循环for 循环语法形式如下for(表达式1;表达式2;表达式3) 语句;//同样的,如果想在循环体包含更多语句,可以加上大括号AI写代码cpp运行表达式1:循环变量的初始化表达式2:循环结束条件的判断表达式3:循环变量的调整首先执行表达式1初始化变量,然后执行表达式2的判断部分,表达式2的结果如果等于0,那么循环结束;表达式2的结果如果不等于0,则执行循环语句。当执行语句全部执行后,再去执行表达式3,调整循环变量。然后再去表达式2执行判断在整个循环过程中,表达式1的变量初始化只执行一次,然后在表达式2、循环语句、表达式3中循环。练习:运用for在屏幕上打印1-10 的值#include <stdio.h>int main(){ for(num=1;num<=10;num++) printf("%d\",num); return 0;}AI写代码cpp运行将for和while 做对比两者都有初始化,判断,调整三部分。但for 的三部分比较集中,便于代码的保护。在代码较多的情况下,while 循环的三部分显得分散。练习:计算1~100之间3的倍数数字之和#include <stdio.h>int main(){ int num=0,sum=0; for(num=1;num<=100;num++) { if(num%3==0) sum=sum+num;//相等于sum+=num; } printf("%d\n",sum); return 0;}AI写代码cpp运行8.do-while循环do-while 循环结构如下,该循环体和while 、for 这种需要先判断条件再执行循环的语句不同do-while循环是先执行循环语句,再执行while 后的判断表达式。表达式为真,才会进行下一次;为假则终止。因此在do-while语句中循环体至少是执行一次的do 语句;while(表达式);AI写代码cs运行仍然用在屏幕上打印1-10练习#include <stdio.h>int main(){ int num=1; do { printf("%d\n",num); num=num+1; }while(num<=10); return 0;}AI写代码cpp运行练习2:统计输入数值的位数#include <stdio.h>int main(){ int num=0; scanf("%d",&num); int count=0; do { count++; num=num/10; }while(num); printf("%d\n",count); return 0;}AI写代码cpp运行9. while 和 for 以及 do-while 循环中 break 和 continue9.1 break举例用while 和for 练习在屏幕上打印1-10当打印完1-4之后,num =5 时,循环在break 终止,不再打印和循环break 的作用是永久终止这一层循环。while 循环#include <stdio.h>int main(){ int num=1; while(num<=10) { if(num==5) break; printf("%d\n",num); num++; } return 0;}AI写代码cpp运行for 循环//for循环#include <stdio.h>int main(){ int num=1; for(num=1;num<=10;num++) { if(num==5) break; printf("%d\n",num); } return 0;}AI写代码cpp运行9.2 continue举例while 循环//while循环#include <stdio.h>int main(){ int num=1; while(num<=10) { if(num==5) continue;//当num=5之后,执行continue:跳过num=5时后面所有代码,直接回到条件判断 //但这里在数字打印之后才对num自加,故num数值一直是5,陷入死循环 printf("%d\n",num); num++; } return 0;}AI写代码cpp运行continue 作用是跳过后面所有代码,回到条件判断处,继续进行下一次循环的判断。因此如果循环变量的调整是在continue之后的话,可能陷入死循环。 for 循环#include <stdio.h>int main(){ int num=1; for(num=1;num<=10;num++) { if(num==5) continue;//当num=5之后,执行continue:此次num=5不打印。num继续循环,加1后进入下一次判断 printf("%d\n",num); } return 0;}AI写代码cpp运行在 for 循环中 ,continue 的作用是跳过本次循环中 continue 后的代码,直接去到循环的调整部分。未来当某个条件发生的时候,本次循环无需再执行后续某些操作的时候,就可以使用continue 来实现while 和 for 代码对比 同时,do-while循环的 break和 continue 和上述一样,在此不赘述。10.goto语句goto 语句可以实现在同一个函数内跳转到设置好的标号处。在多层循环的代码中方便快速跳出。#include <stdio.h>int main(){ printf("你好世界\n"); goto next; printf("世界不好\n"); next: printf("Hello World\n");//跳过第二个printf,只打印第一个和第三个printf return 0;}AI写代码cpp运行但如果该语句使用不当,会导致在函数内部随意跳转,打乱程序执行顺序,尽量少用。使用场景举例for(...){ for(...) { for(...) { if(disaster) goto error; } }}AI写代码cpp运行在该循环中想提前退出需要使用break。但一个break 只能跳出一个for 循环。在此处想要完全跳出循环就需要使用3个break。在这种情况下使用goto更方便。11.循环嵌套练习 找出100—200之间的素数并咋屏幕上打印首先用循环找出100—200;再用2~i-1试除#include <stdio.h>int main(){ int i=0; for(i=100;1<=200;i++) { int j=0; int flag=1; for(j=2;j<i;j++) { if(i%j==0) { flag=0; break; } } if(flag==1) printf("%d\n",i); } return 0;}AI写代码cpp运行————————————————版权声明:本文为CSDN博主「H1B2L3D4」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/H1B2L3D4/article/details/154287668
-
1、简述在现代应用中,调用 RESTful API 已成为日常开发中不可或缺的一部分。无论你在开发桌面程序、Web 服务还是后台任务,HttpClient 都是 .NET 提供的官方网络请求利器。本文将带你深入了解 HttpClient 的使用方式,并通过多个实践样例帮助你快速掌握它。2、HttpClient 是什么?HttpClient 是 .NET 中用于发送 HTTP 请求和接收响应的核心类,属于命名空间:1using System.Net.Http;它支持:GET / POST / PUT / DELETE 等 HTTP 方法异步请求(基于 async/await)自定义请求头与内容类型连接复用与超时控制JSON 数据序列化与反序列化创建 HttpClient 实例最基础的创建方式如下:1var client = new HttpClient();但是要注意:不要在每次请求时 new HttpClient()!因为它会导致连接未及时释放,引起端口耗尽问题。正确的做法是:在应用生命周期内 重用 HttpClient 实例;或使用 HttpClientFactory(在 ASP.NET Core 中推荐)。3、实践样例下面我们从最常见的 GET 与 POST 请求 开始。示例 1:GET 请求12345678910111213141516171819202122using System;using System.Net.Http;using System.Threading.Tasks; class Program{ static async Task Main() { using var client = new HttpClient(); var url = "https://api.github.com/repos/dotnet/runtime"; // 设置 User-Agent,否则 GitHub API 会拒绝访问 client.DefaultRequestHeaders.Add("User-Agent", "CSharpHttpClientDemo"); var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); // 确保状态码 200-299 var content = await response.Content.ReadAsStringAsync(); Console.WriteLine("返回内容:"); Console.WriteLine(content); }}输出为 JSON 格式的仓库信息。示例 2:POST 请求(发送 JSON 数据)123456789101112131415161718192021222324using System;using System.Net.Http;using System.Text;using System.Threading.Tasks;using System.Text.Json; class Program{ static async Task Main() { using var client = new HttpClient(); var url = "https://httpbin.org/post"; var data = new { Name = "Alice", Age = 25 }; var json = JsonSerializer.Serialize(data); var content = new StringContent(json, Encoding.UTF8, "application/json"); var response = await client.PostAsync(url, content); var result = await response.Content.ReadAsStringAsync(); Console.WriteLine("响应内容:"); Console.WriteLine(result); }}该示例演示了如何:将 C# 对象序列化为 JSON;使用 StringContent 设置请求体;指定 Content-Type 为 application/json。4、其他常用操作1、设置请求头12client.DefaultRequestHeaders.Add("Authorization", "Bearer your_token_here");client.DefaultRequestHeaders.Add("Accept", "application/json");2、PUT / DELETE 请求123456// PUT 请求var putContent = new StringContent("{\"name\":\"Bob\"}", Encoding.UTF8, "application/json");var putResponse = await client.PutAsync("https://httpbin.org/put", putContent); // DELETE 请求var deleteResponse = await client.DeleteAsync("https://httpbin.org/delete");3、超时与异常处理1234567891011client.Timeout = TimeSpan.FromSeconds(10); try{ var response = await client.GetAsync("https://slowwly.robertomurray.co.uk/delay/5000/url/http://example.com"); Console.WriteLine(await response.Content.ReadAsStringAsync());}catch (TaskCanceledException){ Console.WriteLine("请求超时!");}4、反序列化 JSON 响应12345678910111213using System.Text.Json; var jsonStr = await response.Content.ReadAsStringAsync();var repoInfo = JsonSerializer.Deserialize<Repo>(jsonStr); Console.WriteLine($"项目名称:{repoInfo.name}");Console.WriteLine($"Star 数:{repoInfo.stargazers_count}"); class Repo{ public string name { get; set; } public int stargazers_count { get; set; }}5、天气查询程序 这是一个实际的 API 调用案例,使用 Open-Meteo API 查询天气:123456789101112131415161718192021222324252627282930313233using System;using System.Net.Http;using System.Text.Json;using System.Threading.Tasks; class Program{ static async Task Main() { using var client = new HttpClient(); string url = "https://api.open-meteo.com/v1/forecast?latitude=35&longitude=139¤t_weather=true"; var response = await client.GetAsync(url); response.EnsureSuccessStatusCode(); var json = await response.Content.ReadAsStringAsync(); var weather = JsonSerializer.Deserialize<WeatherResponse>(json); Console.WriteLine($"当前温度:{weather.current_weather.temperature} °C"); Console.WriteLine($"风速:{weather.current_weather.windspeed} km/h"); }} class WeatherResponse{ public CurrentWeather current_weather { get; set; }} class CurrentWeather{ public double temperature { get; set; } public double windspeed { get; set; }}运行结果示例:12当前温度:21.3 °C风速:5.2 km/h6、HttpClientFactory(进阶用法)在 ASP.NET Core 中,推荐使用 IHttpClientFactory 管理 HttpClient 实例:123456// Startup.csservices.AddHttpClient("GitHub", client =>{ client.BaseAddress = new Uri("https://api.github.com/"); client.DefaultRequestHeaders.Add("User-Agent", "MyApp");});使用时:123456789101112131415public class GitHubService{ private readonly HttpClient _client; public GitHubService(IHttpClientFactory factory) { _client = factory.CreateClient("GitHub"); } public async Task<string> GetRepoAsync(string name) { var response = await _client.GetAsync($"repos/{name}"); return await response.Content.ReadAsStringAsync(); }}优点:自动管理连接生命周期;支持命名客户端;避免 Socket 耗尽;更易于测试与扩展。功能方法GET 请求GetAsync()POST 请求PostAsync()PUT 请求PutAsync()DELETE 请求DeleteAsync()添加头部DefaultRequestHeaders.Add()设置超时client.Timeout反序列化 JSONJsonSerializer.Deserialize<T>()7、结语通过本文你学到了:如何在 C# 中使用 HttpClient 发起各种 HTTP 请求;如何发送 JSON、处理响应与异常;如何在实际项目中使用 HttpClientFactory 优化性能。建议:在生产环境中,始终重用 HttpClient 或使用 IHttpClientFactory,并注意请求超时与重试机制。
-
一、环境准备必要工具安装1. Protocol Buffers 编译器 (protoc)Protocol Buffers 是一种轻便高效的结构化数据存储格式,gRPC 使用它来定义服务接口和数据结构。你需要下载并安装 Protocol Buffers 编译器 protoc,下载地址为:https://github.com/protocolbuffers/protobuf/releases。安装完成后,确保将 protoc 添加到系统的 PATH 环境变量中,这样在命令行中就可以直接使用它。2. gRPC 相关工具(C++)对于 C++ 开发,你需要安装 gRPC 和 protobuf 库。在 Ubuntu 系统上,可以使用以下命令进行安装:sudo apt install libgrpc++-dev libprotobuf-dev protobuf-compiler grpc-plugins3. .NET 环境C# 开发需要安装 .NET SDK(版本 ≥ 6.0),你可以从 https://dotnet.microsoft.com/download 下载并安装。安装完成后,在 C# 项目中添加必要的 NuGet 包:dotnet add package Grpc.Net.Clientdotnet add package Google.Protobufdotnet add package Grpc.Tools二、定义服务接口创建 proto 文件首先,我们需要创建一个 .proto 文件来定义服务接口和数据结构。创建一个名为 sample_service.proto 的文件,内容如下:syntax = "proto3";option csharp_namespace = "SampleService.Client";package sampleservice;// 请求消息message CalculationRequest { double a = 1; double b = 2;}// 响应消息message CalculationResult { double result = 1;}// 服务定义service Calculator { // 加法运算 rpc Add (CalculationRequest) returns (CalculationResult); // 减法运算 rpc Subtract (CalculationRequest) returns (CalculationResult);}在这个 .proto 文件中,我们定义了一个名为 Calculator 的服务,包含两个方法:Add 和 Subtract,分别用于执行加法和减法运算。同时,我们还定义了请求消息 CalculationRequest 和响应消息 CalculationResult。三、C++ 服务端实现1. 生成 gRPC 代码为了根据 .proto 文件生成 C++ 代码,我们需要创建一个脚本 generate.sh:#!/bin/bashprotoc -I=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` sample_service.protoprotoc -I=. --cpp_out=. sample_service.proto运行这个脚本后,会生成以下文件:sample_service.grpc.pb.h:包含服务接口的定义。sample_service.grpc.pb.cc:服务接口的实现代码。sample_service.pb.h:消息类型的定义。sample_service.pb.cc:消息类型的实现代码。2. 实现服务逻辑创建 calculator_service.h 文件,实现服务逻辑的头文件:#include <grpcpp/grpcpp.h>#include "sample_service.grpc.pb.h"using grpc::ServerContext;using grpc::Status;using sampleservice::CalculationRequest;using sampleservice::CalculationResult;using sampleservice::Calculator;class CalculatorServiceImpl final : public Calculator::Service {public: Status Add(ServerContext* context, const CalculationRequest* request, CalculationResult* response) override; Status Subtract(ServerContext* context, const CalculationRequest* request, CalculationResult* response) override;};然后,创建 calculator_service.cpp 文件,实现具体的服务逻辑:#include "calculator_service.h"Status CalculatorServiceImpl::Add(ServerContext* context, const CalculationRequest* request, CalculationResult* response) { double result = request->a() + request->b(); response->set_result(result); return Status::OK;}Status CalculatorServiceImpl::Subtract(ServerContext* context, const CalculationRequest* request, CalculationResult* response) { double result = request->a() - request->b(); response->set_result(result); return Status::OK;}3. 实现服务端主程序创建 server.cpp 文件,实现服务端的主程序:#include <iostream>#include <memory>#include <string>#include <grpcpp/grpcpp.h>#include "calculator_service.h"using grpc::Server;using grpc::ServerBuilder;using grpc::ServerContext;using grpc::Status;void RunServer() { std::string server_address("0.0.0.0:50051"); CalculatorServiceImpl service; ServerBuilder builder; builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); builder.RegisterService(&service); std::unique_ptr<Server> server(builder.BuildAndStart()); std::cout << "Server listening on " << server_address << std::endl; server->Wait();}int main() { RunServer(); return 0;}4. 编译服务端为了编译服务端代码,我们需要创建一个 CMakeLists.txt 文件:cmake_minimum_required(VERSION 3.10)project(grpc_server)set(CMAKE_CXX_STANDARD 17)find_package(Protobuf REQUIRED)find_package(gRPC REQUIRED)add_executable(server server.cpp calculator_service.cpp sample_service.pb.cc sample_service.grpc.pb.cc)target_link_libraries(server PRIVATE gRPC::grpc++ gRPC::grpc protobuf::libprotobuf)然后,按照以下步骤进行编译和运行:mkdir build && cd buildcmake .. && make./server四、C# 客户端实现1. 创建 C# 项目使用以下命令创建一个新的 C# 控制台项目:dotnet new console -n GrpcClientcd GrpcClientdotnet add package Grpc.Net.Clientdotnet add package Google.Protobufdotnet add package Grpc.Tools2. 添加 proto 文件将之前创建的 sample_service.proto 文件复制到 C# 项目目录下,并修改 .csproj 文件,添加以下内容:<ItemGroup> <Protobuf Include="sample_service.proto" GrpcServices="Client" /></ItemGroup>3. 实现客户端修改 Program.cs 文件,实现客户端代码:using System;using System.Threading.Tasks;using Grpc.Net.Client;using SampleService.Client;class Program { static async Task Main(string[] args) { using var channel = GrpcChannel.ForAddress("http://localhost:50051"); var client = new Calculator.CalculatorClient(channel); try { // 加法示例 var addRequest = new CalculationRequest { A = 5, B = 3 }; var addResult = await client.AddAsync(addRequest); Console.WriteLine($"5 + 3 = {addResult.Result}"); // 减法示例 var subRequest = new CalculationRequest { A = 10, B = 4 }; var subResult = await client.SubtractAsync(subRequest); Console.WriteLine($"10 - 4 = {subResult.Result}"); } catch (Grpc.Core.RpcException ex) { Console.WriteLine($"RPC failed: {ex.Status.Detail}"); } }}4. 运行客户端在项目目录下,使用以下命令运行客户端:dotnet run五、测试与验证1. 启动 C++ 服务端在终端中运行编译好的服务端程序:./server服务端启动后,会监听 0.0.0.0:50051 端口。2. 运行 C# 客户端在另一个终端中,进入 C# 客户端项目目录,运行客户端程序:dotnet run如果一切正常,你将看到以下输出:5 + 3 = 810 - 4 = 6AI写代码12这表明客户端成功调用了服务端的方法,并得到了正确的结果。六、常见问题解决1. 连接失败确保服务端地址正确:检查客户端代码中指定的服务端地址和端口是否与服务端实际监听的地址和端口一致。检查防火墙设置:确保防火墙没有阻止客户端与服务端之间的通信。可以临时关闭防火墙进行测试,或者在防火墙中开放相应的端口。服务端和客户端使用相同的协议:确保客户端和服务端使用相同的协议(HTTP/HTTPS)。如果服务端使用的是不安全连接(InsecureServerCredentials),客户端也应该使用不安全连接。2. 序列化错误确保 proto 文件在服务端和客户端完全一致:.proto 文件是服务端和客户端通信的契约,必须保证两端的 .proto 文件内容完全相同。重新生成代码后清理并重新编译:如果修改了 .proto 文件,需要重新生成代码,并清理和重新编译服务端和客户端项目。3. 性能优化对于高频调用,考虑使用连接池:gRPC 本身没有提供连接池的功能,但可以通过第三方库或自定义实现来实现连接池,以减少连接建立和销毁的开销。调整 gRPC 通道参数:可以根据实际情况调整 gRPC 通道的参数,如超时时间、最大消息大小等,以提高性能。七、流程总结以下是按照步骤,用表格形式总结的基于gRPC实现跨语言通信(C++服务端、C#客户端)的流程:gRPC跨语言通信实现流程总结表步骤 任务具体内容 详细描述1. 环境准备 安装必要的工具和库 安装Protocol Buffers编译器(protoc),如在Windows系统中下载安装包并配置环境变量,使其可在命令行中直接调用;安装gRPC相关工具,包括gRPC插件(如grpc_cpp_plugin和grpc_csharp_plugin),用于生成对应语言的源代码;安装.NET环境(如.NET SDK),用于C#客户端开发,可通过官网下载对应版本安装包并安装,安装完成后可通过命令行运行dotnet --version验证安装是否成功;此外,还需安装C++开发环境(如Visual Studio),用于C++服务端的开发,安装时选择对应的开发工作负载,如“桌面开发”等2. 定义服务接口 使用.proto文件定义服务接口和数据结构 创建.proto文件来定义服务接口及数据结构,例如定义一个名为MyService的服务,其中包含一个SayHello方法,该方法接收一个HelloRequest消息类型作为参数,并返回一个HelloReply消息类型,.proto文件中会包含对应的message定义来具体描述HelloRequest和HelloReply的字段,如string name字段等3. 生成代码 根据.proto文件生成C++和C#代码 在命令行中使用protoc编译器,结合相应的gRPC插件来生成代码,对于C++,运行类似protoc --grpc_out=. --plugin=protoc-gen-grpc=grpc_cpp_plugin path/to/your/service.proto的命令,生成C++服务端的桩代码;对于C#,运行类似protoc --grpc_out=. --plugin=protoc-gen-grpc=grpc_csharp_plugin path/to/your/service.proto的命令,生成C#客户端的存根代码4. 实现服务端 在C++中实现服务端逻辑,并编译运行 根据生成的C++桩代码,在C++项目中实现服务端的具体逻辑,如实现MyService服务中的SayHello方法的业务逻辑,编写一个SayHello的重写方法,在该方法中根据输入的HelloRequest来构造HelloReply作为返回值;之后配置C++项目的编译链接环境,添加必要的gRPC、Protocol Buffers库依赖等,编译生成服务端可执行文件,并运行服务端5. 实现客户端 在C#中实现客户端代码,并运行客户端进行测试 根据生成的C#存根代码,在C#项目(如使用.NET SDK创建的控制台应用程序项目)中实现客户端逻辑,创建Channel连接到服务端地址,创建客户端实例,并调用服务端的方法,如调用SayHello方法,构造HelloRequest并发送请求,接收服务端返回的HelloReply;配置C#项目依赖,添加gRPC、Protocol Buffers相关的NuGet包引用,运行客户端进行测试6. 测试与验证 启动服务端和客户端,验证通信是否正常 先启动服务端,确保服务端处于运行状态并监听端口;然后启动客户端,客户端会发送请求到服务端,服务端处理后返回响应给客户端,观察客户端是否能正确接收到预期的响应结果,如在控制台输出“Hello, [name]”之类的响应信息来验证通信是否成功7. 问题解决 遇到问题时,根据具体情况进行排查和解决 如果服务端和客户端无法通信,可以检查网络连接是否正常,如端口是否被正确监听和连接;检查服务端和客户端的.proto文件版本是否一致,以及生成的代码是否正确;检查是否所有必要的依赖库都已正确安装和配置;通过查看日志信息、调试程序等方式来定位问题并解决,根据问题具体表现来逐步排查原因希望这个表格能帮助你清晰地梳理整个gRPC跨语言通信实现的流程。通过以上步骤,我们完成了一个完整的 gRPC 跨语言通信系统的搭建,实现了 C++ 服务端和 C# 客户端之间的通信。这个示例展示了 gRPC 的核心功能,你可以基于此扩展更复杂的业务逻辑。gRPC 的强大之处在于它的跨语言特性和高性能通信能力,非常适合微服务架构。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/Z_oioihoii/article/details/148904653
-
1、为什么需要窗口?流是无限的,而许多聚合/统计天然需要有限集合。窗口就是把无限流按时间或会话等规则切成有限桶(bucket),对每个“桶”执行聚合或自定义逻辑,从而在低延迟与有界计算之间取得平衡。 2、Keyed vs Non-Keyed 与通用编程模型Keyed 窗口(并行): stream .keyBy(...) // 必须:按 key 切分逻辑流 .window(...) // 必须:窗口分配器 [.trigger(...)] // 可选:自定义触发器 [.evictor(...)] // 可选:逐出器(禁用预聚合) [.allowedLateness(...)] // 可选:允许迟到 [.sideOutputLateData(...)] // 可选:迟到侧输出 .reduce/aggregate/apply()/process() // 必须:窗口函数 [.getSideOutput(...)] // 可选:获取侧输出 Non-Keyed 窗口(单并行度): stream .windowAll(...) [同上可选项] .reduce/aggregate/apply()/process(); 经验:优先使用 Keyed 窗口(可扩展、并行),windowAll() 仅用于确有必要的全局聚合。 3、窗口分配器(Window Assigners)3.1 滚动(Tumbling)窗口:固定大小、不重叠input.keyBy(x -> x.userId) .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5))) // 每5分钟一个窗口 .reduce(...); 支持 Duration 指定大小;支持 offset 调整对齐(如中国时区 Duration.ofHours(-8))。3.2 滑动(Sliding)窗口:固定大小、按步长滑动,可重叠input.keyBy(x -> x.userId) .window(SlidingEventTimeWindows.of(Duration.ofMinutes(10), Duration.ofMinutes(5))); // 大小10min,步长5min 步长越小,元素被复制到的窗口越多,状态与计算成倍增加。3.3 会话(Session)窗口:按“活跃-空闲-活跃”分段// 静态会话gapinput.keyBy(x -> x.userId) .window(EventTimeSessionWindows.withGap(Duration.ofMinutes(10))); // 动态会话gap(基于元素自定义)input.keyBy(x -> x.userId) .window(EventTimeSessionWindows.withDynamicGap(ev -> computeGap(ev)));会话窗口可合并:两个会话之间的间隔小于 gap 会被合并。因此它需要可合并的触发器/窗口函数。3.4 全局(Global)窗口:同一 key 的所有元素都进一个窗口input.keyBy(x -> x.userId) .window(GlobalWindows.create()) .trigger(/* 必填:自定义触发器,否则永不触发 */);4、窗口函数(Window Functions)4.1 ReduceFunction:同类型增量聚合(高效)input.keyBy(x -> x.userId) .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5))) .reduce((a, b) -> Tuple2.of(a.f0, a.f1 + b.f1)); // e.g. 按用户金额滚动累加 4.2 AggregateFunction:IN/ACC/OUT 三段式增量聚合(灵活)static class AvgAgg implements AggregateFunction<Tuple2<String, Long>, Tuple2<Long, Long>, Double> { public Tuple2<Long, Long> createAccumulator() { return Tuple2.of(0L, 0L); } public Tuple2<Long, Long> add(Tuple2<String, Long> v, Tuple2<Long, Long> acc){ return Tuple2.of(acc.f0+v.f1, acc.f1+1); } public Double getResult(Tuple2<Long, Long> acc){ return (double) acc.f0 / acc.f1; } public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b){ return Tuple2.of(a.f0+b.f0, a.f1+b.f1); }} input.keyBy(t -> t.f0) .window(SlidingEventTimeWindows.of(Duration.ofMinutes(10), Duration.ofMinutes(5))) .aggregate(new AvgAgg()); 4.3 ProcessWindowFunction:拿到所有元素 + 窗口上下文(最灵活)static class MyProc extends ProcessWindowFunction<Tuple2<String, Long>, String, String, TimeWindow> { @Override public void process(String key, Context ctx, Iterable<Tuple2<String, Long>> in, Collector<String> out) { long cnt = 0; for (var e : in) cnt++; out.collect("key=" + key + ", window=[" + ctx.window().getStart() + "," + ctx.window().getEnd() + "), count=" + cnt); }} 适合需要窗口元信息或自定义逻辑的场景,但会缓存全量元素,内存压力大。可结合 Reduce/Aggregate 实现“增量聚合 + 窗口上下文”的折中:// 组合:先 Reduce(增量最小值),再 Process 输出窗口起始时间input.keyBy(x -> x.key) .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5))) .reduce(new MinReduce(), new MinWithWindowStartProc()); 5、触发器(Triggers)作用:决定“何时触发计算、是否清空窗口内容”。TriggerResult: CONTINUE(不动作)FIRE(触发计算)PURGE(清空窗口内容)FIRE_AND_PURGE(触发后再清空)默认触发器: 事件时间窗口 → EventTimeTrigger(watermark 超过窗口结束时触发)全局窗口 → NeverTrigger(永不触发)自定义:基于计数/时间/复合规则都可以;需要自行注册定时器并在回调里返回相应 TriggerResult。 6、逐出器(Evictors)在触发后、窗口函数前/后剔除元素。内置:CountEvictor / DeltaEvictor / TimeEvictor。 注意:使用 Evictor 会禁用所有预聚合(必须传入全量元素),状态与开销显著上升;Python DataStream 暂不支持。 7、允许迟到(Allowed Lateness)与侧输出迟到元素:watermark 已超过窗口结束时间仍到达的元素。 默认:丢弃。设置允许迟到:窗口在 end < wm ≤ end + lateness 区间仍接收数据,并可能再次触发(late firing)。迟到侧输出:把过晚被丢弃的元素放到 side output。final OutputTag<Event> lateTag = new OutputTag<>("late"){}; SingleOutputStreamOperator<Result> out = input .keyBy(e -> e.key) .window(TumblingEventTimeWindows.of(Duration.ofMinutes(5))) .allowedLateness(Duration.ofMinutes(1)) // 允许迟到1分钟 .sideOutputLateData(lateTag) // 过晚数据走侧输出 .reduce(new SumReduce()); DataStream<Event> lateStream = out.getSideOutput(lateTag); // 过晚数据单独处理 会话窗口在 late firing 时,还可能引发窗口合并。下游要去重/幂等地消费“更新结果”。 8、连续窗口串联 & 水位线交互窗口输出元素的时间戳被设置为 end-1(结束时间不含)。当 operator 收到 watermark:会触发所有 maxTimestamp < wm 的窗口,并将 watermark 原样下发。因此可以串联相同大小的窗口,让上游 [0,5) 的结果落入下游 [0,5):DataStream<Integer> perKey = input .keyBy(x -> x.key) .window(TumblingEventTimeWindows.of(Duration.ofSeconds(5))) .reduce(new Sum()); DataStream<Integer> global = perKey .windowAll(TumblingEventTimeWindows.of(Duration.ofSeconds(5))) .process(new TopKProc()); 9、状态大小与性能考量元素会在所属的每个窗口各存一份。滑动窗口(大size/小step)状态容易爆炸。Reduce / Aggregate 能显著降低状态(急切聚合仅存一个累积值)。使用 Evictor 会禁用预聚合 → 状态显著增加。合理设置 allowedLateness,避免窗口被长时间保留。10、端到端可运行 Demo(事件时间 + 迟到 + 侧输出 + 组合窗口函数)说明:Java 11+,Flink 1.15+,本地可直接跑。示例用内存集合模拟两条流:订单与支付。展示: 事件时间与水位线5 分钟滚动窗口 + 允许迟到 1 分钟侧输出过晚数据AggregateFunction + ProcessWindowFunction 组合(输出窗口元信息)自定义触发器(基于计数的额外早触发)import org.apache.flink.api.common.eventtime.WatermarkStrategy;import org.apache.flink.api.common.functions.AggregateFunction;import org.apache.flink.api.java.tuple.Tuple2;import org.apache.flink.streaming.api.TimeCharacteristic; // 1.12+ 默认事件时间,无需设置import org.apache.flink.streaming.api.datastream.DataStream;import org.apache.flink.streaming.api.datastream.SingleOutputStreamOperator;import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;import org.apache.flink.streaming.api.functions.windowing.ProcessWindowFunction;import org.apache.flink.streaming.api.windowing.assigners.TumblingEventTimeWindows;import org.apache.flink.streaming.api.windowing.time.Time;import org.apache.flink.streaming.api.windowing.triggers.CountTrigger;import org.apache.flink.streaming.api.windowing.windows.TimeWindow;import org.apache.flink.util.Collector;import org.apache.flink.util.OutputTag; import java.time.Duration;import java.util.Arrays; /** 事件模型:订单(userId, amount, ts) */class Order { public String userId; public long amount; public long ts; // 事件时间戳(毫秒) public Order() {} public Order(String userId, long amount, long ts) { this.userId = userId; this.amount = amount; this.ts = ts; } @Override public String toString() { return "Order{user=" + userId + ", amt=" + amount + ", ts=" + ts + "}"; }} /** 聚合输出:每窗口的总金额与订单数 */class WindowStat { public String userId; public long sum; public long cnt; public long winStart; public long winEnd; // 左闭右开 public WindowStat() {} public WindowStat(String userId, long sum, long cnt, long winStart, long winEnd) { this.userId = userId; this.sum = sum; this.cnt = cnt; this.winStart = winStart; this.winEnd = winEnd; } @Override public String toString() { return "WindowStat{user=" + userId + ", sum=" + sum + ", cnt=" + cnt + ", window=[" + winStart + "," + winEnd + ")}"; }} /** 增量聚合(ACC= sum,cnt) */class SumCntAgg implements AggregateFunction<Order, Tuple2<Long, Long>, Tuple2<Long, Long>> { @Override public Tuple2<Long, Long> createAccumulator() { return Tuple2.of(0L, 0L); } @Override public Tuple2<Long, Long> add(Order o, Tuple2<Long, Long> acc) { return Tuple2.of(acc.f0 + o.amount, acc.f1 + 1); } @Override public Tuple2<Long, Long> getResult(Tuple2<Long, Long> acc) { return acc; } @Override public Tuple2<Long, Long> merge(Tuple2<Long, Long> a, Tuple2<Long, Long> b) { return Tuple2.of(a.f0 + b.f0, a.f1 + b.f1); }} /** 聚合后再 Process:补充 window 元信息与 key */class WithWindowInfoProc extends ProcessWindowFunction<Tuple2<Long, Long>, WindowStat, String, TimeWindow> { @Override public void process(String key, Context ctx, Iterable<Tuple2<Long, Long>> in, Collector<WindowStat> out) { Tuple2<Long, Long> acc = in.iterator().next(); out.collect(new WindowStat(key, acc.f0, acc.f1, ctx.window().getStart(), ctx.window().getEnd())); }} public class WindowsEndToEndDemo { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.setParallelism(2); // 定义事件时间的水位线策略:允许 2 秒乱序;从 Order.ts 提取时间戳 WatermarkStrategy<Order> wm = WatermarkStrategy .<Order>forBoundedOutOfOrderness(Duration.ofSeconds(2)) .withTimestampAssigner((o, ts) -> o.ts); // 构造样例数据:注意包含迟到与过晚数据 DataStream<Order> orders = env.fromCollection(Arrays.asList( new Order("alice", 20, 1_000L), new Order("alice", 30, 2_000L), new Order("bob", 10, 2_500L), new Order("alice", 40, 4_000L), new Order("bob", 50, 6_100L) // 可能落在下个窗口 )).assignTimestampsAndWatermarks(wm); // 过晚数据的侧输出标签 OutputTag<Order> lateTag = new OutputTag<>("late"){}; // 5 分钟滚动窗口 + 允许迟到 1 分钟 + 计数触发器(每来 3 条先早触发一次) SingleOutputStreamOperator<WindowStat> stats = orders .keyBy(o -> o.userId) // Keyed 窗口可并行 .window(TumblingEventTimeWindows.of(Time.minutes(5))) .allowedLateness(Time.minutes(1)) // 允许迟到 .sideOutputLateData(lateTag) // 过晚数据放侧输出 .trigger(CountTrigger.of(3)) // 额外的计数早触发(演示用) .aggregate(new SumCntAgg(), new WithWindowInfoProc()) // 组合:增量聚合 + 窗口上下文 .name("window-agg"); // 主结果 stats.print().name("main-result"); // 过晚侧输出 DataStream<Order> lateStream = stats.getSideOutput(lateTag); lateStream.print().name("late-orders"); env.execute("Flink Windows End-to-End Demo"); }}你能在这段 Demo 里看到: 事件时间 + 乱序 2s 的水位线;5 分钟滚动窗口,允许迟到 1 分钟;计数早触发(与默认的时间触发并存);侧输出接收过晚数据;AggregateFunction + ProcessWindowFunction 组合,既高效又能拿到窗口元信息;控制台会看到多次触发(主触发 + 早/迟触发)的输出差异。11、实战建议与常见坑优先事件时间:只要数据里有可靠时间戳,就用事件时间 + 水位线,避免处理时间抖动。滑动窗口慎重:size 大、step 小会造成多倍复制与状态爆炸。尽量增量聚合:优先 Reduce/Aggregate;如需窗口元信息再结合 ProcessWindowFunction。迟到处理要闭环:设置 allowedLateness 与 sideOutputLateData,下游做好更新/去重/幂等。会话窗口可能合并:注意 late firing 会导致 merge,要设计可再处理的下游逻辑。Evictor 慎用:会禁用预聚合,状态显著增加;仅在确需移除窗口内元素时启用。串联窗口:相同大小窗口可自然串联(时间戳为 end-1),上游 [0,5) 可落入下游 [0,5)。监控与排障:给关键窗口算子 name()/setDescription(),结合 Web UI/metrics/日志定位延迟与背压。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/weixin_43114209/article/details/153249959
-
实验测试有人说 C++ 比 Python快几十倍,有人说快上百倍,不如直接来进行实验测试。1. 简单计算场景任务:计算从 1 到 N 的所有整数的平方和N 取值为100万:python代码:import timedef sum_of_squares(n): total = 0 for i in range(1, n + 1): total += i * i return totalif __name__ == "__main__": N = 1000000 start = time.time() result = sum_of_squares(N) end = time.time() print("Result:", result) print("Time (Python):", end - start, "seconds")C++代码:#include <iostream>#include <chrono>using namespace std;long long sum_of_squares(int n) { long long total = 0; for (int i = 1; i <= n; i++) { total += 1LL * i * i; } return total;}int main() { int N = 1000000; auto start = chrono::high_resolution_clock::now(); long long result = sum_of_squares(N); auto end = chrono::high_resolution_clock::now(); chrono::duration<double> elapsed = end - start; cout << "Result: " << result << endl; cout << "Time (C++): " << elapsed.count() << " seconds" << endl; return 0;}运行时间:Time (Python): 0.06529355049133301 secondsTime (C++): 0.0010278 secondsAI写代码bash12C++ 的速度约是 python 的 65 倍。如果 N 取值为 1000万:输出时间:Time (C++): 0.0167507 secondsTime (Python): 0.6048719882965088 secondsC++ 的速度约是 python 的 36 倍。2. 包含IO场景的测试写一个包含100万条的信息的日志文件。python代码import timedef generate_log_file(filename, n): with open(filename, "w") as f: for i in range(n): f.write(f"[INFO] line {i}: this is a test log message\n")if __name__ == "__main__": N = 1000000 filename = "log_python.txt" start = time.perf_counter() generate_log_file(filename, N) end = time.perf_counter() print(f"Generated {N} lines into {filename}") print("Time (Python):", end - start, "seconds")C++代码#include <iostream>#include <fstream>#include <chrono>using namespace std;void generate_log_file(const string &filename, int n) { ofstream fout(filename); for (int i = 0; i < n; i++) { fout << "[INFO] line " << i << ": this is a test log message\n"; } fout.close();}int main() { int N = 1000000; string filename = "log_cpp.txt"; auto start = chrono::high_resolution_clock::now(); generate_log_file(filename, N); auto end = chrono::high_resolution_clock::now(); chrono::duration<double> elapsed = end - start; cout << "Generated " << N << " lines into " << filename << endl; cout << "Time (C++): " << elapsed.count() << " seconds" << endl; return 0;}结果Time (Python): 0.6496610003523529 secondsTime (C++): 0.442306 secondsAI写代码12受到磁盘写入速度的限制,在 IO 场景下,C++ 的速度仅为是 python 的 1.46 倍。3. 矩阵密集计算下面再测试一个矩阵相乘的计算场景。python代码,测试普通循环和使用numpy两种方式。import timeimport randomimport numpy as npN = 300 # 矩阵大小# 用 Python 列表生成矩阵A = [[random.random() for _ in range(N)] for _ in range(N)]B = [[random.random() for _ in range(N)] for _ in range(N)]# ---------- Python 三重循环 ----------start = time.perf_counter()C = [[0.0] * N for _ in range(N)]for i in range(N): for j in range(N): s = 0.0 for k in range(N): s += A[i][k] * B[k][j] C[i][j] = send = time.perf_counter()print(f"Time (Python triple loop): {end - start:.6f} seconds")# ---------- NumPy 实现 ----------A_np = np.array(A)B_np = np.array(B)start = time.perf_counter()C_np = np.dot(A_np, B_np)end = time.perf_counter()print(f"Time (NumPy): {end - start:.6f} seconds")C++代码#include <iostream>#include <vector>#include <cstdlib>#include <ctime>using namespace std;int main() { int N = 300; // 矩阵大小 vector<vector<double>> A(N, vector<double>(N)); vector<vector<double>> B(N, vector<double>(N)); vector<vector<double>> C(N, vector<double>(N, 0.0)); srand(time(0)); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { A[i][j] = rand() / (double)RAND_MAX; B[i][j] = rand() / (double)RAND_MAX; } } clock_t start = clock(); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { double s = 0.0; for (int k = 0; k < N; k++) { s += A[i][k] * B[k][j]; } C[i][j] = s; } } clock_t end = clock(); cout << "Time (C++): " << (double)(end - start) / CLOCKS_PER_SEC << " seconds\n"; return 0;}结果Time (Python triple loop): 3.641190 secondsTime (NumPy): 0.000746 secondsTime (C++): 0.207 secondsAI写代码C++ 的速度约是 python 相同写法的 17 倍,但使用numpy之后,python的速度约是C++的 277 倍。总结一般情况下,C++ 确实比 Python 会快很多,在普通计算效率上,至少有10倍以上的提升,主要原因是语言本身的差异性:C++ 是编译型语言,源代码会被编译为机器码,直接在 CPU 上运行,几乎没有额外的解释开销。Python 是解释型语言,运行时需要解释器逐行执行代码,每一步操作都要经过额外的对象管理和动态类型检查,计算效率天然落后。但是,当任务涉及文件写入、磁盘读写或网络通信 时,性能瓶颈转移到操作系统和硬件的 IO 延迟上,两者的速度差异会减小。此外,Python在借 助NumPy 等第三库之后,计算效率反而比未经优化的C++速度更快,主要原因是 Numpy 的矩阵乘法核心调用了底层 C/Fortran 的 BLAS/LAPACK 库,这些库经过几十年的优化,包含一系列优化策略:循环展开、SIMD 向量化(利用 SSE/AVX 指令集,一次处理多个数据);Cache 优化(分块算法 block matrix multiplication,减少 cache miss);多线程并行(OpenBLAS/MKL 可以利用多核 CPU 并行计算);硬件加速(在某些场景下甚至能利用 GPU)。因此,直接认为 C++ 比 Python 快是不准确的,C++ 的优化空间会比 Python 更多,但需要的操作会更繁琐。在比较运行效率时,需要考虑测试场景和代码质量。———————————————— 版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 原文链接:https://blog.csdn.net/qq1198768105/article/details/151805091
-
C++ TensorRT YOLOv8-SAHI 高性能部署指南项目介绍本项目将介绍如何在Jetson等嵌入式设备上实现YOLOv8-SAHI的高性能部署,特别是使用Int8引擎的优化方案。在Jetson Orin Nano (8GB)设备上,图像切片和批量推理的测试时间消耗小于0.05秒,对1080p视频进行切分检测和bytetrack跟踪性能接近15FPS。# 代码仓库: https://github.com/HouYanSong/tensorrtx-yolov8-sahi导出 YOLOv8 Int8 量化模型我们固定输入图像尺寸1440x1080进行切分,其中每张切分子图的大小为640x640重叠度>20%,加上原始图像一次推理对8张图像进行检测,导出Int8量化后BatchSize=8的模型。从yolov8.pt生成yolov8s.wts权重文件pip install ultralytics python gen_wts.py从yolov8s.wts导出yolov8s.engine引擎文件,BatchSize大小为8sudo apt install libeigen3-devrm -fr build cmake -S . -B build cmake --build build cd build ./yolov8_sahi -s ../weights/yolov8s.wts ../weights/yolov8s.engine s模型的参数配置模型的配置文件为include/config.h,这里我们使用yolov8s官方预训练模型,模型的输入大小为640x640总共有80个类别,并且设置模型的kBatchSize = 8,一次最多可8以推理8张图像,指定量化图片的路径导出Int8量化后的模型。#ifndef CONFIG_H #define CONFIG_H // #define USE_FP16 #define USE_INT8 #include <string> #include <vector> const static char *kInputTensorName = "images"; const static char *kOutputTensorName = "output"; const static int kNumClass = 80; const static int kBatchSize = 8; const static int kGpuId = 0; const static int kInputH = 640; const static int kInputW = 640; const static float kNmsThresh = 0.55f; const static float kConfThresh = 0.45f; const static int kMaxInputImageSize = 3000 * 3000; const static int kMaxNumOutputBbox = 1000; const std::string trtFile = "../weights/yolov8s.engine"; const std::string cacheFile = "./int8calib.table"; const std::string calibrationDataPath = "../images/"; // 存放用于 int8 量化校准的图像 const std::vector<std::string> vClassNames { "person", "bicycle", "car", "motorcycle", "airplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "couch", "potted plant", "bed", "dining table", "toilet", "tv", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush" }; #endif // CONFIG_H YOLOv8-SAHI 切分检测为了验证量化后模型精度以及Batch推理的性能,这里我们使用Int8量化后的模型直接对量化图片进行切分检测,推理命令如下:cd build ./yolov8_sahi -d ../weights/yolov8s.engine ../images/在Jetson Orin Nano (8GB)上使用Int8引擎的YOLOv8-SAHI性能表现如下:sample0102.png YOLOv8-SAHI: 1775ms sample0206.png YOLOv8-SAHI: 46ms sample0121.png YOLOv8-SAHI: 44ms sample0058.png YOLOv8-SAHI: 44ms sample0070.png YOLOv8-SAHI: 44ms sample0324.png YOLOv8-SAHI: 43ms sample0122.png YOLOv8-SAHI: 44ms sample0086.png YOLOv8-SAHI: 45ms sample0124.png YOLOv8-SAHI: 45ms sample0230.png YOLOv8-SAHI: 45ms ...可以看到模型对单张图片的推理时间小于0.5毫秒,可以达到实时检测的要求。YOLOv8-SAHI-ByteTrack 视频跟踪我们可以结合ByteTrack跟踪算法对视频文件进行实时的切分检测和跟踪,在build目录下执行:cd build ./yolov8_sahi_track ../media/c3_1080.mp4 在Jetson Orin Nano (8GB)上YOLOv8-SAHI-ByteTrack性能表现如下:Total frames: 341 Init ByteTrack! Processing frame 20 (8 fps) Processing frame 40 (11 fps) Processing frame 60 (12 fps) Processing frame 80 (12 fps) Processing frame 100 (13 fps) Processing frame 120 (13 fps) Processing frame 140 (13 fps) Processing frame 160 (14 fps) Processing frame 180 (14 fps) Processing frame 200 (14 fps) Processing frame 220 (14 fps) Processing frame 240 (14 fps) Processing frame 260 (14 fps) Processing frame 280 (14 fps) Processing frame 300 (14 fps) Processing frame 320 (14 fps) Processing frame 340 (15 fps) FPS: 15 可以看到模型在1080p的视频上切分检测的帧率接近15FPS,并且ByteTrack的跟踪效果非常优秀。小结通过本项目,开发者可以在资源受限的嵌入式设备上实现高效的YOLOv8切分检测和跟踪,特别适用于需要实时处理的边缘计算场景。
推荐直播
-
华为云码道-玩转OpenClaw,在线养虾2026/03/11 周三 19:00-21:00
刘昱,华为云高级工程师/谈心,华为云技术专家/李海仑,上海圭卓智能科技有限公司CEO
OpenClaw 火爆开发者圈,华为云码道最新推出 Skill ——开发者只需输入一句口令,即可部署一个功能完整的「小龙虾」智能体。直播带你玩转华为云码道,玩转OpenClaw
回顾中 -
华为云码道-AI时代应用开发利器2026/03/18 周三 19:00-20:00
童得力,华为云开发者生态运营总监/姚圣伟,华为云HCDE开发者专家
本次直播由华为专家带你实战应用开发,看华为云码道(CodeArts)代码智能体如何在AI时代让你的创意应用快速落地。更有华为云HCDE开发者专家带你用码道玩转JiuwenClaw,让小艺成为你的AI助理。
回顾中 -
Skill 构建 × 智能创作:基于华为云码道的 AI 内容生产提效方案2026/03/25 周三 19:00-20:00
余伟,华为云软件研发工程师/万邵业(万少),华为云HCDE开发者专家
本次直播带来两大实战:华为云码道 Skill-Creator 手把手搭建专属知识库 Skill;如何用码道提效 OpenClaw 小说文本,打造从大纲到成稿的 AI 原创小说全链路。技术干货 + OPC创作思路,一次讲透!
回顾中
热门标签