計算機網絡 : 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編程的核心概念和實現方法。內容涵蓋:
- UDP編程:介紹無連接通信的實現,包括地址轉換、本地環回、Echo服務器和字典服務器的開發。
- 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_pton
和inet_ntop
不僅可以轉換IPv4
的in_addr
,還可以轉換IPv6
的in6_addr
,因此函數接口是void *addrptr
。-
代碼示例:
-
-
關于
inet_ntoa
inet_ntoa
這個函數返回了一個char*
,顯然是函數自己在內部申請了一塊內存來保存 IP 的結果。那么是否需要調用者手動釋放嗎?根據 man 手冊,
inet_ntoa
將返回結果存放在靜態存儲區,因此不需要手動釋放。但需要注意的是,由于
inet_ntoa
使用內部靜態存儲區,第二次調用的結果會覆蓋上一次的結果。思考:如果有多個線程調用
inet_ntoa
,是否會出現異常情況?- 在 APUE中明確指出,
inet_ntoa
不是線程安全的函數; - 但在 CentOS7 上測試時并未出現問題,可能是內部實現加了互斥鎖;
- 在多線程環境下,推薦使用
inet_ntop
,該函數要求調用者提供緩沖區存儲結果,從而規避線程安全問題。
- 在 APUE中明確指出,
-
總結:以后進行網絡地址與端口轉換,就是用下面四個函數
-
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);
-
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));
-
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); // 網絡轉主機
-
注意事項
- 字節序問題:
htons/ntohs
用于解決不同機器的字節序差異,網絡傳輸必須用大端。- 即使主機字節序本身就是大端,調用這些函數也不會出錯(無操作)。
- 緩沖區大小:
inet_ntop
的緩沖區需足夠大(IPv4用INET_ADDRSTRLEN
,IPv6用INET6_ADDRSTRLEN
)。
- 錯誤檢查:
inet_pton
和inet_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/Unix:
ifconfig lo
或ip addr show lo
- Windows:
ipconfig
可以看到127.0.0.1
綁定在環回接口上
特點:
- 不需要物理網卡,純軟件實現。
- 即使沒有網絡連接,環回接口仍然可用。
- Linux/Unix:
-
常見用途
- 測試網絡服務
- 例如運行一個本地 Web 服務器(如
http://127.0.0.1:8080
),檢查服務是否正常。
- 例如運行一個本地 Web 服務器(如
- 進程間通信(IPC)
- 兩個本地進程可以通過
127.0.0.1
進行 Socket 通信,而無需經過外部網絡。
- 兩個本地進程可以通過
- 屏蔽外部訪問
- 某些服務(如數據庫)可以只監聽
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_WAITALL
、MSG_DONTWAIT
,通常設為0
)src_addr
存放發送方地址的 struct sockaddr
(可以是sockaddr_in
或sockaddr_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; - 對于 IPv4,
family
參數指定為AF_INET
; - 對于 TCP 協議,type 參數指定為
SOCK_STREAM
,表示面向流的傳輸協議;protocol
參數可指定為 0。
-
bind
- 服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后就可以向服務器發起連接;服務器需要調用
bind
綁定一個固定的網絡地址和端口號; bind()
成功返回 0,失敗返回 -1。bind()
的作用是將參數sockfd
和myaddr
綁定在一起,使sockfd
這個用于網絡通訊的文件描述符監聽myaddr
所描述的地址和端口號;- 前面講過,
struct sockaddr *
是一個通用指針類型,myaddr
參數實際上可以接受多種協議的sockaddr
結構體,而它們的長度各不相同,所以需要第三個參數addrlen
指定結構體的長度。
- 服務器程序所監聽的網絡地址和端口號通常是固定不變的,客戶端程序得知服務器程序的地址和端口號后就可以向服務器發起連接;服務器需要調用
-
我們的程序對
myaddr
參數是這樣初始化的- 將整個結構體清零;
- 設置地址類型為
AF_INET
; - 網絡地址為
INADDR_ANY
(該宏表示本地的任意 IP 地址,因服務器可能有多個網卡,每個網卡可能綁定多個 IP 地址,此設置可在所有 IP 地址上監聽,直到與客戶端建立連接時才確定具體使用的 IP 地址); - 端口號為
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()
連接服務器; connect
和bind
的參數形式一致,區別在于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; // 服務器運行狀態標志 };