【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…...
时序分析
1、基本概念介绍 1.1、 建立时间 T(su) 建立时间:setup time,它是指有效的边沿信号到来之前,输入端口数据保持稳定的时间。 1.1.1、 建立时间要求: 建立时间要求指的是 想要寄存器如期的工作,在有效时…...
doris:ClickHouse
Doris JDBC Catalog 支持通过标准 JDBC 接口连接 ClickHouse 数据库。本文档介绍如何配置 ClickHouse 数据库连接。 使用须知 要连接到 ClickHouse 数据库,您需要 ClickHouse 23.x 或更高版本 (低于此版本未经充分测试)。 ClickHouse 数据库的 JDBC 驱动程序&a…...
NLP常见任务专题介绍(1)-关系抽取(Relation Extraction, RE)任务训练模板
📌 关系抽取(Relation Extraction, RE)任务训练示例 本示例展示如何训练一个关系抽取模型,以识别两个实体之间的关系。 1️⃣ 任务描述 目标:从文本中提取两个实体之间的语义关系,例如 “人物 - 组织”、“药物 - 疾病”、“公司 - 创始人” 等。输入:句子 + 标注的实…...
大模型Transformer的MOE架构介绍及方案整理
前言:DeepSeek模型最近引起了NLP领域的极大关注,也让大家进一步对MOE(混合专家网络)架构提起了信心,借此机会整理下MOE的简单知识和对应的大模型。本文的思路是MOE的起源介绍、原理解释、再到现有MOE大模型的整理。 一…...
零基础掌握Linux SCP命令:5分钟实现高效文件传输,小白必看!
引言 “为什么我传个文件到服务器要折腾半小时?” 如果你也曾在Linux系统中为文件传输抓狂,今天这篇保姆级教程就是你的救星!SCP命令——一个基于SSH协议的高效传输工具,只需5分钟,彻底告别FTP客户端和繁琐操作&#…...
分类评价指标
基础概念解释 TP、TN、FP、FN 这里T是True,F是False,P为Positive,N为Negative TP:被模型正确地预测为正样本(原本为正样本,预测为正样本) TN:被模型正确地预测为负样本࿰…...
Python项目-基于Django的在线教育平台开发
1. 项目概述 在线教育平台已成为现代教育的重要组成部分,特别是在后疫情时代,远程学习的需求显著增加。本文将详细介绍如何使用Python的Django框架开发一个功能完善的在线教育平台,包括系统设计、核心功能实现以及部署上线等关键环节。 本项…...
子数组问题——动态规划
个人主页:敲上瘾-CSDN博客 动态规划 基础dp:基础dp——动态规划-CSDN博客多状态dp:多状态dp——动态规划-CSDN博客 目录 一、解题技巧 二、最大子数组和 三、乘积最大子数组 四、最长湍流子数组 五、单词拆分 一、解题技巧 区分子数组&…...
linux设置pem免密登录和密码登录
其实现在chatgpt 上面很多东西问题都可以找到比较好答案了,最近换了一个服务器,记录一下。 如果设置root用户,就直接切换到cd .ssh目录下生成ssh key即可,不需要创建用户创建用户的ssh文件夹了 比如说我要让danny这个用户可以用p…...
什么是Flask
Flask是Python中一个简单、灵活和易用的Web框架,适合初学者使用。它提供了丰富的功能和扩展性,可以帮助开发者快速构建功能完善的Web应用程序。 以下是Python Flask框架的一些特点和功能: Flask 是一个使用 Python 编写的轻量级 WSGI 微 Web…...
Spark(8)配置Hadoop集群环境-使用脚本命令实现集群文件同步
一.hadoop的运行模式 二.scp命令————基本使用 三.scp命令———拓展使用 四.rsync远程同步 五.xsync脚本集群之间的同步 一.hadoop的运行模式 hadoop一共有如下三种运行方式: 1. 本地运行。数据存储在linux本地,测试偶尔用一下。我们上一节课使用…...
【cocos creator】热更新
一、介绍 试了官方的热更新功能,总结一下 主要用于安卓包热更新 参考: Cocos Creator 2.2.2 热更新简易教程 基于cocos creator2.4.x的热更笔记 二、使用软件 1、cocos creator v2.4.10 2、creator热更新插件:热更新manifest生成工具&…...
黑金风格人像静物户外旅拍Lr调色教程,手机滤镜PS+Lightroom预设下载!
调色教程 针对人像、静物以及户外旅拍照片,运用 Lightroom 软件进行风格化调色工作。旨在通过软件中的多种工具,如基本参数调整、HSL(色相、饱和度、明亮度)调整、曲线工具等改变照片原本的色彩、明度、对比度等属性,将…...
部署vue+django项目(初版)
1.准备 vscode 插件Remote SSH,连接远程,打开远程中home文件夹。 镜像和容器的一些常用命令 docker images docker ps 查看所有正在运行的容器 docker ps -a docker rmi -f tk-django-app 删除镜像 docker rm xxx 删除容器 docker start xxxx …...
Redis7系列:设置开机自启
前面的文章讲了Redis和Redis Stack的安装,随着服务器的重启,导致Redis 客户端无法连接。原来的是Redis没有配置开机自启。此文记录一下如何配置开机自启。 1、修改配置文件 前面的Redis和Redis Stack的安装的文章中已经讲了redis.config的配置…...
HarmonyOS学习第18天:多媒体功能全解析
一、开篇引入 在当今数字化时代,多媒体已经深度融入我们的日常生活。无论是在工作中通过视频会议进行沟通协作,还是在学习时借助在线课程的音频讲解加深理解,亦或是在休闲时光用手机播放音乐放松身心、观看视频打发时间,多媒体功…...
在rocklinux里面批量部署安装rocklinx9
部署三台Rockylinux9服务器 实验要求 1. 自动安装ubuntu server20以上版本 2. 自动部署三台Rockylinux9服务器,最小化安装,安装基础包,并设定国内源,设静态IP 实验步骤 安装软件 # yum源必须有epel源 # dnf install -y epel-re…...
Manus:成为AI Agent领域的标杆
一、引言 官网:Manus 随着人工智能技术的飞速发展,AI Agent(智能体)作为人工智能领域的重要分支,正逐渐从概念走向现实,并在各行各业展现出巨大的应用潜力。在众多AI Agent产品中,Manus以其独…...
【Java开发指南 | 第三十四篇】IDEA没有Java Enterprise——解决方法
读者可订阅专栏:Java开发指南 |【CSDN秋说】 文章目录 1、新建Java项目2、单击项目名,并连续按两次shift键3、在搜索栏搜索"添加框架支持"4、勾选Web应用程序5、最终界面6、添加Tomcat 1、新建Java项目 2、单击项目名,并连续按两次…...
中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试
作者:Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位:中南大学地球科学与信息物理学院论文标题:BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接:https://arxiv.…...
为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?
在建筑行业,项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升,传统的管理模式已经难以满足现代工程的需求。过去,许多企业依赖手工记录、口头沟通和分散的信息管理,导致效率低下、成本失控、风险频发。例如&#…...
Opencv中的addweighted函数
一.addweighted函数作用 addweighted()是OpenCV库中用于图像处理的函数,主要功能是将两个输入图像(尺寸和类型相同)按照指定的权重进行加权叠加(图像融合),并添加一个标量值&#x…...
ElasticSearch搜索引擎之倒排索引及其底层算法
文章目录 一、搜索引擎1、什么是搜索引擎?2、搜索引擎的分类3、常用的搜索引擎4、搜索引擎的特点二、倒排索引1、简介2、为什么倒排索引不用B+树1.创建时间长,文件大。2.其次,树深,IO次数可怕。3.索引可能会失效。4.精准度差。三. 倒排索引四、算法1、Term Index的算法2、 …...
Hive 存储格式深度解析:从 TextFile 到 ORC,如何选对数据存储方案?
在大数据处理领域,Hive 作为 Hadoop 生态中重要的数据仓库工具,其存储格式的选择直接影响数据存储成本、查询效率和计算资源消耗。面对 TextFile、SequenceFile、Parquet、RCFile、ORC 等多种存储格式,很多开发者常常陷入选择困境。本文将从底…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
rknn toolkit2搭建和推理
安装Miniconda Miniconda - Anaconda Miniconda 选择一个 新的 版本 ,不用和RKNN的python版本保持一致 使用 ./xxx.sh进行安装 下面配置一下载源 # 清华大学源(最常用) conda config --add channels https://mirrors.tuna.tsinghua.edu.cn…...
图解JavaScript原型:原型链及其分析 | JavaScript图解
忽略该图的细节(如内存地址值没有用二进制) 以下是对该图进一步的理解和总结 1. JS 对象概念的辨析 对象是什么:保存在堆中一块区域,同时在栈中有一块区域保存其在堆中的地址(也就是我们通常说的该变量指向谁&…...
二维FDTD算法仿真
二维FDTD算法仿真,并带完全匹配层,输入波形为高斯波、平面波 FDTD_二维/FDTD.zip , 6075 FDTD_二维/FDTD_31.m , 1029 FDTD_二维/FDTD_32.m , 2806 FDTD_二维/FDTD_33.m , 3782 FDTD_二维/FDTD_34.m , 4182 FDTD_二维/FDTD_35.m , 4793...
深度解析:etcd 在 Milvus 向量数据库中的关键作用
目录 🚀 深度解析:etcd 在 Milvus 向量数据库中的关键作用 💡 什么是 etcd? 🧠 Milvus 架构简介 📦 etcd 在 Milvus 中的核心作用 🔧 实际工作流程示意 ⚠️ 如果 etcd 出现问题会怎样&am…...
