計算機網絡 : Socket編程

計算機網絡 : Socket編程

目錄

  • 計算機網絡 : Socket編程
    • 引言
    • 1.UDP網絡編程
      • 1.1 網絡地址與端口轉換函數
      • 1.2 本地環回
      • 1.3 `EchoServer`
      • 1.4 `DictServer`
      • 1.5 `DictServer`封裝版
      • 1.6 簡單聊天室
    • 2.TCP網絡編程
      • 2.1 TCP Socket API詳解
      • 2.2 `Echo Server`
      • 2.3 `Echo Server`多進程版
      • 2.4 `Echo Server`多線程版
      • 2.5 多線程遠程命令執行
      • 2.6 `Echo Server`線程池版

引言

Socket編程是網絡通信的核心技術之一,它允許不同主機之間的進程進行數據交換,是實現網絡應用的基礎。無論是簡單的客戶端-服務器通信,還是復雜的分布式系統,Socket都扮演著至關重要的角色。

本文將從基礎的UDP和TCP協議出發,逐步介紹Socket編程的核心概念和實現方法。內容涵蓋:

  1. UDP編程:介紹無連接通信的實現,包括地址轉換、本地環回、Echo服務器和字典服務器的開發。
  2. TCP編程:深入講解面向連接的通信,包括多進程、多線程和線程池版本的服務器實現,以及遠程命令執行等實際應用。

通過代碼示例和詳細注釋,讀者可以快速掌握Socket編程的核心技術,并能夠根據需求開發出高效、穩定的網絡應用程序。無論你是初學者還是有一定經驗的開發者,本文都能為你提供實用的指導和啟發。


1.UDP網絡編程

1.1 網絡地址與端口轉換函數

本文章只介紹基于IPV4的Socket網絡編程,sockaddr_in中的成員struct in_addr sin_addr表示32位的IP地址。

  • 但是我們通常用點分十進制的字符串表示IP地址,以下函數可以在字符串表示和in_addr表示之間轉換。

    • 字符串轉in_addr的函數:

      在這里插入圖片描述

    • in_addr轉字符串的函數:

      在這里插入圖片描述

    其中 inet_ptoninet_ntop 不僅可以轉換 IPv4in_addr,還可以轉換 IPv6in6_addr,因此函數接口是 void *addrptr

    • 代碼示例:

      在這里插入圖片描述

  • 關于inet_ntoa

    inet_ntoa 這個函數返回了一個 char*,顯然是函數自己在內部申請了一塊內存來保存 IP 的結果。那么是否需要調用者手動釋放嗎?

    在這里插入圖片描述

    根據 man 手冊,inet_ntoa 將返回結果存放在靜態存儲區,因此不需要手動釋放。

    在這里插入圖片描述

    在這里插入圖片描述

    但需要注意的是,由于 inet_ntoa 使用內部靜態存儲區,第二次調用的結果會覆蓋上一次的結果。

    思考:如果有多個線程調用 inet_ntoa,是否會出現異常情況?

    • 在 APUE中明確指出,inet_ntoa 不是線程安全的函數;
    • 但在 CentOS7 上測試時并未出現問題,可能是內部實現加了互斥鎖;
    • 在多線程環境下,推薦使用 inet_ntop,該函數要求調用者提供緩沖區存儲結果,從而規避線程安全問題。
  • 總結:以后進行網絡地址與端口轉換,就是用下面四個函數

    1. inet_pton (地址字符串 -> 二進制)

      功能:將點分十進制的IP字符串轉換為網絡字節序的二進制形式
      參數

      • af:地址族(AF_INET for IPv4, AF_INET6 for IPv6)
      • src:輸入字符串(如 "192.168.1.1"
      • dst:輸出緩沖區(需提前分配)

      返回值

      • 成功返回 1,失敗返回 0-1

      示例

      #include <arpa/inet.h>struct in_addr addr;
      inet_pton(AF_INET, "192.168.1.1", &addr);
      
    2. inet_ntop (二進制 -> 地址字符串)

      功能:將網絡字節序的二進制IP轉換為可讀字符串
      參數

      • af:地址族
      • src:二進制地址(如 struct in_addr
      • dst:輸出字符串緩沖區
      • size:緩沖區大小(推薦用 INET_ADDRSTRLEN 宏)

      返回值:成功返回 dst 指針,失敗返回 NULL

      示例

      char str[INET_ADDRSTRLEN];
      inet_ntop(AF_INET, &addr, str, sizeof(str));
      
    3. htons & ntohs (主機字節序 <-> 網絡字節序)

      功能

      • htons:主機字節序(小端/大端) -> 網絡字節序(大端)
      • ntohs:網絡字節序 -> 主機字節序

      參數uint16_t 類型的端口號
      返回值:轉換后的值

      示例

      uint16_t port_host = 8080;
      uint16_t port_net = htons(port_host);  // 主機轉網絡
      uint16_t port_back = ntohs(port_net); // 網絡轉主機
      
    4. 注意事項

      1. 字節序問題
        • htons/ntohs 用于解決不同機器的字節序差異,網絡傳輸必須用大端。
        • 即使主機字節序本身就是大端,調用這些函數也不會出錯(無操作)。
      2. 緩沖區大小
        • inet_ntop 的緩沖區需足夠大(IPv4用 INET_ADDRSTRLEN,IPv6用 INET6_ADDRSTRLEN)。
      3. 錯誤檢查
        • inet_ptoninet_ntop 需檢查返回值,無效輸入會失敗。

1.2 本地環回

本地環回(Local Loopback) 是指網絡通信中,數據不經過物理網卡,而是直接在本地計算機內部回送(loop back)的一種機制。它主要用于測試本機的網絡協議棧(如 TCP/IP)是否正常工作,或者用于本地進程間通信(IPC)。

  • 環回地址

    在 IPv4 中,標準的環回地址是 127.0.0.1(通常用 localhost 表示)。
    在 IPv6 中,環回地址是 ::1

    • 當你訪問 127.0.0.1 時,數據不會真正發送到網絡上,而是在操作系統內部直接回送。
    • 所有發送到 127.0.0.1 的數據都會被本機接收,適用于本地服務測試(如 Web 服務器、數據庫等)。
  • 環回接口

    操作系統會虛擬一個 環回網卡(lo 或 lo0),專門用于處理環回流量:

    • Linux/Unixifconfig loip addr show lo
    • Windowsipconfig 可以看到 127.0.0.1 綁定在環回接口上

    特點

    • 不需要物理網卡,純軟件實現。
    • 即使沒有網絡連接,環回接口仍然可用。
  • 常見用途

    1. 測試網絡服務
      • 例如運行一個本地 Web 服務器(如 http://127.0.0.1:8080),檢查服務是否正常。
    2. 進程間通信(IPC)
      • 兩個本地進程可以通過 127.0.0.1 進行 Socket 通信,而無需經過外部網絡。
    3. 屏蔽外部訪問
      • 某些服務(如數據庫)可以只監聽 127.0.0.1,防止外部機器連接,提高安全性。
  • 示例(C++ Socket 綁定環回地址)

    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>int main() {int sockfd = socket(AF_INET, SOCK_STREAM, 0);struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(8080);  // 綁定 8080 端口local.sin_addr.s_addr = inet_addr("127.0.0.1");  // 綁定到環回地址// 綁定 Socketbind(sockfd, (struct sockaddr*)&local, sizeof(local));// ... 其他操作(listen, accept, ...)return 0;
    }
    //這樣綁定的服務只能通過本機訪問,外部機器無法連接。
    

1.3 EchoServer

簡單的回顯服務器和客戶端代碼

  • UdpServer.hpp

    #pragma once  // 防止頭文件被重復包含// 包含必要的系統頭文件
    #include <iostream>    // 標準輸入輸出
    #include <string>      // 字符串處理
    #include <cerrno>      // 錯誤號定義
    #include <cstring>     // 字符串操作函數
    #include <unistd.h>    // POSIX系統調用
    #include <strings.h>   // bzero等函數//套接字編程必備4個頭文件
    #include <sys/types.h> // 系統數據類型
    #include <sys/socket.h> // 套接字相關
    #include <netinet/in.h> // 網絡地址結構
    #include <arpa/inet.h>  // IP地址轉換// 包含自定義頭文件
    #include "nocopy.hpp"  // 禁止拷貝的基類
    #include "Log.hpp"     // 日志系統
    #include "Comm.hpp"    // 通用通信定義
    #include "InetAddr.hpp" // IP地址處理類// 定義默認常量
    const static uint16_t defaultport = 8888;  // 默認端口號
    const static int defaultfd = -1;           // 默認無效文件描述符
    const static int defaultsize = 1024;       // 默認緩沖區大小// UDP服務器類,繼承自nocopy表示禁止拷貝
    class UdpServer : public nocopy
    {
    public:
    // 構造函數,初始化端口號和socket文件描述符
    UdpServer(uint16_t port = defaultport): _port(port), _sockfd(defaultfd)
    {
    }// 初始化UDP服務器
    void Init()
    {// 1. 創建socket文件描述符// AF_INET表示IPv4網絡協議而非本地通信// SOCK_DGRAM表示UDP協議數據報通信// 0表示使用默認協議_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0)  // 創建失敗處理{// 記錄錯誤日志,包括錯誤號和錯誤信息lg.LogMessage(Fatal, "socket errr, %d : %s\n", errno, strerror(errno));exit(Socket_Err);  // 退出程序并返回錯誤碼}// 記錄socket創建成功的日志lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);// 2. 綁定socket到指定端口struct sockaddr_in local;  // 定義IPv4地址結構bzero(&local, sizeof(local));  // 清空結構體全部置0,等同于memset// 設置地址族為IPv4local.sin_family = AF_INET;// 設置端口號,htons將主機字節序轉換為網絡字節序local.sin_port = htons(_port);// 設置IP地址為INADDR_ANY(0.0.0.0),表示監聽所有網絡接口local.sin_addr.s_addr = INADDR_ANY;//local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1. 4字節 IP 2. 變成網絡序列// 綁定socket到指定的地址和端口int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));//::表示調用全局命名空間if (n != 0)  // 綁定失敗處理{// 記錄綁定錯誤日志lg.LogMessage(Fatal, "bind errr, %d : %s\n", errno, strerror(errno));exit(Bind_Err);  // 退出程序并返回錯誤碼}
    }// 啟動服務器主循環
    void Start()
    {// 定義接收緩沖區char buffer[defaultsize];// 服務器主循環,永不退出for (;;){// 定義客戶端地址結構struct sockaddr_in peer;// 獲取客戶端地址結構長度socklen_t len = sizeof(peer);// 接收UDP數據報ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr *)&peer, &len);// 參數說明:// _sockfd: socket文件描述符// buffer: 接收緩沖區// sizeof(buffer)-1: 緩沖區大小(保留一個字節給字符串結束符)// 0: 默認標志// (struct sockaddr *)&peer: 客戶端地址結構// &len: 地址結構長度// 如果接收到數據(n>0)if (n > 0){// 將接收到的數據以字符串形式處理(添加結束符)buffer[n] = 0;// 創建客戶端地址對象并打印調試信息InetAddr addr(peer);// 輸出接收到的消息和客戶端地址信息std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;// 將接收到的消息原樣返回給客戶端(echo服務)sendto(_sockfd, buffer, strlen(buffer), 0, (struct sockaddr *)&peer, len);}}
    }// 析構函數
    ~UdpServer()
    {// 注意:這里應該關閉socket文件描述符// 可以添加: if(_sockfd != defaultfd) close(_sockfd);
    }private:
    uint16_t _port;  // 服務器監聽端口
    int _sockfd;     // socket文件描述符
    };
    
    • INADDR_ANY宏定義

    在網絡編程中,云服務器不允許直接 bind 公有 IP,也不推薦編寫服務器時綁定明確的 IP 地址,推薦直接寫成 INADDR_ANY。當一個進程需要綁定網絡端口進行通信時,使用 INADDR_ANY 作為 IP 地址參數意味著該端口可以接受來自任何 IP 地址的連接請求(無論是本地主機還是遠程主機)。例如,若服務器有多個網卡(每個網卡對應不同 IP 地址),使用 INADDR_ANY 可省去確定數據具體來自哪個網卡/IP 地址的步驟。

    /* Address to accept any incoming messages. */
    #define INADDR_ANY ((in_addr_t) 0x00000000)
    
  • UdpClient.hpp

    #include <iostream>       // 標準輸入輸出流
    #include <cerrno>         // 錯誤號定義
    #include <cstring>        // 字符串操作函數
    #include <string>         // C++字符串類
    #include <unistd.h>       // POSIX操作系統API
    #include <sys/types.h>    // 基本系統數據類型
    #include <sys/socket.h>   // 套接字接口
    #include <arpa/inet.h>    // 互聯網操作函數
    #include <netinet/in.h>   // 互聯網地址族// 使用方法提示函數
    void Usage(const std::string &process)
    {std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
    }// 主函數:./udp_client server_ip server_port
    int main(int argc, char *argv[])
    {// 1. 參數校驗if (argc != 3){Usage(argv[0]);  // 打印使用方法return 1;        // 參數錯誤返回1}// 2. 解析命令行參數std::string serverip = argv[1];       // 服務器IP地址uint16_t serverport = std::stoi(argv[2]);  // 服務器端口號// 3. 創建UDP套接字// AF_INET: IPv4地址族// SOCK_DGRAM: 數據報套接字(UDP)// 0: 默認協議int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0){std::cerr << "socket error: " << strerror(errno) << std::endl;return 2;  // 套接字創建失敗返回2}std::cout << "create socket success: " << sock << std::endl;//client需要bind,但是不需要顯式bind,client會在首次發送數據的時候會自動進行bind。//server 端的端口號,一定是眾所周知,不可改變的;client 需要端口bind 隨機端口號,因為client 會非常多,所以讓本地OS自動隨機bind端口號。// 4. 準備服務器地址信息struct sockaddr_in server;memset(&server, 0, sizeof(server));  // 清空結構體server.sin_family = AF_INET;         // IPv4地址族server.sin_port = htons(serverport); // 端口號(主機字節序轉網絡字節序)server.sin_addr.s_addr = inet_addr(serverip.c_str()); // IP地址轉換// 5. 主循環:發送和接收數據while (true){// 5.1 獲取用戶輸入std::string inbuffer;std::cout << "Please Enter# ";std::getline(std::cin, inbuffer);// 5.2 發送數據到服務器ssize_t n = sendto(sock,                  // 套接字描述符inbuffer.c_str(),        // 發送數據緩沖區inbuffer.size(),        // 數據長度0,                      // 標志位(通常為0)(struct sockaddr*)&server, // 服務器地址sizeof(server));        // 地址長度if(n > 0)  // 發送成功{// 5.3 準備接收服務器響應char buffer[1024];  // 接收緩沖區// 臨時存儲對端地址信息struct sockaddr_in temp;socklen_t len = sizeof(temp);// 接收服務器響應ssize_t m = recvfrom(sock,           // 套接字描述符buffer,          // 接收緩沖區sizeof(buffer)-1, // 緩沖區大小(留1字節給'\0')0,              // 標志位(通常為0)(struct sockaddr*)&temp, // 對端地址&len);          // 地址長度if(m > 0)  // 接收成功{buffer[m] = 0;  // 手動添加字符串結束符std::cout << "server echo# " << buffer << std::endl;}else{break;  // 接收失敗退出循環}}else{break;  // 發送失敗退出循環}}// 6. 關閉套接字close(sock);return 0;
    }
    
    • recvfrom參數說明:

      recvfrom 是用于 無連接協議(如 UDP) 的 Socket 接收函數,它不僅能接收數據,還能獲取發送方的地址信息(IP + Port)。

      參數說明
      sockfd已創建的 UDP Socket 描述符(由 socket(AF_INET, SOCK_DGRAM, 0) 返回)
      buf存儲接收數據的緩沖區
      len緩沖區大小(字節數)
      flags控制選項(如 MSG_WAITALLMSG_DONTWAIT,通常設為 0
      src_addr存放發送方地址的 struct sockaddr(可以是 sockaddr_insockaddr_in6
      addrlen輸入時指定 src_addr 的大小,返回時是實際地址長度
  • Comm.hpp

    #pragma once// 定義一個枚舉類型來表示各種錯誤代碼
    // 枚舉(enum)是一種用戶定義的類型,包含一組命名的整數常量
    enum {// 用法錯誤,賦值為1// 枚舉默認從0開始,這里顯式指定從1開始Usage_Err = 1,  // 套接字創建錯誤,自動賦值為2 (前一個值+1)// 表示創建網絡套接字(socket)時發生的錯誤Socket_Err,// 綁定錯誤,自動賦值為3// 表示將套接字綁定到特定地址和端口時發生的錯誤Bind_Err// 注意:枚舉值后面可以加逗號,也可以不加// 這里選擇不加逗號以保持簡潔
    };
    
  • nocopy.hpp

    // 防止頭文件被重復包含的預處理指令
    // 這是C/C++中防止多次包含同一頭文件的常用方式
    #pragma once  // 包含標準輸入輸出流庫,雖然當前類未使用,但通常保留以備后續擴展
    #include <iostream>  // 定義一個名為nocopy的類,其功能是禁止對象的拷貝操作
    class nocopy  
    {
    public:  // 默認構造函數(無參構造函數)// 使用空實現,因為該類僅用于禁止拷貝,不需要特殊構造邏輯nocopy() {}  // 刪除拷貝構造函數// = delete 是C++11特性,表示顯式禁止該函數的自動生成和調用// 任何嘗試拷貝nocopy對象的操作都會引發編譯錯誤nocopy(const nocopy &) = delete;  // 刪除拷貝賦值運算符// 同樣使用=delete禁止,任何嘗試賦值的操作都會引發編譯錯誤const nocopy& operator=(const nocopy &) = delete;  // 析構函數// 使用空實現,因為該類沒有需要特殊清理的資源// 聲明為虛函數是更安全的做法(如果考慮繼承),但當前實現未使用~nocopy() {}  
    };// 該類典型用法:
    // class MyClass : private nocopy { ... };
    // 這樣MyClass將自動禁用拷貝構造和拷貝賦值功能
    
  • InetAddr.hpp

    #pragma once  // 防止頭文件被重復包含#include <iostream>      // 標準輸入輸出流
    #include <string>        // 字符串處理
    #include <sys/types.h>   // 系統類型定義
    #include <sys/socket.h>  // 套接字相關函數和結構體
    #include <netinet/in.h>  // 互聯網地址族定義
    #include <arpa/inet.h>   // 互聯網操作函數// InetAddr類:封裝網絡地址信息(IP和端口)
    class InetAddr 
    {
    public:// 構造函數:通過sockaddr_in結構體初始化// 參數:addr - 包含IP和端口信息的socket地址結構體InetAddr(struct sockaddr_in &addr) : _addr(addr){// 將網絡字節序的端口號轉換為主機字節序_port = ntohs(_addr.sin_port);// 將網絡字節序的IP地址轉換為點分十進制字符串_ip = inet_ntoa(_addr.sin_addr);}// 獲取IP地址// 返回值:IP地址字符串std::string Ip() { return _ip; }// 獲取端口號// 返回值:端口號(16位無符號整數)uint16_t Port() { return _port; };// 生成調試信息字符串// 返回值:格式為"IP:端口"的字符串(如"127.0.0.1:8080")std::string PrintDebug(){std::string info = _ip;      // 添加IP部分info += ":";                // 添加分隔符info += std::to_string(_port); // 添加端口部分return info;}// 析構函數~InetAddr(){}private:std::string _ip;      // 存儲IP地址(點分十進制字符串)uint16_t _port;       // 存儲端口號(主機字節序)struct sockaddr_in _addr;  // 存儲原始socket地址結構體
    };
    
  • Log.hpp

    #pragma once  // 防止頭文件被重復包含// 包含必要的標準庫頭文件
    #include <iostream>    // 標準輸入輸出流
    #include <string>      // 字符串處理
    #include <fstream>     // 文件流操作
    #include <memory>      // 智能指針
    #include <ctime>       // 時間處理
    #include <sstream>     // 字符串流
    #include <filesystem>  // 文件系統操作(C++17)
    #include <unistd.h>    // POSIX操作系統API
    #include "Lock.hpp"    // 自定義鎖實現namespace LogModule
    {// 使用我們自己封裝的鎖模塊,也可以替換為C++標準庫的鎖using namespace LockModule;/********************** 常量定義 **********************/const std::string defaultpath = "./log/";  // 默認日志文件存儲路徑const std::string defaultname = "log.txt"; // 默認日志文件名/********************** 日志等級枚舉 **********************/// 定義日志級別,用于區分日志的重要程度enum class LogLevel{DEBUG,   // 調試信息,用于開發階段調試程序INFO,    // 普通信息,記錄程序運行狀態WARNING, // 警告信息,表示可能出現問題但不影響程序運行ERROR,   // 錯誤信息,表示程序出現錯誤但可以繼續運行FATAL    // 致命錯誤,表示程序無法繼續運行};/********************** 工具函數 **********************//*** @brief 將日志等級枚舉轉換為可讀字符串* @param level 日志等級枚舉值* @return 對應的字符串描述*/std::string LogLevelToString(LogLevel level){switch (level){case LogLevel::DEBUG:   return "DEBUG";   // 返回調試級別字符串case LogLevel::INFO:    return "INFO";    // 返回信息級別字符串case LogLevel::WARNING: return "WARNING"; // 返回警告級別字符串case LogLevel::ERROR:   return "ERROR";   // 返回錯誤級別字符串case LogLevel::FATAL:   return "FATAL";   // 返回致命錯誤字符串default:                return "UNKNOWN"; // 未知級別處理}}/*** @brief 獲取當前格式化的時間字符串* @return 格式為"YYYY-MM-DD HH:MM:SS"的時間字符串*/std::string GetCurrTime(){time_t tm = time(nullptr);  // 獲取當前時間戳struct tm curr;             // 定義tm結構體localtime_r(&tm, &curr);    // 轉換為本地時間(線程安全版本)// 使用snprintf格式化時間字符串,保證緩沖區安全char timebuffer[64];snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",curr.tm_year + 1900,  // 年份(需要加1900)curr.tm_mon,         // 月份(0-11)curr.tm_mday,        // 日(1-31)curr.tm_hour,        // 時(0-23)curr.tm_min,         // 分(0-59)curr.tm_sec);        // 秒(0-59)return timebuffer;}/********************** 策略模式接口 **********************//*** @brief 日志策略抽象基類* 定義日志輸出的通用接口,具體實現由派生類完成*/class LogStrategy{public:// 虛析構函數,確保派生類對象能正確釋放資源virtual ~LogStrategy() = default;/*** @brief 同步日志接口* @param message 需要輸出的日志消息*/virtual void SyncLog(const std::string &message) = 0;};/********************** 具體策略實現 **********************//*** @brief 控制臺日志策略* 將日志輸出到標準錯誤流(std::cerr)*/class ConsoleLogStrategy : public LogStrategy{public:/*** @brief 實現日志同步輸出到控制臺* @param message 需要輸出的日志消息*/void SyncLog(const std::string &message) override{// 使用鎖保護控制臺輸出,防止多線程競爭LockGuard LockGuard(_mutex);std::cerr << message << std::endl;  // 輸出到標準錯誤流}// 析構函數(調試時可取消注釋查看對象生命周期)~ConsoleLogStrategy(){// std::cout << "~ConsoleLogStrategy" << std::endl;}private:Mutex _mutex;  // 互斥鎖,保證控制臺輸出的線程安全};/*** @brief 文件日志策略* 將日志輸出到指定文件中*/class FileLogStrategy : public LogStrategy{public:/*** @brief 構造函數,初始化日志文件路徑* @param logpath 日志文件存儲路徑* @param logfilename 日志文件名*/FileLogStrategy(const std::string logpath = defaultpath, std::string logfilename = defaultname): _logpath(logpath), _logfilename(logfilename){// 使用鎖保護目錄創建操作LockGuard lockguard(_mutex);// 檢查目錄是否已存在if (std::filesystem::exists(_logpath))return;try{// 遞歸創建目錄結構std::filesystem::create_directories(_logpath);}catch (const std::filesystem::filesystem_error &e){// 捕獲并輸出文件系統異常std::cerr << e.what() << '\n';}}/*** @brief 實現日志同步輸出到文件* @param message 需要輸出的日志消息*/void SyncLog(const std::string &message) override{// 使用鎖保護文件寫入操作LockGuard lockguard(_mutex);// 拼接完整文件路徑std::string log = _logpath + _logfilename;// 以追加模式打開文件std::ofstream out(log.c_str(), std::ios::app);if (!out.is_open())return;  // 文件打開失敗直接返回out << message << "\n";  // 寫入日志內容out.close();            // 關閉文件}// 析構函數(調試時可取消注釋查看對象生命周期)~FileLogStrategy(){// std::cout << "~FileLogStrategy" << std::endl;}public:std::string _logpath;     // 日志文件存儲路徑std::string _logfilename; // 日志文件名Mutex _mutex;             // 互斥鎖,保證文件寫入的線程安全};/********************** 日志器主類 **********************//*** @brief 日志器主類* 提供統一的日志接口,內部使用策略模式實現不同輸出方式*/class Logger{public:/*** @brief 默認構造函數* 初始化時默認使用控制臺輸出策略*/Logger(){UseConsoleStrategy();  // 默認使用控制臺策略}// 默認析構函數~Logger() = default;/*** @brief 切換到控制臺輸出策略*/void UseConsoleStrategy(){_strategy = std::make_unique<ConsoleLogStrategy>();}/*** @brief 切換到文件輸出策略*/void UseFileStrategy(){_strategy = std::make_unique<FileLogStrategy>();}/********************** 日志消息內部類 **********************//*** @brief 日志消息內部類* 采用RAII技術管理單條日志的生命周期*/class LogMessage{private:LogLevel _type;        // 日志等級std::string _curr_time; // 日志時間戳pid_t _pid;           // 進程IDstd::string _filename; // 源文件名int _line;            // 源代碼行號Logger &_logger;      // 引用外部Logger對象std::string _loginfo;  // 完整的日志信息public:/*** @brief 構造函數,初始化日志頭部信息* @param type 日志等級* @param filename 源文件名* @param line 源代碼行號* @param logger 外部Logger引用*/LogMessage(LogLevel type, std::string &filename, int line, Logger &logger): _type(type),_curr_time(GetCurrTime()),_pid(getpid()),_filename(filename),_line(line),_logger(logger){// 使用字符串流格式化日志頭部信息std::stringstream ssbuffer;ssbuffer << "[" << _curr_time << "] "    // 時間<< "[" << LogLevelToString(type) << "] "  // 等級<< "[" << _pid << "] "          // 進程ID<< "[" << _filename << "] "     // 文件名<< "[" << _line << "]"          // 行號<< " - ";                       // 分隔符_loginfo = ssbuffer.str();  // 保存頭部信息}/*** @brief 重載<<運算符,支持鏈式日志輸入* @tparam T 任意可輸出類型* @param info 需要輸出的信息* @return 當前LogMessage對象的引用*/template <typename T>LogMessage &operator<<(const T &info){std::stringstream ssbuffer;ssbuffer << info;            // 格式化用戶數據_loginfo += ssbuffer.str();  // 追加到日志信息return *this;                // 返回自身支持鏈式調用}/*** @brief 析構函數,在對象銷毀時輸出完整日志*/~LogMessage(){// 如果策略存在,則使用策略輸出日志if (_logger._strategy){_logger._strategy->SyncLog(_loginfo);}}};/*** @brief 重載函數調用運算符,創建LogMessage臨時對象* @param type 日志等級* @param filename 源文件名* @param line 源代碼行號* @return 構造的LogMessage臨時對象*/LogMessage operator()(LogLevel type, std::string filename, int line){return LogMessage(type, filename, line, *this);}private:std::unique_ptr<LogStrategy> _strategy;  // 日志輸出策略智能指針};/********************** 全局對象和宏定義 **********************/Logger logger;  // 全局日志器對象// 定義日志宏,自動填充文件名和行號// 使用示例: LOG(LogLevel::INFO) << "This is a message";#define LOG(type) logger(type, __FILE__, __LINE__)// 定義策略切換宏#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy()  // 切換到控制臺輸出#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy()       // 切換到文件輸出
    }
    

1.4 DictServer

實現一個簡單的英譯漢的網絡字典

  • dict.txt

    apple: 蘋果
    banana: 香蕉
    cat: 貓
    dog: 狗
    book: 書
    pen: 筆
    happy: 快樂的
    sad: 悲傷的
    run: 跑
    jump: 跳
    teacher: 老師
    student: 學生
    car: 汽車
    bus: 公交車
    love: 愛
    hate: 恨
    hello: 你好
    goodbye: 再見
    summer: 夏天
    winter: 冬天
    
  • Dict.hpp

    #pragma once  // 防止頭文件被重復包含#include <iostream>    // 標準輸入輸出流
    #include <string>      // 字符串操作
    #include <fstream>     // 文件流操作
    #include <unordered_map> // 無序哈希表容器const std::string sep = ": ";  // 定義分隔符,用于分割鍵值對// 字典類,用于加載和查詢鍵值對數據
    class Dict 
    {
    private:// 加載字典文件到內存void LoadDict() {// 以輸入模式打開配置文件std::ifstream in(_confpath);// 檢查文件是否成功打開if (!in.is_open()) {// 文件打開失敗,輸出錯誤信息(后續可用日志系統替代)std::cerr << "open file error" << std::endl; return;}std::string line;  // 用于存儲讀取的每一行內容// 逐行讀取文件內容while (std::getline(in, line)) {// 跳過空行if (line.empty()) continue;// 查找分隔符位置auto pos = line.find(sep);// 如果沒有找到分隔符,跳過該行if (pos == std::string::npos) continue;// 提取鍵(分隔符前的部分)std::string key = line.substr(0, pos);// 提取值(分隔符后的部分)std::string value = line.substr(pos + sep.size());// 將鍵值對插入到字典中_dict.insert(std::make_pair(key, value));}in.close();  // 關閉文件}public:// 構造函數,接受配置文件路徑作為參數Dict(const std::string &confpath) : _confpath(confpath) {LoadDict();  // 構造時自動加載字典}// 查詢方法:根據鍵查找對應的值std::string Translate(const std::string &key) {// 在字典中查找鍵auto iter = _dict.find(key);// 如果沒找到,返回"Unknown"if (iter == _dict.end()) return std::string("Unknown");else return iter->second;  // 找到則返回對應的值}// 析構函數(空實現)~Dict() {}private:std::string _confpath;  // 存儲配置文件路徑std::unordered_map<std::string, std::string> _dict;  // 存儲鍵值對的哈希表
    };
    
  • UdpServer.hpp

    #pragma once  // 防止頭文件重復包含// 系統頭文件
    #include <iostream>
    #include <string>
    #include <cerrno>      // 錯誤號相關
    #include <cstring>     // 字符串操作
    #include <unistd.h>    // POSIX系統調用
    #include <strings.h>   // bzero等函數
    #include <sys/types.h>  // 系統數據類型
    #include <sys/socket.h> // socket相關
    #include <netinet/in.h> // 網絡地址結構
    #include <arpa/inet.h>  // 地址轉換函數
    #include <unordered_map> // 哈希表
    #include <functional>   // 函數對象// 自定義頭文件
    #include "nocopy.hpp"   // 禁止拷貝的基類
    #include "Log.hpp"      // 日志系統
    #include "Comm.hpp"     // 通用定義
    #include "InetAddr.hpp" // 網絡地址封裝類// 默認配置常量
    const static uint16_t defaultport = 8888;  // 默認端口號
    const static int defaultfd = -1;           // 默認無效文件描述符
    const static int defaultsize = 1024;       // 默認緩沖區大小// 定義函數類型別名:處理請求并生成響應
    using func_t = std::function<void(const std::string &req, std::string *resp)>;/*** @class UdpServer* @brief UDP服務器類,繼承自不可拷貝的基類*/
    class UdpServer : public nocopy
    {
    public:/*** @brief 構造函數* @param func 業務處理函數* @param port 服務器監聽端口,默認為8888*/UdpServer(func_t func, uint16_t port = defaultport): _func(func), _port(port), _sockfd(defaultfd){}/*** @brief 初始化服務器* 1. 創建socket* 2. 綁定端口*/void Init(){// 1. 創建socket文件描述符// AF_INET: IPv4協議// SOCK_DGRAM: UDP協議// 0: 自動選擇協議_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){// 創建失敗記錄日志并退出lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);// 2. 綁定端口和地址struct sockaddr_in local;bzero(&local, sizeof(local));  // 清空結構體// 設置地址族、端口和IP地址local.sin_family = AF_INET;           // IPv4地址族local.sin_port = htons(_port);         // 端口號轉為網絡字節序local.sin_addr.s_addr = INADDR_ANY;    // 監聽所有網絡接口// 綁定socket到指定地址int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n != 0){// 綁定失敗記錄日志并退出lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));exit(Bind_Err);}}/*** @brief 啟動服務器主循環*/void Start(){char buffer[defaultsize];  // 接收緩沖區// 服務器主循環for (;;){// 準備接收客戶端信息struct sockaddr_in peer;      // 客戶端地址結構socklen_t len = sizeof(peer); // 地址結構長度// 接收數據ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,(struct sockaddr *)&peer, &len);if (n > 0)  // 接收成功{buffer[n] = 0;  // 添加字符串結束符// 打印客戶端信息和消息內容InetAddr addr(peer);  // 封裝客戶端地址std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;// 處理業務邏輯std::string value;_func(buffer, &value);  // 調用回調函數處理請求// 發送響應sendto(_sockfd, value.c_str(), value.size(), 0,(struct sockaddr *)&peer, len);}}}/*** @brief 析構函數*/~UdpServer(){// 可以在這里關閉socket,但現代操作系統會在進程退出時自動關閉}private:uint16_t _port;     // 服務器監聽端口int _sockfd;        // socket文件描述符func_t _func;       // 業務處理回調函數
    };
    
  • Main.cc

    // 引入必要的頭文件
    #include "UdpServer.hpp"  // UDP服務器實現
    #include "Comm.hpp"       // 通信相關定義
    #include "Dict.hpp"       // 字典類定義
    #include <memory>         // 智能指針// 使用說明函數
    void Usage(std::string proc)
    {// 打印程序使用說明// proc 參數是程序名std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
    }// 全局字典對象,從dict.txt文件初始化
    Dict gdict("./dict.txt");// 請求處理函數
    void Execute(const std::string &req, std::string *resp)
    {// req: 客戶端請求的字符串// resp: 用于返回響應結果的字符串指針// 調用字典對象的翻譯功能處理請求*resp = gdict.Translate(req);
    }// 主函數
    // 程序啟動方式示例: ./udp_server 8888
    int main(int argc, char *argv[])
    {// 檢查參數數量是否正確// 預期參數: 程序名 + 端口號 (共2個參數)if(argc != 2){// 參數不正確時打印使用說明Usage(argv[0]);return Usage_Err;  // 返回使用錯誤碼(定義在Comm.hpp中)}// 將字符串形式的端口號轉換為整數uint16_t port = std::stoi(argv[1]);// 創建UDP服務器對象// 使用智能指針管理服務器對象生命周期// 參數1: 請求處理函數Execute// 參數2: 監聽端口號std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(Execute, port);// 初始化服務器usvr->Init();// 啟動服務器(進入事件循環)usvr->Start();return 0;  // 程序正常退出
    }
    

1.5 DictServer封裝版

  • udp_socket.hpp

    #pragma once  // 防止頭文件被重復包含// 包含必要的頭文件
    #include <stdio.h>      // 標準輸入輸出
    #include <string.h>     // 字符串操作
    #include <stdlib.h>     // 標準庫函數
    #include <cassert>      // 斷言
    #include <string>       // C++字符串類
    #include <unistd.h>     // POSIX系統調用
    #include <sys/socket.h> // 套接字相關
    #include <netinet/in.h> // 網絡地址結構
    #include <arpa/inet.h>  // 地址轉換函數// 類型別名定義,簡化代碼
    typedef struct sockaddr sockaddr;      // 通用套接字地址結構
    typedef struct sockaddr_in sockaddr_in; // IPv4套接字地址結構// UDP套接字封裝類
    class UdpSocket {
    public:// 構造函數,初始化fd_為無效值UdpSocket() : fd_(-1) {}// 創建UDP套接字bool Socket() {// 創建IPv4的UDP套接字fd_ = socket(AF_INET, SOCK_DGRAM, 0);if (fd_ < 0) {perror("socket");  // 打印錯誤信息return false;}return true;}// 關閉套接字bool Close() {close(fd_);fd_ = -1;  // 重置為無效值return true;}// 綁定套接字到指定IP和端口bool Bind(const std::string& ip, uint16_t port) {sockaddr_in addr;addr.sin_family = AF_INET;  // IPv4地址族addr.sin_addr.s_addr = inet_addr(ip.c_str());  // 將字符串IP轉換為網絡字節序addr.sin_port = htons(port);  // 將主機字節序端口轉換為網絡字節序// 綁定套接字int ret = bind(fd_, (sockaddr*)&addr, sizeof(addr));if (ret < 0) {perror("bind");return false;}return true;}// 接收UDP數據報bool RecvFrom(std::string* buf, std::string* ip = NULL, uint16_t* port = NULL) {char tmp[1024 * 10] = {0};  // 10KB的接收緩沖區sockaddr_in peer;  // 存儲對端地址socklen_t len = sizeof(peer);  // 地址結構長度// 接收數據ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp) - 1, 0,(sockaddr*)&peer, &len);if (read_size < 0) {perror("recvfrom");return false;}// 將接收到的數據存入輸出參數buf->assign(tmp, read_size);// 如果調用者需要,返回對端IP和端口if (ip != NULL) {*ip = inet_ntoa(peer.sin_addr);  // 網絡字節序IP轉字符串}if (port != NULL) {*port = ntohs(peer.sin_port);  // 網絡字節序端口轉主機字節序}return true;}// 發送UDP數據報bool SendTo(const std::string& buf, const std::string& ip, uint16_t port) {sockaddr_in addr;addr.sin_family = AF_INET;  // IPv4地址族addr.sin_addr.s_addr = inet_addr(ip.c_str());  // 字符串IP轉網絡字節序addr.sin_port = htons(port);  // 主機字節序端口轉網絡字節序// 發送數據ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0,(sockaddr*)&addr, sizeof(addr));if (write_size < 0) {perror("sendto");return false;}return true;}private:int fd_;  // 套接字文件描述符
    };
    
  • udp_server.hpp

    #pragma once  // 防止頭文件被重復包含#include "udp_socket.hpp"  // 包含UDP socket的實現// 定義請求處理函數的類型
    // C風格寫法(已注釋):
    // typedef void (*Handler)(const std::string& req, std::string* resp);
    // C++11風格寫法,兼容函數指針、仿函數和lambda表達式
    #include <functional>
    typedef std::function<void(const std::string&, std::string*)> Handler;/*** UDP服務器類* 封裝了UDP服務器的基本操作*/
    class UdpServer {
    public:/*** 構造函數* 創建UDP socket,如果創建失敗會觸發斷言*/UdpServer() {assert(sock_.Socket());  // 斷言確保socket創建成功}/*** 析構函數* 關閉socket連接*/~UdpServer() {sock_.Close();  // 關閉socket}/*** 啟動UDP服務器* @param ip 服務器綁定的IP地址* @param port 服務器綁定的端口號* @param handler 請求處理函數* @return 啟動是否成功*/bool Start(const std::string& ip, uint16_t port, Handler handler) {// 1. 綁定IP和端口bool ret = sock_.Bind(ip, port);if (!ret) {return false;  // 綁定失敗返回false}// 2. 進入事件循環for (;;) {// 3. 接收客戶端請求std::string req;            // 存儲請求數據std::string remote_ip;      // 存儲客戶端IPuint16_t remote_port = 0;   // 存儲客戶端端口// 從socket接收數據bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);if (!ret) {continue;  // 接收失敗則繼續循環}std::string resp;  // 存儲響應數據// 4. 調用處理函數處理請求并生成響應handler(req, &resp);// 5. 將響應發送回客戶端sock_.SendTo(resp, remote_ip, remote_port);// 打印日志信息printf("[%s:%d] req: %s, resp: %s\n", remote_ip.c_str(), remote_port,req.c_str(), resp.c_str());}// 理論上不會執行到這里sock_.Close();return true;}private:UdpSocket sock_;  // UDP socket對象,封裝了底層socket操作
    };
    
  • dict_server.cc

    // 引入必要的頭文件
    #include "udp_server.hpp"  // UDP服務器實現頭文件
    #include <unordered_map>    // C++標準庫中的哈希表容器
    #include <iostream>         // 標準輸入輸出流// 全局字典,用于存儲單詞及其翻譯
    // key: 英文單詞
    // value: 對應的翻譯
    std::unordered_map<std::string, std::string> g_dict;/*** @brief 翻譯函數,根據請求查詢字典并返回結果* @param req 客戶端請求的單詞* @param resp 用于存儲翻譯結果的字符串指針*/
    void Translate(const std::string& req, std::string* resp) {// 在字典中查找請求的單詞auto it = g_dict.find(req);// 如果沒找到,返回提示信息if (it == g_dict.end()) {*resp = "未查到!";return;}// 找到則返回對應的翻譯*resp = it->second;
    }/*** @brief 主函數,程序入口* @param argc 命令行參數個數* @param argv 命令行參數數組* @return 程序執行狀態碼*/
    int main(int argc, char* argv[]) {// 檢查命令行參數是否正確if (argc != 3) {printf("Usage ./dict_server [ip] [port]\n");return 1;  // 參數錯誤返回非零狀態碼}// 1. 初始化字典數據g_dict.insert(std::make_pair("hello", "你好"));g_dict.insert(std::make_pair("world", "世界"));g_dict.insert(std::make_pair("c++", "最好的編程語言"));g_dict.insert(std::make_pair("bit", "特別 NB"));// 2. 創建并啟動UDP服務器UdpServer server;  // 創建UDP服務器實例// 啟動服務器,參數依次為:// argv[1] - IP地址// atoi(argv[2]) - 端口號(轉換為整數)// Translate - 請求處理回調函數server.Start(argv[1], atoi(argv[2]), Translate);return 0;  // 正常退出
    }
    
  • udp_client.hpp

    // 防止頭文件被重復包含的預處理指令
    #pragma once  // 包含UDP套接字封裝類的頭文件
    #include "udp_socket.hpp"  // UDP客戶端類定義
    class UdpClient {
    public:/*** 構造函數* @param ip 服務器IP地址,字符串類型* @param port 服務器端口號,16位無符號整數* 功能:初始化客戶端并創建UDP套接字*/UdpClient(const std::string& ip, uint16_t port) : ip_(ip),       // 初始化服務器IPport_(port) {  // 初始化服務器端口// 斷言檢查套接字是否創建成功,失敗則程序終止assert(sock_.Socket());}/*** 析構函數* 功能:關閉UDP套接字,釋放資源*/~UdpClient() {sock_.Close();  // 調用套接字關閉方法}/*** 接收數據方法* @param buf 輸出參數,用于存儲接收到的數據* @return bool 接收成功返回true,失敗返回false* 功能:從套接字接收數據(會阻塞直到收到數據)*/bool RecvFrom(std::string* buf) {return sock_.RecvFrom(buf);  // 調用套接字的接收方法}/*** 發送數據方法* @param buf 要發送的數據內容* @return bool 發送成功返回true,失敗返回false* 功能:向構造函數指定的服務器地址發送數據*/bool SendTo(const std::string& buf) {// 調用套接字發送方法,目標地址已在構造函數中指定return sock_.SendTo(buf, ip_, port_);  }private:UdpSocket sock_;      // UDP套接字對象,封裝了底層socket APIstd::string ip_;      // 服務器IP地址(IPv4格式,如"192.168.1.1")uint16_t port_;       // 服務器端口號(0-65535)
    };
    
  • main.cc

    // 引入必要的頭文件
    #include "udp_client.hpp"  // 自定義的UDP客戶端類頭文件
    #include <iostream>        // 標準輸入輸出流
    #include <cstdlib>         // 用于atoi函數(字符串轉整數)// 主函數
    int main(int argc, char* argv[]) {// 參數檢查:程序需要接收2個參數(IP地址和端口號)// argc是參數個數,argv[0]是程序名,argv[1]是IP,argv[2]是端口if (argc != 3) {// 打印使用說明printf("Usage ./dict_client [ip] [port]\n");return 1;  // 非正常退出}// 創建UDP客戶端對象// argv[1]是服務器IP地址,atoi(argv[2])將端口字符串轉換為整數UdpClient client(argv[1], atoi(argv[2]));// 主循環:持續接收用戶輸入并查詢for (;;) {std::string word;  // 存儲用戶輸入的單詞// 提示用戶輸入std::cout << "請輸入您要查的單詞: ";std::cin >> word;  // 讀取用戶輸入// 檢查輸入流狀態(用戶可能輸入EOF,如Ctrl+D)if (!std::cin) {std::cout << "Good Bye" << std::endl;  // 告別信息break;  // 退出循環}// 發送查詢請求到服務器client.SendTo(word);// 準備接收服務器響應std::string result;// 接收服務器返回的查詢結果client.RecvFrom(&result);// 輸出查詢結果std::cout << word << " 意思是 " << result << std::endl;}return 0;  // 正常退出
    }
    

1.6 簡單聊天室

  • UdpServer.hpp

    #pragma once// 系統頭文件
    #include <iostream>
    #include <string>
    #include <cerrno>
    #include <cstring>
    #include <unistd.h>
    #include <strings.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <functional>
    #include <pthread.h>// 自定義頭文件
    #include "nocopy.hpp"
    #include "Log.hpp"
    #include "Comm.hpp"
    #include "InetAddr.hpp"
    #include "ThreadPool.hpp"// 默認配置常量
    const static uint16_t defaultport = 8888;  // 默認端口號
    const static int defaultfd = -1;          // 默認文件描述符(無效值)
    const static int defaultsize = 1024;     // 默認緩沖區大小// 類型別名定義
    using task_t = std::function<void()>;    // 線程任務類型/*** @class UdpServer* @brief UDP服務器類,實現基于UDP的網絡通信服務* * 繼承自nocopy類,禁止拷貝構造和賦值操作* 使用線程池處理客戶端消息,支持多客戶端在線通信*/
    class UdpServer : public nocopy
    {
    public:/*** @brief 構造函數* @param port 服務器監聽端口,默認為defaultport*/UdpServer(uint16_t port = defaultport) : _port(port), _sockfd(defaultfd){// 初始化用戶列表互斥鎖pthread_mutex_init(&_user_mutex, nullptr);}/*** @brief 初始化服務器* 1. 創建socket* 2. 綁定端口* 3. 啟動線程池*/void Init(){// 1. 創建UDP socket_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));exit(Socket_Err);}lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);// 2. 綁定服務器地址struct sockaddr_in local;bzero(&local, sizeof(local));  // 清空結構體local.sin_family = AF_INET;    // IPv4地址族local.sin_port = htons(_port); // 端口號(主機序轉網絡序)local.sin_addr.s_addr = INADDR_ANY; // 監聽所有網卡// 綁定socket到指定地址int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n != 0){lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));exit(Bind_Err);}// 3. 啟動線程池ThreadPool<task_t>::GetInstance()->Start();}/*** @brief 添加在線用戶* @param addr 要添加的用戶地址信息* * 線程安全操作,使用互斥鎖保護在線用戶列表*/void AddOnlineUser(InetAddr addr){LockGuard lockguard(&_user_mutex);  // 自動加鎖解鎖// 檢查用戶是否已存在for (auto &user : _online_user){if (addr == user)return;}// 添加新用戶并記錄日志_online_user.push_back(addr);lg.LogMessage(Debug, "%s:%d is add to onlineuser list...\n", addr.Ip().c_str(), addr.Port());}/*** @brief 消息路由函數* @param sock 發送消息的socket* @param message 要發送的消息內容* * 將消息廣播給所有在線用戶*/void Route(int sock, const std::string &message){LockGuard lockguard(&_user_mutex);  // 自動加鎖解鎖// 遍歷所有在線用戶發送消息for (auto &user : _online_user){sendto(sock, message.c_str(), message.size(), 0,(struct sockaddr *)&user.GetAddr(), sizeof(user.GetAddr()));lg.LogMessage(Debug, "server send message to %s:%d, message: %s\n", user.Ip().c_str(), user.Port(), message.c_str());}}/*** @brief 啟動服務器主循環* * 循環接收客戶端消息,并將消息轉發給所有在線用戶*/void Start(){char buffer[defaultsize];  // 接收緩沖區// 服務器主循環for (;;){struct sockaddr_in peer;  // 客戶端地址socklen_t len = sizeof(peer);  // 地址長度// 接收客戶端消息ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0)  // 接收到有效數據{// 1. 處理客戶端地址信息InetAddr addr(peer);// 2. 添加用戶到在線列表AddOnlineUser(addr);// 3. 處理接收到的消息(添加結束符)buffer[n] = 0;// 4. 構造轉發消息格式: [IP:Port]# 消息內容std::string message = "[";message += addr.Ip();message += ":";message += std::to_string(addr.Port());message += "]# ";message += buffer;// 5. 創建轉發任務并提交到線程池task_t task = std::bind(&UdpServer::Route, this, _sockfd, message);ThreadPool<task_t>::GetInstance()->Push(task);}}}/*** @brief 析構函數* * 釋放資源,銷毀互斥鎖*/~UdpServer(){pthread_mutex_destroy(&_user_mutex);}private:uint16_t _port;                // 服務器監聽端口int _sockfd;                   // 服務器socket文件描述符std::vector<InetAddr> _online_user;  // 在線用戶列表pthread_mutex_t _user_mutex;   // 保護在線用戶列表的互斥鎖
    };
    
  • 引入線程池,這里就不重復貼代碼了。

  • InetAddr.hpp

    #pragma once  // 防止頭文件重復包含#include <iostream>      // 標準輸入輸出流
    #include <string>        // 字符串處理
    #include <sys/types.h>   // 系統數據類型定義
    #include <sys/socket.h>  // 套接字相關函數和數據結構
    #include <netinet/in.h>  // 互聯網地址族定義
    #include <arpa/inet.h>  // 互聯網操作聲明(如inet_ntoa等)// 網絡地址封裝類
    class InetAddr {
    public:// 構造函數:通過sockaddr_in結構體初始化// 參數:addr - 包含IP和端口信息的sockaddr_in結構體InetAddr(struct sockaddr_in &addr) : _addr(addr) {// 將網絡字節序的端口號轉換為主機字節序_port = ntohs(_addr.sin_port);// 將網絡字節序的IP地址轉換為點分十進制字符串_ip = inet_ntoa(_addr.sin_addr);}// 獲取IP地址字符串std::string Ip() {return _ip;}// 獲取端口號uint16_t Port() {return _port;};// 生成調試信息字符串,格式如:"127.0.0.1:4444"std::string PrintDebug() {std::string info = _ip;info += ":";info += std::to_string(_port);return info;}// 獲取內部的sockaddr_in結構體引用const struct sockaddr_in& GetAddr() {return _addr;}// 重載==運算符,比較兩個InetAddr對象是否相等bool operator==(const InetAddr& addr) {// 比較IP和端口是否相同return this->_ip == addr._ip && this->_port == addr._port;}// 析構函數~InetAddr() {}private:std::string _ip;      // 存儲IP地址的字符串uint16_t _port;       // 存儲端口號struct sockaddr_in _addr;  // 存儲原始的網絡地址結構
    };
    
  • UdpClient.hpp

    #include <iostream>      // 標準輸入輸出流
    #include <cerrno>       // 錯誤號定義
    #include <cstring>      // 字符串操作函數
    #include <string>       // C++字符串類
    #include <unistd.h>     // POSIX標準函數
    #include <sys/types.h>  // 基本系統數據類型
    #include <sys/socket.h> // 套接字接口
    #include <arpa/inet.h>  // 網絡地址轉換
    #include <netinet/in.h> // 互聯網地址族
    #include "Thread.hpp"   // 自定義線程頭文件
    #include "InetAddr.hpp" // 自定義網絡地址頭文件// 使用方法提示函數
    void Usage(const std::string &process) {std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
    }// 線程數據類,封裝了套接字和服務器地址信息
    class ThreadData {
    public:// 構造函數,初始化套接字和服務器地址ThreadData(int sock, struct sockaddr_in &server) : _sockfd(sock), _serveraddr(server) {}~ThreadData() {}public:int _sockfd;        // 套接字文件描述符InetAddr _serveraddr; // 服務器地址信息
    };// 接收線程的工作函數
    void RecverRoutine(ThreadData &td) {char buffer[4096];  // 接收緩沖區while (true) {struct sockaddr_in temp;  // 臨時存儲發送方地址socklen_t len = sizeof(temp);// 從套接字接收數據ssize_t n = recvfrom(td._sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&temp, &len);if (n > 0) {buffer[n] = 0;  // 確保字符串以null結尾std::cerr << buffer << std::endl;  // 打印接收到的數據} else {break;  // 接收出錯則退出循環}}
    }// 發送線程的工作函數
    void SenderRoutine(ThreadData &td) {while (true) {std::string inbuffer;  // 存儲用戶輸入std::cout << "Please Enter# ";std::getline(std::cin, inbuffer);  // 獲取用戶輸入auto server = td._serveraddr.GetAddr();  // 獲取服務器地址// 向服務器發送數據ssize_t n = sendto(td._sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr *)&server, sizeof(server));if (n <= 0) {std::cout << "send error" << std::endl;  // 發送失敗提示}}
    }// 主函數
    // 使用方法: ./udp_client server_ip server_port
    int main(int argc, char *argv[]) {// 檢查參數數量if (argc != 3) {Usage(argv[0]);return 1;}std::string serverip = argv[1];       // 獲取服務器IPuint16_t serverport = std::stoi(argv[2]);  // 獲取服務器端口// 1. 創建UDP套接字// UDP是全雙工的,可以同時讀寫,不會有多線程讀寫問題int sock = socket(AF_INET, SOCK_DGRAM, 0);if (sock < 0) {std::cerr << "socket error: " << strerror(errno) << std::endl;return 2;}std::cout << "create socket success: " << sock << std::endl;// 2. 客戶端不需要顯式bind,首次發送數據時會自動bind隨機端口// 服務器端口是固定的,客戶端端口由OS自動分配// 2.1 填充服務器地址信息struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;                  // IPv4地址族server.sin_port = htons(serverport);          // 端口號(網絡字節序)server.sin_addr.s_addr = inet_addr(serverip.c_str());  // IP地址// 創建線程數據對象ThreadData td(sock, server);// 創建接收和發送線程Thread<ThreadData> recver("recver", RecverRoutine, td);Thread<ThreadData> sender("sender", SenderRoutine, td);// 啟動線程recver.Start();sender.Start();// 等待線程結束recver.Join();sender.Join();close(sock);  // 關閉套接字return 0;
    }
    

在這里插入圖片描述


2.TCP網絡編程

2.1 TCP Socket API詳解

  • socket

    在這里插入圖片描述

    • socket() 打開一個網絡通訊端口,如果成功則像 open() 一樣返回一個文件描述符;
    • 應用程序可以像讀寫文件一樣用 read/write 在網絡上收發數據;
    • 如果 socket() 調用出錯則返回 -1
    • 對于 IPv4family 參數指定為 AF_INET
    • 對于 TCP 協議,type 參數指定為 SOCK_STREAM,表示面向流的傳輸協議;protocol 參數可指定為 0
  • bind

    在這里插入圖片描述

    • 服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后就可以向服務器發起連接;服務器需要調用 bind 綁定一個固定的網絡地址和端口號;
    • bind() 成功返回 0,失敗返回 -1
    • bind() 的作用是將參數 sockfdmyaddr 綁定在一起,使 sockfd 這個用于網絡通訊的文件描述符監聽 myaddr 所描述的地址和端口號;
    • 前面講過,struct sockaddr * 是一個通用指針類型,myaddr 參數實際上可以接受多種協議的 sockaddr 結構體,而它們的長度各不相同,所以需要第三個參數 addrlen 指定結構體的長度。
  • 我們的程序對myaddr參數是這樣初始化的

    在這里插入圖片描述

    1. 將整個結構體清零;
    2. 設置地址類型為 AF_INET
    3. 網絡地址為 INADDR_ANY(該宏表示本地的任意 IP 地址,因服務器可能有多個網卡,每個網卡可能綁定多個 IP 地址,此設置可在所有 IP 地址上監聽,直到與客戶端建立連接時才確定具體使用的 IP 地址);
    4. 端口號為 SERV_PORT(定義為 9999)。
  • listen

    在這里插入圖片描述

    • listen() 聲明 sockfd 處于監聽狀態(只要tcp服務器處于listen狀態,那么他就可以被連接了),并且最多允許有 backlog 個客戶端處于連接等待狀態,如果接收到更多的連接請求就忽略,這里設置不會太大(一般是 5)。
    • listen() 成功返回 0,失敗返回 -1。
  • accept

    在這里插入圖片描述

    • 三次握手完成后,服務器調用 accept() 接受連接;
    • 如果服務器調用 accept() 時還沒有客戶端的連接請求,就阻塞等待直到有客戶端連接上來;
    • addr 是一個傳出參數,accept() 返回時傳出客戶端的地址和端口號;
    • 如果給 addr 參數傳 NULL,表示不關心客戶端的地址;
    • addrlen 參數是一個傳入傳出參數(value-result argument),傳入的是調用者提供的緩沖區 addr 的長度以避免緩沖區溢出問題,傳出的是客戶端地址結構體的實際長度(有可能沒有占滿調用者提供的緩沖區)。
  • 我們的服務器程序結構是這樣的

    在這里插入圖片描述

    listenfd只負責獲取鏈接,accept返回值,就是給我們提供的服務(IO)。

  • connect

    在這里插入圖片描述

    • 客戶端需要調用 connect() 連接服務器
    • connectbind 的參數形式一致,區別在于 bind 的參數是自己的地址,而 connect 的參數是對方的地址;
    • connect() 成功返回 0,出錯返回 -1

2.2 Echo Server

  • TcpServer.hpp

    #pragma once  // 防止頭文件被重復包含#include <iostream>
    #include <string>
    #include <cerrno>      // 錯誤碼相關
    #include <cstring>     // 字符串操作
    #include <cstdlib>     // 退出函數
    #include <sys/types.h> // 系統類型定義
    #include <sys/socket.h> // socket相關
    #include <netinet/in.h> // 網絡地址結構
    #include <arpa/inet.h>  // 地址轉換
    #include "Log.hpp"      // 日志模塊
    #include "nocopy.hpp"   // 禁止拷貝基類
    #include "Comm.hpp"     // 通用定義const static int default_backlog = 6; // 監聽隊列的最大長度// TCP服務器類,繼承自nocopy(禁止拷貝)
    class TcpServer : public nocopy
    {
    public:// 構造函數,初始化端口號和運行狀態TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 初始化服務器void Init(){// 1. 創建socket文件描述符// AF_INET: IPv4, SOCK_STREAM: 流式套接字(TCP), 0: 默認協議_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0)  // 創建失敗{lg.LogMessage(Fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Fatal);  // 致命錯誤退出}// 設置socket選項,允許地址和端口重用int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensock);// 2. 綁定本地網絡信息struct sockaddr_in local;memset(&local, 0, sizeof(local));  // 清空結構體local.sin_family = AF_INET;        // IPv4local.sin_port = htons(_port);      // 端口號(主機序轉網絡序)local.sin_addr.s_addr = htonl(INADDR_ANY);  // 監聽所有網卡// 綁定socketif (bind(_listensock, CONV(&local), sizeof(local))  // CONV可能是類型轉換宏{lg.LogMessage(Fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err);  // 綁定錯誤退出}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);// 3. 設置socket為監聽狀態(TCP特有)if (listen(_listensock, default_backlog))  // backlog指定等待連接隊列長度{lg.LogMessage(Fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Listen_Err);  // 監聽錯誤退出}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);}// 處理客戶端連接的服務函數void Service(int sockfd){char buffer[1024];  // 接收緩沖區// 持續進行IO操作while (true){// 讀取客戶端數據ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0)  // 讀取成功{buffer[n] = 0;  // 添加字符串結束符std::cout << "client say# " << buffer << std::endl;// 構造回顯字符串std::string echo_string = "server echo# ";echo_string += buffer;// 回寫給客戶端write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0)  // 對端關閉連接{lg.LogMessage(Info, "client quit...\n");break;}else  // 讀取錯誤{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}// 啟動服務器void Start(){_isrunning = true;  // 設置運行標志while (_isrunning)  // 主循環{// 4. 接受客戶端連接struct sockaddr_in peer;  // 客戶端地址信息socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);if (sockfd < 0)  // 接受連接失敗{lg.LogMessage(Warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;  // 繼續等待下一個連接}lg.LogMessage(Debug, "accept success, get n new sockfd: %d\n", sockfd);// 5. 為客戶端提供服務Service(sockfd);  // 處理客戶端請求close(sockfd);  // 關閉連接}}// 析構函數~TcpServer(){// 可以在這里關閉_listensock,但通常由操作系統自動回收}private:uint16_t _port;      // 服務器端口號int _listensock;     // 監聽套接字描述符bool _isrunning;     // 服務器運行狀態標志
    };
    
  • TcpClient.cc

    #include <iostream>
    #include <string>
    #include <cstring>    // 提供memset等字符串操作函數
    #include <cstdlib>     // 提供基本工具函數
    #include <unistd.h>    // 提供POSIX操作系統API
    #include <sys/types.h> // 提供系統數據類型定義
    #include <sys/socket.h> // 提供套接字相關函數和數據結構
    #include <netinet/in.h> // 提供Internet地址族相關定義
    #include <arpa/inet.h>  // 提供IP地址轉換函數
    #include "Comm.hpp"    // 自定義通信頭文件
    using namespace std;// 使用說明函數
    void Usage(const std::string &process)
    {std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
    }// 主函數:TCP客戶端實現
    // 參數:./tcp_client serverip serverport
    int main(int argc, char *argv[])
    {// 1. 參數檢查if (argc != 3){Usage(argv[0]);  // 打印使用說明return 1;        // 參數錯誤返回1}// 2. 解析命令行參數std::string serverip = argv[1];        // 獲取服務器IP地址uint16_t serverport = stoi(argv[2]);  // 獲取服務器端口號并轉換為整數// 3. 創建客戶端套接字// AF_INET: IPv4地址族// SOCK_STREAM: 流式套接字(TCP)// 0: 默認協議int sockfd = socket(AF_INET, SOCK_STREAM, 0);if(sockfd < 0){cerr << "socket error" << endl;  // 套接字創建失敗return 1;}// 4. 準備服務器地址結構體struct sockaddr_in server;memset(&server, 0, sizeof(server));  // 清空結構體server.sin_family = AF_INET;          // 設置為IPv4地址族server.sin_port = htons(serverport);  // 將端口號轉換為網絡字節序// 將點分十進制IP地址轉換為網絡字節序的二進制形式// inet_pton: p(表示presentation) to n(表示network)inet_pton(AF_INET, serverip.c_str(), &server.sin_addr);// 5. 連接服務器// CONV宏可能定義在Comm.hpp中,用于將sockaddr_in*轉換為sockaddr*int n = connect(sockfd, CONV(&server), sizeof(server));if(n < 0){cerr << "connect error" << endl;  // 連接失敗return 2;}// 6. 連接成功后,進入通信循環while(true){string inbuffer;  // 存儲用戶輸入cout << "Please Enter# ";  // 提示用戶輸入getline(cin, inbuffer);    // 讀取用戶輸入// 7. 向服務器發送數據ssize_t n = write(sockfd, inbuffer.c_str(), inbuffer.size());if(n > 0)  // 發送成功{// 8. 準備接收服務器響應char buffer[1024];  // 接收緩沖區// 讀取服務器響應ssize_t m = read(sockfd, buffer, sizeof(buffer)-1);if(m > 0)  // 成功讀取到數據{buffer[m] = 0;  // 添加字符串結束符cout << "get a echo messsge -> " << buffer << endl;  // 打印響應}else if(m == 0 || m < 0)  // 連接關閉或讀取錯誤{break;  // 退出循環}}else  // 發送失敗{break;  // 退出循環}}// 9. 關閉套接字close(sockfd);return 0;  // 正常退出
    }
    
  • Comm.hpp

    // 防止頭文件被重復包含的預處理指令
    #pragma once  // 包含必要的系統頭文件
    #include <sys/types.h>      // 提供基本系統數據類型定義
    #include <sys/socket.h>     // 提供socket相關函數和數據結構
    #include <netinet/in.h>     // 提供Internet地址族相關定義
    #include <arpa/inet.h>      // 提供IP地址轉換函數// 定義錯誤碼枚舉,用于標識不同類型的錯誤
    enum {Usage_Err = 1,   // 用法錯誤(如參數錯誤)Socket_Err,      // 創建socket失敗Bind_Err,        // 綁定地址失敗Listen_Err       // 監聽端口失敗
    };// 定義類型轉換宏:將任意指針轉換為struct sockaddr*類型
    // 用于簡化socket API中地址結構的類型轉換
    #define CONV(addr_ptr) ((struct sockaddr *)addr_ptr)
    
  • nocopy.hpp

    // 防止頭文件被重復包含的預處理指令
    // 這是C/C++中防止多重包含的標準做法
    #pragma once  // 包含標準輸入輸出流頭文件
    // 雖然當前類未直接使用iostream,但保留以備后續擴展
    #include <iostream>  // 定義一個名為nocopy的類
    // 該類的設計目的是禁止對象的拷貝構造和拷貝賦值操作
    class nocopy  
    {
    public:  // 公有成員訪問權限區域// 默認構造函數// 使用空實現(因為不需要特殊初始化)nocopy() {}  // 刪除拷貝構造函數// = delete語法表示顯式禁止拷貝構造// 任何嘗試拷貝該類型對象的操作都會導致編譯錯誤nocopy(const nocopy&) = delete;  // 刪除拷貝賦值運算符// = delete語法表示顯式禁止拷貝賦值// 任何嘗試賦值該類型對象的操作都會導致編譯錯誤const nocopy& operator=(const nocopy&) = delete;  // 析構函數// 使用空實現(因為沒有資源需要釋放)// 聲明為虛函數會更好(如果預期有繼承)~nocopy() {}  
    };// 該類典型用法:
    // class MyResource : private nocopy { ... };
    // 這樣MyResource就自動獲得了不可拷貝的特性
    
    • 由于客戶端不需要固定的端口號,因此不必調用 bind(),客戶端的端口號由內核自動分配。

    • 注意:

      • 客戶端不是不允許調用 bind(),只是沒有必要顯式調用 bind() 固定一個端口號,否則如果在同一臺機器上啟動多個客戶端,就會出現端口號被占用導致不能正確建立連接;
      • 服務器也不是必須調用 bind(),但如果服務器不調用 bind(),內核會自動給服務器分配監聽端口,每次啟動服務器時端口號都不一樣,客戶端要連接服務器就會遇到麻煩。
    • 測試多個連接的情況:

      再啟動一個客戶端嘗試連接服務器,發現第二個客戶端不能正確和服務器進行通信。分析原因是因為我們 accept 了一個請求之后,就在一直 while 循環嘗試 read,沒有繼續調用 accept,導致不能接受新的請求。我們當前的 TCP 實現只能處理一個連接,這是不科學的。


2.3 Echo Server多進程版

  • InetAddr.hpp

    #pragma once  // 防止頭文件被重復包含#include <iostream>      // 標準輸入輸出流
    #include <string>        // 字符串操作
    #include <sys/types.h>   // 系統數據類型定義
    #include <sys/socket.h>  // 套接字相關函數和數據結構
    #include <netinet/in.h>  // 互聯網地址族定義
    #include <arpa/inet.h>   // IP地址轉換函數// 網絡地址封裝類
    class InetAddr {
    public:// 構造函數,通過sockaddr_in結構體初始化// @param addr: 傳入的sockaddr_in結構體引用InetAddr(struct sockaddr_in &addr) : _addr(addr) {// 將網絡字節序的端口號轉換為主機字節序_port = ntohs(_addr.sin_port);// 將網絡字節序的IP地址轉換為點分十進制字符串_ip = inet_ntoa(_addr.sin_addr);}// 獲取IP地址字符串// @return: 返回IP地址字符串std::string Ip() { return _ip; }// 獲取端口號// @return: 返回端口號uint16_t Port() { return _port; }// 生成調試信息字符串// @return: 返回"IP:端口"格式的字符串std::string PrintDebug() {std::string info = _ip;info += ":";info += std::to_string(_port);  // 例如 "127.0.0.1:4444"return info;}// 獲取內部的sockaddr_in結構體引用// @return: 返回sockaddr_in結構體常引用const struct sockaddr_in& GetAddr() {return _addr;}// 重載==運算符,用于比較兩個InetAddr對象// @param addr: 要比較的另一個InetAddr對象// @return: 如果IP和端口都相同返回true,否則falsebool operator==(const InetAddr& addr) {return this->_ip == addr._ip && this->_port == addr._port;}// 析構函數~InetAddr() {}private:std::string _ip;       // 存儲IP地址字符串uint16_t _port;        // 存儲端口號struct sockaddr_in _addr;  // 存儲原始的網絡地址結構
    };
    
  • TcpServer.hpp

    #pragma once  // 防止頭文件重復包含// 包含必要的系統頭文件
    #include <iostream>
    #include <string>
    #include <cerrno>      // 錯誤號相關
    #include <cstring>     // 字符串操作
    #include <cstdlib>     // 標準庫函數
    #include <sys/types.h> // 系統數據類型
    #include <sys/socket.h> // 套接字接口
    #include <netinet/in.h> // 網絡地址結構
    #include <arpa/inet.h>  // IP地址轉換
    #include <sys/wait.h>   // 進程等待// 包含自定義頭文件
    #include "Log.hpp"      // 日志系統
    #include "nocopy.hpp"   // 禁止拷貝的基類
    #include "Comm.hpp"     // 通用通信定義
    #include "InetAddr.hpp" // IP地址處理const static int default_backlog = 6; // 監聽隊列的最大長度// TcpServer類,繼承自nocopy表示禁止拷貝
    class TcpServer : public nocopy
    {
    public:// 構造函數,初始化端口號和運行狀態TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 初始化服務器void Init(){// 1. 創建監聽套接字// AF_INET: IPv4地址族// SOCK_STREAM: 流式套接字(TCP)// 0: 默認協議_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){// 創建失敗記錄日志并退出lg.LogMessage(Fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Fatal);}// 設置套接字選項,允許地址和端口重用int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensock);// 2. 填充本地網絡信息并綁定struct sockaddr_in local;memset(&local, 0, sizeof(local));  // 清空結構體local.sin_family = AF_INET;         // IPv4地址族local.sin_port = htons(_port);      // 端口號,轉換為網絡字節序local.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有網絡接口// 綁定套接字到本地地址if (bind(_listensock, CONV(&local), sizeof(local)) != 0){lg.LogMessage(Fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err);}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);// 3. 設置套接字為監聽狀態if (listen(_listensock, default_backlog) != 0){lg.LogMessage(Fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Listen_Err);}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);}// 處理客戶端連接的服務函數void Service(int sockfd){char buffer[1024];  // 接收緩沖區// 持續進行IO操作while (true){// 從客戶端讀取數據ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0)  // 讀取成功{buffer[n] = 0;  // 添加字符串結束符std::cout << "client say# " << buffer << std::endl;// 構造回顯消息并發送std::string echo_string = "server echo# ";echo_string += buffer;write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0)  // 客戶端關閉連接{lg.LogMessage(Info, "client quit...\n");break;}else  // 讀取錯誤{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}// 處理連接的多進程方法void ProcessConnection(int sockfd, struct sockaddr_in &peer){// 創建子進程處理連接pid_t id = fork();if (id < 0)  // fork失敗{close(sockfd);return;}else if (id == 0)  // 子進程{close(_listensock);  // 子進程不需要監聽套接字// 二次fork創建孫子進程(避免僵尸進程)if (fork() > 0)exit(0);// 孫子進程(孤兒進程,由init進程接管)InetAddr addr(peer);  // 獲取客戶端地址信息lg.LogMessage(Info, "process connection: %s:%d\n", addr.Ip().c_str(), addr.Port());// 處理客戶端請求Service(sockfd);close(sockfd);exit(0);}else  // 父進程{close(sockfd);  // 父進程不需要連接套接字// 等待子進程結束(避免僵尸進程)pid_t rid = waitpid(id, nullptr, 0);if (rid == id){// 子進程已結束,無需特殊處理}}}// 啟動服務器void Start(){_isrunning = true;while (_isrunning){// 4. 接受客戶端連接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);if (sockfd < 0){lg.LogMessage(Warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue;  // 接受失敗繼續嘗試}lg.LogMessage(Debug, "accept success, get n new sockfd: %d\n", sockfd);// 處理客戶端連接ProcessConnection(sockfd, peer);}}// 析構函數~TcpServer(){// 可以在這里添加資源清理代碼}private:uint16_t _port;     // 服務器監聽端口int _listensock;    // 監聽套接字描述符bool _isrunning;    // 服務器運行狀態標志
    };
    

2.4 Echo Server多線程版

  • Thread.hpp

    #pragma once  // 防止頭文件重復包含// 包含必要的系統頭文件和自定義頭文件
    #include <iostream>
    #include <string>
    #include <cerrno>      // 錯誤碼相關
    #include <cstring>     // 字符串操作
    #include <cstdlib>     // 標準庫函數
    #include <sys/types.h> // 系統數據類型
    #include <sys/socket.h> // 套接字相關
    #include <netinet/in.h> // 網絡地址結構
    #include <arpa/inet.h>  // IP地址轉換
    #include <sys/wait.h>   // 進程等待
    #include <pthread.h>    // 線程相關
    #include "Log.hpp"      // 自定義日志模塊
    #include "nocopy.hpp"   // 禁止拷貝的基類
    #include "Comm.hpp"     // 通用通信功能
    #include "InetAddr.hpp" // 自定義網絡地址類const static int default_backlog = 6; // 監聽隊列的最大長度// TCP服務器類,繼承自nocopy(禁止拷貝)
    class TcpServer : public nocopy
    {
    public:// 構造函數,初始化端口號和運行狀態TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 初始化TCP服務器void Init(){// 1. 創建監聽套接字_listensock = socket(AF_INET, SOCK_STREAM, 0); // IPv4, TCP協議if (_listensock < 0) // 創建失敗處理{lg.LogMessage(Fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Fatal); // 嚴重錯誤,退出程序}// 設置套接字選項:地址和端口可重用int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensock);// 2. 綁定本地地址和端口struct sockaddr_in local;memset(&local, 0, sizeof(local)); // 清空結構體local.sin_family = AF_INET;      // IPv4local.sin_port = htons(_port);    // 端口號(主機字節序轉網絡字節序)local.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有網絡接口// 綁定套接字if (bind(_listensock, CONV(&local), sizeof(local)) != 0){lg.LogMessage(Fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err); // 綁定失敗,退出程序}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);// 3. 設置套接字為監聽狀態if (listen(_listensock, default_backlog) != 0){lg.LogMessage(Fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Listen_Err); // 監聽失敗,退出程序}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);}// 線程數據類,用于傳遞連接信息給線程class ThreadData{public:ThreadData(int sockfd, struct sockaddr_in addr): _sockfd(sockfd), _addr(addr){}~ThreadData(){}public:int _sockfd;      // 連接套接字描述符InetAddr _addr;   // 客戶端地址信息};// 靜態服務方法,處理客戶端連接static void Service(ThreadData &td){char buffer[1024]; // 數據緩沖區// 持續處理客戶端請求while (true){// 讀取客戶端數據ssize_t n = read(td._sockfd, buffer, sizeof(buffer) - 1);if (n > 0) // 讀取成功{buffer[n] = 0; // 添加字符串結束符std::cout << "client say# " << buffer << std::endl;// 構造回顯消息std::string echo_string = "server echo# ";echo_string += buffer;// 發送回顯消息write(td._sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0) // 客戶端關閉連接{lg.LogMessage(Info, "client[%s:%d] quit...\n", td._addr.Ip().c_str(), td._addr.Port());break;}else // 讀取錯誤{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}// 線程執行函數(靜態方法)static void *threadExcute(void *args){pthread_detach(pthread_self()); // 分離線程(自動回收資源)ThreadData *td = static_cast<ThreadData *>(args); // 轉換參數類型TcpServer::Service(*td); // 調用服務方法close(td->_sockfd);     // 關閉連接套接字delete td;             // 釋放線程數據return nullptr;}// 處理新連接(多線程版本)void ProcessConnection(int sockfd, struct sockaddr_in &peer){InetAddr addr(peer); // 轉換地址格式pthread_t tid;ThreadData *td = new ThreadData(sockfd, peer); // 創建線程數據// 創建新線程處理連接pthread_create(&tid, nullptr, threadExcute, (void*)td);}// 啟動服務器void Start(){_isrunning = true;// 主循環:接受并處理連接while (_isrunning){// 4. 接受新連接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);if (sockfd < 0) // 接受連接失敗{lg.LogMessage(Warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue; // 繼續等待下一個連接}lg.LogMessage(Debug, "accept success, get n new sockfd: %d\n", sockfd);ProcessConnection(sockfd, peer); // 處理新連接}}// 析構函數~TcpServer(){// TODO: 應該在這里關閉監聽套接字}private:uint16_t _port;      // 服務器監聽端口int _listensock;     // 監聽套接字描述符bool _isrunning;     // 服務器運行狀態標志
    };
    

2.5 多線程遠程命令執行

  • Command.hpp

    #pragma once  // 防止頭文件被重復包含#include <iostream>    // 標準輸入輸出流
    #include <string>      // 字符串處理
    #include <set>         // 集合容器
    #include <unistd.h>    // POSIX操作系統API(用于recv/send等)// 命令處理類
    class Command
    {
    private:std::set<std::string> _safe_command;  // 允許執行的安全命令集合int _sockfd;                          // 關聯的套接字文件描述符std::string _command;                 // 存儲接收到的命令public:// 默認構造函數Command() {}// 帶參數的構造函數,初始化套接字并設置允許的安全命令Command(int sockfd) : _sockfd(sockfd){// 初始化允許執行的安全命令集合(白名單)_safe_command.insert("ls");       // 列出目錄內容_safe_command.insert("pwd");      // 顯示當前工作目錄_safe_command.insert("ls -l");    // 詳細列出目錄內容_safe_command.insert("ll");       // ls -l的別名(某些系統)_safe_command.insert("touch");     // 創建空文件_safe_command.insert("who");      // 顯示已登錄用戶_safe_command.insert("whoami");    // 顯示當前用戶名}// 檢查命令是否安全(是否在白名單中)bool IsSafe(const std::string &command){// 在安全命令集合中查找該命令auto iter = _safe_command.find(command);if(iter == _safe_command.end()) return false;  // 命令不在白名單中,不安全else return true;  // 命令在白名單中,安全}// 執行命令并返回結果std::string Execute(const std::string &command){// 首先檢查命令是否安全if(!IsSafe(command)) return "unsafe";  // 不安全命令直接返回// 使用popen執行命令并獲取輸出FILE *fp = popen(command.c_str(), "r");  // "r"表示讀取命令輸出if (fp == nullptr)return std::string();  // 執行失敗返回空字符串char buffer[1024];         // 讀取緩沖區std::string result;        // 存儲命令執行結果// 逐行讀取命令輸出while (fgets(buffer, sizeof(buffer), fp)){result += buffer;      // 將每行輸出追加到結果字符串}pclose(fp);               // 關閉管道return result;             // 返回執行結果}// 從套接字接收命令std::string RecvCommand(){char line[1024];  // 接收緩沖區// 從套接字接收數據(暫時簡化處理,不考慮完整協議)ssize_t n = recv(_sockfd, line, sizeof(line) - 1, 0);if (n > 0)  // 接收成功{line[n] = 0;          // 添加字符串結束符return line;           // 返回接收到的命令}else       // 接收失敗或連接關閉{return std::string();  // 返回空字符串}}// 通過套接字發送命令執行結果void SendCommand(std::string result){// 如果結果為空,發送"done"(例如touch命令沒有輸出)if(result.empty()) result = "done";  // 通過套接字發送結果send(_sockfd, result.c_str(), result.size(), 0);}// 析構函數~Command(){// 目前沒有需要特殊清理的資源}
    };  
    
  • Tcpserver.hpp

    #pragma once  // 防止頭文件重復包含// 包含必要的系統頭文件和自定義頭文件
    #include <iostream>
    #include <string>
    #include <cerrno>      // 錯誤號相關
    #include <cstring>     // 字符串操作
    #include <cstdlib>     // 標準庫函數
    #include <sys/types.h> // 系統數據類型
    #include <sys/socket.h> // 套接字相關
    #include <netinet/in.h> // 網絡地址結構
    #include <arpa/inet.h>  // IP地址轉換
    #include <sys/wait.h>   // 進程等待
    #include <pthread.h>    // 線程相關
    #include "Log.hpp"      // 日志模塊
    #include "nocopy.hpp"   // 禁止拷貝基類
    #include "Comm.hpp"     // 通用通信模塊
    #include "InetAddr.hpp" // IP地址封裝
    #include "Command.hpp"  // 命令執行模塊const static int default_backlog = 6; // 監聽隊列的最大長度// TCP服務器類,繼承自nocopy(禁止拷貝)
    class TcpServer : public nocopy
    {
    public:// 構造函數,初始化端口號和運行狀態TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 初始化服務器void Init(){// 1. 創建socket文件描述符_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){lg.LogMessage(Fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Fatal); // 創建失敗則退出程序}// 設置socket選項:地址和端口可重用int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensock);// 2. 填充本地網絡信息并綁定struct sockaddr_in local;memset(&local, 0, sizeof(local)); // 清空結構體local.sin_family = AF_INET;       // IPv4協議local.sin_port = htons(_port);    // 端口號(主機字節序轉網絡字節序)local.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有網卡// 2.1 綁定socketif (bind(_listensock, CONV(&local), sizeof(local)) != 0){lg.LogMessage(Fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err); // 綁定失敗則退出程序}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);// 3. 設置socket為監聽狀態(TCP特有)if (listen(_listensock, default_backlog) != 0){lg.LogMessage(Fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Listen_Err); // 監聽失敗則退出程序}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);}// 線程數據類(用于傳遞數據給線程)class ThreadData{public:ThreadData(int sockfd, struct sockaddr_in addr): _sockfd(sockfd), _addr(addr){}~ThreadData(){}public:int _sockfd;      // 客戶端socket描述符InetAddr _addr;   // 客戶端地址信息};// 服務處理函數(靜態方法,處理客戶端請求)static void Service(ThreadData &td){char buffer[1024];// 持續進行IO通信while (true){Command command(td._sockfd);  // 創建命令對象std::string commandstr = command.RecvCommand(); // 接收命令if (commandstr.empty())  // 如果接收為空則退出return;std::string result = command.Execute(commandstr); // 執行命令command.SendCommand(result); // 發送執行結果}}// 線程執行函數(靜態方法)static void *threadExcute(void *args){pthread_detach(pthread_self()); // 設置線程為分離狀態ThreadData *td = static_cast<ThreadData *>(args); // 轉換參數類型TcpServer::Service(*td);       // 調用服務處理函數close(td->_sockfd);            // 關閉socketdelete td;                     // 釋放線程數據return nullptr;}// 處理連接(創建線程處理每個客戶端)void ProcessConnection(int sockfd, struct sockaddr_in &peer){// v3 多線程版本InetAddr addr(peer);  // 封裝客戶端地址pthread_t tid;        // 線程IDThreadData *td = new ThreadData(sockfd, peer); // 創建線程數據pthread_create(&tid, nullptr, threadExcute, (void *)td); // 創建線程}// 啟動服務器void Start(){_isrunning = true;while (_isrunning){// 4. 獲取客戶端連接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);if (sockfd < 0){lg.LogMessage(Warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue; // 接受失敗則繼續嘗試}lg.LogMessage(Debug, "accept success, get n new sockfd: %d\n", sockfd);ProcessConnection(sockfd, peer); // 處理客戶端連接}}// 析構函數~TcpServer(){}private:uint16_t _port;      // 服務器端口號int _listensock;     // 監聽socket描述符bool _isrunning;     // 服務器運行狀態標志
    };
    

2.6 Echo Server線程池版

  • TcpServer.hpp

    #pragma once  // 防止頭文件重復包含// 引入必要的系統頭文件和自定義頭文件
    #include <iostream>
    #include <string>
    #include <cerrno>      // 錯誤號相關
    #include <cstring>     // 字符串操作
    #include <cstdlib>     // 退出函數
    #include <sys/types.h> // 系統數據類型
    #include <sys/socket.h> // socket相關
    #include <netinet/in.h> // 網絡地址結構
    #include <arpa/inet.h>  // 地址轉換
    #include <sys/wait.h>   // 進程等待
    #include <pthread.h>    // 線程相關
    #include <functional>   // 函數對象
    #include "Log.hpp"      // 自定義日志模塊
    #include "nocopy.hpp"   // 禁止拷貝的基類
    #include "Comm.hpp"     // 通用通信定義
    #include "InetAddr.hpp" // IP地址封裝
    #include "ThreadPool.hpp" // 線程池const static int default_backlog = 6; // 監聽隊列的最大長度// Tcp服務器類,繼承自nocopy(禁止拷貝)
    class TcpServer : public nocopy
    {
    public:// 構造函數,初始化端口號和運行狀態TcpServer(uint16_t port) : _port(port), _isrunning(false){}// 初始化服務器void Init(){// 1. 創建socket文件描述符_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0){lg.LogMessage(Fatal, "create socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Fatal); // 創建失敗直接退出}// 設置socket選項(地址重用)int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensock);// 2. 填充本地網絡信息并綁定struct sockaddr_in local;memset(&local, 0, sizeof(local)); // 清空結構體local.sin_family = AF_INET;        // IPv4協議local.sin_port = htons(_port);     // 端口號(主機序轉網絡序)local.sin_addr.s_addr = htonl(INADDR_ANY); // 監聽所有IP地址// 綁定socketif (bind(_listensock, CONV(&local), sizeof(local)) != 0){lg.LogMessage(Fatal, "bind socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Bind_Err); // 綁定失敗退出}lg.LogMessage(Debug, "bind socket success, sockfd: %d\n", _listensock);// 3. 設置socket為監聽狀態(TCP特有)if (listen(_listensock, default_backlog) != 0){lg.LogMessage(Fatal, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));exit(Listen_Err); // 監聽失敗退出}lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensock);}// 服務處理函數(處理單個連接)void Service(int sockfd, InetAddr addr){char buffer[1024]; // 接收緩沖區// 持續進行IO操作while (true){// 讀取客戶端數據ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);if (n > 0) // 讀取成功{buffer[n] = 0; // 添加字符串結束符std::cout << "client say# " << buffer << std::endl;// 構造回顯消息std::string echo_string = "server echo# ";echo_string += buffer;// 回寫給客戶端write(sockfd, echo_string.c_str(), echo_string.size());}else if (n == 0) // 對端關閉連接{lg.LogMessage(Info, "client[%s:%d] quit...\n", addr.Ip().c_str(), addr.Port());break;}else // 讀取錯誤{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}}// 處理新連接(將任務放入線程池)void ProcessConnection(int sockfd, struct sockaddr_in &peer){using func_t = std::function<void()>; // 定義函數對象類型InetAddr addr(peer); // 封裝客戶端地址信息// 使用bind綁定Service函數和參數func_t func = std::bind(&TcpServer::Service, this, sockfd, addr);// 將任務推送到線程池ThreadPool<func_t>::GetInstance()->Push(func);}// 啟動服務器void Start(){_isrunning = true;while (_isrunning){// 4. 接受新連接struct sockaddr_in peer;socklen_t len = sizeof(peer);int sockfd = accept(_listensock, CONV(&peer), &len);if (sockfd < 0) // 接受失敗{lg.LogMessage(Warning, "accept socket error, errno code: %d, error string: %s\n", errno, strerror(errno));continue; // 繼續等待新連接}lg.LogMessage(Debug, "accept success, get n new sockfd: %d\n", sockfd);// 處理新連接ProcessConnection(sockfd, peer);}_isrunning = false;}// 析構函數~TcpServer(){// 可以在這里添加資源釋放代碼}private:uint16_t _port;     // 服務器端口號int _listensock;    // 監聽socket文件描述符bool _isrunning;    // 服務器運行狀態標志
    };
    

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

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

相關文章

Elasticsearch/OpenSearch 中doc_values的作用

目錄 1. 核心作用 2. 適用場景 3. 與 index 參數的對比 4. 典型配置示例 場景 1&#xff1a;僅用于聚合&#xff0c;禁止搜索 場景 2&#xff1a;優化大字段存儲 5. 性能調優建議 6. 底層原理 doc_values 是 Elasticsearch/OpenSearch 中用于優化查詢和聚合的列式存儲結…

使用mermaid 語言繪畫時序圖和鏈路圖

給大家展示一下效果&#xff0c; 官方地址&#xff1a;https://mermaid.nodejs.cn/ 官方開發地&#xff1a;https://mermaid.nodejs.cn/intro/#google_vignette graph LR%% 樣式定義&#xff08;完全保留&#xff09; classDef user fill:#E1F5FE,stroke:#0288D1;classDef …

C++ Kafka客戶端(cppkafka)安裝與問題解決指南

一、cppkafka簡介 cppkafka是一個現代C的Apache Kafka客戶端庫&#xff0c;它是對librdkafka的高級封裝&#xff0c;旨在簡化使用librdkafka的過程&#xff0c;同時保持最小的性能開銷。 #mermaid-svg-qDUFSYLBf8cKkvdw {font-family:"trebuchet ms",verdana,arial,…

STM32的ADC模塊中,**采樣時機(Sampling Time)**和**轉換時機(Conversion Time),獲取數據的時機詳解

在STM32的ADC模塊中&#xff0c;**采樣時機&#xff08;Sampling Time&#xff09;和轉換時機&#xff08;Conversion Time&#xff09;**是ADC工作流程中的兩個關鍵階段&#xff0c;直接影響采樣精度和系統實時性。以下是詳細解析&#xff1a; 1. 采樣時機&#xff08;Samplin…

Pageassist安裝(ollama+deepseek-r1)

page-assist網站&#xff1a;https://github.com/n4ze3m/page-assist 首先電腦配置node.js&#xff0c;管理員打開命令窗口輸入下面命令下載bun npm install -g buncd 到你想要安裝page-assist的地方&#xff08;推薦桌面&#xff09; 輸入下列命令 git clone https://gith…

APC 熒光通道專用!Elabscience? CD11b 抗體激發 / 發射光譜精準匹配流式檢測

內容概要 Elabscience APC Anti-Mouse/Human CD11b Antibody [M1/70]&#xff08;貨號&#xff1a;E-AB-F1081E&#xff09;是一款高特異性熒光標記抗體&#xff0c;適用于流式細胞術&#xff08;FCM&#xff09;&#xff0c;可精準檢測小鼠和人類樣本中的 CD11b 髓系細胞&…

entity線段材質設置

在cesium中,我們可以改變其entity線段材質,這里以直線為例. 首先我們先創建一條直線 const redLine viewer.entities.add({polyline: {positions: Cesium.Cartesian3.fromDegreesArray([-75,35,-125,35,]),width: 5,material:material, 保存后可看到在地圖上創建了一條線段…

大模型數據分析破局之路20250512

大模型數據分析破局之路 本文面向 AI 初學者、數據分析從業者與企業技術負責人&#xff0c;圍繞大模型如何為數據分析帶來范式轉變展開&#xff0c;從傳統數據分析困境談起&#xff0c;延伸到 LLM MCP 的協同突破&#xff0c;最終落腳在企業實踐建議。 &#x1f30d; 開篇導語…

【MySQL】索引太多會怎樣?

在 MySQL 中&#xff0c;雖然索引可以顯著提高查詢效率&#xff0c;但過多的索引&#xff08;如超過 5-6 個&#xff09;會帶來以下弊端&#xff1a; 1. 存儲空間占用增加 每個索引都需要額外的磁盤空間存儲索引樹&#xff08;BTree&#xff09;。對于大表來說&#xff0c;多個…

使用PocketFlowSharp創建一個Human_Evaluation示例

效果 實踐 有時候AI生成的結果我們并不滿意在進入下一步之前&#xff0c;我們需要對AI生成的結果進行人工審核&#xff0c;同意了才能進入下一個流程。 Human_Evaluation就是人工判斷的一個簡單示例。 internal class Program{static async Task Main(string[] args){// Load…

【項目】自主實現HTTP服務器:從Socket到CGI全流程解析

00 引言 ? 在構建高效、可擴展的網絡應用時&#xff0c;理解HTTP服務器的底層原理是一項必不可少的技能。現代瀏覽器與移動應用大量依賴HTTP協議完成前后端通信&#xff0c;而這一過程的背后&#xff0c;是由網絡套接字驅動的請求解析、響應構建、數據傳輸等一系列機制所支撐…

SQL練習(6/81)

目錄 1.尋找連續值 方法一&#xff1a;使用自連接&#xff08;Self-Join&#xff09; 方法二&#xff1a;使用窗口函數&#xff08;Window Functions&#xff09; 2.尋找有重復的值 GROUP BY子句 HAVING子句 常用聚合函數&#xff1a; 3.找不存在某屬性的值 not in no…

【流程控制結構】

流程控制結構 流程控制結構1、順序結構2、選擇結構if基本選擇結構if else語法多重if語法嵌套if語法switch選擇結構 3、循環結構循環結構while循環結構程序調試for循環跳轉語句區別 流程控制結構 1、順序結構 流程圖 優先級 2、選擇結構 if基本選擇結構 單if 語法 if&…

【機器人】復現 UniGoal 具身導航 | 通用零樣本目標導航 CVPR 2025

UniGoal的提出了一個通用的零樣本目標導航框架&#xff0c;能夠統一處理多種類型的導航任務。 支持 對象類別導航、實例圖像目標導航和文本目標導航&#xff0c;而無需針對特定任務進行訓練或微調。 本文分享UniGoal復現和模型推理的過程&#xff5e; 查找沙發&#xff0c;模…

python + flask 做一個圖床

1. 起因&#xff0c; 目的: 對這個網站&#xff1a;https://img.vdoerig.com/ &#xff0c; 我也想實現這種效果。做一個簡單的圖床&#xff0c;后面&#xff0c;可以結合到其他項目中。 2. 先看效果 實際效果。 3. 過程: Grok 聊天&#xff1a; https://img.vdoerig.co…

Java生產環境設限參數教學

哈哈&#xff0c;這個問題問得好&#xff01;咱們用開餐廳的比喻來理解生產環境的四大必須設限參數&#xff0c;保證你聽完再也不會忘&#xff01;&#xff08;搓手手&#xff09; 1. 堆內存上限&#xff1a;-Xmx&#xff08;廚房的最大容量&#xff09; 問題&#xff1a;想象…

電腦出故障驅動裝不上?試試驅動人生的遠程服務支持

在日常工作或學習中&#xff0c;驅動問題時常成為電腦用戶的一大困擾。尤其是在更換硬件、重裝系統、驅動沖突等情況下&#xff0c;許多用戶往往手足無措&#xff0c;不知道從何下手。而“驅動人生”作為國內領先的驅動管理工具&#xff0c;一直以高效、便捷、智能著稱。現在&a…

JS手寫代碼篇---手寫 instanceof 方法

2、手寫 instanceof 方法 instancecof用于檢測一個對象是否是某個構造函數的實例。它通常用于檢查對象的類型&#xff0c;尤其是在處理繼承關系時。 eg: const arr [1,2,3,4,5]console.log(arr instanceof Array); // trueconsole.log(arr instanceof Object); // true那這是…

使用exceljs將excel文件轉化為html預覽最佳實踐(完整源碼)

前言 在企業應用中&#xff0c;我們時常會遇到需要上傳并展示 Excel 文件的需求&#xff0c;以實現文件內容的在線預覽。經過一番探索與嘗試&#xff0c;筆者最終借助 exceljs 這一庫成功實現了該功能。本文將以 Vue 3 為例&#xff0c;演示如何實現該功能&#xff0c;代碼示例…

PMP-第十二章 項目采購管理

項目采購管理核心概念 項目采購管理包括從項目團隊外部采購或獲取所需產品、服務或成果的各個過程項目組織既可以是買方&#xff08;甲方&#xff09; &#xff0c;也可以是賣方&#xff08;乙 方&#xff09;項目采購管理過程圍繞協議來進行&#xff0c;協議是買賣雙方之間具…