WebServer -- 架构图 && 面试题(上)

目录

🎂前言

[🌼流程图 && 架构图](#🌼流程图 && 架构图)

[1)什么是 WebServer](#1)什么是 WebServer)

2)服务器基本框架

[3)Reactor && Proactor 模式](#3)Reactor && Proactor 模式)

[4)同步 I/O 模拟Proactor模式(Linux)](#4)同步 I/O 模拟Proactor模式(Linux))

5)主从Reactor模式

[6)并发模式中的 同步读 && 异步读](#6)并发模式中的 同步读 && 异步读)

7)半同步/半反应堆

8)半同步/半异步

[9)解析报文(主从状态机 && 状态转移过程)](#9)解析报文(主从状态机 && 状态转移过程))

10)从状态机逻辑

11)响应报文

12)信号处理机制

13)日志系统

[14)GET,POST 请求下页面跳转](#14)GET,POST 请求下页面跳转)

15)架构图

16)源码目录解析

🚩面试题(上)

1)项目介绍

[为什么要做 WebServer?](#为什么要做 WebServer?)

介绍下你的项目

2)线程池

手写线程池

线程同步机制有哪些

线程池中的工作线程是一直等待吗?

线程池中工作线程处理完一个任务后的状态是?

同时2000个客户端访问,线程数不多,如何及时响应?

一个请求占用线程很长事件,影响到了接下来的请求处理,如何解决

3)并发模型

服务器使用的并发模型是?

[Reactor,Proactor,主从Reactor 模型的区别](#Reactor,Proactor,主从Reactor 模型的区别)

[为什么用 epoll,还有其他IO复用方式吗,区别是](#为什么用 epoll,还有其他IO复用方式吗,区别是)


🎂前言

本项目即将结束,手敲 ++14 篇万字博客++,画了 20 多个流程图,整理了 40 多道相关八股

最后3篇博客:架构图 && 面试题(上),面试题(下) && 八股(上),八股(下)

🌼流程图 && 架构图

所有图我都画了一遍,然后,结合画的图,对照着看一遍源码的实现

(实际上,就是对照着流程图,回顾前面写过的博客,将每一个接口联系起来)

Excalidraw | Hand-drawn look & feel • Collaborative • Secure

++哈哈哈,回看之前,自己亲手写的 11 篇博客,敲代码过程中的困惑也慢慢解开,融会贯通的感觉++

1)什么是 WebServer

2)服务器基本框架

3)Reactor && Proactor 模式

4)同步 I/O 模拟Proactor模式(Linux)

5)主从Reactor模式

6)并发模式中的 同步读 && 异步读

7)半同步/半反应堆

8)半同步/半异步

9)解析报文(主从状态机 && 状态转移过程)

10)从状态机逻辑

11)响应报文

12)信号处理机制

13)日志系统

14)GET,POST 请求下页面跳转

15)架构图

16)源码目录解析

++每个目录,我会结合(README 和 架构图)进行解析++

++总目录++

总目录,我将它拆成上下两部分👇

  1. CGImysql : 处理 ++MySQL数据库++ 相关的 CGI 程序
  2. http : 处理 ++HTTP 请求和响应++
  3. lock : 实现 ++锁机制++ ,确保线程安全
  4. log : 实现 ++日志系统++,记录服务器的运行状态
  5. root : 服务器的根目录,存放网站的 ++静态资源文件++,用于构建用户界面
  6. test_pressure : ++压力测试++
  7. threadpool : 实现 ++线程池++
  8. timer : ++定时器++
  9. LICENSE: 许可证文件,规定使用该项目的条款
  10. README.md: 项目的说明文档
  11. build.sh: 构建项目的脚本
  12. config.cppconfig.h : 存放 ++配置信息++ 的代码文件
  13. main.cpp : 主程序 ++入口文件++
  14. makefile : 用于 ++编译链接项目++ 的 Makefile 文件
  15. webserver.cppwebserver.h : 包含 ++Web服务器++ 的主要逻辑代码
    http -- I/O处理单元

CGImysql -- 数据库连接

log -- 日志系统

lock -- 锁机制

timer -- 定时器处理非活动连接

++CGImysql++

校验 && 数据库连接池

数据库连接池

  • 单例模式,保证唯一
  • list 实现连接池
  • 连接池为静态大小
  • 互斥锁实现线程安全

校验

  • HTTP请求采用POST方式
  • 登录用户名和密码校验
  • 用户注册及多线程安全

++http++

http 连接处理类

根据状态转移,通过 主从状态机 封装了 http连接类

其中,主状态机在内部调用从状态机,从状态机将处理状态和数据传给主状态机

  • 客户端发出 http 连接请求
  • 从状态机读取数据,更新自身状态和接受数据,传给主状态机
  • 主状态机根据从状态机状态,更新自身状态,决定响应请求还是继续读取

++lock++

线程同步机制包装类

多线程同步,确保任一时刻只能有一个线程进入关键代码段

  • 信号量
  • 互斥锁
  • 条件变量

++log++

同步/异步日志系统

同步 / 异步日志系统主要涉及两个模块,一个是日志模块,一个是阻塞队列模块

加入阻塞队列模块的目的:实现 异步写入日志

  • 自定义阻塞队列
  • 单例模式创建日志
  • 同步日志
  • 异步日志
  • 实现按天,超行分类

++root++

界面跳转

对 html 中的 action 行为设置标志位,将 method 设置为 POST

  • 0 注册
  • 1 登录
  • 2 登陆检测
  • 3 注册检测
  • 5 请求图片
  • 6 请求视频
  • 7 关注我

++test_pressure++

服务器压力测试

(1)

(2)

(3)

webbench -- 网站压测工具

  • 测试相同硬件不同服务的性能 && 不同硬件同一个服务的运行状况
  • 展示服务器的两项内容:每秒响应请求数 && 每秒传输数据量

++threadpool++

半同步 / 半反应堆线程池

使用一个工作队列,完全解除了主线程和工作线程的耦合关系:

主线程往工作队列插入任务,工作线程通过竞争来取得任务并执行它

  • 同步 IO 模拟 proactor 模式
  • 半同步 / 半反应堆
  • 线程池

++timer++

定时器处理非活动连接

由于非活跃连接占用连接资源(占着茅坑不拉屎),严重影响服务器性能

通过实现一个服务器定时器,处理这种非活跃连接,释放连接资源

利用 alarm 函数,周期性地出发 SIGALRM 信号

该信号处理函数利用管道通知主循环,执行链表上的定时任务

  • 统一事件源
  • 基于升序链表的定时器
  • 处理非活动连接

接着,重看一遍前面写过的博客(梳理逻辑,熟悉源码和接口<常见手撕>,为下一步搞定面试题做准备)

🚩面试题(上)

1)项目介绍

++为什么要做 WebServer?++

专业课学过C++,Linux,计网,Mysql等知识,所以想通过本项目巩固网络编程和Linux,将分散的知识串联起来,顺便入门服务器。

++介绍下你的项目++

  • 该项目通过C++,在Linux环境下开发
  • 使用webbench进行压力测试,达到上万并发量
  • 引入定时器,处理非活跃连接,及时释放连接资源,提升性能
  • alarm 函数周期性触发 SIGALRM 信号,使用管道通知主循环执行定时任务,实现统一事件源和基于升序链表的定时器
  • 采用线程池,半同步/半反应堆架构,解耦主线程和工作线程,提高并发处理能力
    (因为主线程和工作线程之前有个工作队列,所以两者间没有耦合性)
  • 引入同步/异步日志系统,实现异步写入日志功能
  • 通过主从状态机封装 http 连接类,实现对 HTTP 请求的处理
  • 实现数据库连接池,采用单例和互斥锁保证线程安全
  • CGImysql处理数据库连接,log模块记录日志,lock模块实现锁机制,timer模块处理非活动连接
  • 还支持处理大文件(包括视频文件和图片)
  • 同时支持Reactor和Proactor模式,ET / LT均支持

++补充解释👇++

利用CGI与MySQL提升网站性能(cgimysql)-数据运维技术 (dbs724.com)

2)线程池

详情请看👇

webserver 之 线程同步 && 线程池(半同步半反应堆)-CSDN博客

++手写线程池++

++a. 定义++

线程处理函数 worker() 和 执行任务函数 run() ---- 私有

只提供 构造,析构,添加任务 的公共接口

cpp 复制代码
template<typename T>
class threadpol {
    public:
        // thread_num 线程数量
        // max_requests 请求数量(请求队列中 最多允许 && 等待处理)
        // connpool 数据库连接池 指针
        threadpool(connection_pool *connpool,
                   int thread_number = 8,
                   int max_request = 10000);
        ~threadpool();

        // 请求队列 插入任务请求
        bool append(T* request);

    private:
        // 工作线程运行的函数
        // 不断从工作队列取出任务 并执行
        static void *worker(void *arg); // 声明为 static 的原因,下面构造函数解释

        void run();

    private:
        int thread_number; // 线程数

        int m_max_requests; // 请求队列最大请求书

        pthread_t *m_threads; // 描述线程池的数组,大小 m_thread_num

        std::list<T *> m_workqueue; // 请求队列

        locker m_queuelocker; // 保护请求队列的互斥锁

        sem m_queuestat; // 是否有任务需要处理

        bool m_stop; // 结束线程
        
        connection_pool *m_connPool; // 数据库连接池
};

++b. 构造++

涉及线程池的 创建和回收

pthread_create() 将类的对象作为参数,传递给 静态函数 worker()

在静态函数引用这个独享,并调用其动态方法 run()

具体地,类对象传递时用 this 指针,传递给静态函数后,转换为线程池类,并调用私有 run()

cpp 复制代码
template<typename T>
threadpool<T>::threadpool( connection_pool *connPool,
                           int thrad_number,
                           int max_requests) // 构造函数参数列表
                           :
                           m_thread_number(thread_number), // 线程数
                           m_max_requests(max_requests), // 最大请求数
                           m_stop(false), m_threads(NULL), // 结束线程 && 线程池数组
                           m_connPool(connPool) // 数据库连接池指针
{
    if (thread_number <= 0 || max_requests <= 0)
        throw std::exception();

    // 线程 id 初始化
    m_threads = new pthread_t[m_thread_number]; // 数组

    if (!m_threads)
        throw std::exception();

    for (int i = 0; i < thread_number; ++i) {
        // thread_create(线程标识符, 线程属性, worker()指针, worker()的参数)
        // 因为 worker指针,指向线程处理函数的地址,而且 worker() 作为类成员函数
        // 且指向threadpool对象的 this 指针,作为默认参数被传入 worker() 中
        // 此时会和 worker(void* arg) 的类型 void* 不匹配
        // 所以上面才将 worker() 声明为 static

        // 循环创建线程
        if (thread_create(m_threads + i, NULL, worker, this) != 0) {
            delete [] m_threads;
            throw std::exception();
        }

        // 线程分离后,不用单独回收工作线程,便于资源释放
        if (thread_detach(m_threads[i])) {
            delete [] m_threads;
            throw std::exception();
        }
    }

}

++c. 析构++

cpp 复制代码
template<typename T>
threadpool<T>::~threadpool()
{
    delete[] m_threads;
}

++d. append() 添加任务++

++list 容器++创建 请求队列

向队列添加任务时,通过 互斥锁 保证线程安全

添加完毕后,通过 信号量 提醒 "有任务要处理"

最后注意线程同步**↓↓↓**
使用了互斥锁 ++m_queuelocker.lock()++ 和 ++m_queuelocker.unlock()++ 来保护对任务队列 m_workqueue 的访问,防止多个线程同时访问引起数据竞争。

使用信号量 ++m_queuestat.post()++ 来通知空闲线程有任务需要处理,避免了一个线程获取多个任务的情况,也确保每个任务都能得到及时处理

cpp 复制代码
template<typename T>
bool threadpool<T>::append(T* request)
{
    m_queuelocker.lock(); // 关键代码段加锁

    // 根据硬件,预先设置请求队列最大值
    if (m_workqueue.size() > m_max_requests) {
        m_queuelocker.unlock();
        return false;
    }

    // 添加任务
    m_workqueue.push_back(request);
    m_queuelocker.unlock(); // 解锁

    // 信号量 提醒有任务处理
    m_queuestat.post();
    return true;
}

++e. worker() 线程处理++

内部访问私有函数 run(),完成线程处理要求

cpp 复制代码
// 前面构造函数中 pthread_create 调用了 worker
template<typename T>
void* threadpool<T>::worker(void* arg)
{
    // 调用时 *arg 是 this
    // 所以该操作其实是获取 threadpool 对象地址

    // 参数强转线程池类,调用成员方法
    threadpool* pool = (threadpool*)arg;
    // 线程池每一个线程创建都会调用 run(),睡眠在队列中
    pool->run();
    return pool;
}

++f. run() 执行任务++

工作线程 从 请求队列 取出某个任务进行处理,注意线程同步

cpp 复制代码
// 线程池所有线程睡眠状态,等待请求队列新增任务
template<typename T>
void threadpool<T>::run()
{
    while (!m_stop) {
        // 信号量等待
        // m_queuestat 是否有任务需要处理
        m_queuestat.wait();

        // 被唤醒后先加互斥锁
        // m_workqueue 请求队列
        m_queuelocker.lock();

        if (m_workqueue.empty()) {
            m_queuelocker.unlock();
            continue;
        }

        // 请求队列取 第一个任务request
        // 任务从请求队列 删除
        T* request = m_workqueue.front();
        m_workqueue.pop_front();
        m_queuelocker.unlock();
        if (!request) continue;

        // 连接池取出一个 数据库连接
        request->mysql = m_connPool->GetConnection();

        // process(模板类中的方法,这里是 http 类) 进行处理
        request->process();

        // 数据库连接 放回连接池
        m_connPool->ReleaseConnection(request->mysql);
    }
}

++线程同步机制有哪些++

++1)RAII++

  • 之所以把 RAII 加到线程同步机制里,因为它可以用来管理 信号量,互斥量,条件变量等资源
  • RAII -- Resource Acquisition is Initialization,资源获取即初始化
  • 资源与对象的生命周期绑定,构造函数分配资源,析构函数释放资源
  • 比如智能指针

++2)信号量(限制访问某个资源的线程数量)++

cpp 复制代码
a. sem_init() 初始化 信号量
 
b. sem_destory() 销毁 信号量
 
c. sem_wait() 原子操作方式,信号量 -1;信号量 == 0,sem_wait() 阻塞
 
d. sem_post() 原子操作方式,信号量 +1;信号量 > 0,唤醒调用 sem_post()的线程

信号量就像是一个可以控制进程访问共享资源的门禁系统。这个门禁系统支持两种操作👇

  • 等待 (P) 操作:当一个进程试图访问共享资源时(比如想要通过门禁进入),它首先检查信号量的值。如果信号量大于 0(门禁开着),那么进程可以顺利通过,同时信号量减一(门禁关闭)。但是,如果信号量等于 0(门禁关着,有其他进程在使用资源),进程必须等待(挂起执行),不能继续执行直到有其他进程释放资源(信号量增加)
  • 信号 (V) 操作:当一个进程使用完共享资源时(比如离开了房间),它会执行信号操作。如果有其他进程因为等待资源而被挂起,那么这个信号会唤醒其中一个等待的进程,让其继续执行。否则,如果没有进程在等待资源,信号量会自增,表示资源又变得可用

++3)条件变量(线程间通信;实现线程等待唤醒机制)++

cpp 复制代码
a. pthread_cond_init() 初始化
 
b. pthread_cond_destory() 销毁
 
c. pthread_cond_broadcast() 广播方式,唤醒所有等待目标条件变量的 线程
 
d. pthread_cond_wait() 等待目标条件变量
调用时,传入 mutex 参数 (加锁的互斥锁)
执行时,1) 调用线程 放入条件变量的 请求队列
       2) 互斥锁 mutex 解锁
       3) 函数返回 0 时,互斥锁再次被锁上
       4) 也就是说,函数内部,会有一次 解锁 和 加锁 操作

当一个线程调用 pthread_cond_wait() 等待目标条件变量时,它会将自己放入条件变量的请求队列中,并传入一个已经加锁的互斥锁(mutex参数)。接着,互斥锁会被解锁,让其他线程有机会操作共享资源。当函数返回 0 时,表示条件满足,此时会再次对互斥锁进行加锁操作,以确保线程安全地访问共享资源。因此,在 pthread_cond_wait() 函数内部会有一次解锁和加锁的操作,确保线程的正确执行顺序和共享资源的安全访问

++4)互斥量(一次只允许一个线程访问资源)++

即 互斥锁:保护关键代码段,确保 独占式 访问

a. 进入关键代码段 -- 获得互斥锁并加锁

b. 离开关键代码段 -- 唤醒等待该互斥锁的线程

cpp 复制代码
a. pthread_mutex_init() 初始化互斥锁
 
b. pthread_mutex_destory() 销毁互斥锁
 
c. pthread_mutex_lock() 原子操作方式,给互斥锁,加锁
 
d. pthread_mutex_unlock() 原子操作方式,给互斥锁,解锁

++线程池中的工作线程是一直等待吗?++

  • 工作线程睡眠在工作队列上,当主线程将新任务添加到工作队列时,就会唤醒某个一直在等待的工作线程
  • 该工作线程从队列中取出任务并执行,其他工作线程则继续睡眠在工作队列上

++线程池中工作线程处理完一个任务后的状态是?++

  • 请求队列为空,则该线程进入线程池继续等待
  • 队列不为空,就和其他线程一起竞争任务

++同时2000个客户端访问,线程数不多,如何及时响应?++

1,采用 线程池

  • 主线程与工作线程分离,使用线程池管理工作线程,避免主线程阻塞
  • 考虑扩大线程池容量
  • 线程池复用线程资源,避免频繁创建和销毁小城

2,采用 ET 模式和非阻塞 socket

  • ET模式和非阻塞socket,确保及时处理
  • 通过 epoll 的EPOLLONESHOT特性,降低可读,可写和异常事件被触发次数

3,采用 半同步/半异步并发模式

  • 通过 Reactor 或 Proactor 模式,满足并发量要求

4,采用 集群或者分布式

  • 通过分布式,将大任务拆分成小任务,各节点协同完成
  • 通过集群,增加服务器(节点)数量

什么是分布式,分布式和集群的区别又是什么?这一篇让你彻底明白!_什么叫分布式-CSDN博客

++一个请求占用线程很长事件,影响到了接下来的请求处理,如何解决++

  • 采取 异步非阻塞 模式,接收到新的请求后,不立即处理,而是安排一个以后的时间再发起请求,并且继续执行当前请求
  • 采取 超时设置,对每个请求设置合理的超时时间,如果处理时间超过给定阈值,则取消该请求 或 返回超时错误信息,及时释放线程资源
  • 采取 断点续传,对任务进行分段处理,某个处理阶段完成后暂停任务,等待下一次触发继续处理
  • 任务分片,长时间任务拆分成多个小任务,每个小任务完成后释放线程资源

++补充理解++

深入理解同步阻塞、同步非阻塞、异步阻塞、异步非阻塞_同步阻塞 同步非阻塞 异步阻塞 异步非阻塞-CSDN博客

3)并发模型

++服务器使用的并发模型是?++

采用 半同步半反应堆 作为并发模型

以 Proactor 事件处理模式为例

  1. 主线程充当异步线程,负责监听所有 socket 上的时间和处理 I/O 操作
  2. 新请求到达时,主线程接收连接 socket,并注册读写时间到 epoll 内核事件表中
  3. 如果连接 socket 上有读写事件发生,主线程从 socket 上接收数据,并将数据封装成请求对象插入请求队列
  4. 所有工作线程睡眠在请求队列上,当有任务到来时,通过竞争(互斥锁)获得任务接管权
  5. 工作线程仅负责业务逻辑,比如处理客户请求;主线程与内核配合实现底层的异步 I/O 操作
    ++总的来说++

采用半同步半反应堆并发模型的 Proactor 事件处理模式,主线程负责底层 I/O 操作和事件监听,工作线程专注于业务逻辑处理,通过异步 I/O 实现高效并发处理

Reactor,Proactor,主从Reactor 模型的区别

先通过Java了解下 主从Reactor👇

Reactor(主从)原理详解与实现_主从reactor-CSDN博客

回答:

1,Reactor(事件来了,我通知你,你来处理)(我 -- 操作系统内核;事件 -- 新连接)

  • 采用同步 I/O,负责监听文件描述符上是否有事件发生
  • 主线程(I/O 处理单元)负责事件感知和通知工作线程(同步IO向应用程序通知的是IO就绪事件),将读写事件放入请求队列,并由工作线程完成实际的数据读写,接收新连接 和 处理客户请求
  • 应用进程需要主动调用 read / write 方法,进行数据的读取和写入,处理过程是同步的
  • 比如,快递员在楼下通知你快递送达,需要你自己下楼拿快递


2,Proactor(事件来了,我处理完,再通知你)

  • 采用异步 I/O,只负责发起 I/O 操作,真正的 I/O 实现由操作系统处理
  • 主线程和操作系统负责处理读写数据,接收新连接等 IO 操作,工作线程仅负责业务逻辑,如处理客户请求
  • 应用进程无需主动发起读写操作,操作系统完成读写后,会通知应用进程直接处理数据(异步IO向应用程序通知的是IO完成事件)
  • 比如,快递员将快递送达你家门口后,再通知你


3,主从Reactor模式

  • 主反应堆线程,负责分发连接建立事件,已连接套接字上的 IO 事件,交给子反应堆线程处理
  • 子反应堆线程数量,可以根据CPU核数来设置,负责具体的 I/O 事件处理
  • 主反应堆线程,只负责调用 accept 获取已连接套接字,并将其分配给响应的子反应堆线程
  • 结合了 Reactor 和 Proactor 的特点


概括地说,Reactor模式和Proactor模式都是基于事件分发的网络编程模式,区别在于 I/O 事件处理的方式

Reactor基于待完成的 I/O 事件,而Proactor基于已完成的 I/O 事件,主从Reactor结合了两者的优点

++为什么用 epoll,还有其他IO复用方式吗,区别是++

常用的是 select,poll,epoll,当然,这里会补充下对 io_uring 的说明
++选择 epoll 从两点出发:++

  1. 性能:
    1)在文件描述符数量较多且活跃度不一的情况下,epoll 能提升性能
    2)因为 epoll 将文件描述符维护在内核态,每次添加文件描述符,只需要执行一个系统调用
    3)而且能够直接返回触发事件的文件描述符,避免了遍历整个文件描述符集合的性能损耗
  2. 数据结构:
    1)select 使用线性表创建文件描述符集合,而且上限 1024
    2)poll 使用链表
    3)epoll 底层采用红黑树来构建,并维护一个 ready list,能够在 epoll_wait() 调用时,只观察已就绪事件
    4)epoll 支持 LT 和 ET 两种工作模式,而 select 和 poll 只能工作在相对低效的 LT 下
    ++区别是:++
  • select, poll 都需要将文件描述符集合从用户态拷贝到内核态,而epoll只用拷贝需要修改的文件描述符,避免了集体拷贝的开销
  • select,poll 最大开销来自内核判断是否有文件描述符就绪,需要遍历整个文件描述符集合,而 epoll 直接返回触发事件的文件描述符
  • select, poll, epoll 都是同步 I/O,而 io_uring 是异步 I/O,它通过用户和内核之间的共享内存映射来避免数据拷贝,减少CPU开销,还支持事件批处理,降低系统调用次数和上下文切换的开销
    ++下面介绍下 io_uring:++
  1. 零拷贝:io_uring 通过共享内存映射来避免数据拷贝,将用户空间和内核空间之间的数据传输最小化
  2. 批处理:它还支持事件批处理,即一次性可以提交多个 I/O 请求给内核,减少系统调用和上下文切换的开销,提高吞吐量
  3. Ring Buffer 机制:io_uring 使用环形缓冲区(ring buffer),作为用户和内核之间的通信机制。用户将 IO 请求放入Ring Buffer,内核会异步处理这些请求,并将结果返回到 Ring Buffer,用户再从 Ring Buffer 读取
  4. 高效的事件通知机制:使用 IO Completion Event(IO完成事件)机制,通过事件通知的方式告知用户空间 IO 操作的完成情况,避免用户频繁的轮询内核,提高响应速度
相关推荐
一尘之中几秒前
网 络 安 全
网络·人工智能·学习·安全
无夜_20 分钟前
Prototype(原型模式)
开发语言·c++
刘好念39 分钟前
[图形学]smallpt代码详解(1)
c++·计算机图形学
fpcc43 分钟前
并行编程实战——TBB框架的应用之一Supra的基础
c++·并行编程
兵哥工控43 分钟前
MFC工控项目实例二十二主界面计数背景颜色改变
c++·mfc
兵哥工控1 小时前
MFC工控项目实例二十手动测试界面模拟量输入实时显示
c++·mfc
jyan_敬言1 小时前
【Linux】Linux命令与操作详解(一)文件管理(文件命令)、用户与用户组管理(创建、删除用户/组)
linux·运维·服务器·c语言·开发语言·汇编·c++
笑非不退2 小时前
C++ 异步编程 并发编程技术
开发语言·c++
T0uken2 小时前
【QT Qucik】C++交互:接收QML信号
c++·qt·交互
车载诊断技术2 小时前
什么是汽车中的SDK?
网络·架构·汽车·soa·电子电器架构