1. 核心接口
1.1 監聽連接:listen()
使 TCP 套接字進入被動監聽狀態,準備接受客戶端連接(僅服務器端使用)。
#include <sys/socket.h>int listen(int sockfd, int backlog);
- 參數:
- sockfd:已綁定的 TCP 套接字描述符。
- backlog:未完成連接隊列的最大長度(超過則新連接會被拒絕)。
- 返回值:成功返回0,失敗返回-1。
1.2?接受連接:accept()
從監聽隊列中取出一個已完成的連接,返回一個新的套接字描述符用于與該客戶端通信(僅服務器端使用,會阻塞等待連接)。
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
- 參數:
- sockfd:監聽狀態的套接字描述符(監聽套接字)。
- addr:輸出參數,用于存儲客戶端的 IP 和端口(可設為NULL)。
- addrlen:輸入輸出參數,傳入addr的長度,輸出實際存儲的長度(可設為NULL)。
- 返回值:成功返回新的通信套接字描述符,失敗返回-1。
1.3?發起連接:connect()
客戶端使用該函數向服務器發起 TCP 連接。
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 參數:
- sockfd:客戶端套接字描述符(socket()創建)。
- addr:服務器的地址結構(包含 IP 和端口)。
- addrlen:addr結構的長度。
- 返回值:成功返回0(連接建立),失敗返回-1。
1.4?TCP 發送:send()、write()
#include <sys/socket.h>ssize_t send(int sockfd, const void *buf, size_t len, int flags);
- 功能:向已連接的 TCP 套接字發送數據。
- 參數:sockfd(通信套接字)、buf(數據緩沖區)、len(數據長度)、flags(通常為 0)。
- 返回值:成功返回發送的字節數,失敗返回-1。
除此之外,TCP建立連接之后,可以將sockfd當作一個文件描述符,使用write函數進行發送。
1.5?TCP 接收:recv()、read()
#include <sys/socket.h>ssize_t recv(int sockfd, void *buf, size_t len, int flags);
- 功能:從已連接的 TCP 套接字接收數據。
- 參數:sockfd(通信套接字)、buf(接收緩沖區)、len(緩沖區大小)、flags(通常為 0)。
- 返回值:成功返回接收的字節數(0表示對方關閉連接),失敗返回-1。
同樣地,可以將套接字當作文件,使用sockfd以及read函數讀取消息。
1.6 輔助函數(地址轉換)
在Linux筆記---UDP套接字編程-CSDN博客中,我們介紹的用于將32位整數轉換為點分十進制的函數inet_ntoa是線程不安全的函數。而在網絡編程中難免與多線程/多進程打交道,所以我們推薦以下兩個函數來代替inet_addr和inet_ntoa。
1.6.1 inet_ntop
將網絡字節序的二進制 IP 地址轉換為人類可讀的字符串形式(二進制形式 → 字符串形式)。
#include <arpa/inet.h>const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
- 參數:
- af:地址族,AF_INET(IPv4)或 AF_INET6(IPv6)。
- src:源數據,二進制 IP 地址(如 struct in_addr 或 struct in6_addr 指針)。
- dst:目標緩沖區,存儲轉換后的字符串 IP。
- 返回值:成功返回指向 dst 緩沖區的指針(即轉換后的字符串); 失敗返回 NULL(如 size 不足,需檢查 errno)。
1.6.2 inet_pton
將人類可讀的 IP 地址字符串(如 192.168.1.1 或 2001:db8::1)轉換為網絡字節序的二進制數值(字符串形式 → 二進制形式,供套接字 API 使用)。
#include <arpa/inet.h>int inet_pton(int af, const char *src, void *dst);
- 參數:
- af:地址族,AF_INET(IPv4)或 AF_INET6(IPv6)。
- src:源數據,IP 字符串(如 "192.168.1.1")。
- dst:目標緩沖區,存儲轉換后的二進制 IP。
- size:指定 dst 緩沖區的大小(需足夠容納最長的 IP 字符串,如 IPv6 需至少INET6_ADDRSTRLEN 字節)
- 返回值:成功返回 1(轉換有效); 失敗返回 0(src 格式無效,非合法 IP 地址); 錯誤返回 -1(如 af 不是 AF_INET 或 AF_INET6,需檢查 errno)。
建議:ip統一使用上述兩個接口進行轉換,端口號統一使用htons和ntohs進行轉換。
2. TCP客戶/服務器通信流程
注意,在服務器端套接字分為兩種:監聽套接字和傳輸套接字。
監聽套接字:socket接口創建,顯式綁定服務器的網絡地址信息,然后調用listen接口設置為監聽套接字。該套接字不進行數據傳輸,只負責監聽來自客戶端的連接請求。當客戶端的連接請求被服務器端接收到時,會將該請求放到一個隊列當中,此時accept函數就會從隊列當中取出一個請求,與其建立連接并創建一個與客戶端進行通信的傳輸套接字。
所以,TCP服務端通常都是多線程/多進程的運行方式:主線程/進程持有監聽套接字,每當accept函數返回時就創建一個線程/子進程,使用新獲得的傳輸套接字為客戶端提供服務。
當使用多進程的實現方式時,父進程需要關閉新獲得的傳輸套接字,子進程需要關閉監聽套接字;使用多線程實現方式時新獲得的套接字在子線程使用完畢之后自己關閉。
2.1 客戶端示例
#include "Common.hpp"void Usage(const std::string &name)
{std::cout << "usage: " << name << " + ip" << " + port" << std::endl;
}int main(int argc, char *args[])
{if (argc != 3){Usage(args[0]);exit(USAGE_ERROR);}in_port_t port = std::stoi(args[2]);InetAddr server(args[1], port);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1){LOG(LogLevel::FATAL) << "socket: 套接字創建失敗! " << std::strerror(errno);exit(SOCKET_ERROR);}int n = connect(sockfd, server.NetAddrPtr(), server.AddrLen());if (n == -1){LOG(LogLevel::FATAL) << "connect: 建立連接失敗! " << std::strerror(errno);exit(CONNECT_ERROR);}// 開始與服務器端交互(循環發送讀取)std::string message;char buffer[BUFFER_SIZE];while (true){std::cout << "Send to server# ";std::getline(std::cin, message);ssize_t n = write(sockfd, message.c_str(), message.size());if (n == -1){LOG(LogLevel::ERROR) << "write: [" << server.Info() << "]寫入數據時出錯! " << std::strerror(errno);exit(WRITE_ERROR);}n = read(sockfd, buffer, sizeof(buffer) - 1);if(n == -1){LOG(LogLevel::ERROR) << "read: [" << server.Info() << "]讀取數據時出錯! " << std::strerror(errno);exit(READ_ERROR);}buffer[n] = 0;std::cout << "Recive from server: " << buffer << std::endl;}return 0;
}
2.3 封裝服務器端示例
#pragma once
#include "Common.hpp"
#include <pthread.h>
#include "ThreadPool.hpp"
#define MAX_PENDING_LEN 15// NoCopy類拷貝構造函數和賦值重載都已刪掉
// 任何類繼承該類就無法被拷貝
class TCPServer : public NoCopy
{
private:// 默認消息處理方式---回顯static void DefaultMessageHandler(int sockfd, const InetAddr &client, const std::string &message){ssize_t size = write(sockfd, message.c_str(), message.size());if (size == -1){LOG(LogLevel::ERROR) << "write: [" << client.Info() << "]寫入數據時出錯! " << std::strerror(errno);exit(WRITE_ERROR);}}// 為客戶端提供服務的函數void Service(int sockfd, const InetAddr &client){char buffer[BUFFER_SIZE];int n;while ((n = read(sockfd, buffer, sizeof(buffer) - 1)) > 0){buffer[n] = 0;LOG(LogLevel::INFO) << "[" << client.Info() << "]# " << buffer;_message_handler(sockfd, client, buffer);}if (n == -1){LOG(LogLevel::ERROR) << "read: [" << client.Info() << "]讀取數據時出錯! " << std::strerror(errno);exit(READ_ERROR);}LOG(LogLevel::INFO) << "[" << client.Info() << "]已斷開連接! ";}public:TCPServer(in_port_t port, message_handler_t message_handler = DefaultMessageHandler): _message_handler(message_handler){// 多進程實現方式下,避免等待子進程的推薦方式// signal(SIGCHLD, SIG_IGN);_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockfd == -1){LOG(LogLevel::FATAL) << "socket: 套接字創建失敗! " << std::strerror(errno);exit(SOCKET_ERROR);}InetAddr local(INADDR_ANY, port);int n = bind(_listen_sockfd, local.NetAddrPtr(), local.AddrLen());if (n == -1){LOG(LogLevel::FATAL) << "bind: 綁定地址失敗! " << std::strerror(errno);exit(BIND_ERROR);}}~TCPServer(){close(_listen_sockfd);}void Run(){int n = listen(_listen_sockfd, MAX_PENDING_LEN);if (n == -1){LOG(LogLevel::FATAL) << "listen: 設置監聽套接字失敗! " << std::strerror(errno);exit(LISTEN_ERROR);}LOG(LogLevel::INFO) << "監聽套接字已就緒, 等待連接請求...";while (true){InetAddr client;int sockfd = accept(_listen_sockfd, client.NetAddrPtr(), &client.AddrLen());client = InetAddr(client.NetAddr());if (sockfd == -1){LOG(LogLevel::WARNING) << "accept: [" << client.Info() << "]建立連接失敗! " << std::strerror(errno);continue;}LOG(LogLevel::INFO) << "accept: [" << client.Info() << "]建立連接成功! ";// ...與客戶端的一個連接建立成功...// 三種處理方式: 多進程、多線程、線程池// todo}}private:int _listen_sockfd;message_handler_t _message_handler; // 客戶端消息的處理函數
};
上面的代碼中,在accept函數成功與客戶端建立連接之后的邏輯沒寫,這里有三種實現方式。
2.3.1 多進程實現方式
int id = fork();
if (id < 0)
{// 創建子進程失敗LOG(LogLevel::FATAL) << "fork: 子進程創建失敗! " << std::strerror(errno);exit(FORK_ERROR);
}
else if (id == 0)
{// 子進程// 使用孫子進程, 避免等待子進程if (fork() > 0)exit(NORMAL);close(_listen_sockfd);Service(sockfd, client);exit(NORMAL);
}
else
{// 父進程close(sockfd);
}
2.3.2 多進程實現方式
pthread_t thread;
ThreadData data = {this, sockfd, client};
pthread_create(&thread, nullptr, ThreadHandler, &data);// 需要新增如下內容
struct ThreadData
{TCPServer *self;int sockfd;InetAddr client;
};
static void *ThreadHandler(void *args)
{pthread_detach(pthread_self());ThreadData *data = static_cast<ThreadData *>(args);data->self->Service(data->sockfd, data->client);close(data->sockfd);return nullptr;
}
2.3.3 線程池實現方式
auto task = std::bind(&TCPServer::Service, this, sockfd, client);
ThreadPoolModule::ThreadPool<std::function<void()>>::GetInstance()->PushTask(task);
長服務建議采用多線程/多進程的實現方式(長時間占用一個線程),短服務建議采用線程池的實現方式(頻繁提供服務,線程池能減少線程/進程創建或釋放的開銷)。
3. C/S遠程指令執行程序
即,客戶端輸入命令行指令,客戶端遠程執行并返回結果。
3.1 Common.hpp
#pragma once
#include <functional>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"
using namespace LogModule;using message_handler_t = std::function<void(int, const InetAddr&, const std::string&)>;enum TCPExitCode
{NORMAL = 0,SOCKET_ERROR,BIND_ERROR,LISTEN_ERROR,FORK_ERROR,READ_ERROR,WRITE_ERROR,USAGE_ERROR,CONNECT_ERROR
};class NoCopy
{
public:NoCopy(){}NoCopy(const NoCopy&) = delete;NoCopy& operator=(const NoCopy&) = delete;
};
3.2 InetAddr.hpp
#pragma once
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <cstring>
#include "Log.hpp"
#define BUFFER_SIZE 1024using namespace LogModule;class InetAddr
{
public:// 默認構造函數InetAddr() : _port(0), _len(sizeof(_addr)){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;}// 從sockaddr_in構造InetAddr(const struct sockaddr_in &addr): _addr(addr), _len(sizeof(_addr)){char buffer[BUFFER_SIZE];_ip = inet_ntop(AF_INET, &addr.sin_addr, buffer, sizeof(buffer));_port = ntohs(_addr.sin_port); // 網絡字節序轉主機字節序}// 從IP字符串和端口構造InetAddr(const std::string &ip, in_port_t port): _ip(ip), _port(port), _len(sizeof(_addr)){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(port); }// 從網絡字節序IP和端口構造(用于INADDR_ANY)InetAddr(in_addr_t ip, in_port_t port): _port(port), _len(sizeof(_addr)){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_addr.s_addr = ip;_addr.sin_port = htons(port);// 轉換為IP字符串char buffer[BUFFER_SIZE];_ip = inet_ntop(AF_INET, &_addr.sin_addr, buffer, sizeof(buffer));}bool operator==(const InetAddr &addr) const{return (_ip == addr._ip && _port == addr._port);}std::string &Ip() { return _ip; }const std::string &Ip() const { return _ip; }in_port_t &Port() { return _port; }const in_port_t &Port() const { return _port; }struct sockaddr_in &NetAddr() { return _addr; }const struct sockaddr_in &NetAddr() const { return _addr; }struct sockaddr *NetAddrPtr() { return (struct sockaddr *)&_addr; }const struct sockaddr *NetAddrPtr() const { return (struct sockaddr *)&_addr; }std::string Info() const { return _ip + ":" + std::to_string(_port); }socklen_t &AddrLen() { return _len; }const socklen_t &AddrLen() const { return _len; }private:std::string _ip;in_port_t _port; // 主機字節序的端口struct sockaddr_in _addr; // 網絡字節序的地址結構socklen_t _len;
};
3.3?Command.hpp
#pragma once
#include <unordered_set>
#include "Common.hpp"
#include <string>class Command
{
public:Command(){// 允許調用的命令_white_list.emplace("ls -l");_white_list.emplace("pwd");_white_list.emplace("tree");_white_list.emplace("whoami");_white_list.emplace("touch");}void Excute(int sockfd, const InetAddr& client, const std::string& command){if(!_white_list.count(command)){std::string info = "非法的命令! [" + command + "]";LOG(LogLevel::ERROR) << info;write(sockfd, info.c_str(), info.size());return;}FILE *fp = popen((command + " 2>&1").c_str(), "r");if(fp == nullptr){std::string info = std::string("popen: 創建子進程或管道失敗! ") + strerror(errno);LOG(LogLevel::ERROR) << info;write(sockfd, info.c_str(), info.size());return;}char buffer[BUFFER_SIZE];std::string result;while(fgets(buffer, sizeof(buffer), fp)){result += buffer;}pclose(fp);ssize_t size = write(sockfd, result.c_str(), result.size());if (size == -1){LOG(LogLevel::ERROR) << "write: [" << client.Info() << "]寫入數據時出錯! " << std::strerror(errno);exit(WRITE_ERROR);}}
private:std::unordered_set<std::string> _white_list;
};
3.4 Server.cpp
#include <iostream>
#include <string>
#include "TCPServer.hpp"
#include "Command.hpp"void Usage(const std::string &name)
{std::cout << "usage: " << name << " + port" << std::endl;
}int main(int argc, char *args[])
{if (argc != 2){Usage(args[0]);exit(USAGE_ERROR);}in_port_t port = std::stoi(args[1]);Command command;auto message_handler = std::bind(&Command::Excute, &command, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);TCPServer tcp(port, message_handler);tcp.Run();return 0;
}
3.5 TCPServer.hpp
#pragma once
#include "Common.hpp"
#include <pthread.h>
#include "ThreadPool.hpp"
#define MAX_PENDING_LEN 15// NoCopy類拷貝構造函數和賦值重載都已刪掉
// 任何類繼承該類就無法被拷貝
class TCPServer : public NoCopy
{
private:// 默認消息處理方式---回顯static void DefaultMessageHandler(int sockfd, const InetAddr &client, const std::string &message){ssize_t size = write(sockfd, message.c_str(), message.size());if (size == -1){LOG(LogLevel::ERROR) << "write: [" << client.Info() << "]寫入數據時出錯! " << std::strerror(errno);exit(WRITE_ERROR);}}// 為客戶端提供服務的函數void Service(int sockfd, const InetAddr &client){char buffer[BUFFER_SIZE];int n;while ((n = read(sockfd, buffer, sizeof(buffer) - 1)) > 0){buffer[n] = 0;LOG(LogLevel::INFO) << "[" << client.Info() << "]# " << buffer;_message_handler(sockfd, client, buffer);}if (n == -1){LOG(LogLevel::ERROR) << "read: [" << client.Info() << "]讀取數據時出錯! " << std::strerror(errno);exit(READ_ERROR);}LOG(LogLevel::INFO) << "[" << client.Info() << "]已斷開連接! ";}// 多線程實現的處理函數=============================================struct ThreadData{TCPServer* self;int sockfd;InetAddr client;};static void *ThreadHandler(void *args){pthread_detach(pthread_self());ThreadData *data = static_cast<ThreadData *>(args);data->self->Service(data->sockfd, data->client);close(data->sockfd);return nullptr;}// ===============================================================
public:TCPServer(in_port_t port, message_handler_t message_handler = DefaultMessageHandler): _message_handler(message_handler){// 多進程實現方式下,避免等待子進程的推薦方式// signal(SIGCHLD, SIG_IGN);_listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);if (_listen_sockfd == -1){LOG(LogLevel::FATAL) << "socket: 套接字創建失敗! " << std::strerror(errno);exit(SOCKET_ERROR);}InetAddr local(INADDR_ANY, port);int n = bind(_listen_sockfd, local.NetAddrPtr(), local.AddrLen());if (n == -1){LOG(LogLevel::FATAL) << "bind: 綁定地址失敗! " << std::strerror(errno);exit(BIND_ERROR);}}~TCPServer(){close(_listen_sockfd);}void Run(){int n = listen(_listen_sockfd, MAX_PENDING_LEN);if (n == -1){LOG(LogLevel::FATAL) << "listen: 設置監聽套接字失敗! " << std::strerror(errno);exit(LISTEN_ERROR);}LOG(LogLevel::INFO) << "監聽套接字已就緒, 等待連接請求...";while (true){InetAddr client;int sockfd = accept(_listen_sockfd, client.NetAddrPtr(), &client.AddrLen());client = InetAddr(client.NetAddr());if (sockfd == -1){LOG(LogLevel::WARNING) << "accept: [" << client.Info() << "]建立連接失敗! " << std::strerror(errno);continue;}LOG(LogLevel::INFO) << "accept: [" << client.Info() << "]建立連接成功! ";// 多進程===================================================================// int id = fork();// if(id < 0)// {// // 創建子進程失敗// LOG(LogLevel::FATAL) << "fork: 子進程創建失敗! " << std::strerror(errno);// exit(FORK_ERROR);// }// else if(id == 0)// {// // 子進程// // 使用孫子進程, 避免等待子進程// if(fork() > 0) exit(NORMAL);// close(_listen_sockfd);// Service(sockfd, client);// exit(NORMAL);// }// else// {// // 父進程// close(sockfd);// }// 多進程===================================================================// 多線程===================================================================pthread_t thread;ThreadData data = {this, sockfd, client};pthread_create(&thread, nullptr, ThreadHandler, &data);// 多線程===================================================================// 線程池===================================================================// auto task = std::bind(&TCPServer::Service, this, sockfd, client);// ThreadPoolModule::ThreadPool<std::function<void()>>::GetInstance()->PushTask(task);// 線程池===================================================================}}private:int _listen_sockfd;message_handler_t _message_handler; // 客戶端消息的處理函數
};
3.6 Client.cpp
#include "Common.hpp"void Usage(const std::string &name)
{std::cout << "usage: " << name << " + ip" << " + port" << std::endl;
}int main(int argc, char *args[])
{if (argc != 3){Usage(args[0]);exit(USAGE_ERROR);}in_port_t port = std::stoi(args[2]);InetAddr server(args[1], port);int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd == -1){LOG(LogLevel::FATAL) << "socket: 套接字創建失敗! " << std::strerror(errno);exit(SOCKET_ERROR);}int n = connect(sockfd, server.NetAddrPtr(), server.AddrLen());if (n == -1){LOG(LogLevel::FATAL) << "connect: 建立連接失敗! " << std::strerror(errno);exit(CONNECT_ERROR);}std::string message;char buffer[BUFFER_SIZE];while (true){std::cout << "Send to server# ";std::getline(std::cin, message);ssize_t n = write(sockfd, message.c_str(), message.size());if (n == -1){LOG(LogLevel::ERROR) << "write: [" << server.Info() << "]寫入數據時出錯! " << std::strerror(errno);exit(WRITE_ERROR);}n = read(sockfd, buffer, sizeof(buffer) - 1);if(n == -1){LOG(LogLevel::ERROR) << "read: [" << server.Info() << "]讀取數據時出錯! " << std::strerror(errno);exit(READ_ERROR);}buffer[n] = 0;std::cout << "Recive from server: " << buffer << std::endl;}return 0;
}