【網絡編程】TCP 服務器并發編程:多進程、線程池與守護進程實踐


請添加圖片描述


半桔:個人主頁

?🔥 個人專欄: 《Linux手冊》《手撕面試算法》《網絡編程》

🔖很多人在喧囂聲中登場,也有少數人在靜默中退出。 -張方宇-

文章目錄

  • 前言
  • 套接字接口
  • TCP服務器
  • TCP + 多進程
  • TCP + 線程池
    • 重寫Task任務
    • 放函數對象
  • 客戶端重連
  • 進程組與守護進程
    • 進程組和會話
  • 守護進程

前言

在互聯網技術蓬勃發展的今天,高并發、高可靠的網絡服務已成為各類應用的核心訴求 —— 從支撐海量用戶的 Web 服務器,到實時交互的分布式系統,甚至是物聯網設備的通信底座,高效的網絡通信設計進程生命周期管理,始終是保障服務穩定運行的基石。

本文將聚焦 Linux 網絡編程與進程管理 的核心技術,以 “從基礎到進階,從實現到優化” 的脈絡展開:

  • 從最基礎的 套接字接口 出發,剖析網絡通信的底層邏輯;
  • 通過 TCP 服務器 的搭建,掌握客戶端 - 服務端交互的核心流程;
  • 針對高并發場景,探索 多進程、線程池 等并行模型的設計,突破服務吞吐量的瓶頸;
  • 最終深入 進程組與守護進程 的實踐,解決服務 “脫離終端、長期穩定運行” 的生產級需求。

TCP通信是面向字節流的,而UDP是面向最字節報的,因此兩者通信方式上有本質的差異。

TCP面向字節流也就意味著,接收方讀取上來的數據可能是不完整的,因此TCP通信要進行協議定制,規定一個消息從哪到哪是一個整體部分。關于協議的定制我們在下一篇博客中詳細講解,本篇文章我們假設通過TCP通信對方就可以拿到一個完整的數據。

套接字接口

TCP的接口和UDP接口有類似的,當時也有一些不同之處。
UDP通信的步驟就是:創建套接字,綁定,接收和發送消息;而TCP與其是不一樣的。

  • TCP通信時面向連接的,需要通信雙方先建立連接,服務器一般是比較“被動”的,服務器一直處于等待外界連接的狀態(監聽狀態)。

因此在進行綁定完成之后,服務器要先進入監聽狀態,與客戶端建立連接后才能進行通信:

int listen(int sockfd , int backlog)

  1. 參數一:套接字;
  2. 參數二: backlog 表示未完成連接隊列(處于三次握手過程中)和已完成連接隊列(三次握手完成,等待 accept 處理)的最大長度之和。用來調節連接時的并發量;
  3. 返回值:成功返回0,失敗-1;

第二個接口,將服務器設置為監聽模式之后,要對客戶端的連接請求做出響應,要接收客戶端的請求:

int accept(int sockfd , struct sockaddr_in *addr , socklen_t *addrlen)

  1. 參數一:套接字;
  2. 參數二:輸出型參數,一個結構體,存儲著客戶端的ip和端口號信息;
  3. 參數三:輸出型參數,表示第二個結構體的大小;
  4. 返回值:返回一個文件描述符,通過該文件描述符可以讓直接使用writeread接口進行通信,就像從文件中進行讀寫一樣。

注意:accept中的sockfd也屬于文件描述符,只不過該描述符主要負責將底層的連接請求來上來,而不負責進行IO交互;而accept返回的文件描述符是專門用來進行IO交互的。

隨著客戶端越來越多,accept返回的文件描述符也就也來越多,每一個都負責與一個客戶端進行通信。

客戶端要與服務端建立連接,所以需要先服務端發送連接請求:

int connet(int sockfd , struct sockaddr* addr , socklen_t addrlen)

  1. 參數一:套接字;
  2. 參數二:結構體,內部包含要進行連接的IP和端口號;
  3. 參數三:參數二結構體的大小;
  4. 返回值:0表示成功,-1表示失敗。

TCP服務器

使用一個類來實現TCP服務器:

  • 內存成員需要有IP和端口號,來進行綁定;
  • 并且需要將套接字存儲起來,否則后續在不到套接字就會導致無法關閉對應的網絡文件位置。
  • 此處在設計一個bool類型的變量,讓用戶可以控制時候打開服務器。

初始化的時候需要外界將這些參數都傳進行保存起來,但是并不在初始化時創建套接字,而是當用戶運行時才進行創建。

const std::string defaultip = "0.0.0.0";class Server
{
public:Server(const uint16_t &port , const std::string &ip = defaultip):port_(port) , ip_(ip){}private:uint16_t port_;std::string ip_;int sockfd_;
};

與UDP一樣,為了保證服務器能夠接收來自各個網卡上的數據,我們再對服務器進行綁定的時候使用ip為0。

在此之前我們需要思考以下接收到的信息如何進行處理?

如果我們直接讓處理方法都在循環內完成,就會導致代碼拓展性差,如果后續希望接入進程池就需要對代碼進行重構,因此此處將對接收到的信息處理方法也單獨封裝一個類:

該類主要負責,將對信息進行處理,處理完后,向客戶端返回數據,因此該類的成員必須有一個string用來存儲待處理的信息,為了進行通信還需要拿到對應的文件描述符

我們可以在類中對調用運算符進行重載,在進行消息調用的時候更簡單。
為了后續測試,我們先不進行太復雜的處理:

class Task
{
public:Task(const int & fd , const std::string message):fd_(fd) , message_(message){}bool operator()(){std::string ret = "I have got your message : " + message_;write(fd_ , ret.c_str() , ret.size()); return true;}
private:int fd_;std::string message_;
};

現在可以對服務器進行初始化了,初始化主要分為3步:

  1. 創建套接字;
  2. 綁定;
  3. 設置監聽模式。
    void Init(){// 1. 創建套接字// 2. 綁定// 3. 設置監聽模式sockfd_ = socket(AF_INET , SOCK_STREAM , 0);if(sockfd_ < 0){Log(Fatal) << "socket failed ";exit(Socket_Err);}struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port_);char clientip[32];inet_aton(ip_.c_str() , &local.sin_addr);if(bind(sockfd_ , (const struct sockaddr*)&local , sizeof(local)) < 0){Log(Fatal) << "bind failed" ;exit(Bind_Err);}if(listen(sockfd_ , 10) < 0){Log(Fatal) << "listen failed" ;exit(Listen_Err);}}

運行服務器了,運行服務器:

  1. 先建立連接;
  2. 讀取數據;
  3. 做出反應。
    void Service(int fd_){   char buffer[1024];while(1){int n = read(fd_ , buffer , sizeof(buffer) - 1);if(n > 0){buffer[n] = 0;Task task(fd_ , buffer);task();}else if(n == 0){close(fd_);break;}else {Log(Error) << "read error";close(fd_);break;}}}void Start(){// 1. 建立連接// 2. 讀取消息// 3. 對消息進行處理,并返回struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(sockfd_ , (struct sockaddr*)&client , &len);if(fd < 0){Log(Warning) << "accept failed";}Service(fd);}

此處我們將服務單獨進行了封裝,方便后面接入多線程/多進程。

服務器的類編寫完成,后面再進行拓展,當前先進行以下簡單測試:
編寫一個源文件來運行一下服務器:在執行的時候,必須給出端口號。

void Menu(char* argv[])
{std::cout << "\r" << argv[0] <<  "  [port] " << "\n";
}int main(int argc , char* argv[])
{if(argc != 2){Menu(argv);exit(1);}uint16_t port = std::stoi(argv[1]);Server server(port);server.Init();server.Start();return 0;
}

當前服務器編寫完成了,但是客戶端還沒進行實現。如果想對服務端進行測試的話,可以先使用telnet工具,綁定本地環回地址127.0.0.1進行測試,但是只能起到本地通信的作用,不會將信息推送到網絡中

下一步就是編寫客戶端了:

客戶端的編寫就比較簡單了:

  1. 創建套接字;
  2. 發送連接請求;
  3. 連接成功,發送數據;
  4. 接收數據。

與服務端的編寫類似,只不過要用到connect接口:

void Menu(char *argv[])
{std::cout << argv[0] << "  [ip] " << " [port] " << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Menu(argv);exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);// 1.創建套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << " socket failed ";exit(2);}// 2.發送連接請求struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port);inet_aton(ip.c_str(), &server.sin_addr);int n = connect(sockfd, (sockaddr *)&server, sizeof(server));if (n < 0){std::cerr << " connect failed ";exit(2);}// 3.進行通信std::string message;char buffer[1024];while (1){std::cout << "Please Enter@";std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}if (message == "quit")break;}close(sockfd);return 0;
}

以上就是客戶端和服務端的所有代碼編寫,只不過給服務端只能處理一個用戶端。

為了能夠同時處理多個用戶端,此處我們需要使用多進程或多線程來實現。

TCP + 多進程

  • 父進程創建子進程,讓子進程來與客戶端進行交互;
  • 父進程只負責與子進程建立連接。

此處需要考慮子進程的回收問題,我們并不希望對子進程進行等待,因此有兩種方案:

  1. 直接將SIGCHLD信號進行屏蔽;
  2. 使用孫子進程來完成與客戶端通信,子進程直接回收;

此處我們采用孫子進程的方式直接回收子進程,讓孫子進程被超卓系統領養。

此處我們僅需要對服務端類中得Start進行修改即可:

    void Start(){// 1. 建立連接// 2. 讀取消息// 3. 對消息進行處理,并返回while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(sockfd_, (struct sockaddr *)&client, &len);if (fd < 0){Log(Warning) << "accept failed";}// 使用多進程來實現pid_t id = fork();if (id < 0){Log(Fatal) << "fork failed";}else if (id == 0){close(sockfd_);if (fork() == 0)   // 使用孫子進程進行通信{Service(fd);exit(0);}exit(0);}// 父進程直接將fd關閉,不允許父進程與客戶端進行通信close(fd);pid_t rid = waitpid(id, nullptr, 0);  // 回收子進程}}

以上就是多進程服務端的修改,也很簡單。

TCP + 線程池

  • 主線程先任務隊列中添加任務,而線程池中的線程負責將任務取出來,執行。

引入線程池,向任務隊列中放什么???

有兩種方案:

  1. 對Task任務類進行從寫;
  2. 向任務隊列中放函數對象,讓線程能夠直接調用。

此處兩種方法都實現一下:

重寫Task任務

  • 我們希望主線程構建一個Task任務,加入到任務隊列中,然后線程池中的線程拿出來執行。
  • 線程池中的線程如果想與用戶端進行通信,就必須拿到文件描述符,因此Task類私有成員有一個文件描述符
  • task任務的調用運算符重載,應該變成原來的Service函數實現.

重寫如下:

class Task
{
public:Task(const int &fd): fd_(fd){}void operator()(){char buffer[1024];while (1){memset(buffer, 0, sizeof(buffer));int n = read(fd_, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::string ret = "I have got your message : " + std::string(buffer);write(fd_, ret.c_str(), ret.size());if (strcmp(buffer, "quit") == 0)break;}else if (n == 0){close(fd_);break;}else{Log(Level::Error) << "read error";close(fd_);break;}}}private:int fd_;
};

下一步就是對服務端的Start的函數進行重寫,主線程負責向線程池放入Task對象:

    void Start(){// 1. 建立連接// 2. 讀取消息// 3. 對消息進行處理,并返回std::unique_ptr<thread_poll<Task>>& ptp = thread_poll<Task>::GetInstance();ptp->run();while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(sockfd_, (struct sockaddr *)&client, &len);if (fd < 0){Log(Level::Warning) << "accept failed";}// 父進程直接將fd關閉,不允許父進程與客戶端進行通信ptp->push(Task(fd));}}

通過這種方式,就實現了主線程向任務隊列中放數據,由線程池中的線程來與用戶端進行溝通。

放函數對象

我們已經有現成的函數調用對象了,就是服務端中的Service函數,但是如果線程池中的線程并沒有在該函數中,因此也就沒有this指針了,所以我們在傳函數對象的時候,可以使用std::bind進行綁定,將this指針綁定到函數對象中,這樣線程池中的線程就可以直接進行調用了。

我們只需要對Service函數進行綁定,保證線程池中的線程在調用的時候,不需要傳遞任何參數,可以直接調用即可:

    void Start(){// 1. 建立連接// 2. 讀取消息// 3. 對消息進行處理,并返回using  fun_t =  std::function<void()>;std::unique_ptr<thread_poll<fun_t>>& ptp = thread_poll<fun_t>::GetInstance();ptp->run();while (1){struct sockaddr_in client;socklen_t len = sizeof(client);int fd = accept(sockfd_, (struct sockaddr *)&client, &len);if (fd < 0){Log(Level::Warning) << "accept failed";}// 父進程直接將fd關閉,不允許父進程與客戶端進行通信fun_t func = std::bind(&Server::Service , this , fd);  // 綁定this指針和文件描述符ptp->push(func);}}

以上兩種方法都比較常用,后一種方法實現上更簡單一些。

客戶端重連

當服務端掛掉或者讀寫出錯時,我們上面的客戶端會直接退出;當服務端出現問題的時候,我們并不應該將客戶端直接退出,而是讓客戶端進行重連,即重新向服務端發送建立連接的請求

下面我們將進行模擬實現,客戶端重連的機制:

  • 客戶端重連,必定需要進行循環;當服務端掛掉時,讓客戶端重新進行connect嘗試重新建立連接;
  • 我們也不能一直讓客戶端進行連接,當嘗試連接的次數達到一定限制時,才讓客戶端退出。

下面時修改后的代碼實現,我們的主循環內部有兩個循環,一個用來控制重連的次數,另一個用來與服務端建立聯系。

void Menu(char *argv[])
{std::cout << argv[0] << "  [ip] " << " [port] " << std::endl;
}int main(int argc, char *argv[])
{if (argc != 3){Menu(argv);exit(1);}std::string ip = argv[1];uint16_t port = std::stoi(argv[2]);struct sockaddr_in server;server.sin_family = AF_INET;server.sin_port = htons(port);inet_aton(ip.c_str(), &server.sin_addr);while (1){int cnt = 0, n = 0 , sockfd = -1;const int max_cnt = 6;do{// 1.創建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << " socket failed ";exit(2);}// 2.connextn = connect(sockfd, (sockaddr *)&server, sizeof(server));if (n < 0){std::cout << "connet failed : " << cnt++ << std::endl;sleep(1);}elsebreak;} while (cnt < max_cnt);if (cnt == max_cnt){std::cout << "server error" << std::endl;return 0;}// 3.進行通信std::string message;char buffer[1024];while (1){std::cout << "Please Enter@";std::getline(std::cin, message);write(sockfd, message.c_str(), message.size());n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = 0;std::cout << buffer << std::endl;}elsebreak;if (message == "quit"){close(sockfd);return 0;}}}return 0;
}

客戶端在直接進行連接的時候,會出現連接失敗,因核心原因是 服務器重啟時,原端口因 TCP TIME_WAIT 狀態被占用,導致無法重新綁定端口(監聽失敗)

所以我們需要對服務器進行設置:在服務器的 socket 創建后、bind 前,添加 端口復用選項

int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
// 防止偶發性的服務器無法進行立即重啟

進程組與守護進程

在操作系統中我們有前臺進程和后臺進程;

  • 通過jobs指令可以查看后臺進程;
  • fg + 任務號:將后臺進程拿到前臺;

但前臺進程被暫停后,如果向前臺進程發送19號信息,即SIGSTOP時,前臺進程會被自動移動到后
臺進程,此時bash命令行解釋器會被移動到前臺。

  • bg + 任務號,將后臺暫停的進程繼續執行。

在設計服務器的時候,我們希望服務器是后臺進程,并且不受到用戶的登錄和退出的影響
下面解釋如何做到:

進程組和會話

  • 在操作系統中有一個進程組的概念,進程組是一個或多個進程的集合,進程組中有一個組長:PID==PGID就是組長;
  • 組長負責創建一個進程組或者在進程組中創建進程;該組長進程執行完畢,并不會影響組內其他進程的執行;

一個進程組中的進程協作來完成任務,最常見的就是通過管道執行命令,管道中的所有命令都屬于一個進程組

可以通過ps aj來查看進程的相關ID信息:

![[Pasted image 20250827194606.png]]

  • 在操作系統中又定義了session會話的概念,session指的是一個或多個進程組。
  • 通常默認一個會話與一個終端進行關聯,在操作系統中會有一個初始會話,該會話與終端直接建立聯系,控制終端可以向初始會話中的進程發送信號,同時當控制終端退出的時候,內部的所有進程,進程組都會被退出,這就會導致我們的服務器也會退出。

但是好在,當我們創建一個新會話的時候,新會話默認沒有控制終端,這也就保證了新會話不受終端的登錄和退出的控制。

因此只要讓服務端自成一個新會話,就可以保證服務端持續運行。該進程不再與鍵盤關聯,不受到登錄和注銷的影響,這種進程就被稱為守護進程。下面看看守護進程如何實現。

守護進程

  • 一個進程組的組長不能自成會話,也就不能當守護進程。

因此在自成會話的時候,需要時子進程,讓父進程直接退出,子進程作為孤兒進程自成會話。
我們通過pid_t setsid(void)來讓一個進程自成會話。

  • 一般我們會選擇將守護進程的一些信號進行忽略,防止收到信號影響;
  • 并且一般會更改目錄,以及輸入輸出,將輸入輸出定向到/dev/null中。

現在讓我們來實現守護進程:

const std::string defaultdir = "/";
const std::string nullfile = "/dev/null";void Deamon(bool ischdir , bool isclose)
{// 1.忽略信號signal(SIGPIPE , SIG_IGN);signal(SIGPIPE , SIG_IGN);signal(SIGSTOP , SIG_IGN);// 2. 自成會話if(fork() > 0 ) exit(0);   // 父進程直接退出setsid();if(ischdir)chdir(defaultdir.c_str());if(isclose)    // 是否關閉文件{close(0);close(1);close(2);}else{int fd = open(nullfile.c_str() , O_RDWR);dup2(fd , 0);dup2(fd , 1);dup2(fd , 2);}
}

以上就是自己實現的守護進程接口。

實際上操作系統也提供了接口,讓一個進程自成會話int daemon(int nochdir , int noclose),在這里就不再介紹了。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/922237.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/922237.shtml
英文地址,請注明出處:http://en.pswp.cn/news/922237.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

還停留在批處理時代嗎?增量計算架構詳解

在當今的數字化環境中&#xff0c;企業不再只是一味地囤積數據——他們癡迷于盡快把數據轉化為可付諸行動的洞察。真正的優勢來自于實時發現變化并立即做出反應&#xff0c;無論是調整推薦策略還是規避危機。 十年前&#xff0c;硬件與平臺技術的進步讓我們能夠從容應對海量數…

DataSet-深度學習中的常見類

深度學習中Dataset類通用的架構思路 Dataset 類設計的必備部分 1. 初始化 __init__ 配置和路徑管理&#xff1a;保存 config&#xff0c;區分 train/val/test 路徑。加載原始數據&#xff1a;CSV、JSON、Numpy、Parquet 等。預處理器/歸一化器&#xff1a;如 StandardScaler&am…

【VC】 error MSB8041: 此項目需要 MFC 庫

? 目錄 ?&#x1f6eb; 導讀問題背景環境1?? 核心原因&#xff1a;MFC 組件缺失或配置不當2?? 解決方案&#xff1a;安裝 MFC 組件并驗證配置2.1 步驟1&#xff1a;檢查并安裝 MFC 組件2.2 步驟2&#xff1a;檢查并修正項目配置2.3 步驟3&#xff1a;針對特定場景的補充方…

Java零基礎學習Day10——面向對象高級

一.認識final1.含義final關鍵字是最終的意思&#xff0c;可以修飾&#xff1a;類&#xff0c;方法&#xff0c;變量修飾類&#xff1a;該類被稱為最終類&#xff0c;特點是不能被繼承修飾方法&#xff1a;該方法被稱為最終方法&#xff0c;特點是不能被重寫了修飾變量&#xff…

Qt中解析JSON文件

Qt中解析JSON文件 在Qt中解析JSON字符串主要有兩種方式&#xff1a;使用QJsonDocument類或使用QJsonDocument結合QVariant。以下是詳細的解析方法&#xff1a; 使用QJsonDocument&#xff08;推薦&#xff09; 這種方式的主要相關類如下&#xff1a; QJsonDocument: QJsonDocum…

深度解析HTTPS:從加密原理到SSL/TLS的演進之路

在互聯網時代,數據安全已成為不可忽視的基石。當我們在瀏覽器地址欄看到"https://"前綴和那把小小的綠色鎖圖標時,意味著正在進行一場受保護的通信。但這層保護究竟是如何實現的?HTTPS、SSL和TLS之間又存在著怎樣的聯系與區別?本文將深入剖析這些技術細節,帶你全…

Flutter 官方 LLM 動態 UI 庫 flutter_genui 發布,讓 App UI 自己生成 UI

今日&#xff0c;Flutter 官方正式發布了它們關于 AI 大模型的 package 項目&#xff1a; genui &#xff0c;它是一個非常有趣和前沿的探索類型的項目&#xff0c;它的目標是幫助開發者構建由生成式 AI 模型驅動的動態、對話式用戶界面&#xff1a; 也就是它與傳統 App 中“寫…

Redis常用數據結構及其底層實現

Redis常用數據結構主要有String List Set Zset Hash BitMap Hyperloglog Stream GeoString:Redis最常用的一種數據結構,Sting類型的數據存儲結構有三種int、embstr、raw1.int:用來存儲long以下的整形embstr raw 都是用來存字符串&#xff0c;其中小于44字節的字符串用embstr存 …

O3.4 opencv圖形拼接+答題卡識別

一圖形拼接邏輯導入必要的庫pythonimport cv2 import numpy as np import sys導入cv2庫用于圖像處理&#xff0c;numpy庫用于數值計算&#xff0c;sys庫用于與 Python 解釋器進行交互&#xff0c;例如退出程序。定義圖像顯示函數def cv_show(name, img):cv2.imshow(name, img)c…

SQL注入常見攻擊點與防御詳解

SQL注入是一種非常常見且危險的Web安全漏洞。攻擊者通過將惡意的SQL代碼插入到應用程序的輸入參數中&#xff0c;欺騙后端數據庫執行這些非預期的命令&#xff0c;從而可能竊取、篡改、刪除數據或獲得更高的系統權限。以下是詳細、準確的SQL注入點分類、說明及舉例&#xff1a;…

EKSPod 資源利用率配置修復:從占位符到完整資源分析系統

概述 在 Kubernetes 集群管理過程中,資源利用率的監控和優化是保證應用性能和成本效益的關鍵環節。近期,我們對 EKSPod 管理界面的資源利用率顯示功能進行了全面修復,將原先簡單的占位符文本升級為完整的資源分析系統。本文將詳細介紹這次修復的背景、方案、實現細節和最終…

Linux內核(架構)

文章目錄Linux內核架構概述核心子系統詳解1、進程管理2、內存管理3、虛擬文件系統(VFS)4、設備驅動模型掌握Linux內核核心技術階段1&#xff1a;基礎準備階段2&#xff1a;內核基礎階段3&#xff1a;深入子系統階段4&#xff1a;高級主題&#xff08;持續學習&#xff09;調試和…

基于數據挖掘的單純冠心病與冠心病合并糖尿病的證治規律對比研究

標題:基于數據挖掘的單純冠心病與冠心病合并糖尿病的證治規律對比研究內容:1.摘要 背景&#xff1a;冠心病和冠心病合并糖尿病在臨床上較為常見&#xff0c;且二者在證治方面可能存在差異&#xff0c;但目前相關系統研究較少。目的&#xff1a;對比基于數據挖掘的單純冠心病與冠…

即夢AI快速P圖

原圖&#xff1a; 模型選擇3.0效果比較好&#xff0c;提示詞“根據提供圖片&#xff0c;要求把兩邊臉變小&#xff0c;要求把臉變尖&#xff0c;要求眼妝變淡&#xff0c;眼睛更有神&#xff0c;要求提亮面部膚色要求面部均勻&#xff0c;面部要磨皮!鼻頭高光和鼻翼兩邊陰影變淡…

【辦公類-109-04】20250913圓牌卡片(接送卡被子卡床卡入園卡_word編輯單面)

背景需求: 為了發被子,我做了全校批量的圓形掛牌,可以綁在“被子包”提手上,便于再操場上發放被子時,很多老師可以協助根據學號發放。 https://blog.csdn.net/reasonsummer/article/details/149755556?spm=1011.2415.3001.5331https://blog.csdn.net/reasonsummer/arti…

Shoptnt 促銷計算引擎詳解:策略模式與責任鏈的完美融合

在電商系統中&#xff0c;促銷計算是業務邏輯最復雜、變更最頻繁的模塊之一。它不僅需要處理多種促銷類型&#xff08;滿減、折扣、優惠券等&#xff09;&#xff0c;還要管理它們之間的優先級和互斥關系。 Shoptnt 設計了一套基于 策略模式 (Strategy Pattern) 和 責任鏈模式…

【HTTP 請求格式】從請求行 到 請求體

引言 在前后端開發中&#xff0c;前端和后端之間的交互主要依賴于 HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本傳輸協議&#xff09;。HTTP 是互聯網通信的基礎&#xff0c;它定義了客戶端&#xff08;通常是瀏覽器或App&#xff09;和服務器之間如何交換數…

【自記】SQL 中 GROUPING 和 GROUPING SETS 語句的案例說明

我們用一個生活中的例子來理解&#xff0c;比如你開了家小超市&#xff0c;想統計「銷售額」&#xff0c;但需要從多個角度看&#xff08;比如按 “日期 商品”、“僅日期”、“僅商品”、“整體總銷售額”&#xff09;。假設你的銷售數據長這樣&#xff08;簡化版&#xff09…

C語言第五課:if、else 、if else if else 控制語句

C語言第五課&#xff1a;if、else 、if else if else 控制語句if else 、if else if else 聯合使用編程快速學習平臺if else 、if else if else 聯合使用 代碼示列 #include <stdio.h> int main(){//設置中文編碼輸出到控制臺system("chcp 65001");//今天星…

七彩喜智慧養老:用科技溫暖晚年,讓關愛永不掉線

“當銀發潮遇見科技力&#xff0c;養老方式正在發生一場靜悄悄的變革。”你有沒有想過&#xff1a;當父母年邁獨居時&#xff0c;如何確保他們的安全&#xff1f;當老人突然摔倒&#xff0c;如何第一時間獲得救助&#xff1f;當慢性病需要長期管理&#xff0c;如何避免頻繁奔波…