【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;
  1. va_list 用于存储可变参数的信息
  2. va_start 初始化可变参数列表
  3. va_arg 获取下一个参数
  4. va_end 清理参数列表
  5. 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)是在后台运行的一种特殊进程,它具有以下特点和用途:

特点:

  1. 脱离终端运行
  2. 在后台运行
  3. 生命周期长(通常一直运行到系统关闭)
  4. 不受用户登录、注销影响

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

关键点说明:

  1. 客户端connect时会自动bind随机端口
  2. 支持断线重连机制
  3. 每次请求都重新建立连接
  4. 使用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. 模块依赖关系

  1. 基础设施模块:
    • Log系统:被其他模块广泛使用的基础设施
    • Daemon:提供守护进程化的基础功能
    • Init:提供数据初始化服务
  2. 网络核心模块:
    • TcpServer:服务器核心,管理网络连接
    • Task:具体的业务处理逻辑
  3. 并发处理模块:
    • ThreadPool:线程池实现,管理工作线程
    • Task:作为线程池的工作单元
  4. 客户端模块:
    • TcpClient:独立的客户端程序

3. 设计模式应用

  1. 单例模式:
    • ThreadPool 使用单例确保只有一个线程池实例
  2. 工厂模式:
    • Task 的创建和管理
  3. 观察者模式:
    • 日志系统的实现

4. 主要类的职责

TcpServer
├── 初始化服务器
├── 监听连接
└── 任务分发ThreadPool
├── 线程管理
├── 任务队列
└── 任务分发Task
├── 业务逻辑
└── 网络IO处理Log
├── 日志级别
├── 输出方式
└── 格式化输出Init
├── 配置加载
└── 数据管理

5. 程序执行流程

  1. 服务器启动流程:
main() 
→ TcpServer初始化
→ 守护进程化
→ 启动线程池
→ 开始接受连接
  1. 请求处理流程:
接收新连接 
→ 创建Task
→ 提交到线程池
→ 线程池分配线程
→ 处理请求
→ 记录日志

这种模块化的结构设计使得程序具有良好的可维护性和扩展性,各个模块之间职责明确,耦合度较低。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/32117.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux中的TCP编程接口基本使用

TCP编程接口基本使用 本篇介绍 在UDP编程接口基本使用已经介绍过UDP编程相关的接口&#xff0c;本篇开始介绍TCP编程相关的接口。有了UDP编程的基础&#xff0c;理解TCP相关的接口会更加容易&#xff0c;下面将按照两个方向使用TCP编程接口&#xff1a; 基本使用TCP编程接口…

wireshark 如何关闭混杂模式 wireshark操作

Fiddler和Wireshark都是进行抓包的工具&#xff1a;所谓抓包就是将网络传输发送与接收的数据包进行截获、重发、编辑、转存等操作&#xff0c;也用来检查网络安全。抓包也经常被用来进行数据截取等。黑客常常会用抓包软件获取你非加密的上网数据&#xff0c;然后通过分析&#…

IDEA2024又一坑:连接Docker服务连不上,提示:Cannot run program “docker“: CreateProcess error=2

为新电脑安装了IDEA2024版&#xff0c;因为局域网中安装有Docker,所以这台电脑上没有安装&#xff0c;当运行时发现死活连不上Docker报&#xff1a;Cannot run program “docker“: CreateProcess error2 分析&#xff1a; Docker服务有问题 其它电脑都能连&#xff0c;排除 网…

文件包含漏洞第一关

一、什么是文件包含漏洞 1.文件包含漏洞概述 和SQL注入等攻击方式一样&#xff0c;文件包含漏洞也是一种注入型漏洞&#xff0c;其本质就是输入一段用户能够控制的脚本或者代码&#xff0c;并让服务端执行。 什么叫包含呢&#xff1f;以PHP为例&#xff0c;我们常常把可重复使…

网络安全事件响应--应急响应(windows)

应用系统日志 Windows主要有以下三类日志记录系统事件&#xff1a;应用程序日志、系统日志和安全日志。 系统和应用程序日志存储着故障排除信息&#xff0c;对于系统管理员更为有用。安全日志记录着事件审计信息&#xff0c;包括用户验证&#xff08;登录、远程访问等&#x…

C++蓝桥杯基础篇(九)

片头 嗨&#xff01;小伙伴们&#xff0c;大家好~ 今天我们将学习蓝桥杯基础篇&#xff08;十&#xff09;&#xff0c;学习函数相关知识&#xff0c;准备好了吗&#xff1f;咱们开始咯&#xff01; 一、函数基础 一个典型的函数定义包括以下部分&#xff1a;返回类型、函数名…

JVM内存结构笔记01-运行时数据区域

文章目录 前言运行时数据区域1.程序计数器定义特点总结 2.虚拟机栈2.1 定义局部变量表 ★操作数栈动态链接方法返回地址(方法出口) 2.2 栈内存溢出演示栈内存溢出 java.lang.StackOverflowError 2.3问题辨析1. 垃圾回收是否涉及栈内存&#xff1f;2. 栈内存分配越大越好吗&…

01-简单几步!在Windows上用llama.cpp运行DeepSeek-R1模型

1.llama.cpp介绍 Llama.cpp 是一个开源的、轻量级的项目&#xff0c;旨在实现 Meta 推出的开源大语言模型 Llama 的推理&#xff08;inference&#xff09;。Llama 是 Meta 在 2023 年开源的一个 70B 参数的高质量大语言模型&#xff0c;而 llama.cpp 是一个用 C 实现的轻量化…

对开源VLA sota π0的微调——如何基于各种开源数据集、以及你自己的私有数据集微调π0(含我司的微调实践)

前言 25年2.4日&#xff0c;几个月前推出π0的公司Physical Intelligence (π)宣布正式开源π0及π0-FAST&#xff0c;如之前所介绍的&#xff0c;他们对用超过 10,000 小时的机器人数据进行了预训练 该GitHub代码仓库「 π0及π0-FAST的GitHub地址&#xff1a;github.com/Ph…

Redis网络模型

redis为什么快 1.主要原因是因为redis是基于内存操作的&#xff0c;比起直接操作磁盘速度快好几倍 2.基于内存的数据库瓶颈主要是在网络io这一块&#xff0c;redis网络模型采用io多路复用技术能够高效的处理并发连接。 3.redis使用单线程执行命令&#xff0c;可以避免上下文…

PyTorch系列教程:Tensor.view() 方法详解

这篇简明扼要的文章是关于PyTorch中的tensor.view()方法的介绍与应用&#xff0c;与reshape()方法的区别&#xff0c;同时给出示例进行详细解释。 Tensor基础 Tensor(张量)的视图是一个新的Tensor&#xff0c;它与原始Tensor共享相同的底层数据&#xff0c;但具有不同的形状或…

Python数据分析之数据可视化

Python 数据分析重点知识点 本系列不同其他的知识点讲解&#xff0c;力求通过例子让新同学学习用法&#xff0c;帮助老同学快速回忆知识点 可视化系列&#xff1a; Python基础数据分析工具数据处理与分析数据可视化机器学习基础 四、数据可视化 图表类型与选择 根据数据特…

swift -(5) 汇编分析结构体、类的内存布局

一、结构体 在 Swift 标准库中&#xff0c;绝大多数的公开类型都是结构体&#xff0c;而枚举和类只占很小一部分 比如Bool、 Int、 Double、 String、 Array、 Dictionary等常见类型都是结构体 ① struct Date { ② var year: Int ③ var month: Int ④ …

推荐一个比较好的开源的工作流引擎

由于DeepSeek等AI大模型的出现&#xff0c;工作流模式再次流行起来&#xff0c;低代码甚至零代码就可以实现应用开发&#xff0c;而且有DeepSeek这样的超级AI作为大脑&#xff0c;人人都可以开发自动化工作流。 比如搭建邮件助手工作流&#xff0c;可以自动润色各种邮件内容。…

CarPlanner:用于自动驾驶大规模强化学习的一致性自回归轨迹规划

25年2月来自浙大和菜鸟网络的论文“CarPlanner: Consistent Auto-regressive Trajectory Planning for Large-scale Reinforcement Learning in Autonomous Driving”。 轨迹规划对于自动驾驶至关重要&#xff0c;可确保在复杂环境中安全高效地导航。虽然最近基于学习的方法&a…

Fedora41安装MySQL8.4.4

Fedora41安装MySQL8.4.4 Fedora41用yum仓库安装MySQL8.4.4 笔记250310下载安装启动mysqld服务查看生成的初始密码 , 用初始密码登录登录后,必须修改初始密码才能执行其它操作可选设置降低密码强度要求, 使用简单密码降低 validate_password 组件对密码强度的要求 用SET GLOBAL命…

信息安全意识之安全组织架构图

一、信息安全技术概论1.网络在当今社会中的重要作用2.信息安全的内涵 网络出现前&#xff1a;主要面向数据的安全&#xff0c;对信息的机密性、完整性和可用性的保护&#xff0c;即CIA三元组 网络出现后&#xff0c;还涵盖了面向用户的安全&#xff0c;即鉴别&#xff0c;授权&…

安卓Android与iOS设备管理对比:企业选择指南

目录 一、管理方式差异 Android Enterprise方案包含三种典型模式&#xff1a; Apple MDM方案主要提供两种模式&#xff1a; 二、安全防护能力 Android系统特点&#xff1a; 三、应用管理方案 四、设备选择建议 五、典型场景推荐 需求场景 推荐方案 六、决策建议要点…

linunx ubuntu24.04.02装libfuse2导致无法开机进不了桌面解决办法

osu.appimage运行需要libfuse2 然后我就下了fuse,打了两把第二天无法开机 这样是不能开机的 这样是可以开机的 解决办法一&#xff1a;玩星火商店的osu&#xff0c;好了问题解决 解决办法二&#xff1a; 在这个页面 ctrl alt f2进入tty6 sudo apt install ubuntu-desktop 进…

mysql-8.0.41-winx64 手动安装详细教程(2025版)

mysql-8.0.41-winx64 手动安装详细教程&#xff08;2025版&#xff09; 一、下载安装包二、配置环境变量三、安装配置四、启动 MySQL 服务&#xff0c;修改密码 一、下载安装包 安装地址如下&#xff1a; https://dev.mysql.com/downloads/mysql/使用7-zip或其他解压软件&…