【Linux】36.简单的TCP网络程序
文章目录
- 1. TCP socket API 详解
- 1.1 socket():打开一个网络通讯端口
- 1.2 bind():绑定一个固定的网络地址和端口号
- 1.3 listen():声明sockfd处于监听状态
- 1.4 accept():接受连接
- 1.5 connect():连接服务器
- 2. 实现一个TCP网络服务器
- 2.1 Log.hpp - "多级日志系统"
- 2.2 Daemon.hpp - "守护进程管理器"
- 2.3 Init.hpp - "字典初始化管理器"
- 2.4 Task.hpp - "网络任务处理器"
- 2.5 TcpClient.cc - "TCP客户端程序"
- 2.6 TcpServer.hpp - "TCP服务器核心"
- 2.7 ThreadPool.hpp - "线程池管理器"
- 2.8 main.cc - "服务器启动程序"
- 程序结构:
- 1. 核心层级结构
- 2. 模块依赖关系
- 3. 设计模式应用
- 4. 主要类的职责
- 5. 程序执行流程
1. TCP socket API 详解
下面介绍程序中用到的socket API,这些函数都在sys/socket.h中
1.1 socket():打开一个网络通讯端口
int socket(int domain, int type, int protocol);
关键参数说明:domain: 协议族,常用值有 AF_INET(IPv4)、AF_INET6(IPv6)type: Socket类型,常用 SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)protocol: 协议,通常为0,表示使用默认协议
返回值:成功时返回非负整数(socket文件描述符)失败时返回-1
- socket()打开一个网络通讯端口,如果成功的话,就像open()一样返回一个文件描述符;
- 应用程序可以像读写文件一样用read/write在网络上收发数据;
- 如果socket()调用出错则返回-1;
- 对于IPv4, family参数指定为AF_INET;
- 对于TCP协议,type参数指定为SOCK_STREAM, 表示面向流的传输协议
1.2 bind():绑定一个固定的网络地址和端口号
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
关键参数说明:sockfd: socket文件描述符,由socket()函数返回addr: 指向要绑定的地址结构体的指针addrlen: 地址结构体的长度
返回值:成功返回0失败返回-1
- 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接; 服务器需要调用bind绑定一个固定的网络地址和端口号;
- bind()成功返回0,失败返回-1。
- bind()的作用是将参数sockfd和myaddr绑定在一起, 使sockfd这个用于网络通讯的文件描述符监听myaddr所描述的地址和端口号;
- 前面讲过,struct sockaddr *是一个通用指针类型,myaddr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度;
1.3 listen():声明sockfd处于监听状态
int listen(int sockfd, int backlog);
关键参数说明:sockfd: socket文件描述符backlog: 待处理连接队列的最大长度,表示服务器同时可以处理的最大连接请求数
返回值:成功返回0失败返回-1
- listen()声明sockfd处于监听状态, 并且最多允许有backlog个客户端处于连接等待状态, 如果接收到更多的连接请求就忽略, 这里设置不会太大(一般是5), 具体细节同学们课后深入研究;
- listen()成功返回0,失败返回-1;
1.4 accept():接受连接
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
关键参数说明:sockfd: 监听的socket文件描述符addr: 用于返回客户端地址信息的结构体指针addrlen: 指向地址结构体长度的指针
返回值:成功返回一个新的socket文件描述符(用于与客户端通信)失败返回-1
- 三次握手完成后, 服务器调用accept()接受连接;
- 如果服务器调用accept()时还没有客户端的连接请求,就阻塞等待直到有客户端连接上来;
- addr是一个传出参数,accept()返回时传出客户端的地址和端口号;
- 如果给addr 参数传NULL,表示不关心客户端的地址;
- addrlen参数是一个传入传出参数(value-result argument), 传入的是调用者提供的, 缓冲区addr的长度以避免缓冲区溢出问题, 传出的是客户端地址结构体的实际长度(有可能没有占满调用者提供的缓冲区);
1.5 connect():连接服务器
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
关键参数说明:sockfd: 客户端的socket文件描述符addr: 服务器地址信息的结构体指针addrlen: 地址结构体的长度
返回值:成功返回0失败返回-1
- 客户端需要调用connect()连接服务器;
- connect和bind的参数形式一致, 区别在于bind的参数是自己的地址, 而connect的参数是对方的地址;
- connect()成功返回0,出错返回-1;
2. 实现一个TCP网络服务器
2.1 Log.hpp - “多级日志系统”
Log.hpp
#pragma once // 防止头文件重复包含// 包含必要的头文件
#include <iostream> // 标准输入输出
#include <time.h> // 时间相关函数
#include <stdarg.h> // 可变参数函数
#include <sys/types.h> // 基本系统数据类型
#include <sys/stat.h> // 文件状态
#include <fcntl.h> // 文件控制
#include <unistd.h> // POSIX系统调用
#include <stdlib.h> // 标准库函数// 缓冲区大小
#define SIZE 1024// 日志级别定义
#define Info 0 // 普通信息
#define Debug 1 // 调试信息
#define Warning 2 // 警告信息
#define Error 3 // 错误信息
#define Fatal 4 // 致命错误// 日志输出方式
#define Screen 1 // 输出到屏幕
#define Onefile 2 // 输出到单个文件
#define Classfile 3 // 根据日志级别输出到不同文件// 默认日志文件名
#define LogFile "log.txt"// 日志类定义
class Log
{
public:// 构造函数,设置默认输出方式和路径Log(){printMethod = Screen; // 默认输出到屏幕path = "./log/"; // 默认日志目录}// 设置日志输出方式void Enable(int method){printMethod = method;}// 将日志级别转换为对应的字符串std::string levelToString(int level){switch (level){case Info:return "Info";case Debug:return "Debug";case Warning:return "Warning";case Error:return "Error";case Fatal:return "Fatal";default:return "None";}}// 日志输出函数,根据不同的输出方式进行处理void printLog(int level, const std::string &logtxt){switch (printMethod){case Screen: // 输出到屏幕std::cout << logtxt << std::endl;break;case Onefile: // 输出到单个文件printOneFile(LogFile, logtxt);break;case Classfile: // 根据日志级别输出到不同文件printClassFile(level, logtxt);break;default:break;}}// 输出到单个文件void printOneFile(const std::string &logname, const std::string &logtxt){std::string _logname = path + logname;// 打开文件:写入、创建(如果不存在)、追加模式int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd < 0)return;// 写入日志内容write(fd, logtxt.c_str(), logtxt.size());close(fd);}// 根据日志级别输出到不同文件void printClassFile(int level, const std::string &logtxt){std::string filename = LogFile;filename += ".";filename += levelToString(level); // 例如: "log.txt.Debug"printOneFile(filename, logtxt);}// 析构函数~Log(){}// 重载函数调用运算符,实现日志记录功能void operator()(int level, const char *format, ...){// 获取当前时间time_t t = time(nullptr);struct tm *ctime = localtime(&t);// 格式化时间和日志级别信息char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);// 处理可变参数va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 组合完整的日志信息char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);// 输出日志printLog(level, logtxt);}private:int printMethod; // 日志输出方式std::string path; // 日志文件路径
};// 创建全局日志对象
Log lg;
va_list
用于存储可变参数的信息va_start
初始化可变参数列表va_arg
获取下一个参数va_end
清理参数列表vsnprintf
用于格式化可变参数到字符串
2.2 Daemon.hpp - “守护进程管理器”
Daemon.hpp
#pragma once#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const std::string nullfile = "/dev/null";void Daemon(const std::string &cwd = "")
{// 1. 忽略特定信号signal(SIGCLD, SIG_IGN); // 忽略子进程状态改变信号signal(SIGPIPE, SIG_IGN); // 忽略管道破裂信号signal(SIGSTOP, SIG_IGN); // 忽略停止进程信号// 2. 创建守护进程if (fork() > 0) // 父进程退出exit(0);setsid(); // 创建新会话,使进程成为会话组长// 3. 设置工作目录if (!cwd.empty()) // 如果指定了工作目录chdir(cwd.c_str()); // 则更改到指定目录// 4. 重定向标准输入输出int fd = open(nullfile.c_str(), O_RDWR); // 打开/dev/nullif(fd > 0){dup2(fd, 0); // 重定向标准输入dup2(fd, 1); // 重定向标准输出dup2(fd, 2); // 重定向标准错误close(fd); // 关闭文件描述符}
}
守护进程(Daemon Process)是在后台运行的一种特殊进程,它具有以下特点和用途:
特点:
- 脱离终端运行
- 在后台运行
- 生命周期长(通常一直运行到系统关闭)
- 不受用户登录、注销影响
2.3 Init.hpp - “字典初始化管理器”
Init.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"// 字典文件路径和分隔符配置
const std::string dictname = "./dict.txt"; // 字典文件名
const std::string sep = ":"; // key-value分隔符// 辅助函数:分割字符串
// 格式:key:value 例如 yellow:黄色
static bool Split(std::string &s, std::string *part1, std::string *part2)
{auto pos = s.find(sep); // 查找分隔符位置if(pos == std::string::npos) return false; // 未找到分隔符*part1 = s.substr(0, pos); // 提取key*part2 = s.substr(pos+1); // 提取valuereturn true;
}class Init
{
public:// 构造函数:加载字典文件Init(){// 1. 打开字典文件std::ifstream in(dictname);if(!in.is_open()){lg(Fatal, "ifstream open %s error", dictname.c_str());exit(1);}// 2. 逐行读取并解析std::string line;while(std::getline(in, line)){std::string part1, part2;Split(line, &part1, &part2); // 分割key:valuedict.insert({part1, part2}); // 插入哈希表}in.close();}// 翻译查询函数std::string translation(const std::string &key){auto iter = dict.find(key); // 查找keyif(iter == dict.end()) return "Unknow"; // 未找到返回Unknowelse return iter->second; // 找到返回对应value}private:std::unordered_map<std::string, std::string> dict; // 存储字典的哈希表
};
2.4 Task.hpp - “网络任务处理器”
Task.hpp
#pragma once
#include <iostream>
#include <string>
#include "Log.hpp"
#include "Init.hpp"extern Log lg; // 外部日志对象
Init init; // 字典初始化对象class Task
{
public:// 构造函数:初始化连接信息Task(int sockfd, const std::string &clientip, const uint16_t &clientport): sockfd_(sockfd), clientip_(clientip), clientport_(clientport){}Task(){}// 任务处理函数void run(){char buffer[4096];// 读取客户端数据// FIXME: TCP粘包问题未处理// 需要定义应用层协议来确保数据完整性ssize_t n = read(sockfd_, buffer, sizeof(buffer));if (n > 0) // 读取成功{// 1. 处理客户端请求buffer[n] = 0; // 字符串结束符std::cout << "client key# " << buffer << std::endl;// 2. 查询翻译结果std::string echo_string = init.translation(buffer);/* 测试代码:模拟连接异常sleep(5);close(sockfd_);lg(Warning, "close sockfd %d done", sockfd_);sleep(2);*/// 3. 发送响应给客户端n = write(sockfd_, echo_string.c_str(), echo_string.size());if(n < 0){// 写入失败记录警告日志lg(Warning, "write error, errno : %d, errstring: %s", errno, strerror(errno));}}else if (n == 0) // 客户端关闭连接{lg(Info, "%s:%d quit, server close sockfd: %d", clientip_.c_str(), clientport_, sockfd_);}else // 读取错误{lg(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd_, clientip_.c_str(), clientport_);}// 任务完成,关闭套接字close(sockfd_);}// 重载()运算符,使对象可调用void operator()(){run();}~Task(){}private:int sockfd_; // 客户端连接套接字std::string clientip_; // 客户端IPuint16_t clientport_; // 客户端端口
};
2.5 TcpClient.cc - “TCP客户端程序”
TcpClient.cc
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>// 使用说明函数
void Usage(const std::string &proc)
{std::cout << "\n\rUsage: " << proc << " serverip serverport\n" << std::endl;
}int main(int argc, char *argv[])
{// 1. 检查命令行参数if (argc != 3){Usage(argv[0]);exit(1);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 2. 初始化服务器地址结构// 2.1 声明IPv4地址结构体struct sockaddr_in server;/*struct sockaddr_in {sa_family_t sin_family; // 地址族(2字节)in_port_t sin_port; // 端口号(2字节)struct in_addr sin_addr; // IPv4地址(4字节)char sin_zero[8]; // 填充字节(8字节)};struct in_addr {in_addr_t s_addr; // 32位IPv4地址};*/// 2.2 清零地址结构体memset(&server, 0, sizeof(server));// | | |// | | └─ 结构体大小(16字节)// | └─ 填充值(0)// └─ 结构体地址/*清零的目的:1. 确保所有字段都被初始化2. 特别是sin_zero字段必须为03. 避免随机值导致的问题*/// 2.3 设置地址族为IPv4server.sin_family = AF_INET;// | |// | └─ IPv4协议族(值为2)// └─ 地址族字段/*常见地址族:AF_INET - IPv4协议AF_INET6 - IPv6协议AF_UNIX - UNIX域协议*/server.sin_port = htons(serverport); // 主机字节序转网络字节序inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr)); // 字符串IP转网络字节序// 3. 主循环 - 支持断线重连while (true){int cnt = 5; // 重连次数int isreconnect = false; // 重连标志int sockfd = 0;// 3.1 创建套接字sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return 1;}// 3.2 连接服务器(支持重试)do{// 客户端connect时会自动bind随机端口int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));if (n < 0){isreconnect = true;cnt--;std::cerr << "connect error..., reconnect: " << cnt << std::endl;sleep(2); // 重试间隔}else{break; // 连接成功}} while (cnt && isreconnect);// 3.3 重试次数用完,退出程序if (cnt == 0){std::cerr << "user offline..." << std::endl;break;}// 3.4 业务处理// while (true) // 注释掉的循环处理多次请求// {// 发送请求std::string message;std::cout << "Please Enter# ";std::getline(std::cin, message);int n = write(sockfd, message.c_str(), message.size());if (n < 0){std::cerr << "write error..." << std::endl;// break;}// 接收响应char inbuffer[4096];n = read(sockfd, inbuffer, sizeof(inbuffer));if (n > 0){inbuffer[n] = 0; // 字符串结束符std::cout << inbuffer << std::endl;}else{// break;}// }close(sockfd); // 关闭连接}return 0;
}
关键点说明:
- 客户端connect时会自动bind随机端口
- 支持断线重连机制
- 每次请求都重新建立连接
- 使用TCP确保数据可靠传输
2.6 TcpServer.hpp - “TCP服务器核心”
TcpServer.hpp
#pragma once#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <signal.h>
#include <signal.h>
#include "Log.hpp"
#include "ThreadPool.hpp"
#include "Task.hpp"
#include "Daemon.hpp"const int defaultfd = -1;
const std::string defaultip = "0.0.0.0";
const int backlog = 10; // 但是一般不要设置的太大
extern Log lg;enum
{UsageError = 1,SocketError,BindError,ListenError,
};class TcpServer;class ThreadData
{
public:ThreadData(int fd, const std::string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t){}
public:int sockfd;std::string clientip;uint16_t clientport;TcpServer *tsvr;
};class TcpServer
{
public:// 构造函数:初始化服务器配置TcpServer(const uint16_t &port, const std::string &ip = defaultip) : listensock_(defaultfd), port_(port), ip_(ip){}// 初始化服务器void InitServer(){// 1. 创建监听套接字listensock_ = socket(AF_INET, SOCK_STREAM, 0);// | | | | |// | | | | └─ 协议号(0表示自动选择)// | | | └─ 套接字类型(TCP)// | | └─ 地址族(IPv4)// | └─ 创建套接字的系统调用// └─ 保存套接字描述符的变量
/*
// domain: 地址族
AF_INET // IPv4协议
AF_INET6 // IPv6协议
AF_UNIX // UNIX域协议// type: 套接字类型
SOCK_STREAM // 流式套接字(TCP)
SOCK_DGRAM // 数据报套接字(UDP)// protocol: 协议
0 // 自动选择协议
*/// 检查套接字创建是否成功(socket()返回值小于0表示失败)if (listensock_ < 0){// 记录致命错误日志lg(Fatal, "create socket, errno: %d, errstring: %s", errno, // 错误码(系统全局变量)strerror(errno)); // 将错误码转换为易读的字符串描述// 退出程序,使用自定义的错误码exit(SocketError); // SocketError可能在某个头文件中定义的错误码}// 记录信息级别日志,表示套接字创建成功lg(Info, "create socket success, listensock_: %d", listensock_);// | | |// | | └─ 套接字描述符值// | └─ 日志信息内容// └─ 日志级别(Info)// 2. 设置地址重用int opt = 1; // 选项值,1表示启用,0表示禁用setsockopt(listensock_, // 要设置的套接字描述符SOL_SOCKET, // 套接字级别的选项SO_REUSEADDR|SO_REUSEPORT, // 要设置的选项(这里用位或组合了两个选项)&opt, // 选项值的指针sizeof(opt)); // 选项值的大小// 3. 绑定地址和端口// 创建并初始化IPv4地址结构体struct sockaddr_in local;// 将地址结构体清零,避免出现随机值memset(&local, 0, sizeof(local));// 设置地址族为IPv4local.sin_family = AF_INET;// 设置端口号(htons转换为网络字节序)// port_是程序指定的端口号,htons处理大小端问题local.sin_port = htons(port_);// 将IP地址字符串转换为网络字节序的32位整数// ip_.c_str():将string转为C风格字符串// inet_aton:将点分十进制IP转换为网络字节序inet_aton(ip_.c_str(), &(local.sin_addr));// 绑定套接字与地址// 将sockaddr_in转换为通用sockaddr结构if (bind(listensock_, // 套接字描述符(struct sockaddr *)&local, // 地址结构体指针sizeof(local)) < 0) // 地址结构体大小{// 绑定失败,记录错误信息并退出lg(Fatal, "bind error, errno: %d, errstring: %s", errno, // 错误码strerror(errno)); // 错误描述exit(BindError); // 退出程序}// 绑定成功,记录日志lg(Info, "bind socket success, listensock_: %d", listensock_);// 4. 开始监听if (listen(listensock_, backlog) < 0){lg(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));exit(ListenError);}lg(Info, "listen socket success, listensock_: %d", listensock_);}// 启动服务器void Start(){// 1. 守护进程化Daemon();// 2. 启动线程池ThreadPool<Task>::GetInstance()->Start();lg(Info, "tcpServer is running....");// 3. 主循环:接受新连接for (;;){// 3.1 接受新连接// 1. 创建客户端地址结构体struct sockaddr_in client; // IPv4地址结构/*struct sockaddr_in {sa_family_t sin_family; // 地址族(AF_INET)in_port_t sin_port; // 端口号(网络字节序)struct in_addr sin_addr; // IP地址char sin_zero[8]; // 填充字节};*/// 2. 设置地址结构体长度socklen_t len = sizeof(client);// socklen_t是专门用于socket相关长度的类型// accept需要这个长度参数来确保不会发生缓冲区溢出// 3. 接受新的连接int sockfd = accept(listensock_, // 监听套接字(服务器套接字)(struct sockaddr *)&client, // 客户端地址结构体&len); // 地址结构体长度(传入传出参数)/*accept()的工作:1. 从已完成三次握手的连接队列中取出一个连接2. 创建新的套接字用于与客户端通信3. 将客户端的地址信息填入client结构体4. 返回新创建的套接字描述符*/// 4. 错误处理if (sockfd < 0) // accept失败返回-1{// 记录警告日志lg(Warning, "accept error, errno: %d, errstring: %s", errno, // 错误码strerror(errno)); // 错误描述字符串continue; // 继续循环,尝试接受下一个连接}// 3.2 获取客户端信息// 1. 获取客户端端口号uint16_t clientport = ntohs(client.sin_port);// | | | |// | | | └─ 网络字节序的端口号// | | └─ 客户端地址结构体// | └─ 网络字节序转主机字节序// └─ 16位无符号整型(0-65535)/*ntohs: Network TO Host Short- 网络字节序(大端)转换为主机字节序- 用于16位整数(如端口号)- 确保不同平台字节序一致性*/// 2. 获取客户端IP地址char clientip[32]; // 存储IP地址字符串的缓冲区inet_ntop(AF_INET, // 地址族(IPv4)&(client.sin_addr), // IP地址(网络字节序)clientip, // 输出缓冲区sizeof(clientip)); // 缓冲区大小/*inet_ntop: Internet Network TO Presentation- 将网络字节序的IP地址转换为点分十进制字符串- 例如: 将0x0100007F转换为"127.0.0.1"*/// 3. 记录连接信息日志lg(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, // 新连接的套接字描述符clientip, // 客户端IP地址字符串clientport); // 客户端端口号// 3.3 创建任务并加入线程池// 1. 创建Task对象,封装客户端连接信息Task t(sockfd, clientip, clientport);// | | | |// | | | └─ 客户端端口号(如:8080)// | | └─ 客户端IP地址(如:"192.168.1.1")// | └─ 客户端连接的文件描述符(accept返回值)// └─ 任务对象,包含了处理一个客户端所需的所有信息// 2. 提交任务到线程池ThreadPool<Task>::GetInstance()->Push(t);// | | | |// | | | └─ 任务对象// | | └─ 将任务加入线程池队列// | └─ 获取线程池单例对象// └─ Task类型的线程池}}~TcpServer() {}private:int listensock_; // 监听套接字uint16_t port_; // 服务器端口std::string ip_; // 服务器IP
};
2.7 ThreadPool.hpp - “线程池管理器”
ThreadPool.hpp
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>struct ThreadInfo
{pthread_t tid;std::string name;
};static const int defalutnum = 10;template <class T>
class ThreadPool
{
public:// 同步互斥相关方法// 1. 加锁操作void Lock() { pthread_mutex_lock(&mutex_); }// 2. 解锁操作void Unlock() { pthread_mutex_unlock(&mutex_); }// 3. 唤醒等待的线程void Wakeup() { pthread_cond_signal(&cond_); }// cond_是条件变量对象// 唤醒一个等待在该条件变量上的线程// 4. 使线程睡眠等待void ThreadSleep() { pthread_cond_wait(&cond_, &mutex_); }// 原子操作:释放mutex_并使线程等待在cond_上// 被唤醒时,会自动重新获取mutex_// 任务队列判空bool IsQueueEmpty() { return tasks_.empty(); }// 根据线程ID获取线程名std::string GetThreadName(pthread_t tid){for (const auto &ti : threads_){if (ti.tid == tid)return ti.name;}return "None";}// 线程函数 - 处理任务static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while (true){tp->Lock();// 无任务时等待while (tp->IsQueueEmpty()){tp->ThreadSleep();}// 取任务并执行T t = tp->Pop();tp->Unlock();t(); // 执行任务}}// 启动线程池void Start(){// 获取线程池中预设的线程数量// threads_是存储ThreadInfo的vector,在构造时已指定大小int num = threads_.size();// 循环创建工作线程for (int i = 0; i < num; i++){// 1. 为每个线程设置名称// 格式:"thread-1", "thread-2", ...threads_[i].name = "thread-" + std::to_string(i + 1);// | | | |// | | | └─ 将数字转为字符串// | | └─ 字符串拼接// | └─ ThreadInfo结构体的name成员// └─ 线程信息数组// 2. 创建线程pthread_create(&(threads_[i].tid), // 线程ID的存储位置nullptr, // 线程属性(默认)HandlerTask, // 线程函数this); // 传递给线程函数的参数(线程池对象)// | |// | └─ ThreadInfo结构体的tid成员// └─ 创建新线程的系统调用}}// 任务队列操作T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t){Lock();tasks_.push(t);Wakeup(); // 唤醒等待线程Unlock();}// 单例模式获取实例static ThreadPool<T> *GetInstance(){if (nullptr == tp_){pthread_mutex_lock(&lock_);if (nullptr == tp_) // 双重检查{std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&lock_);}return tp_;}private:// 构造函数 - 初始化线程池ThreadPool(int num = defalutnum) : threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}// 析构函数 - 清理资源~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}// 禁止拷贝和赋值ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;private:std::vector<ThreadInfo> threads_; // 线程信息数组std::queue<T> tasks_; // 任务队列pthread_mutex_t mutex_; // 任务队列互斥锁pthread_cond_t cond_; // 条件变量static ThreadPool<T> *tp_; // 单例指针static pthread_mutex_t lock_; // 单例锁
};// 静态成员初始化
template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
流程:
创建线程池 -> 启动工作线程 -> 等待任务 -> 获取任务 -> 执行任务 -> 循环等待
2.8 main.cc - “服务器启动程序”
main.c
#include "TcpServer.hpp"
#include <iostream>
#include <memory> // for std::unique_ptr// 使用说明函数
void Usage(std::string proc)
{std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}// ./tcpserver 8080
/*
运行int main(int argc, char *argv[])的时候,终端运行:./tcpserver 8080 debug
操作系统会这样传递参数:
argc = 3 // 总共3个参数
argv[0] = "./tcpserver" // 程序名称
argv[1] = "8080" // 第一个参数
argv[2] = "debug" // 第二个参数
*/
int main(int argc, char *argv[])
{//argc 是程序启动时传入的参数数量://argv[0] 是程序名称//argv[1] 是第一个参数// 1. 检查命令行参数if(argc != 2) // 如果参数数量不等于2{Usage(argv[0]); // 显示使用说明exit(UsageError); // 退出程序}// 2. 获取端口号uint16_t port = std::stoi(argv[1]);
// | | | |
// | | | └─ 命令行传入的第一个参数(字符串形式,如"8080")
// | | └─ 将字符串转换为整数的函数
// | └─ 变量名
// └─ 16位无符号整型(0-65535)// 3. 启用日志系统(写入文件)lg.Enable(Classfile);
/*
设置日志要输出到哪里
lg.Enable(Screen); // 输出到屏幕
lg.Enable(Onefile); // 输出到单个文件
lg.Enable(Classfile); // 根据日志级别输出到不同文件
*/// 4. 创建服务器实例// 使用智能指针管理服务器对象// std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port, "127.0.0.1")); // 指定IPstd::unique_ptr<TcpServer> tcp_svr(new TcpServer(port)); // 默认IP(0.0.0.0)// | | | | | |// | | | | | └─ 端口号参数// | | | | └─ TcpServer构造函数// | | | └─ 创建TcpServer对象// | | └─ 智能指针变量名// | └─ 要管理的对象类型// └─ 智能指针类型 // 5. 初始化并启动服务器tcp_svr->InitServer(); // 创建、绑定、监听套接字tcp_svr->Start(); // 开始接受连接return 0;
}
程序结构:
1. 核心层级结构
顶层应用层
└── main.cc (服务器入口程序)└── TcpServer (TCP服务器核心)├── ThreadPool (线程池)│ └── Task (任务处理单元)├── Log (日志系统)├── Init (字典初始化)└── Daemon (守护进程)
2. 模块依赖关系
- 基础设施模块:
- Log系统:被其他模块广泛使用的基础设施
- Daemon:提供守护进程化的基础功能
- Init:提供数据初始化服务
- 网络核心模块:
- TcpServer:服务器核心,管理网络连接
- Task:具体的业务处理逻辑
- 并发处理模块:
- ThreadPool:线程池实现,管理工作线程
- Task:作为线程池的工作单元
- 客户端模块:
- TcpClient:独立的客户端程序
3. 设计模式应用
- 单例模式:
- ThreadPool 使用单例确保只有一个线程池实例
- 工厂模式:
- Task 的创建和管理
- 观察者模式:
- 日志系统的实现
4. 主要类的职责
TcpServer
├── 初始化服务器
├── 监听连接
└── 任务分发ThreadPool
├── 线程管理
├── 任务队列
└── 任务分发Task
├── 业务逻辑
└── 网络IO处理Log
├── 日志级别
├── 输出方式
└── 格式化输出Init
├── 配置加载
└── 数据管理
5. 程序执行流程
- 服务器启动流程:
main()
→ TcpServer初始化
→ 守护进程化
→ 启动线程池
→ 开始接受连接
- 请求处理流程:
接收新连接
→ 创建Task
→ 提交到线程池
→ 线程池分配线程
→ 处理请求
→ 记录日志
这种模块化的结构设计使得程序具有良好的可维护性和扩展性,各个模块之间职责明确,耦合度较低。
相关文章:
【Linux】36.简单的TCP网络程序
文章目录 1. TCP socket API 详解1.1 socket():打开一个网络通讯端口1.2 bind():绑定一个固定的网络地址和端口号1.3 listen():声明sockfd处于监听状态1.4 accept():接受连接1.5 connect():连接服务器 2. 实现一个TCP网络服务器2.1 Log.hpp - "多级日志系统"2.2 Daem…...
Win 转 MacBook Pro 踩坑指南
前言 Window 和 macOS 系统的差异还是很大的,我从 Thinkpad 转用 M1 的 Macbook pro 已经一年了,几乎没有任何不适应,整体感受那是真的牛👃,速度和续航惊艳到我了,同时开启 6个 vscode 加几十个浏览器标签…...
【模拟CMOS集成电路设计】带隙基准(Bandgap)设计与仿真(基于运放的电流模BGR)
【模拟CMOS集成电路设计】带隙基准(Bandgap)设计与仿真 前言工程文件&部分参数计算过程,私聊~ 一、 设计指标指标分析: 二、 电路分析三、 仿真3.1仿真电路图3.2仿真结果(1)运放增益(2)基准温度系数仿真(3)瞬态启动仿真(4)静态…...
手写一个Tomcat
Tomcat 是一个广泛使用的开源 Java Servlet 容器,用于运行 Java Web 应用程序。虽然 Tomcat 本身功能强大且复杂,但通过手写一个简易版的 Tomcat,我们可以更好地理解其核心工作原理。本文将带你一步步实现一个简易版的 Tomcat,并深…...
QT显示网页控件QAxWidget、QWebEngineView及区别
一.QT种显示网页控件QAxWidget 1.介绍 QAxWidget 属于 QtAxContainer 模块,ActiveX 是微软提出的一种组件对象模型(COM)技术,允许不同的软件组件在 Windows 操作系统上进行交互和集成。QAxWidget 为开发者提供了在 Qt 应用程序中…...
【AI智能体报告】开源AI助手的革命:OpenManus深度使用报告
一、引言:当开源智能体走进生活 2025年3月,MetaGPT团队用一场"开源闪电战"改写了AI Agent的竞争格局。面对商业产品Manus高达10万元的邀请码炒作,他们仅用3小时便推出开源替代品OpenManus,首日即登顶GitHub趋势榜。 …...
VS Code连接服务器教程
VS Code是什么 VS Code(全称 Visual Studio Code)是一款由微软推出的免费、开源、跨平台的代码编辑神器。VS Code 支持 所有主流操作系统,拥有强大的功能和灵活的扩展性。 官网:https://code.visualstudio.com/插件市场࿱…...
装饰器模式的C++实现示例
核心思想 装饰器设计模式是一种结构型设计模式,它允许动态地为对象添加额外的行为或职责,而无需修改其原始类。装饰器模式通过创建一个装饰器类来包装原始对象,并在保持原始对象接口一致性的前提下,扩展其功能。 装饰器模式的核…...
C 语言数据结构(二):顺序表和链表
目录 1. 线性表 2. 顺序表 2.1 概念及结构 2.1.1 静态顺序表(不常用) 2.1.2 动态顺序表(常用) 编辑 2.2 练习 2.2.1 移除元素 2.2.2 删除有序数组中的重复项 2.2.3 合并两个有序数组 2.3 顺序表存在的问题 3. 链表 …...
TDengine 服务无法启动常见原因
taosd 是 TDengine 的核心服务进程,如果无法启动将导致整个数据库无法使用,了解常导致无法启动的原因,可以帮你快速解决问题。 1. 如何查找日志 无法启动的原因记录在日志中,日志文件默认在 /var/log/taos 的 taosdlog.0 或者 t…...
在 UniApp 中实现stream流式输出 AI 聊天功能,AI输出内容用Markdown格式展示
在 UniApp 中实现流式 AI 聊天功能 介绍 在现代 Web 开发中,流式 API 响应能够显著提升用户体验,尤其是在与 AI 聊天接口进行交互时。本文将介绍如何在 UniApp 中使用 Fetch API 实现一个流式响应的 AI 聊天功能,包括实时更新聊天内容和滚动…...
数据库SQL的配置和练习题
一、MySQL的安装 1.安装包下载 下载地址:https://downloads.mysql.com/archives/community/ 2.解压软件包 将MySQL软件包解压在没有中文和空格的目录下 3.设置配置文件 在解压目录下创建my.ini文件并添加内容如下: [client] #客户端设置&…...
Pytorch的一小步,昇腾芯片的一大步
Pytorch的一小步,昇腾芯片的一大步 相信在AI圈的人多多少少都看到了最近的信息:PyTorch最新2.1版本宣布支持华为昇腾芯片! 1、 发生了什么事儿? 在2023年10月4日PyTorch 2.1版本的发布博客上,PyTorch介绍的beta版本…...
AI+办公 Task1
作业 题目1:提示词除了三要素“角色”、“背景”、“要求”之外,还有哪些关键要素 提示词有一个框架叫CO-STAR框架,还有的关键要素有风格、任务、响应格式等。 要素适用场景实际案例Context需要限定领域或场景的任务"作为医学助手&…...
文件系统调用─── linux第17课
目录 linux 中man 2和man 3的区别 文件内容介绍 C语言文件接口 示例: 输出信息到显示器,你有哪些方法 总结: 系统文件I/O 文件类的系统调用接口介绍 示例 open 函数具体使用哪个,和具体应用场景相关, write read close lseek ,类比C文件相关接…...
概念|RabbitMQ 消息生命周期 待消费的消息和待应答的消息有什么区别
目录 消息生命周期 一、消息创建与发布阶段 二、消息路由与存储阶段 三、消息存活与过期阶段 四、消息投递与消费阶段 五、消息生命周期终止 关键配置建议 待消费的消息和待应答的消息 一、待消费的消息(Unconsumed Messages) 二、待应答的消息…...
Javaweb后端文件上传@value注解
文件本地存储磁盘 阿里云oss准备工作 阿里云oss入门程序 要重启一下idea,上面有cmd 阿里云oss案例集成 优化 用spring中的value注解...
DeepSeek技术演进与发展前瞻
如果喜欢可以订阅专栏哟(^U^)ノ~YO,至少更新6年 以下DeepSeek未来发展的技术分析框架及核心内容示范 # -*- coding: utf-8 -*- """ DeepSeek技术演进模拟器(概念验证代码) 本代码展示动态架构调整的核心逻辑 """class DynamicArchitect…...
Java常见面试技术点整理讲解——后端框架(整理中,未完成)
前言: 对于后端常用框架的技术整理,其实框架在平时就是会用就行,但面试时多半需要描述实现原理,这个要靠自己理解,不推荐死记硬背。 这篇和另外几篇文章区分开,主要用于规整Java后端各种框架,…...
目标检测YOLO实战应用案例100讲-基于毫米波雷达的多目标检测 (续)
目录 3.2 改进的CFAR目标检测算法 3.3 算法步骤描述 3.4 实验结果与分析 基于VGG16-Net的毫米波雷达目标检测算法 4.1 VGG16-Net网络模型 4.2 改进VGG16-Net网络的目标检测算法 4.3 算法步骤描述 4.4 实验结果与分析 知识拓展 基于毫米波雷达的多目标检测:使…...
python爬虫:Android自动化工具Auto.js的详细使用
更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 1. Auto.js 简介2. 安装与配置2.1 安装 Auto.js2.2 安装 Python 环境2.3 安装 ADB 工具3. Python 与 Auto.js 结合3.1 通过 ADB 执行 Auto.js 脚本3.2 通过 Python 控制 Auto.js3.3 通过 Python 与 Auto.js 交互4. 常用…...
MyBatis-Plus 注解大全
精心整理了最新的面试资料和简历模板,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 MyBatis-Plus 注解大全 MyBatis-Plus 是基于 MyBatis 的增强工具,通过注解简化了单表 CRUD 操作和复杂查询的配置。以下是常用注解的分类及详细说…...
牛客周赛 Round 84——小红的陡峭值(四)
牛客竞赛_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ 小红的陡峭值(四) 题目: 思路: 题目告诉我们关于树的陡峭值的定义,那一开始看起来无从下手,但是当我们选取某一个节点为根节点时&#…...
Redis 内存淘汰策略深度解析
Redis 作为高性能的内存数据库,其内存资源的高效管理直接关系到系统的稳定性和性能。当 Redis 的内存使用达到配置的最大值(maxmemory)时,新的写入操作将触发内存淘汰机制(Eviction Policy),以释…...
微前端之 Garfish.js 的基础使用教程和进阶配置
前言 在现代前端开发中,微前端架构逐渐成为一种流行的解决方案。它允许将大型应用拆分成多个小型独立的子应用,从而提高开发效率和可维护性。Garfish.js 是一个强大的微前端框架,可以帮助我们轻松实现这一架构。在本文中,通过一个…...
Rabbitmq--延迟消息
13.延迟消息 延迟消息:生产者发送消息时指定一个时间,消费者不会立刻收到消息,而是在指定时间之后才会收到消息 延迟任务:一定时间之后才会执行的任务 1.死信交换机 当一个队列中的某条消息满足下列情况之一时,就会…...
Webshell原理与利用
本文内容仅用于技术研究、网络安全防御及合法授权的渗透测试,严禁用于任何非法入侵、破坏或未经授权的网络活动。 1. WebShell的定义与原理 定义:WebShell是一种基于Web脚本语言(如PHP、ASP、JSP)编写的恶意后门程序,…...
Android 内存泄漏实战:从排查到修复的完整指南
通过实战示例和工具使用,帮助开发者理解、排查和修复 Android 应用中的内存泄漏问题 1. 什么是内存泄漏? 定义:内存泄漏是指程序中已动态分配的内存由于某种原因未能释放,导致系统内存的浪费,最终可能导致应用崩溃或性…...
Liunx系统 : 进程间通信【IPC-Shm共享内存】
文章目录 System V共享内存创建共享内存shmget 控制共享内存shmctl shm特性 System V System V是Liunx中的重要的进程间通信机制,它包括(shm)共享内存,(msg)消息队列和(sem)信号量。…...
c语言笔记 数组指针
数组指针是指针类型的一种,一般数组指针跟二维数组,一维数组结合比较多,下面我们通过图片来探讨一下数组指针的使用以及结合起来的联系。 1.数组指针与一维数组 int a[3]; //一维数组 int aa[2][3];//二维数组 数组元素类型 int [3] int (*p…...
SpringBoot + vue 管理系统
SpringBoot vue 管理系统 文章目录 SpringBoot vue 管理系统 1、成品效果展示2、项目准备3、项目开发 3.1、部门管理 3.1.1、前端核心代码3.1.2、后端代码实现 3.2、员工管理 3.2.1、前端核心代码3.2.2、后端代码实现 3.3、班级管理 3.3.1、前端核心代码3.3.2、后端代码实现 …...
Python语法核心架构与核心知识点:从理论到实践
一、Python的核心设计哲学 Python以“简洁优雅”为核心理念,遵循以下原则: # Zen of Python(输入 import this 可查看) >>> import this The Zen of Python, by Tim Peters ... Simple is better than complex. Readab…...
OpenHarmony子系统开发 - 编译构建Kconfig可视化配置指导
OpenHarmony子系统开发 - 编译构建Kconfig可视化配置指导 概述 功能简介 该功能基于Kconfiglib与Kconfig实现,方便用户个性化配置OpenHarmony产品子系统部件。 基于Kconfig实现的可视化配置功能具有以下优点: 能直观且全面地展示软件的部件选项。可…...
管中窥豹数字预失真(DPD)
管中窥豹数字预失真(DPD) 数字预失真在通信领域发挥了巨大的作用,对提高功放效率、改善误码率起了不可忽略的作用,广泛运用与通信、雷达等各种领域。但是对于普通用户,它显得及其高深神秘。今天就用这个短文ÿ…...
spring-boot-starter和spring-boot-starter-web的关联
maven的作用是方便jar包的管理,所以每一个依赖都是对应着相应的一个或者一些jar包,从网上看到很多对spring-boot-starter的描述就是“这是Spring Boot的核心启动器,包含了自动配置、日志和YAML。”没看太明白,所参与的项目上也一直…...
梯度计算中常用的矩阵微积分公式
标量对向量求导的常用数学公式 设标量函数 y f ( x ) y f(\boldsymbol{x}) yf(x),其中 x ( x 1 , x 2 , ⋯ , x n ) T \boldsymbol{x} (x_1, x_2, \cdots, x_n)^{\rm T} x(x1,x2,⋯,xn)T是一个 n n n维列向量。标量 y y y对向量 x \boldsymbol{x} x的导数…...
vim 编写/etc/docker/daemon.json文件时,E212: 无法打开并写入文件
目录 问题描述 解决方法 1、创建/etc/docker目录 2、打开/etc/docker目录 3、创建daemon.json文件 4、vim 编辑daemon.json文件 问题描述 当我们输入代码:vim /etc/docker/daemon.json时,报E212: 无法打开并写入文件错误,如下图 vim /e…...
http 模块的概念及作用详细介绍
目录 1. http 模块概述 2. http 模块的作用 3. http 服务器代码示例 运行代码 4. http 客户端代码示例 运行代码 5. 总结 1. http 模块概述 http 模块是 Node.js 内置的核心模块之一,它用于创建 HTTP 服务器和客户端,支持处理 HTTP 请求和响应。…...
重生之我在学Vue--第5天 Vue 3 路由管理(Vue Router)
重生之我在学Vue–第5天 Vue 3 路由管理(Vue Router) 文章目录 重生之我在学Vue--第5天 Vue 3 路由管理(Vue Router)前言一、路由配置与导航1.1 什么是 Vue Router?1.2 安装 Vue Router1.3 基本路由配置步骤代码示例 1…...
常见排序算法深度评测:从原理到10万级数据实战
常见排序算法深度评测:从原理到10万级数据实战 摘要 本文系统解析冒泡排序、选择排序、插入排序、希尔排序、归并排序、快速排序、堆排序和基数排序8种经典算法,通过C语言实现10万随机数排序并统计耗时。测试显示:快速排序综合性能最优&…...
搭建BOA服务器
BOA服务器是嵌入式常用的服务器类型,嵌入式程序作为后端时候如果想配合网页进行显示,利用BOA服务器搭建网络界面是不错的选择 首先下载boa官方安装包 Boa Webserver 下载后传输到Ubuntu随便文件夹,解压 tar -xvf boa-0.94.13.tar.gz 进入…...
JSON.parse(JSON.stringify())深拷贝不会复制函数
深拷贝 是指创建一个新对象,并递归地复制原对象中所有层级的属性和值,从而确保新对象与原对象完全独立 深拷贝的实现方法 : 1. 使用 JSON.parse(JSON.stringify()) 函数会被忽略复制,比如,下面的对象的forma…...
debug_unpack_ios failed: Exception: Failed to codesign 解决方案(亲测有效)
debug_unpack_ios failed: Exception: Failed to codesign 解决方案(亲测有效) 背景原因解决方案tipsresult 背景 执行flutter doctor全通过后run项目依然报错 原因 1、检查flutter Mac的flutter项目在哪个文件夹内 2、检查flutter Sdk在哪个文件夹内 …...
Docker篇
1.docker环境搭建: 1.1软件仓库的配置rhel9: #cd/etc/yum.repos.d #vim docker.repo [docker] namedocker-ce baseurlhttps://mirrors.aliyun.com/docker-ce/linux/rhel/9/x86_64/stable gpgcheck0 1.2安装docker并且启动服务 yum install -y dock…...
【Linux】基本命令
目录 🔥一、基础命令 1.sudo su(superuser do) 2.pwd(print working directory) 3.ls(list) 4.cd(change directory) 5.mkdir(make directoryÿ…...
win10电脑鼠标速度突然变的很慢?
电脑鼠标突然变很慢,杀毒检测后没问题,鼠标设置也没变,最后发现可能是误触鼠标的“DPI”调节键。 DPI调节键在鼠标滚轮下方,再次点击即可恢复正常鼠标速度。 如果有和-的按键,速度变快,-速度变慢。 图源&…...
前端(vue)学习笔记(CLASS 3):生命周期工程化开发入门
1、生命周期 Vue生命周期:一个Vue实例从创建到销毁的整个过程 生命周期四个阶段:创建、挂载、更新、销毁 1、创建阶段:响应式数据 2、挂载阶段:渲染模板 3、更新阶段:数据修改、更新视图(执行多次&…...
Python写一个查星座的小程序,适合初学者练手——字典和if语句练习
一、界面预览 二、完整代码 # 导入必要的库 import tkinter as tk from tkinter import ttk # 导入ttk模块用于更现代的控件 from PIL import Image, ImageTk # 用于处理图片 import os # 用于文件路径操作class ZodiacApp:def __init__(self, root):self.root rootself.r…...
云上特权凭证攻防启示录:从根账号AK泄露到安全体系升级的深度实践
事件全景:一场持续17分钟的云上攻防战 2025年3月9日15:39,阿里云ActionTrail日志突现异常波纹——根账号acs:ram::123456789:root(已脱敏)从立陶宛IP(164.92.91.227)发起高危操作。攻击者利用泄露的AccessKey(AK)在17分钟内完成侦察→提权→持久化攻击链,完整操作序列…...
blazemeter工具使用--用于自动生成jmeter脚本并进行性能测试
1、安装blazemeter(网上有很多详情的教程) 2、开始录制:设置号你的文件名称后开始录制 3、录制完成后保存为jmeter(jmx)文件 4、在jmeter中打开文件 5、添加一个后置处理器:查看结果树,后运行看看能否成功…...