【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/null
if(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); // 提取value
return 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:value
dict.insert({part1, part2}); // 插入哈希表
}
in.close();
}
// 翻译查询函数
std::string translation(const std::string &key)
{
auto iter = dict.find(key); // 查找key
if(iter == dict.end())
return "Unknow"; // 未找到返回Unknow
else
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_; // 客户端IP
uint16_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字段必须为0
3. 避免随机值导致的问题
*/
// 2.3 设置地址族为IPv4
server.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));
// 设置地址族为IPv4
local.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")); // 指定IP
std::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
→ 提交到线程池
→ 线程池分配线程
→ 处理请求
→ 记录日志
这种模块化的结构设计使得程序具有良好的可维护性和扩展性,各个模块之间职责明确,耦合度较低。