一、Socket编程UDP
服务端流程:
socket() -> bind() -> recvfrom() -> (处理业务逻辑) -> sendto() -> close()
客户端流程:
socket() -> sendto()/recvfrom() -> close()
1.1 EchoServer
1.1.1 头文件包含
A. UdpServer.hpp
#pragma once
#include <iostream> // std::cout / std::cerr / std::endl
#include <string> // std::string
#include <cstdint> // uint16_t
#include <sys/socket.h> // socket / bind / recvfrom / sendto
#include <netinet/in.h> // struct sockaddr_in / AF_INET
#include <unistd.h> // close
#include <cerrno> // errno
#include <cstring> // strerror
#include "myLog.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
namespace UdpServerModule
{
using namespace LogModule;
class UdpServer : public NoCopy
{
public:
UdpServer(uint16_t port)
: _port(port)
{
}
// 返回值: Common.hpp 中的 ExitCode, NormalExit=0 表示成功, 失败由 main 决定退出码
int Init()
{
// 1. 创建socket,就是创建了文件细节
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd == -1)
{
LOG(LogLevel::ERROR) << "socket failed: " << strerror(errno) << std::endl;
return SOCKET_ERR;
}
LOG(LogLevel::INFO) << "socket created, socketfd: " << _socketfd << std::endl;
// 2.绑定网络
InetAddr local(_port);
if (bind(_socketfd, local.GetAddrPtr(), local.GetLen()) == -1)
{
LOG(LogLevel::ERROR) << "bind failed: " << strerror(errno) << std::endl;
return BIND_ERR;
}
return NormalExit;
}
void Start()
{
// 3.接受消息
char buffer[defaultsize];
for (;;)
{
// 接受发送方的信息
struct sockaddr_in peer; //(ip + 端口)
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0,
(sockaddr *)&peer, &len);
if (n == -1)
{
LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
continue; /* UDP 服务器不应因单次错误退出 */
}
buffer[n] = 0;
// 打印接受到的消息
std::cout << "[UdpClient] say # " << std::string(buffer) << std::endl;
// 向客户端回显消息
std::string text = "[UdpServer] echo # " + std::string(buffer, n);
ssize_t sent = sendto(_socketfd, text.c_str(), text.size(), 0,
(sockaddr *)&peer, len);
if (sent == -1)
{
LOG(LogLevel::ERROR) << "sendto failed: " << strerror(errno) << std::endl;
continue;
}
}
}
~UdpServer()
{
close(_socketfd);
}
private:
int _socketfd = defaultfd;
uint16_t _port;
static constexpr int defaultfd = -1;
static constexpr int defaultsize = 1024;
};
}
1.1.2 源代码
#include "InetAddr.hpp"
#include "Common.hpp"
#include "myLog.hpp"
using namespace LogModule;
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " ip " << "port" << std::endl;
return USAGE_ERR;
}
// 1.创建socket文件
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd == -1)
{
LOG(LogLevel::ERROR) << "socket failed" << std::endl;
return 1;
}
// 1.5 用于设置接受超时
// 设置接收超时: UDP不可靠, 包被安全组/防火墙丢弃时不会报错,
// 只用阻塞在recvfrom上表现为"卡死"。3秒收不到回显即提示超时, 便于定位问题。
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
if (setsockopt(sfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1)
{
LOG(LogLevel::ERROR) << "setsockopt failed: " << strerror(errno) << std::endl;
return 1;
}
// 2.构建服务端地址: "ip port" 两个参数交给 InetAddr 完成解析与主机序/网络序转换
InetAddr dest_addr(argv[1], argv[2]);
for (;;)
{
// 3.向服务端发送消息
std::string msg;
std::cout << "Please Enter# ";
if (!std::getline(std::cin, msg))
break; // stdin结束(Ctrl+D或管道输入完毕), 退出循环而不是用空消息刷屏
ssize_t sent = sendto(sfd, msg.c_str(), msg.size(), 0,
dest_addr.GetAddrPtr(), dest_addr.GetLen());
if (sent == -1)
{
LOG(LogLevel::ERROR) << "sendto failed" << std::endl;
return 1;
}
// 4.接受服务端的回显消息
// UDP客户端无需bind,内核会在第一次sendto时自动分配临时端口
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(sfd, buffer, sizeof(buffer) - 1, 0,
(sockaddr *)&peer, &len);
if (n == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 超时: 数据报已发出但未收到回显 -> 包被安全组/防火墙丢弃, 或服务端未监听该端口
LOG(LogLevel::WARNING) << "recvfrom timeout: 发送成功但未收到回显"
<< " (请检查: 服务端是否运行 / 云安全组是否放行UDP端口 / 系统防火墙)"
<< std::endl;
continue; /* 丢包是 UDP 的常态, 提示后继续等待下一条输入 */
}
LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
return 1;
}
buffer[n] = 0;
std::cout << buffer << std::endl;
}
close(sfd);
return 0;
}
#include "UdpServer.hpp"
#include "Common.hpp"
#include <memory>
#include <iostream>
#include <string>
#include <exception>
using namespace UdpServerModule;
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return USAGE_ERR;
}
// 校验端口: 非数字输入 stoi 会抛异常, 超出 [0, 65535] 会被截断, 都提前拦截
int portValue;
try
{
portValue = std::stoi(argv[1]);
}
catch (const std::exception &)
{
std::cerr << "invalid port: " << argv[1] << std::endl;
return USAGE_ERR;
}
if (portValue < 0 || portValue > 65535)
{
std::cerr << "port out of range [0, 65535]: " << portValue << std::endl;
return USAGE_ERR;
}
std::unique_ptr<UdpServer> udp = std::make_unique<UdpServer>(static_cast<uint16_t>(portValue));
int rc = udp->Init();
if (rc != NormalExit)
{
LOG(LogLevel::ERROR) << "server init failed, exit code: " << rc << std::endl;
return rc;
}
udp->Start();
return 0;
}
1.2 DictServer
1.2.1 头文件包含
A. Dict.hpp
#pragma once
#include <string>
#include <fstream>
#include <unordered_map>
#include <cctype>
#include <filesystem>
#include "myLog.hpp"
using namespace LogModule; // 使得 LOG 宏中的 LogLevel 可用
// 词库翻译类: 纯业务逻辑, 与网络收发完全解耦
// 使用约定:
// - 词库仅由构造函数从文件一次性载入, 之后全程只读 (只有 const 查询) ->
// 因此无需加锁; 即便未来换用线程池并发应答, 并发查询也天然安全。
// - 文件格式: 每行一条 "英文单词 空格 中文释义"; 空行与 '#' 开头视为注释。
class Dict
{
public:
explicit Dict(const std::string &path)
: _path(path)
{
Load();
}
// 查询单词释义: 入参忽略大小写; 未命中返回非空提示, 保证客户端总能收到应答
std::string translate(const std::string &word) const
{
auto it = _dic.find(ToLower(word));
if (it != _dic.end())
return it->second;
return "未收录单词: " + word;
}
private:
void Load()
{
std::ifstream in(_path);
if (!in.is_open())
{
LOG(LogLevel::ERROR) << "open dict file failed: " << _path << std::endl;
LOG(LogLevel::ERROR) << "current working directory: "
<< std::filesystem::current_path() << std::endl;
return; // 文件缺失时以空词库降级, 不中断服务
}
std::string line;
while (std::getline(in, line))
{
if (line.empty() || line[0] == '#')
continue; // 空行 / 注释行
size_t pos = line.find(' ');
if (pos == std::string::npos)
continue; // 无分隔空格, 视为坏行跳过
std::string key = ToLower(line.substr(0, pos));
std::string val = line.substr(pos + 1);
_dic[key] = val; // 同词条后者覆盖前者
}
}
// 统一转小写, 使 "Hello" / "HELLO" / "hello" 命中同一词条
static std::string ToLower(std::string s)
{
for (char &c : s)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return s;
}
std::string _path;
std::unordered_map<std::string, std::string> _dic;
};
B. UdpServer.hpp
#pragma once
#include <iostream> // std::cout / std::cerr / std::endl
#include <string> // std::string
#include <cstdint> // uint16_t
#include <sys/socket.h> // socket / bind / recvfrom / sendto
#include <netinet/in.h> // struct sockaddr_in / AF_INET
#include <unistd.h> // close
#include <cerrno> // errno
#include <cstring> // strerror
#include <functional>
#include "myLog.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
namespace UdpServerModule
{
using namespace LogModule;
using func_t = std::function<std::string(const std::string &req)>;
class UdpServer : public NoCopy
{
public:
UdpServer(uint16_t port, func_t func)
: _port(port),
_func(func)
{
}
// 返回值: Common.hpp 中的 ExitCode, NormalExit=0 表示成功, 失败由 main 决定退出码
int Init()
{
// 1. 创建socket,就是创建了文件细节
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd == -1)
{
LOG(LogLevel::ERROR) << "socket failed: " << strerror(errno) << std::endl;
return SOCKET_ERR;
}
LOG(LogLevel::INFO) << "socket created, socketfd: " << _socketfd << std::endl;
// 2.绑定网络
InetAddr local(_port);
if (bind(_socketfd, local.GetAddrPtr(), local.GetLen()) == -1)
{
LOG(LogLevel::ERROR) << "bind failed: " << strerror(errno) << std::endl;
return BIND_ERR;
}
return NormalExit;
}
void Start()
{
// 3.接受消息
char buffer[defaultsize];
for (;;)
{
// 接受发送方的信息
struct sockaddr_in peer; //(ip + 端口)
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0,
(sockaddr *)&peer, &len);
if (n == -1)
{
LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
continue; /* UDP 服务器不应因单次错误退出 */
}
buffer[n] = 0;
// 调用业务回调(翻译服务), 得到应答文本
std::string value = _func(std::string(buffer, n));
// 将应答回传给发起请求的客户端
ssize_t sent = sendto(_socketfd, value.c_str(), value.size(), 0,
(sockaddr *)&peer, len);
if (sent == -1)
{
LOG(LogLevel::ERROR) << "sendto failed: " << strerror(errno) << std::endl;
continue; /* UDP 单条失败不拖垮整个服务 */
}
}
}
~UdpServer()
{
close(_socketfd);
}
private:
int _socketfd = defaultfd;
uint16_t _port;
func_t _func; // 执行任务回调
static constexpr int defaultfd = -1;
static constexpr int defaultsize = 1024;
};
}
1.2.2 源代码
#include "InetAddr.hpp"
#include "Common.hpp"
#include "myLog.hpp"
using namespace LogModule;
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " ip " << "port" << std::endl;
return USAGE_ERR;
}
// 1.创建socket文件
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd == -1)
{
LOG(LogLevel::ERROR) << "socket failed" << std::endl;
return 1;
}
// 1.5 用于设置接受超时
// 设置接收超时: UDP不可靠, 包被安全组/防火墙丢弃时不会报错,
// 只用阻塞在recvfrom上表现为"卡死"。3秒收不到回显即提示超时, 便于定位问题。
struct timeval tv;
tv.tv_sec = 3;
tv.tv_usec = 0;
if (setsockopt(sfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1)
{
LOG(LogLevel::ERROR) << "setsockopt failed: " << strerror(errno) << std::endl;
return 1;
}
// 2.构建服务端地址: "ip port" 两个参数交给 InetAddr 完成解析与主机序/网络序转换
InetAddr dest_addr(argv[1], argv[2]);
for (;;)
{
// 3.向服务端发送消息
std::string msg;
std::cout << "Please Enter# ";
if (!std::getline(std::cin, msg))
break; // stdin结束(Ctrl+D或管道输入完毕), 退出循环而不是用空消息刷屏
ssize_t sent = sendto(sfd, msg.c_str(), msg.size(), 0,
dest_addr.GetAddrPtr(), dest_addr.GetLen());
if (sent == -1)
{
LOG(LogLevel::ERROR) << "sendto failed" << std::endl;
return 1;
}
// 4.接受服务端的回显消息
// UDP客户端无需bind,内核会在第一次sendto时自动分配临时端口
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(sfd, buffer, sizeof(buffer) - 1, 0,
(sockaddr *)&peer, &len);
if (n == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
// 超时: 数据报已发出但未收到回显 -> 包被安全组/防火墙丢弃, 或服务端未监听该端口
LOG(LogLevel::WARNING) << "recvfrom timeout: 发送成功但未收到回显"
<< " (请检查: 服务端是否运行 / 云安全组是否放行UDP端口 / 系统防火墙)"
<< std::endl;
continue; /* 丢包是 UDP 的常态, 提示后继续等待下一条输入 */
}
LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
return 1;
}
buffer[n] = 0;
std::cout << buffer << std::endl;
}
close(sfd);
return 0;
}
#include "UdpServer.hpp"
#include "Common.hpp"
#include "Dict.hpp"
#include <memory>
#include <iostream>
#include <string>
#include <exception>
using namespace UdpServerModule;
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return USAGE_ERR;
}
// 校验端口: 非数字输入 stoi 会抛异常, 超出 [0, 65535] 会被截断, 都提前拦截
int portValue;
try
{
portValue = std::stoi(argv[1]);
}
catch (const std::exception &)
{
std::cerr << "invalid port: " << argv[1] << std::endl;
return USAGE_ERR;
}
if (portValue < 0 || portValue > 65535)
{
std::cerr << "port out of range [0, 65535]: " << portValue << std::endl;
return USAGE_ERR;
}
// 词库文件: 可执行文件通常在 src/ 下运行, 词库位于其上级目录
std::unique_ptr<Dict> dict = std::make_unique<Dict>("../dict.txt");
std::unique_ptr<UdpServer> udp = std::make_unique<UdpServer>(static_cast<uint16_t>(portValue),
[&dict](const std::string &req)
{
return dict->translate(req);
});
int rc = udp->Init();
if (rc != NormalExit)
{
LOG(LogLevel::ERROR) << "server init failed, exit code: " << rc << std::endl;
return rc;
}
udp->Start();
return 0;
}
1.3 ChatServer
1.3.1 头文件包含
A. Route.hpp
B. UdpServer.hpp
#pragma once
#include <iostream> // std::cout / std::cerr / std::endl
#include <string> // std::string
#include <cstdint> // uint16_t
#include <sys/socket.h> // socket / bind / recvfrom / sendto
#include <netinet/in.h> // struct sockaddr_in / AF_INET
#include <unistd.h> // close
#include <cerrno> // errno
#include <cstring> // strerror
#include <functional>
#include "myLog.hpp"
#include "Common.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
namespace UdpServerModule
{
using namespace LogModule;
using namespace ThreadPoolModule;
using func_t = std::function<void(const InetAddr &peer, int socketfd, const std::string &msg)>;
class UdpServer : public NoCopy
{
public:
UdpServer(uint16_t port, func_t func)
: _port(port),
_func(func)
{
}
// 返回值: Common.hpp 中的 ExitCode, NormalExit=0 表示成功, 失败由 main 决定退出码
int Init()
{
// 1. 创建socket,就是创建了文件细节
_socketfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_socketfd == -1)
{
LOG(LogLevel::ERROR) << "socket failed: " << strerror(errno) << std::endl;
return SOCKET_ERR;
}
LOG(LogLevel::INFO) << "socket created, socketfd: " << _socketfd << std::endl;
// 2.绑定网络
InetAddr local(_port);
if (bind(_socketfd, local.GetAddrPtr(), local.GetLen()) == -1)
{
LOG(LogLevel::ERROR) << "bind failed: " << strerror(errno) << std::endl;
return BIND_ERR;
}
return NormalExit;
}
void Start()
{
// 3.接受消息
char buffer[defaultsize];
for (;;)
{
// 接受发送方的信息
struct sockaddr_in peer; //(ip + 端口)
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_socketfd, buffer, sizeof(buffer) - 1, 0,
(sockaddr *)&peer, &len);
if (n == -1)
{
LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
continue; /* UDP 服务器不应因单次错误退出 */
}
buffer[n] = 0;
InetAddr client(peer);
_func(client, _socketfd, std::string(buffer));
}
}
~UdpServer()
{
close(_socketfd);
}
private:
int _socketfd = defaultfd;
uint16_t _port;
func_t _func; // 执行任务回调
static constexpr int defaultfd = -1;
static constexpr int defaultsize = 1024;
};
}
1.3.2 源代码
#include "InetAddr.hpp"
#include "Common.hpp"
#include "myLog.hpp"
#include "myThread.hpp"
#include <sys/time.h>
#include <atomic>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <string>
using namespace LogModule;
using namespace ThreadModule; // 接收线程使用项目自研的 pthread 封装 Thread
// 双线程全双工客户端:
// 接收线程 —— 独立 recvfrom + 打印: 主线程停在 stdin 等待输入时也能随时收到广播
// 主线程 —— 读输入 + sendto: 发送是非阻塞的, 不需要任何超时
// 为什么用双线程分工而不是线程池打印:
// 线程池适合"大量相似、独立、昂贵的并行任务"; 这里只有一个 socket、
// 打印极快, 需要的只是"收发两条线各干各的"。且多 worker 并发写 cout
// 会让一行消息被另一行切开(输出交错)、顺序也与到达顺序不一致。
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " ip " << "port" << std::endl;
return USAGE_ERR;
}
// 1.创建socket文件
int sfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sfd == -1)
{
LOG(LogLevel::ERROR) << "socket failed" << std::endl;
return 1;
}
// 1.5 设置接收超时:
// UDP不可靠, 包被安全组/防火墙丢弃时阻塞在 recvfrom 上会表现为"卡死"。
// 这里给接收线程 500ms 短超时: 收不到包时轮询退出标志, 让进程能快速退出。
// (收到数据会立即返回, 超时只影响"无数据"的空转, 不影响正常收发)
struct timeval tv;
tv.tv_sec = 0;
tv.tv_usec = 500 * 1000; // 500ms
if (setsockopt(sfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)) == -1)
{
LOG(LogLevel::ERROR) << "setsockopt failed: " << strerror(errno) << std::endl;
close(sfd);
return 1;
}
// 2.构建服务端地址: "ip port" 两个参数交给 InetAddr 完成解析与主机序/网络序转换
InetAddr dest_addr(argv[1], argv[2]);
// 3.接收线程: 唯一消费 socket 下行数据的地方, 收到即打印
// Thread(项目自研 pthread 封装): 构造时传入"线程名 + 回调"(内部 bind 成无参任务),
// start() 启动, join() 回收。回调 [&] 捕获 running/sfd, join 发生在这些对象销毁前,
// 所以捕获的引用始终有效。char buffer[1024] 声明在回调体内, 归线程栈所有。
std::atomic<bool> running{true};
Thread recv_thread("recv",
[&]()
{
char buffer[1024];
while (running.load(std::memory_order_relaxed))
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(sfd, buffer, sizeof(buffer) - 1, 0,
(sockaddr *)&peer, &len);
if (n == -1)
{
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR)
continue; // 超时/被信号中断: 回到循环顶部, 用退出标志判断是否停止
LOG(LogLevel::ERROR) << "recvfrom failed: " << strerror(errno) << std::endl;
break;
}
buffer[n] = 0;
// 全客户端唯一的打印点: 单线程输出, 天然有序且不会交错
std::cout << buffer << std::endl;
}
});
recv_thread.start();
// 4.主线程: 读输入 -> 发送, 循环直到 EOF 或输入 exit
for (;;)
{
std::string msg;
std::cout << "Please Enter# ";
if (!std::getline(std::cin, msg))
break; // stdin结束(Ctrl+D或管道输入完毕), 退出循环而不是用空消息刷屏
if (msg == "exit")
break; // 显式退出命令
ssize_t sent = sendto(sfd, msg.c_str(), msg.size(), 0,
dest_addr.GetAddrPtr(), dest_addr.GetLen());
if (sent == -1)
{
LOG(LogLevel::ERROR) << "sendto failed: " << strerror(errno) << std::endl;
running.store(false, std::memory_order_relaxed);
recv_thread.join(); // 先收线程再关 socket, 避免在途 recvfrom 撞上 close
close(sfd);
return 1;
}
}
// 5.收尾: 先令接收线程退出并等它结束, 再关 socket。
// 顺序不能反: 若先 close, 正在阻塞的 recvfrom 可能与新 fd 产生复用竞态;
// 先 join 则接收线程至多再空转 500ms(SO_RCVTIMEO)就会返回并读到退出标志。
running.store(false, std::memory_order_relaxed);
recv_thread.join();
close(sfd);
return 0;
}
#include "UdpServer.hpp"
#include "Common.hpp"
#include "Route.hpp"
#include <memory>
#include <iostream>
#include <string>
#include <exception>
using namespace UdpServerModule;
using task_t = std::function<void()>;
int main(int argc, char *argv[])
{
// 1.校验启动进程: 进程名+端口号
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " port" << std::endl;
return USAGE_ERR;
}
// 2.校验端口: 非数字输入 stoi 会抛异常, 超出 [0, 65535] 会被截断, 都提前拦截
int portValue;
try
{
portValue = std::stoi(argv[1]);
}
catch (const std::exception &)
{
std::cerr << "invalid port: " << argv[1] << std::endl;
return USAGE_ERR;
}
if (portValue < 0 || portValue > 65535)
{
std::cerr << "port out of range [0, 65535]: " << portValue << std::endl;
return USAGE_ERR;
}
// 2.注册路由服务
std::unique_ptr<Route> route = std::make_unique<Route>();
// 3.启动线程池
std::unique_ptr<ThreadPool<task_t>> pool = std::make_unique<ThreadPool<task_t>>(15);
// 4.启动Udp服务器: 网络回调把消息包装成无参任务交给线程池, worker 拿到即可执行
std::unique_ptr<UdpServer> udp = std::make_unique<UdpServer>(static_cast<uint16_t>(portValue),
[&route, &pool](const InetAddr &peer, int socketfd, const std::string &msg)
{
// 参数在入队现场按值捕获定型; route 以裸指针捕获 (unique_ptr 不可拷贝)
pool->PushTask([r = route.get(), peer, socketfd, msg]()
{
r->MessageRoute(peer, socketfd, msg);
});
});
int rc = udp->Init();
if (rc != NormalExit)
{
LOG(LogLevel::ERROR) << "server init failed, exit code: " << rc << std::endl;
return rc;
}
pool->Start(); // 启动 worker 线程, 否则任务入队后无人消费
udp->Start();
pool->Stop();
return 0;
}
二、附录
2.1 头文件补充
2.1.1 Common.hpp
#pragma once
#include <iostream>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string>
#include <sys/time.h>
#include <cerrno>
// 错误码
enum ExitCode
{
NormalExit,
USAGE_ERR,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
ACCEPT_ERR
};
class NoCopy
{
public:
NoCopy() = default; // 显式保留默认构造(声明了删除的拷贝构造会抑制隐式默认构造)
NoCopy(const NoCopy &) = delete;
NoCopy &operator=(const NoCopy &) = delete;
~NoCopy() = default;
private:
};
2.1.2 InetAddr.hpp
#pragma once
#include <string> // std::string / std::stoi
#include <cstring> // memset
#include <cstdint> // uint16_t
#include <netinet/in.h> // struct sockaddr_in / htons / INADDR_ANY
#include <arpa/inet.h> // inet_pton / inet_ntop
#include "Common.hpp"
class InetAddr
{
public:
InetAddr() = default;
//点分十进制ip 和 字符串型port
InetAddr(const std::string &ip, const std::string port)
: _ip(ip),
_port(std::stoi(port))
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr.s_addr);
}
//点分十进制ip 和 短整型port
InetAddr(const std::string &ip, uint16_t port)
: _ip(ip),
_port(port)
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr.s_addr);
}
//默认ip "0.0.0.0" 监听所有窗口 和 短整型port
InetAddr(uint16_t port)
: _ip("0.0.0.0"),
_port(port)
{
memset(&_addr, 0, sizeof(_addr));
_addr.sin_family = AF_INET;
_addr.sin_port = htons(port);
_addr.sin_addr.s_addr = INADDR_ANY;
}
// 网络套接字 -> 本地套接字
InetAddr(const struct sockaddr_in &addr)
: _addr(addr)
{
char buffer[64];
_ip = inet_ntop(AF_INET, &_addr.sin_addr.s_addr, buffer, sizeof(buffer));
_port = ntohs(_addr.sin_port);
}
std::string GetIp() const
{
return _ip;
}
uint16_t GetPort() const
{
return _port;
}
socklen_t GetLen() const
{
return sizeof(_addr);
}
std::string GetStringAddr() const
{
return _ip + ":" + std::to_string(_port);
}
const struct sockaddr_in &GetAddr() const
{
return _addr;
}
const struct sockaddr *GetAddrPtr() const
{
return (const struct sockaddr *)&_addr;
}
private:
struct sockaddr_in _addr;
std::string _ip;
uint16_t _port;
};
2.1.3 myLog.hpp
#pragma once
#include <sys/types.h>
#include <unistd.h>
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem>
#include "myMutex.hpp"
namespace LogModule
{
// 默认路径
const std::string defaultpath = "./log/";
const std::string defaultname = "log.txt";
// 获取当前时间
static std::string GetCurrTime()
{
time_t now = time(nullptr); // 获取当前时间
struct tm t;
localtime_r(&now, &t); // 线程安全,必须 _r
char buf[64] = {0};
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &t);
return buf;
}
// 日志等级
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 等级枚举 → 字符串
std::string LogLevelToString(LogLevel level)
{
switch (level)
{
case LogLevel::DEBUG:
return "DEBUG";
case LogLevel::INFO:
return "INFO";
case LogLevel::WARNING:
return "WARNING";
case LogLevel::ERROR:
return "ERROR";
case LogLevel::FATAL:
return "FATAL";
default:
return "UNKNOWN";
}
}
// LevelTag: 流式日志的"参数包"
// LOG(INFO) << "x"; 中, LOG 宏把 等级/文件/行号 打包后经 << 传给 Logger
struct LevelTag
{
LogLevel level;
const char *file;
int line;
};
// LogStrategy: 抽象的日志刷新策略
// 通过多态实现: a.在显示器打印 b.向指定文件写入
class LogStrategy
{
public:
LogStrategy() = default;
virtual ~LogStrategy() = default; // 让编译器合成默认析构,不抑制移动语义
virtual void SyncLog(const std::string &message) = 0; // 纯虚函数
private:
};
// ConsoleLogStrategy:将日志输出到显示器
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy() = default;
void SyncLog(const std::string &message) override
{
{
MutexModule::LockGuard lg(_mutex);
std::cout << message << std::endl;
}
}
private:
MutexModule::Mutex _mutex;
};
// FileLogStrategy : 将日志信息输出到文件
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &path = defaultpath, // 默认参数已覆盖无参调用
const std::string &name = defaultname)
: _fullpath(path + name)
{
std::filesystem::create_directories(path);
_ofs.open(_fullpath, std::ios::app);
}
void SyncLog(const std::string &message) override
{
{
MutexModule::LockGuard lg(_mutex);
_ofs << message << '\n';
}
}
~FileLogStrategy()
{
if (_ofs.is_open())
_ofs.close();
}
private:
std::string _fullpath;
std::ofstream _ofs;
MutexModule::Mutex _mutex;
};
class Message; // 前置声明, 完整定义见 Logger 之后 (它需要调用 Logger 的公开接口)
class Logger
{
public:
static Logger &GetInstance() // 单例入口
{
static Logger instance;
return instance;
}
void Log(LogLevel level, const char *file, int line, std::string message); // 定义见 Message 之后
// 判断某等级是否会被记录 (供 Message 构造时提前短路)
bool ShouldLog(LogLevel level) const { return level >= _logLevel; }
// 统一落盘出口: 只做策略输出 (格式化由 Message 完成)
void SyncText(const std::string &line); // 定义见 Message 之后
void SetLogLevel(LogLevel level) { _logLevel = level; }
void SetStrategy(std::unique_ptr<LogStrategy> strategy) { _strategy = std::move(strategy); }
// 流式入口: LOG(INFO) << "x" << 42;
// 返回临时 Message, 整个表达式结束后由析构函数统一落盘
Message operator<<(const LevelTag &tag);
private:
Logger()
: _logLevel(LogLevel::DEBUG),
_strategy(std::make_unique<ConsoleLogStrategy>())
{
}
Logger(const Logger &) = delete;
Logger &operator=(const Logger &) = delete;
~Logger() = default;
LogLevel _logLevel; // 日志的等级
std::unique_ptr<LogStrategy> _strategy;
};
// Message: 一条正在构建的日志行 (流式收集 + 格式化 + 落盘 合一)
// 流式用法: LOG(INFO) << a << b; 内容先收集进 _oss,
// 整个表达式结束时临时对象析构 → 格式化并交给策略落盘。
// 函数式用法: Logger::Log() 内部也构造 Message 填入文本, 复用同一通道。
// 只通过 Logger 的公开接口 (ShouldLog / SyncText) 协作, 不触碰内部实现。
// 每条语句一个独立缓冲 → 线程安全, 也不需要写 std::endl。
class Message
{
public:
Message(LogLevel level, const char *file, int line, Logger &logger)
: _level(level),
_file(file),
_line(line),
_pid(getpid()),
_valid(logger.ShouldLog(level)), // 等级过滤: 不达标直接短路
_logger(logger)
{
}
~Message()
{
if (!_valid)
return;
std::string text = _oss.str();
if (text.empty()) // 空语句 LOG(INFO); 什么都不做
return;
_logger.SyncText(Format(text)); // 拼好完整一行, 交给策略落盘
}
template <typename T>
Message &operator<<(const T &value) // 数字/字符串/自定义类型均可用
{
if (_valid)
_oss << value;
return *this;
}
// 兼容 std::endl / std::flush 等流操纵符: 忽略即可, 析构时才真实落盘
Message &operator<<(std::ostream &(*)(std::ostream &))
{
return *this;
}
Message(Message &&) = default; // 允许临时对象移动 (C++17 返回值优化也兜底)
Message(const Message &) = delete; // 拷贝会重复落盘, 禁止
private:
// 拼完整行: [时间] [等级] [pid] [文件] [行号] - 内容
std::string Format(const std::string &content) const
{
std::ostringstream oss;
oss << '[' << GetCurrTime() << "] [" << LogLevelToString(_level) << "] ["
<< _pid << "] [" << _file << "] [" << _line << "] - " << content;
return oss.str();
}
LogLevel _level; // 日志的等级
const char *_file; // 在哪个文件打印
int _line; // 文件的行号
pid_t _pid; // 进程 id
bool _valid; // 等级过滤结果
Logger &_logger; // 落盘时回交给 Logger
std::ostringstream _oss; // 流式内容的缓冲
};
// ---- 以下成员定义必须放在 Message 之后 (需要 Message 的完整类型) ----
// 函数式入口: 构造一个 Message 填入文本, 由 m 析构时落盘 (过滤由 Message 内部短路)
inline void Logger::Log(LogLevel level, const char *file, int line, std::string message)
{
Message m(level, file, line, *this);
m << message;
}
inline void Logger::SyncText(const std::string &line)
{
_strategy->SyncLog(line); // 策略输出
}
// 流式入口: operator<< 按值返回也需要 Message 的完整类型
inline Message Logger::operator<<(const LevelTag &tag)
{
return Message(tag.level, tag.file, tag.line, *this);
}
}
#define LOG(level) \
LogModule::Logger::GetInstance() << LogModule::LevelTag { (level), __FILE__, __LINE__ }
2.1.4 myMutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_mutex, nullptr);
}
Mutex(const Mutex &) = delete;
Mutex &operator=(const Mutex &) = delete;
void lock()
{
pthread_mutex_lock(&_mutex);
}
void unlock()
{
pthread_mutex_unlock(&_mutex);
}
// 暴露底层句柄,供条件变量等需要原生 pthread_mutex_t* 的场景使用
pthread_mutex_t *native_handle()
{
return &_mutex;
}
~Mutex()
{
pthread_mutex_destroy(&_mutex);
}
private:
pthread_mutex_t _mutex;
};
class LockGuard
{
public:
LockGuard(Mutex &mutex)
: _mutex(mutex)
{
_mutex.lock();
}
LockGuard(const LockGuard &) = delete;
LockGuard &operator=(const LockGuard &) = delete;
~LockGuard()
{
_mutex.unlock();
}
private:
Mutex &_mutex;
};
}
2.1.5 myThread.hpp
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cstring>
#include <cstdlib>
// 基于 C 接口 (pthread) 的 C++ 线程封装
namespace ThreadModule
{
// 已绑参的可调用对象类型: 无参无返回
using func_t = std::function<void()>;
// 内部上下文: 承载可调用对象 + 线程名
// 独立分配在堆上, 是为了把生命周期与 Thread 对象解耦
// (线程可能在 Thread 对象析构后仍在运行, 不能让回调引用悬空)
class ThreadData
{
friend class Thread; // 须带 class: 否则 Thread 尚未声明, 会被当成友元函数声明
public:
ThreadData(const func_t &func, const std::string &name)
: _func(func),
_name(name)
{
}
private:
func_t _func;
std::string _name;
};
// 线程状态: 防止非法操作 (重复 join / join 已 detach 的线程等)
enum class ThreadStatus
{
NEW, // 已构造, 未启动
RUNNING, // 运行中
DETACHED, // 已分离
JOINED // 已回收
};
// 线程自动编号: inline 函数 + static 局部变量
// C++11 下 inline 函数的 static 局部量跨编译单元共享, 头文件安全
inline int NextThreadId()
{
static int id = 1;
return id++;
}
class Thread
{
public:
// 接受任意可调用对象 + 参数, 用 std::bind 绑参后转为 std::function<void()>
template <typename F, typename... Args>
Thread(const std::string &name, F &&f, Args &&...args)
{
_name = name + "-" + std::to_string(NextThreadId());
_status = ThreadStatus::NEW;
_td = new ThreadData(
std::bind(std::forward<F>(f), std::forward<Args>(args)...),
_name);
}
~Thread()
{
// 析构策略 B : 仍在运行就自动 detach, 保证不泄漏
if (_status == ThreadStatus::RUNNING)
{
pthread_detach(_tid); // 不断言, 容错 (线程可能已结束)
}
// 从未启动: 跳板不会运行, 自己清理 _td
if (_status == ThreadStatus::NEW && _td != nullptr)
{
delete _td;
_td = nullptr;
}
// 已启动 (RUNNING/DETACHED/JOINED): _td 由跳板结束时释放, 此处不动
}
// 禁用拷贝: 一个 pthread_t 不能被两个对象管理 (double join / double free)
Thread(const Thread &) = delete;
Thread &operator=(const Thread &) = delete;
// 启动线程 (构造与启动分离, 便于统一管理一批线程)
void start()
{
if (_status != ThreadStatus::NEW)
{
std::cerr << "[Thread:" << _name << "] already started\n";
return;
}
int n = pthread_create(&_tid, nullptr, start_routine, _td);
if (n != 0)
{
std::cerr << "pthread_create error: " << strerror(n) << "\n";
std::abort();
}
_status = ThreadStatus::RUNNING;
}
// 等待回收
void join()
{
if (_status != ThreadStatus::RUNNING)
{
std::cerr << "[Thread:" << _name << "] cannot join (status not RUNNING)\n";
return;
}
int n = pthread_join(_tid, nullptr);
if (n != 0)
{
std::cerr << "pthread_join error: " << strerror(n) << "\n";
std::abort();
}
_status = ThreadStatus::JOINED;
_td = nullptr; // _td 已被跳板释放, 仅置空标记
}
// 分离
void detach()
{
if (_status != ThreadStatus::RUNNING)
{
std::cerr << "[Thread:" << _name << "] cannot detach (status not RUNNING)\n";
return;
}
int n = pthread_detach(_tid);
if (n != 0)
{
std::cerr << "pthread_detach error: " << strerror(n) << "\n";
std::abort();
}
_status = ThreadStatus::DETACHED;
_td = nullptr; // _td 由跳板结束时释放
}
const std::string &name() const { return _name; }
bool isRunning() const { return _status == ThreadStatus::RUNNING; }
private:
// 跳板: 签名必须匹配 pthread 要求的 void*(*)(void*)
// 由于非 static 成员函数隐含 this, 签名不匹配, 因此必须用 static
static void *start_routine(void *arg)
{
ThreadData *td = static_cast<ThreadData *>(arg);
td->_func(); // 执行用户回调
delete td; // 跳板作为最终消费者, 释放上下文
return nullptr;
}
private:
pthread_t _tid;
std::string _name; // 自己保留一份, name() 在 join/detach 后仍可用
ThreadStatus _status;
ThreadData *_td = nullptr;
};
} // namespace ThreadModule
2.1.6 ThreadPool.hpp
#pragma once
#include "myLog.hpp"
#include "myThread.hpp"
#include "myMutex.hpp"
#include "myCond.hpp"
#include <vector>
#include <queue>
#include <memory>
namespace ThreadPoolModule
{
using namespace LogModule;
const int defalutnum = 5;
template <typename T>
class ThreadPool
{
public:
ThreadPool(const int num = defalutnum)
{
for (int i = 0; i < num; i++)
{
_threads.emplace_back(std::make_unique<ThreadModule::Thread>(
"worker",
[this]()
{
HandleTask();
}));
}
}
// 工作线程调用:处理任务队列中的任务
void HandleTask()
{
while (true)
{
T task;
{
MutexModule::LockGuard lg(_mutex);
// 1.用while循环防止虚假唤醒 和 线程池是否退出
while (_task_q.empty() && _isrunning)
{
_cond.Wait(_mutex);
}
// 线程退出
if (_task_q.empty() && !_isrunning)
{
return;
}
// 2.线程获取任务
task = _task_q.front();
_task_q.pop();
}
// 3.线程处理任务
task();
}
}
// 用户线程调用:向任务队列中添加任务
void PushTask(const T &task)
{
{
MutexModule::LockGuard lg(_mutex);
_task_q.push(task);
}
_cond.Signal();
}
// 创建所有线程
void Start()
{
{
MutexModule::LockGuard lg(_mutex);
if (_isrunning)
return;
_isrunning = true;
}
for (const auto &t : _threads)
{
t->start();
}
}
// 广播唤醒所有阻塞的 worker
void Stop()
{
// 注意:需要加锁对_isrunning进行设置
{
MutexModule::LockGuard lg(_mutex);
if (!_isrunning)
return;
_isrunning = false;
}
_cond.Broadcast();
}
void Join()
{
for (auto &t : _threads)
{
if (t->isRunning()) // 只回收运行中的;未启动/已回收的跳过
t->join();
}
}
~ThreadPool()
{
Stop();
Join();
}
private:
std::vector<std::unique_ptr<ThreadModule::Thread>> _threads; // 管理线程
std::queue<T> _task_q;
MutexModule::Mutex _mutex;
CondModule::Cond _cond;
bool _isrunning = false;
};
}