前言:
TCP(傳輸控制協議)是一種面向連接、可靠的流式傳輸協議,與 UDP 的無連接特性不同,它通過三次握手建立連接、四次揮手斷開連接,提供數據確認、重傳機制,保證數據有序且完整傳輸。本文將基于 TCP Socket 編程,實現一個支持多客戶端連接的英譯漢服務,并詳細解析 TCP 核心 API 及不同并發處理方案。
一、TCP 通信基本流程與核心 API
1. 通信流程概覽
- 服務器端:創建套接字 → 綁定地址端口 → 監聽連接 → 接受連接 → 數據交互 → 關閉連接
- 客戶端:創建套接字 → 連接服務器 → 數據交互 → 關閉連接
2. 核心 API 詳解(sys/socket.h
)
socket()
:創建套接字int socket(int domain, int type, int protocol);
- 作用:打開一個網絡通信端口,返回文件描述符(類似文件操作的
open()
)。- 參數:
domain
:協議族,IPv4 用AF_INET
;type
:套接字類型,TCP 用SOCK_STREAM
(面向流);protocol
:協議,默認填 0(自動匹配 type 對應的協議)。- 返回值:成功返回非負文件描述符,失敗返回 - 1。
bind()
:綁定地址與端口(服務器)int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:將套接字與特定 IP 和端口綁定,使服務器能被客戶端找到。
- 參數:
sockfd
:socket()
返回的套接字描述符;addr
:通用地址結構體(需轉換為struct sockaddr_in
具體設置 IPv4 信息);addrlen
:地址結構體長度。- 關鍵設置:
struct sockaddr_in local; local.sin_family = AF_INET; // IPv4 local.sin_port = htons(9999); // 端口(主機字節序→網絡字節序) local.sin_addr.s_addr = htonl(INADDR_ANY); // 綁定所有本地IP
- 返回值:成功返回 0,失敗返回 - 1。
listen()
:監聽連接(服務器)int listen(int sockfd, int backlog);
- 作用:將套接字設為監聽狀態,允許接收客戶端連接。
- 參數:
backlog
:最大等待連接隊列長度(通常設 5~10)。- 返回值:成功返回 0,失敗返回 - 1。
accept()
:接受連接(服務器)int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 作用:從監聽隊列中取出一個連接,創建新的套接字用于與客戶端通信(原套接字繼續監聽)。
- 參數:
addr
:傳出參數,存儲客戶端地址信息;addrlen
:傳入傳出參數,地址結構體長度。- 返回值:成功返回新套接字描述符(用于通信),失敗返回 - 1。
connect()
:連接服務器(客戶端)int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 作用:客戶端向服務器發起連接(三次握手在此過程完成)。
- 參數:
addr
為服務器的地址信息(IP + 端口)。- 返回值:成功返回 0,失敗返回 - 1。
數據讀寫:
read()
/write()
- TCP 是流式傳輸,可直接用文件讀寫函數:
ssize_t read(int fd, void *buf, size_t count); // 從套接字讀數據 ssize_t write(int fd, const void *buf, size_t count); // 向套接字寫數據
二、英譯漢服務實現(單連接版本)
1. 功能設計
- 客戶端發送英文單詞,服務器返回對應的中文翻譯;
- 支持 “quit” 退出連接。
2. 核心代碼實現
輔助類:nocopy
(禁止拷貝,避免套接字描述符重復釋放)
// nocopy.hpp
#pragma once
class nocopy {
public:nocopy() = default;nocopy(const nocopy&) = delete; // 禁止拷貝構造nocopy& operator=(const nocopy&) = delete; // 禁止賦值~nocopy() = default;
};
服務器端:TcpServer.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "nocopy.hpp"#define CONV(addr_ptr) ((struct sockaddr*)addr_ptr) // 地址轉換宏
const int PORT = 9999;
const int BUFFER_SIZE = 1024;// 英譯漢字典(簡化版)
std::string translate(const std::string& english) {std::string chinese;if (english == "hello") chinese = "你好";else if (english == "world") chinese = "世界";else if (english == "computer") chinese = "電腦";else if (english == "program") chinese = "程序";else chinese = "未知單詞";return chinese;
}class TcpServer : public nocopy {
private:int _listensock; // 監聽套接字bool _isrunning; // 運行狀態public:TcpServer() : _isrunning(false) {}// 初始化服務器:創建套接字→綁定→監聽void Init() {// 1. 創建監聽套接字_listensock = socket(AF_INET, SOCK_STREAM, 0);if (_listensock < 0) {std::cerr << "創建套接字失敗: " << strerror(errno) << std::endl;exit(1);}// 設置端口復用(避免服務器重啟時端口占用)int opt = 1;setsockopt(_listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 2. 綁定地址和端口struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(PORT);local.sin_addr.s_addr = htonl(INADDR_ANY);if (bind(_listensock, CONV(&local), sizeof(local)) < 0) {std::cerr << "綁定失敗: " << strerror(errno) << std::endl;close(_listensock);exit(1);}// 3. 監聽連接if (listen(_listensock, 5) < 0) {std::cerr << "監聽失敗: " << strerror(errno) << std::endl;close(_listensock);exit(1);}_isrunning = true;std::cout << "服務器啟動成功,監聽端口 " << PORT << std::endl;}// 處理客戶端通信:英譯漢void Service(int client_sock) {char buffer[BUFFER_SIZE];while (true) {// 讀取客戶端發送的英文單詞ssize_t n = read(client_sock, buffer, BUFFER_SIZE - 1);if (n > 0) {buffer[n] = '\0';std::cout << "客戶端發送: " << buffer << std::endl;// 若客戶端發送"quit",斷開連接if (std::string(buffer) == "quit") {std::cout << "客戶端請求斷開連接" << std::endl;break;}// 翻譯并返回結果std::string chinese = translate(buffer);write(client_sock, chinese.c_str(), chinese.size());}else if (n == 0) { // 客戶端關閉連接std::cout << "客戶端已斷開" << std::endl;break;}else { // 讀取出錯std::cerr << "讀取失敗: " << strerror(errno) << std::endl;break;}}close(client_sock); // 關閉通信套接字}// 啟動服務器:循環接受連接void Start() {while (_isrunning) {struct sockaddr_in peer; // 客戶端地址socklen_t peer_len = sizeof(peer);// 接受連接(阻塞等待)int client_sock = accept(_listensock, CONV(&peer), &peer_len);if (client_sock < 0) {std::cerr << "接受連接失敗: " << strerror(errno) << std::endl;continue;}std::cout << "新客戶端連接: " << inet_ntoa(peer.sin_addr) << ":" << ntohs(peer.sin_port) << std::endl;Service(client_sock); // 處理該客戶端(單連接版本:一次處理一個)}close(_listensock); // 關閉監聽套接字}
};
服務器主函數:server.cc
#include "TcpServer.hpp"int main() {TcpServer server;server.Init();server.Start();return 0;
}
客戶端:client.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>const int PORT = 9999;
const int BUFFER_SIZE = 1024;int main(int argc, char* argv[]) {if (argc != 2) {std::cerr << "用法: " << argv[0] << " 服務器IP" << std::endl;return 1;}// 1. 創建客戶端套接字int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {std::cerr << "創建套接字失敗: " << strerror(errno) << std::endl;return 1;}// 2. 連接服務器struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(PORT);if (inet_pton(AF_INET, argv[1], &server.sin_addr) <= 0) { // IP字符串→二進制std::cerr << "無效的IP地址" << std::endl;close(sockfd);return 1;}if (connect(sockfd, (struct sockaddr*)&server, sizeof(server)) < 0) {std::cerr << "連接服務器失敗: " << strerror(errno) << std::endl;close(sockfd);return 1;}// 3. 交互:發送英文,接收中文翻譯char buffer[BUFFER_SIZE];while (true) {std::cout << "請輸入英文單詞(輸入quit退出): ";std::string input;std::getline(std::cin, input);// 發送數據到服務器write(sockfd, input.c_str(), input.size());if (input == "quit") break;// 接收翻譯結果ssize_t n = read(sockfd, buffer, BUFFER_SIZE - 1);if (n > 0) {buffer[n] = '\0';std::cout << "翻譯結果: " << buffer << std::endl;}}close(sockfd);return 0;
}
三、支持多客戶端:并發處理方案
單連接版本一次只能處理一個客戶端,實際應用中需支持多并發。以下是三種常見方案:
1. 多進程版本
- 原理:每接受一個客戶端連接,創建子進程處理該客戶端,父進程繼續接受新連接。
- 關鍵代碼:
void ProcessConnection(int client_sock, const struct sockaddr_in& peer) {pid_t pid = fork();if (pid == 0) { // 子進程close(_listensock); // 子進程不需要監聽套接字Service(client_sock); // 處理客戶端exit(0); // 處理完退出} else if (pid > 0) { // 父進程close(client_sock); // 父進程不需要通信套接字// 回收子進程資源(避免僵尸進程)waitpid(pid, nullptr, WNOHANG);} }
- 優缺點:簡單實現,但進程創建開銷大,適合連接數少的場景。
2. 多線程版本
- 原理:每接受一個客戶端連接,創建線程處理該客戶端,主線程繼續接受新連接。
- 關鍵代碼:
// 線程數據:通信套接字+客戶端地址 struct ThreadData {int sockfd;struct sockaddr_in addr; };// 線程處理函數 static void* ThreadHandler(void* arg) {pthread_detach(pthread_self()); // 分離線程,自動回收資源ThreadData* data = (ThreadData*)arg;Service(data->sockfd); // 處理客戶端close(data->sockfd);delete data;return nullptr; }void ProcessConnection(int client_sock, const struct sockaddr_in& peer) {ThreadData* data = new ThreadData{client_sock, peer};pthread_t tid;pthread_create(&tid, nullptr, ThreadHandler, data); // 創建線程 }
- 優缺點:線程開銷小于進程,但大量連接時線程創建銷毀仍有開銷。
3. 線程池版本
- 原理:預先創建一批線程,客戶端連接到來時,將任務(處理邏輯)加入線程池隊列,線程池中的線程異步處理。
- 關鍵代碼:
// 線程池(簡化版) template <typename Task> class ThreadPool { private:// 線程池實現(隊列+互斥鎖+條件變量)// ... public:void Push(const Task& task) { // 添加任務// 加鎖入隊,喚醒線程} };// 服務器中使用線程池 void ProcessConnection(int client_sock, const struct sockaddr_in& peer) {// 綁定處理函數與參數auto task = std::bind(&TcpServer::Service, this, client_sock);ThreadPool<decltype(task)>::GetInstance()->Push(task); // 任務入池 }
- 優缺點:避免頻繁創建銷毀線程,適合高并發場景,是工業級常用方案。
四、編譯與運行
# 編譯服務器(以多線程版本為例)
g++ server.cc -o translator_server -lpthread
# 編譯客戶端
g++ client.cc -o translator_client
運行步驟
- 啟動服務器:
./translator_server
- 啟動客戶端(多終端可啟動多個):
./translator_client 127.0.0.1
- 客戶端輸入英文單詞(如 “hello”),接收中文翻譯;輸入 “quit” 退出。
五、總結
本文通過實現一個英譯漢服務,詳細講解了 TCP Socket 編程的核心流程與 API,并對比了單連接、多進程、多線程、線程池四種處理方案的優缺點。TCP 的可靠性使其適合需要確保數據完整傳輸的場景(如本文的翻譯服務、文件傳輸等),而并發方案的選擇需根據實際業務的連接量和性能需求決定。