计算机网络 : Socket编程
计算机网络 : 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; // 服务器运行状态标志 };