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;
}