《TCP/IP网络编程》中多线程HTTP服务器实现代码,线程池改编
文章目录
- 最初代码
- 线程池代码
- locker.h
- threadpool.h
- task.h
- main.cpp
- index.html
- 编译
- 执行结果
最初代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>#define BUF_SIZE 1024
#define SMALL_BUF 100void* request_handler(void* arg);
void send_data(FILE *fp, char *ct, char *file_name);
const char* content_type(char *file);
void send_error(FILE *fp);
void error_handling(const char* message);int main(int argc, char *argv[])
{if(argc!=2){printf("Usage: %s <port>\n", argv[0]);exit(1);}int serv_sock = socket(PF_INET, SOCK_STREAM, 0);int option = 1;int optlen = sizeof(option);setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen );struct sockaddr_in serv_adr;memset(&serv_adr,0 ,sizeof(serv_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); //本机IPserv_adr.sin_port = htons(atoi(argv[1])); //本机端口if(bind(serv_sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr) )== -1 ){error_handling("bind() error");}if(listen(serv_sock, 20) == -1 ){error_handling("listen() error");}while(1){struct sockaddr_in clnt_adr;socklen_t clnt_adr_size = sizeof(clnt_adr);int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_size );printf("Connected Request: %s:%d\n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port) );pthread_t t_id;pthread_create(&t_id, NULL, request_handler, &clnt_sock); pthread_detach(t_id);}close(serv_sock);return 0;
}void* request_handler(void *arg)
{int clnt_sock = *((int*)arg);char req_line[SMALL_BUF];FILE *clnt_read, *clnt_write;clnt_read = fdopen(clnt_sock, "r"); //读缓存clnt_write = fdopen( dup(clnt_sock), "w"); //写缓存fgets(req_line, SMALL_BUF, clnt_read); //读数据(浏览器发数据给客户端)printf("req_line:%s\n",req_line);if(strstr(req_line,"HTTP/")==NULL ) //不是HTTP协议{send_error(clnt_write);fclose(clnt_read);fclose(clnt_write);return NULL;}char method[10],file_name[30],ct[15];strcpy( method, strtok(req_line, " /") );strcpy( file_name, strtok(NULL, " /") );strcpy( ct, content_type(file_name));if(strcmp(method, "GET") !=0) //不是GET方法{send_error(clnt_write);fclose(clnt_read);fclose(clnt_write);return NULL;}fclose(clnt_read);send_data(clnt_write, ct, file_name);return NULL;
}void send_data(FILE *fp, char *ct, char *file_name)
{char protocol[] = "HTTP/1.0 200 OK\r\n";char server[] = "Server:Linux Web Server \r\n"; char cnt_len[] = "Content-length:2048\r\n";char cnt_type[SMALL_BUF];char buf[BUF_SIZE];sprintf(cnt_type, "Content-type:%s\r\n\r\n",ct);FILE* send_file = fopen(file_name, "r");if(send_file == NULL){send_error(fp);fclose(fp);return;}fputs(protocol, fp);fputs(server, fp); fputs(cnt_len, fp);fputs(cnt_type, fp);while(fgets(buf,BUF_SIZE,send_file)!=NULL){fputs(buf, fp);fflush(fp);}fflush(fp);fclose(fp);
}const char* content_type(char *file)
{char extension[SMALL_BUF];char file_name[SMALL_BUF];strcpy(file_name, file);strtok(file_name,".");strcpy(extension, strtok(NULL,".") );if(!strcmp(extension, "html")||!strcmp(extension,"htm") ){return "text/html";}else{return "text/plain";}
} void send_error(FILE *fp)
{char protocol[] = "HTTP/1.0 400 Bad Request\r\n"; //状态行char server[] = "Server:Linux Web Server\r\n"; //消息头char cnt_len[] = "Content-length:2048\r\n";char cnt_type[] = "Content-type:text/html\r\n\r\n"; //空行char content[] = "<html><head><title>NETWORK </title> </head>""<body> <font size=+5> <br>404 Error! </font> </body> </html>";fputs(protocol, fp);fputs(server, fp);fputs(cnt_len, fp);fputs(cnt_type, fp);fputs(content, fp);fflush(fp);}void error_handling(const char *message)
{fputs(message, stderr);fputc('\n',stderr);exit(1);
}
线程池代码
locker.h
#ifndef LOCKER_H
#define LOCKER_H#include <exception>
#include <pthread.h>
#include <semaphore.h>/*封装信号量的类*/
class Sem
{
public:/*创建并初始化信号量*/Sem(){if( sem_init( &m_sem, 0, 0 )!=0 ){/* 构造函数没有返回值,抛出异常来报告错误*/throw std::exception();}}/*销毁信号量*/~Sem(){sem_destroy(&m_sem);}/*等待信号量*/bool wait(){return sem_wait( &m_sem ) == 0;}/*增加信号量*/bool post(){return sem_post( &m_sem ) == 0;}
private:sem_t m_sem;
};/*封装互斥锁*/
class Locker
{
public:/*创建并初始化互斥锁*/Locker(){if( pthread_mutex_init(&m_mutex, NULL) != 0 ){throw std::exception();}}/*销毁互斥锁*/~Locker(){pthread_mutex_destroy( &m_mutex );}/*获取互斥锁*/bool lock(){return pthread_mutex_lock(&m_mutex) == 0 ;}/*释放互斥锁*/bool unlock(){return pthread_mutex_unlock(&m_mutex) == 0 ;}
private:pthread_mutex_t m_mutex;
};#endif
threadpool.h
#ifndef THREADPOOL_H
#define THREADPOOL_H#include <list>
#include <cstdio>
#include <exception>
#include <pthread.h>
#include <semaphore.h>
#include "locker.h"/*线程池类, 将它定义为模板类是为了代码复用。模板参数T是任务类*/
template< typename T >
class ThreadPool
{
public:ThreadPool(int thread_number = 8, int max_requests = 10000 );~ThreadPool();/*往请求队列中添加任务*/bool append(T* request);private:/*工作线程运行的函数,它不断从工作队列中取出任务并执行之*/static void* worker(void* arg);void run();
private:int m_thread_number; //线程池中的线程数int m_max_requests; //请求队列中允许的最大请求数pthread_t *m_threads; //线程描述符数组,大小为m_thread_number std::list<T*> m_workqueue; /*请求队列 T是任务类*/Locker m_queuelocker; //保护请求队列的互斥锁Sem m_queuestat; //是否有任务需要处理,唤醒bool m_stop; //是否结束线程
};template <typename T>
ThreadPool<T>::ThreadPool(int thread_number, int max_requests):m_thread_number(thread_number),m_max_requests(max_requests),m_stop(false),m_threads(NULL){if(thread_number<=0||max_requests<=0 ) {throw std::exception();}m_threads = new pthread_t[m_thread_number]; //动态分配if(!m_threads){throw std::exception();}/*创建thread_number个线程,并将它们设置为脱离线程*/for(int i=0;i<thread_number;i++){printf("create the %dth thread\n", i);if(pthread_create(m_threads+i, NULL, worker, this ) != 0) //线程执行worker函数(工作者),把线程池对象传入线程,执行线程的run()函数{delete[] m_threads;throw std::exception();}if( pthread_detach(m_threads[i]) ){delete[] m_threads;throw std::exception();}}
}template< typename T>
ThreadPool<T>::~ThreadPool()
{delete[] m_threads;m_stop = true;
}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(); //wait() P, post() V V操作唤醒run()return true;
}template< typename T>
void* ThreadPool<T>::worker(void* arg)
{ThreadPool *pool = (ThreadPool*)arg;pool->run();return pool;
}template< typename T>
void ThreadPool<T>::run()
{while(!m_stop){m_queuestat.wait(); //P操作,有请求m_queuelocker.lock();if(m_workqueue.empty() ){m_queuelocker.unlock();continue;}T* request = m_workqueue.front();m_workqueue.pop_front();m_queuelocker.unlock();if(!request){continue;}request->process();delete request;}
}#endif
task.h
#ifndef TASK_H
#define TASK_H#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>#define SMALL_BUF 100
#define BUF_SIZE 1024class Task{
public:Task(int clnt_sock):m_clnt_sock(clnt_sock){}void process();void send_data(FILE *fp, char *ct, char *file_name);const char* content_type(char *file);void send_error(FILE *fp);void error_handling(const char *message);
private:int m_clnt_sock;
};void Task::process()
{ int clnt_sock = m_clnt_sock;char req_line[SMALL_BUF];FILE *clnt_read, *clnt_write;clnt_read = fdopen(clnt_sock, "r"); //读缓存clnt_write = fdopen( dup(clnt_sock), "w"); //写缓存fgets(req_line, SMALL_BUF, clnt_read); //读数据(浏览器发数据给客户端)printf("req_line:%s\n",req_line);if(strstr(req_line,"HTTP/")==NULL ) //不是HTTP协议{send_error(clnt_write);fclose(clnt_read);fclose(clnt_write);return;}char method[10],file_name[30],ct[15];strcpy( method, strtok(req_line, " /") );strcpy( file_name, strtok(NULL, " /") );strcpy( ct, content_type(file_name));if(strcmp(method, "GET") !=0) //不是GET方法{send_error(clnt_write);fclose(clnt_read);fclose(clnt_write);return;}fclose(clnt_read);send_data(clnt_write, ct, file_name);
}void Task::send_data(FILE *fp, char *ct, char *file_name)
{char protocol[] = "HTTP/1.0 200 OK\r\n";char server[] = "Server:Linux Web Server \r\n"; char cnt_len[] = "Content-length:2048\r\n";char cnt_type[SMALL_BUF];char buf[BUF_SIZE];sprintf(cnt_type, "Content-type:%s\r\n\r\n",ct);FILE* send_file = fopen(file_name, "r");if(send_file == NULL){send_error(fp);fclose(fp);return;}fputs(protocol, fp);fputs(server, fp); fputs(cnt_len, fp);fputs(cnt_type, fp);while(fgets(buf,BUF_SIZE,send_file)!=NULL){fputs(buf, fp);fflush(fp);}fflush(fp);fclose(fp);
}const char* Task::content_type(char *file)
{char extension[SMALL_BUF];char file_name[SMALL_BUF];strcpy(file_name, file);strtok(file_name,".");strcpy(extension, strtok(NULL,".") );if(!strcmp(extension, "html")||!strcmp(extension,"htm") ){return "text/html";}else{return "text/plain";}
}void Task::send_error(FILE *fp)
{char protocol[] = "HTTP/1.0 400 Bad Request\r\n"; //状态行char server[] = "Server:Linux Web Server\r\n"; //消息头char cnt_len[] = "Content-length:2048\r\n";char cnt_type[] = "Content-type:text/html\r\n\r\n"; //空行char content[] = "<html><head><title>NETWORK </title> </head>""<body> <font size=+5> <br>404 Error! </font> </body> </html>";fputs(protocol, fp);fputs(server, fp);fputs(cnt_len, fp);fputs(cnt_type, fp);fputs(content, fp);fflush(fp);
} void Task::error_handling(const char *message)
{fputs(message, stderr);fputc('\n',stderr);exit(1);
}#endif
main.cpp
#include "locker.h"
#include "threadpool.h"
#include "task.h"
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <pthread.h>#define BUF_SIZE 1024
#define SMALL_BUF 100void error_handling(const char* message)
{fputs(message, stderr);fputc('\n',stderr);exit(1);
}int main(int argc, char *argv[] )
{/*ThreadPool<Task> *pool = NULL;try{pool = new ThreadPool<Task>;}catch(...){return 1;}for(int i = 0; i<5; i++){pool->append(new Task());} sleep(20);*/if(argc!=2){printf("Usage: %s <port>\n", argv[0]);exit(1);}int serv_sock = socket(PF_INET, SOCK_STREAM, 0);int option = 1;int optlen = sizeof(option);setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen );struct sockaddr_in serv_adr;memset(&serv_adr,0 ,sizeof(serv_adr));serv_adr.sin_family = AF_INET;serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); //本机IPserv_adr.sin_port = htons(atoi(argv[1])); //本机端口if(bind(serv_sock,(struct sockaddr*)&serv_adr, sizeof(serv_adr) )== -1 ){error_handling("bind() error");}if(listen(serv_sock, 20) == -1 ){error_handling("listen() error");}ThreadPool<Task> *pool = NULL;try{pool = new ThreadPool<Task>;}catch(...){return 1;}while(1){struct sockaddr_in clnt_adr;socklen_t clnt_adr_size = sizeof(clnt_adr);int clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_size );printf("Connected Request: %s:%d\n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port) );pool->append(new Task(clnt_sock));/*pthread_t t_id;pthread_create(&t_id, NULL, request_handler, &clnt_sock); pthread_detach(t_id);*/}close(serv_sock);return 0;
}
index.html
<html>
<head><title>Network</title> </head><body>
<font size= +5><br> net_program interesting! </font>
</body></html>
编译
g++ -g locker.h task.h threadpool.h main.cpp -o main -lpthread
执行结果



相关文章:
《TCP/IP网络编程》中多线程HTTP服务器实现代码,线程池改编
文章目录 最初代码线程池代码locker.hthreadpool.htask.hmain.cppindex.html编译 执行结果 最初代码 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <arpa/inet.h> #include <sys/socket.h>…...
Windows®、Linux® 和 UNIX® 系统都适用的远程桌面工具 OpenText ETX
Windows、Linux 和 UNIX 系统都适用的远程桌面工具 OpenText ETX 为 Windows、Linux 和 UNIX 实施精益、经济高效的虚拟化;提供完整的远程 Windows 可用性;以类似本地的性能远程工作;安全地保护系统和知识产权(IP)&am…...
酷柚易汛ERP - 榜店商城对接说明
榜店商城与酷柚易汛ERP对接,需要先在榜店系统中安装对应插件,配置对应的密钥 榜店商城与酷柚易汛ERP的商品进行关联操作,同时订单也会同步,关联不正确会导致订单出库错误 可查看对应的日志...
Linux 多进程开发(上)
第二章 Linux 多进程开发 2.1 进程概述2.2 进程状态转换2.3 进程创建2.4 exec 函数族2.5 进程控制 网络编程系列文章: 第1章 Linux系统编程入门(上) 第1章 Linux系统编程入门(下) 第2章 Linux多进程开发(…...
【DataWhale学习】用免费GPU线上跑StableDiffusion项目实践
用免费GPU线上跑SD项目实践 DataWhale组织了一个线上白嫖GPU跑chatGLM与SD的项目活动,我很感兴趣就参加啦。之前就对chatGLM有所耳闻,是去年清华联合发布的开源大语言模型,可以用来打造个人知识库什么的,一直没有尝试。而SD我…...
基于YOLOv8/YOLOv7/YOLOv6/YOLOv5的铁轨缺陷检测系统(Python+PySide6界面+训练代码)
摘要:开发铁轨缺陷检测系统对于物流行业、制造业具有重要作用。本篇博客详细介绍了如何运用深度学习构建一个铁轨缺陷检测系统,并提供了完整的实现代码。该系统基于强大的YOLOv8算法,并对比了YOLOv7、YOLOv6、YOLOv5,展示了不同模…...
3.基础算法之搜索与图论
1.深度优先搜索 深度优先搜索(DFS,Depth First Search)是一种用于遍历或搜索树或图的算法。它将当前状态按照一定的规则顺序,先拓展一步得到一个新状态,再对这个新状态递归拓展下去。如果无法拓展,则退回…...
Java模板方法模式源码剖析及使用场景
一、原理与通俗理解 模板方法模式定义了一个算法的骨架,将某些步骤推迟到子类中实现。模板方法定义一个算法的骨架,将一些步骤的实现延迟到子类中完成。这样做的目的是确保算法的结构保持不变,同时又可以为不同的子类提供特定步骤的实现。 比如去餐馆吃饭,餐馆有固定的流程(下…...
c++ 新的函数声明语法
右值引用(&&) 右值引用(&&)允许我们定义接受临时对象或移动语义的函数。 void foo(int&& x); // 右值引用参数默认参数 允许在函数声明中指定参数的默认值。 void bar(int x, double y 3.14); // 带有默认参数的函数声明noexcept关键字 指示函数…...
一款好用的AI工具——边界AICHAT
目录 一、简介二、注册及登录三、主要功能介绍3.1、模型介绍3.2、对话模型历史记录3.3、创作中心3.4、AI绘画SD3.5、文生图3.6、图生图3.7、线稿生图3.8、艺术二维码3.9、秀图广场3.10、AI绘画创作人像辅助器 一、简介 人工智能(AI)是一门研究、开发用于…...
谷歌承认“窃取”OpenAI模型关键信息
什么?谷歌成功偷家OpenAI,还窃取到了gpt-3.5-turbo关键信息??? 是的,你没看错。 根据谷歌自己的说法,它不仅还原了OpenAI大模型的整个投影矩阵(projection matrix)&…...
蓝桥杯(3.10)
1219. 移动距离 import java.util.Scanner; public class Main{public static void main(String[] args) {Scanner sc new Scanner(System.in);int w sc.nextInt();int m sc.nextInt();int n sc.nextInt();m--;n--;//由从1开始变为从0开始//求行号int x1 m/w, x2 n/w;//…...
Hololens 2应用开发系列(3)——MRTK基础知识及配置文件配置(中)
Hololens 2应用开发系列(3)——MRTK基础知识及配置文件配置(中) 一、前言二、输入系统2.1 MRTK输入系统介绍2.2 输入数据提供者(Input Data Providers)2.3 输入动作(Input Actions)2…...
吴恩达深度学习笔记:深度学习引言1.1-1.5
目录 第一门课:神经网络和深度学习 (Neural Networks and Deep Learning)第一周:深度学习引言(Introduction to Deep Learning)1.1 欢迎(Welcome)1.2 什么是神经网络?(What is a Neural Network)1.3 神经网络的监督学习(Supervised Learning …...
【Hadoop大数据技术】——Hadoop概述与搭建环境(学习笔记)
📖 前言:随着大数据时代的到来,大数据已经在金融、交通、物流等各个行业领域得到广泛应用。而Hadoop就是一个用于处理海量数据的框架,它既可以为海量数据提供可靠的存储;也可以为海量数据提供高效的处理。 目录 &#…...
蓝桥杯2023年第十四届省赛真题-工作时长
文件数据 把数据复制到excel中 数据按照增序排序 选中列数据,设置单元格格式,选择下述格式。注意,因为求和之后总小时数可能会超过24小时,所以不要选择最前面是hh的 设置B2 A2 - A1, B4 A4 - A3;然后选中已经算出…...
nginx禁止国外ip访问
1.安装geoip2扩展依赖 yum install libmaxminddb-devel -y 2.下载ngx_http_geoip2_module模块 https://github.com/leev/ngx_http_geoip2_module.git 3.编译安装 ./configure --add-module/datasdb/ngx_http_geoip2_module-3.4 4.下载最新数据库文件 模块安装成功后,还要…...
《腾讯音乐》24校招Java后端一面面经
1.手写LRU 2.项目拷打 3.Https客户端校验证书的细节? 4.对称加密和非对称加密的区别?你分别了解哪些算法? 5.在信息传输过程中,Https用的是对称加密还是非对称加密? 6.怎么防止下载的文件被劫持和篡改? 7.H…...
JavaScript:ES至今发展史简说
ECMAScript(简称ES)是JavaScript的标准,它的发展史经历了多个版本的迭代,以下是主要里程碑: ES1 (1997年6月):首个正式发布的ECMAScript标准,基于当时的JavaScript(由Netscape公司开…...
Linux:进程
进程 知识铺垫冯诺依曼体系结构操作系统(OS) 进程概念进程的查看ps 命令获取进程 pid文件内查看进程终止进程的方式kill命令快捷键 进程的创建 forkfork 返回值问题 进程状态运行状态 :R休眠状态:S (可中断)…...
2025年能源电力系统与流体力学国际会议 (EPSFD 2025)
2025年能源电力系统与流体力学国际会议(EPSFD 2025)将于本年度在美丽的杭州盛大召开。作为全球能源、电力系统以及流体力学领域的顶级盛会,EPSFD 2025旨在为来自世界各地的科学家、工程师和研究人员提供一个展示最新研究成果、分享实践经验及…...
【网络安全产品大调研系列】2. 体验漏洞扫描
前言 2023 年漏洞扫描服务市场规模预计为 3.06(十亿美元)。漏洞扫描服务市场行业预计将从 2024 年的 3.48(十亿美元)增长到 2032 年的 9.54(十亿美元)。预测期内漏洞扫描服务市场 CAGR(增长率&…...
最新SpringBoot+SpringCloud+Nacos微服务框架分享
文章目录 前言一、服务规划二、架构核心1.cloud的pom2.gateway的异常handler3.gateway的filter4、admin的pom5、admin的登录核心 三、code-helper分享总结 前言 最近有个活蛮赶的,根据Excel列的需求预估的工时直接打骨折,不要问我为什么,主要…...
视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序
一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...
【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...
并发编程 - go版
1.并发编程基础概念 进程和线程 A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中…...
elementUI点击浏览table所选行数据查看文档
项目场景: table按照要求特定的数据变成按钮可以点击 解决方案: <el-table-columnprop"mlname"label"名称"align"center"width"180"><template slot-scope"scope"><el-buttonv-if&qu…...
华为OD最新机试真题-数组组成的最小数字-OD统一考试(B卷)
题目描述 给定一个整型数组,请从该数组中选择3个元素 组成最小数字并输出 (如果数组长度小于3,则选择数组中所有元素来组成最小数字)。 输入描述 行用半角逗号分割的字符串记录的整型数组,0<数组长度<= 100,0<整数的取值范围<= 10000。 输出描述 由3个元素组成…...
xmind转换为markdown
文章目录 解锁思维导图新姿势:将XMind转为结构化Markdown 一、认识Xmind结构二、核心转换流程详解1.解压XMind文件(ZIP处理)2.解析JSON数据结构3:递归转换树形结构4:Markdown层级生成逻辑 三、完整代码 解锁思维导图新…...
