目錄
1. 簡單分析之前的代碼
2. 多線程服務器設計
2.1 C++11線程的基本使用
2.2 服務器主體邏輯
3. 錯誤處理的封裝
4. 完整的代碼實現
客戶端代碼(client.cpp)
服務器代碼(server.cpp)
5. 運行方式
在我們預想中,服務器端應該能夠同時與多個客戶端建立連接并進行網絡通信。然而,在之前的代碼中,服務器實現只支持單一連接,因為在處理連接時,主線程會被accept()
、read()
或write()
等方法阻塞,導致無法響應新的連接請求。為了解決這一問題,本文將介紹如何實現一個多線程的TCP服務器,讓我們來一步步分析并構建代碼。
1. 簡單分析之前的代碼
在之前的單線程實現中,偽代碼大致如下:
int lfd = socket();
int ret = bind();
ret = listen();int cfd = accept();while(1) {read();write();
}
在此程序中,一旦與客戶端建立連接,程序會進入while(1)
循環,進行數據的接收和發送。這種設計導致了以下幾個問題:
accept()
會阻塞當前進程,直到有新客戶端連接。read()
會阻塞當前進程,直到有數據可以讀取。write()
在寫緩沖接滿時也可能阻塞。
由于這種設計,主要阻塞在read()
和accept()
中,導致服務器無法處理多個客戶端的連接。
2. 多線程服務器設計
在多線程服務器中,我們將主要分為兩個角色:監聽和通信。主線程負責監聽客戶端的連接請求,而子線程則負責與不同的客戶端進行通信。
2.1 C++11線程的基本使用
C++11提供了強大的線程支持。以下是一個簡單的線程使用示例:
void func(int num, std::string str) {for (int i = 0; i < 10; ++i) {std::cout << "子線程: i = " << i << ", num: " << num << ", str: " << str << std::endl;}
}std::thread t(func, 520, "I love you"); // 創建子線程
// 創建子線程對象 t,執行 func() 函數。線程啟動后自動運行,參數 520 和 "I love you" 傳遞給 func()。
// std::thread 的構造函數支持變參,無需擔心參數個數。通常,任務函數 func() 返回 void,因為子線程不處理返回值。
以上代碼會在一個新線程中執行func()
,并傳遞具體參數。
2.2 服務器主體邏輯
偽代碼的主體邏輯如下所示:
void func(int fd) { while(1) {read();write();}close(fd);
}int main() {int lfd = socket(); // 創建監聽套接字int ret = bind(); // 綁定地址和端口ret = listen(); // 開始監聽while(1) {int cfd = accept(); // 接受客戶端連接// 創建新線程來處理通信std::thread t(func, cfd);t.detach(); // 分離線程,使其獨立運行}close(lfd); // 關閉監聽套接字
}
在此代碼中,每當接受到一個新的客戶端連接,就會創建一個新的子線程來負責與該客戶端的通信。
3. 錯誤處理的封裝
為了簡化錯誤處理,我們可以將錯誤判斷和處理封裝到一個函數中,下面是錯誤處理函數的實現:
void perror_if(bool condition, const char* errorMessage) {if (condition) {perror(errorMessage);exit(1);}
}// 使用示例
int lfd = socket(AF_INET, SOCK_STREAM, 0);
perror_if(lfd == -1, "socket");
這樣的封裝可以使代碼更加簡潔且易于維護。
4. 完整的代碼實現
客戶端代碼(client.cpp)
#include <stdlib.h> // 提供exit函數
#include <stdio.h> // 提供printf和perror函數
#include <unistd.h> // 提供close函數
#include <arpa/inet.h> // 提供socket、connect等函數
#include <string.h> // 提供memset和strlen函數// 錯誤處理函數
void perror_if(bool condition, const char* errorMessage) {if (condition) {perror(errorMessage); // 輸出錯誤信息exit(1); // 退出程序}
}int main() {// 1. 創建監聽的套接字int fd = socket(AF_INET, SOCK_STREAM, 0);perror_if(fd == -1, "socket"); // 檢查socket創建是否成功// 2. 綁定IP地址和端口struct sockaddr_in saddr;memset(&saddr, 0, sizeof(saddr)); // 清空結構體saddr.sin_family = AF_INET; // IPv4saddr.sin_port = htons(10000); // 設置端口,使用網絡字節序inet_pton(AF_INET, "127.0.0.1", &saddr.sin_addr.s_addr); // 將IP地址轉換為網絡字節序// 連接到服務器int ret = connect(fd, (struct sockaddr*)&saddr, sizeof(saddr));perror_if(ret == -1, "connect"); // 檢查連接是否成功// 3. 與服務器進行通信int n = 0; // 消息計數while (1) {// 發送數據char buf[512] = {0}; // 初始化緩沖區sprintf(buf, "hi, I am client...%d\n", n++); // 格式化消息write(fd, buf, strlen(buf)); // 發送數據到服務器// 接收數據memset(buf, 0, sizeof(buf)); // 清空緩沖區int len = read(fd, buf, sizeof(buf)); // 從服務器讀取數據if (len > 0) {printf("server say: %s\n", buf); // 打印服務器返回的消息} else if (len == 0) {printf("server disconnect...\n"); // 服務器斷開連接break; // 退出循環} else {perror("read"); // 讀取數據出錯break; // 退出循環}sleep(1); // 每隔1秒發送一條數據}close(fd); // 關閉套接字return 0; // 程序結束
}
服務器代碼(server.cpp)
#include <stdlib.h> // 提供exit函數
#include <stdio.h> // 提供printf和perror函數
#include <unistd.h> // 提供close函數
#include <arpa/inet.h> // 提供socket、bind、listen、accept等函數
#include <string.h> // 提供memset函數
#include <thread> // 提供std::thread類以支持多線程// 錯誤處理函數
void perror_if(bool condition, const char* errorMessage) {if (condition) {perror(errorMessage); // 輸出錯誤信息exit(1); // 退出程序}
}// 子線程函數,負責與客戶端的通信
void working(int clientfd) {char buf[512]; // 用于存儲接收到的數據while (1) {memset(buf, 0, sizeof(buf)); // 清空緩沖區int len = read(clientfd, buf, sizeof(buf)); // 從客戶端讀取數據if (len > 0) {printf("client says: %s\n", buf); // 打印客戶端發送的消息write(clientfd, buf, len); // 將接收到的數據回寫給客戶端(回顯)}else if (len == 0) {printf("client is disconnect..\n"); // 客戶端斷開連接break; // 退出循環}else {// 在多線程環境中,不再使用perror,而使用printfprintf("read error..\n"); // 讀取數據出錯break; // 退出循環}}close(clientfd); // 關閉與客戶端的連接
}int main() {// 1. 創建監聽的套接字int fd = socket(AF_INET, SOCK_STREAM, 0);perror_if(fd == -1, "socket"); // 檢查socket創建是否成功// 2. 綁定IP地址和端口struct sockaddr_in saddr;memset(&saddr, 0, sizeof(saddr)); // 清空結構體saddr.sin_family = AF_INET; // IPv4saddr.sin_port = htons(10000); // 設置端口,使用網絡字節序saddr.sin_addr.s_addr = INADDR_ANY; // 綁定到所有可用的接口// 綁定監聽套接字int ret = bind(fd, (struct sockaddr*)&saddr, sizeof(saddr));perror_if(ret == -1, "bind"); // 檢查綁定是否成功// 3. 設置監聽ret = listen(fd, 64); // 開始監聽連接請求perror_if(ret == -1, "listen"); // 檢查監聽是否成功while (1) {// 4. 等待并建立連接struct sockaddr_in cliaddr; // 保存客戶端IP地址信息socklen_t len = sizeof(cliaddr);// 接受連接int cfd = accept(fd, (struct sockaddr*)&cliaddr, &len);if (cfd == -1) {perror("accept"); // 處理錯誤continue; // 繼續等待新的連接}char ip[64] = { 0 }; // 用于保存客戶端IP地址printf("new client fd:%d ip:%s, port:%d\n", cfd,inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, ip, sizeof(ip)), // 獲取客戶端IP地址ntohs(cliaddr.sin_port)); // 獲取客戶端端口// 創建新的線程來處理客戶端的通信std::thread t(working, cfd);t.detach(); // 分離線程,使其獨立運行}close(fd); // 關閉監聽套接字return 0; // 程序結束
}
5. 運行方式
-
編譯代碼: 使用
g++
編譯器將代碼編譯為可執行文件:g++ server.cpp -o server -std=c++11 -pthread
-
運行服務器: 在終端中運行服務器程序:
./server
-
運行客戶端: 需要在不同的終端中運行多個客戶端程序:
./client
可以打開多個終端來模擬多個客戶端。
-
觀察輸出: 在服務器終端,您將看到每個客戶端的連接消息以及客戶端發送的消息,服務器將響應這些消息。
-
結束運行: 要結束服務器和客戶端,可以在各自的終端使用
Ctrl+C
來終止程序。