从 UDP 回显服务器到多人聊天室:三个项目的渐进式实践
这三个项目是我按顺序完成的:先做一个最小的 UDP 回显服务器,再把固定回显改成可替换的业务处理,最后把服务扩展成支持多个客户端的聊天室。
它们背后是一条清晰的学习路线:
| 阶段 | 项目 | 新增内容 |
|---|---|---|
| 1 | 1_echo_server |
UDP Socket 的创建、绑定和收发 |
| 2 | 2_echo_server |
回调函数、业务逻辑与网络收发解耦 |
| 3 | 3_ChatServer |
用户管理、消息广播、互斥锁、条件变量和线程池 |
三个项目都使用 UDP。UDP 提供数据报收发接口,但不保证数据一定到达,也不保证多个数据报按发送顺序到达。项目中的"聊天室"是一次面向应用层的练习,并没有实现 TCP 那样的可靠连接。
目录
- 先认识程序中的通信对象
- [第一步:完成 UDP 回显](#第一步:完成 UDP 回显)
- 第二步:把业务处理抽成回调
- 第三步:从回显服务扩展到聊天室
- 三个项目串起来看
- 运行方式
- 当前实现的边界与后续改进
- 完整代码
一、先认识程序中的通信对象
网络通信最终要把数据交给正确的应用程序。可以先用下面这组关系理解项目里的地址:
- IP 地址定位一台主机;
- 端口号帮助操作系统把数据交给主机上的某个网络服务;
- Socket是应用程序使用网络收发数据的接口;
- UDP 数据报到达服务器后,
recvfrom不仅返回消息,也会告诉服务器消息来自哪个地址。
因此服务器回复 UDP 客户端时,需要把收到的对端地址交给 sendto。在第一个项目里,这个地址就是客户端的 sockaddr_in。
二、第一步:完成 UDP 回显
2.1 服务器启动时做什么
1_echo_server 的服务器端主要完成三件事:
- 调用
socket创建 UDP Socket; - 填写本地地址和端口,并调用
bind,让操作系统把这个 Socket 与服务器端口关联起来; - 进入循环,用
recvfrom收消息,再用sendto把回复发回消息来源。
下面是服务器收发流程的核心代码:
cpp
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
sockaddr_in local{};
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, reinterpret_cast<sockaddr*>(&local),
sizeof(local)) < 0) {
// 处理绑定失败
}
while (true) {
char buffer[1024] = {};
sockaddr_in peer{};
socklen_t peer_len = sizeof(peer);
ssize_t size = recvfrom(
sockfd, buffer, sizeof(buffer) - 1, 0,
reinterpret_cast<sockaddr*>(&peer), &peer_len);
if (size <= 0) {
continue;
}
buffer[size] = '\0';
std::string reply = "server echo---";
reply += buffer;
sendto(
sockfd, reply.c_str(), reply.size(), 0,
reinterpret_cast<sockaddr*>(&peer), peer_len);
}
这里有几个值得记住的细节:
SOCK_DGRAM表示使用 UDP;htons把端口转换成网络字节序;INADDR_ANY表示监听本机的所有 IPv4 地址;- UDP 不需要像 TCP 那样调用
listen和accept。服务器直接通过recvfrom收到一个数据报,并取得发送方地址; - 客户端第一次发送数据时,操作系统通常会自动分配本地端口,因此示例客户端没有显式调用
bind。
2.2 客户端的一次请求与回复
客户端创建 UDP Socket 后,准备服务器地址。每次输入一条消息,就向该地址调用 sendto,随后用 recvfrom 等待回显:
cpp
sockaddr_in server{};
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
std::string message = "hello";
sendto(sockfd, message.c_str(), message.size(), 0,
reinterpret_cast<sockaddr*>(&server), sizeof(server));
char buffer[1024] = {};
sockaddr_in peer{};
socklen_t peer_len = sizeof(peer);
ssize_t size = recvfrom(
sockfd, buffer, sizeof(buffer) - 1, 0,
reinterpret_cast<sockaddr*>(&peer), &peer_len);
if (size > 0) {
buffer[size] = '\0';
std::cout << buffer << std::endl;
}
到这里,已经建立了一个完整的 UDP 请求---响应闭环。服务器的业务很简单:收到什么,就在前面加上 server echo--- 再发回去。

三、第二步:把业务处理抽成回调
3.1 网络收发与业务逻辑分开
第一个项目把"收到数据后如何处理"写在服务器循环里。这样做适合验证 Socket 流程,但业务一多,网络代码和业务代码就会缠在一起。
在 2_echo_server 中,我把处理函数作为参数传给服务器:
cpp
using callback_t = std::function<std::string(std::string)>;
class UdpServer {
public:
UdpServer(callback_t cb, uint16_t port = default_port)
: _sockfd(default_fd), _port(port), _cb(cb) {}
private:
int _sockfd;
uint16_t _port;
callback_t _cb;
};
收包循环只负责接收数据、调用业务函数,再把业务函数返回的结果发回客户端:
cpp
std::string result = _cb(buffer);
sendto(sockfd, result.c_str(), result.size(), 0,
reinterpret_cast<sockaddr*>(&peer), peer_len);
这样一来,UDP 服务器不必知道具体业务是回显、翻译还是其他操作。它只约定输入和输出:给回调一段消息,拿回一段回复。
3.2 用词典实现一个翻译服务
第二个项目增加了 Dict 类。它从 dict.txt 读取形如 apple: 苹果 的词条,保存到 unordered_map,再通过 Translate 查询结果:
cpp
std::string Translate(std::string word) {
auto it = _dict.find(word);
if (it != _dict.end()) {
return it->second;
}
return "None";
}
主程序把词典查询绑定到服务器回调上:
cpp
Dict dict;
UdpServer server(
[&dict](std::string word) {
return dict.Translate(word);
},
port);
server.Init();
server.Loop();
这里的 [&dict] 表示 Lambda 引用外部的 dict 对象。由于服务器循环在 main 中运行,dict 在整个服务期间都还存在。
这一阶段的变化不是 Socket API,而是程序结构:网络层负责收发,业务层负责回答,回调把两者连接起来。 以后替换业务时,可以保留收发框架,只更换回调实现。

四、第三步:从回显服务扩展到聊天室
第三个项目 3_ChatServer 把单次请求---响应扩展成了多个客户端之间的消息转发。它仍使用 UDP,但服务器需要记录客户端地址,并把一条消息发送给所有已记录的客户端。
4.1 先把客户端地址封装起来
InetAddr 把网络地址 sockaddr_in 和便于程序使用的 IP、端口信息放到同一个对象里。客户端地址通过 IP 和端口比较:
cpp
bool operator==(const InetAddr& other) {
return _ip == other._ip && _port == other._port;
}
对这个 UDP 示例来说,IP 加端口可以区分一次运行中的客户端端点。它并不等于用户身份认证:用户重启客户端后,操作系统可能分配不同的临时端口。
4.2 在线用户表与消息广播
UserManager 用 vector<InetAddr> 保存客户端地址,并提供添加、查找和删除操作。收到客户端数据报后,服务器先检查并记录这个地址,再将带有来源地址的信息交给路由模块。
Route 负责两件事:
CheckUser:把新客户端加入用户表;Broadcast:遍历用户表,对每个客户端调用sendto。
核心广播逻辑如下:
cpp
void Broadcast(int sockfd, std::string message) {
LockGuard guard(_lock);
auto& users = _uma->Users();
for (auto& user : users) {
sendto(sockfd, message.c_str(), message.size(), 0,
reinterpret_cast<sockaddr*>(user.GetNetAddress()),
user.Len());
}
}
这里的互斥锁保护用户表。原因是接收线程可能添加用户,而线程池中的工作线程同时遍历用户表进行广播。若二者并发修改和读取同一个 vector,就会产生数据竞争。用 LockGuard 管理锁,可以在离开作用域时自动解锁。当前这把锁也覆盖整个 sendto 循环,所以多个广播任务仍会在同一把锁上排队;后面的改进建议会缩短这段持锁时间。
4.3 用线程池处理广播任务
如果每次收包后都在网络接收循环里完成整轮广播,客户端越多,接收循环花在发送上的时间就越长。第三个项目把广播封装成任务,放入线程池:
cpp
usvr.RegisterService(
[&route](const InetAddr& address) {
route.CheckUser(address);
},
[&route, thread_pool](int sockfd, std::string message) {
auto task = std::bind(
&Route::Broadcast, &route, sockfd, message);
thread_pool->Enqueue(task);
});
线程池的工作流程可以概括为:
- 网络线程收到消息,创建广播任务;
- 任务进入受互斥锁保护的队列;
- 如果工作线程正在条件变量上等待,生产者发出通知;
- 工作线程醒来后重新检查队列,取出任务;
- 工作线程释放队列锁,再执行广播任务。
等待时需要在循环里重新检查条件,因为线程被唤醒时,队列可能仍为空,也可能已有其他工作线程先取走了任务:
cpp
while (true) {
std::function<void()> task;
{
LockGuard guard(_mutex);
while (_isrunning && _tasks.empty()) {
_cond.Wait(_mutex);
}
if (!_isrunning && _tasks.empty()) {
break;
}
task = _tasks.front();
_tasks.pop();
}
// 离开临界区后执行任务
task();
}
互斥锁保护共享队列,条件变量让没有任务的工作线程休眠,避免空转占用 CPU。这正是生产者---消费者模型在项目中的一个小型应用:网络线程生产任务,工作线程消费任务。
4.4 客户端同时收消息和发消息
聊天室客户端用两个线程分别处理接收和发送:
- 接收线程循环调用
recvfrom,把服务器转发来的消息打印到终端; - 发送线程读取用户输入,并调用
sendto把消息发给服务器。
用户先输入昵称,客户端发送上线消息;之后每条聊天消息都会加上昵称前缀,例如 小明$ 大家好。这是一个简单的文本协议,服务器目前只负责转发,并不解析昵称或消息类型。

五、三个项目串起来看
从项目一到项目三,网络通信的基本骨架一直没有变:
text
创建 Socket
↓
绑定服务器地址和端口
↓
recvfrom 收到数据报和来源地址
↓
调用对应的业务处理
↓
sendto 把数据发给目标地址
变化发生在"业务处理"部分:
| 项目 | 收到消息后做什么 | 程序结构上的进步 |
|---|---|---|
| UDP 回显 | 加前缀后发回原客户端 | 跑通最小通信闭环 |
| 词典服务 | 查询词典并返回释义 | 通过回调分离网络与业务 |
| 聊天室 | 记录发送端,再广播给多个客户端 | 引入用户管理、并发任务和共享数据保护 |
这条路线也把几个知识点自然地连在一起:Socket 地址和端口、UDP 收发、回调函数、容器、线程、互斥锁、条件变量以及线程池。每一步都建立在前一步的基础上,先把功能跑通,再逐步拆分职责、处理并发。
六、运行方式
三个项目的 Makefile 都使用 g++ -std=c++17。代码依赖 Linux/POSIX Socket 和 pthread 接口,下面的命令在 Linux 或 WSL 中、分别进入对应项目目录后执行。词典项目需要从包含 dict.txt 的目录启动服务器,因为词典路径是相对路径。
bash
# 项目一:终端 1 启动服务端
cd 1_echo_server
make
./server_udp 8888
# 项目一:终端 2 启动客户端
cd 1_echo_server
./client_udp 127.0.0.1 8888
bash
# 项目二:终端 1 启动服务端
cd 2_echo_server
make
./server_udp 8888
# 项目二:终端 2 启动客户端
cd 2_echo_server
./client_udp 127.0.0.1 8888
bash
# 项目三:终端 1 启动服务端
cd 3_ChatServer
make
./server_chat 8888
# 项目三:在其他终端各启动一个客户端
cd 3_ChatServer
./client_chat 127.0.0.1 8888
如果工具链要求显式链接 pthread,可在 Makefile 的编译选项中加入 -pthread。
七、当前实现的边界与后续改进
这些项目适合学习和演示基本原理。继续完善时,可以优先考虑以下问题:
- UDP 不保证送达和顺序。 当前程序没有确认、重传、去重或顺序控制;如果业务需要可靠传输,可以改用 TCP,或在应用层设计相应机制。
- 在线用户没有超时清理。 项目中有
OffLineUser,但当前收发流程没有调用它。可以增加心跳和超时检测,清理失联客户端。 - 消息协议还比较简单。 目前把昵称和正文拼成普通字符串。后续可以加入消息类型、长度、发送者标识等字段,并明确编码规则。
- 收发缓冲区有固定大小。 示例一次最多处理有限长度的数据报,较长消息可能被截断;实际服务需要定义最大消息长度并检查发送、接收结果。
- 广播时持有用户表锁。 当前代码在遍历用户表并调用
sendto时一直持锁。可以在锁内复制一份地址列表,解锁后再逐个发送,缩短其他线程等待用户表锁的时间。
对我来说,这三个项目的价值不只是"做出了一个回显服务器和一个聊天室",而是把网络程序从能收发数据,一步步推进到职责拆分和并发处理。每个阶段都不大,但能清楚地看到下一层问题是怎么出现的,也能看到新概念为什么需要被引入。
八、完整代码
以下按项目分别收录源码、头文件、构建文件和运行所需的词典数据。为便于单独查看每个项目,重复文件也按原目录分别保留;编译生成的可执行文件不属于源代码,因此不收录。
8.1 项目一:UDP 回显服务器
EchoServer.hpp
cpp
#pragma once
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int default_fd = -1;
const static uint16_t default_port = 8888;
enum {
SUCCESS = 0,
SOCKET_ERR,
USAGE_ERR,
BIND_ERR
};
class UdpServer
{
public:
// UdpServer(const std::string &ip, uint16_t port = default_port)
UdpServer(uint16_t port = default_port)
:_sockfd(default_fd),
_port(port)
{}
~UdpServer()
{
close(_sockfd);
}
void Init() {
// 第一步:创建socket,本质:打开网络这个文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
// 第二步:填充网络属性信息
struct sockaddr_in local{}; // {}结构体清零
local.sin_family = AF_INET;
local.sin_port = htons(_port); // hton
// inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
local.sin_addr.s_addr = INADDR_ANY;
/*
一台机器(尤其云服务器)通常不止一个 IP:
地址 |说明
公网 IP |云厂商分配的对外地址
内网 IP |比如 172.x.x.x / 10.x.x.x,VPC 内网用的
回环 |127.0.0.1
如果写成 local.sin_addr.s_addr = inet_addr("某个具体IP"),
那这个 socket 就只监听那一个 IP 的端口。比如绑了公网 IP,内网 IP 来的包就收不到。
而 INADDR_ANY(0.0.0.0)会让内核把 socket 挂到所有本地地址上,
无论客户端走公网、内网还是回环,只要目标是这个端口,都能收到。
对服务器来说这是默认、最省心的选择。
*/
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串ip->4字节ip; 2.hton
// 只是填充在了local中,并没有写入内核
// 第三步:绑定套接字socket信息(写入内核)
ssize_t r = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if (r < 0) {
LOG(LogLevel::FATAL) << "绑定套接字失败:" << strerror(errno);
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "绑定套接字成功。" << " port:" << _port;
}
void Loop() {
char inbuffer[1024] = {0};
while (true) {
struct sockaddr_in peer;
socklen_t peerlen = sizeof(peer);
ssize_t r = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &peerlen);
if (r < 0) {
LOG(LogLevel::ERROR) << "recvfrom接收失败:" << strerror(errno);
continue;
}
else if (r == 0) {
LOG(LogLevel::WARNING) << "recvfrom接收空数据。";
}
else {
// 客户端的信息
uint16_t client_port = ntohs(peer.sin_port);
char client_ip_char[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &peer.sin_addr, client_ip_char, sizeof(client_ip_char));
std::string client_ip(client_ip_char);
std::string client_addr = "[" + client_ip + ":" + std::to_string(client_port) + "]";
// 用户发来的数据
inbuffer[r] = 0;
LOG(LogLevel::DEBUG) << "client [" << client_addr << "] say# " << inbuffer;
std::string echo_string = "server echo---";
echo_string += inbuffer;
sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, peerlen);
}
}
}
private:
int _sockfd;
// std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址,给人看) && 4字节IP
uint16_t _port;
};
EchoServerMain.cpp
cpp
#include "EchoServer.hpp"
#include <memory>
static void Usage(const std::string &process) {
std::cerr << "Usage:\n\t";
std::cerr << process << " [local_port]" << std::endl;
}
// ./server_udp [ip] [port]
int main(int argc, char* argv[]) {
if (argc != 2) {
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY()
// std::string socket_ip = argv[1];
uint16_t socket_port = std::stoi(argv[1]);
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(socket_port);
usvr->Init();
usvr->Loop();
return 0;
}
EchoClient.cpp
cpp
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &proc) {
std::cout << "Usage:\n\t";
std::cout << proc << " [server_ip] [server_port]" << std::endl;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "socket error:" << strerror(errno);
exit(2);
}
// 2. 不需要显示的去bind绑定addr(IP+port)信息,udp client首次发送数据的时候,OS底层会隐式自动获取随机端口并绑定IP+port
// 后面发数据的时候,只需要sockaddr就行
struct sockaddr_in server{};
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
// 3. 循环执行客户端
while (true) {
std::string message;
// 1. 获取用户输入
std::cout << "请输入要发送的消息:";
std::getline(std::cin, message);
// 2. sendto发送数据给server
ssize_t r = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
if (r > 0) {
// recvform
char inbuffer[1024] = {0};
struct sockaddr_in tmp{}; // 避免recvfrom()内参数填NULL,可能会出问题,只是存放收到的服务端的套接字
socklen_t tmplen = sizeof(tmp);
ssize_t n = recvfrom(sockfd, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&tmp, &tmplen);
if (n > 0) {
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
}
else {
std::cerr << "sendto seeor:" << strerror(errno);
exit(3);
}
}
return 0;
}
Logger.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/time.h>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
#include "mutex.hpp"
namespace NS_LOG_MODULE
{
// 日志等级信息
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 为避免打印日志时,流式输出等级是数字
std::string LogLevelToMessage(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 "UNKNOW";
}
}
// 详细的时间信息
std::string GetCurrentTime() {
struct timeval current_time;
gettimeofday(¤t_time, nullptr);
// current_time.tv_sec; current_time.tv_usec;
struct tm struct_time;
localtime_r(&(current_time.tv_sec), &struct_time);
char timestr[128];
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
struct_time.tm_year + 1900,
struct_time.tm_mon + 1,
struct_time.tm_mday,
struct_time.tm_hour,
struct_time.tm_min,
struct_time.tm_sec,
current_time.tv_usec
);
return timestr;
}
// 策略模式,策略接口
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印策略
class ConsoleStrategy : public LogStrategy
{
public:
~ConsoleStrategy()
{}
void SyncLog(const std::string &message) override
{
LockGuard lg(_mutex);
std::cout << message << std::endl;
}
private:
Mutex _mutex;
};
// 文件打印策略
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
class FileStrategy : public LogStrategy
{
public:
FileStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename)
:_logpath(path)
,_logfilename(filename)
{
LockGuard lg(_mutex);
if (std::filesystem::exists(_logpath)) return;
try
{
std::filesystem::create_directories(_logpath);
}
catch (std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << std::endl;
}
}
~FileStrategy()
{}
void SyncLog(const std::string &message) override
{
LockGuard lg(_mutex);
if (!_logpath.empty() && _logpath.back() != '/') _logpath += "/";
std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
std::ofstream out(targetlog, std::ios::app); // 追加的方式写入
if (!out.is_open())
{
std::cerr << "open " << targetlog << " failed!" << std::endl;
return;
}
out << message << '\n';
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
class Logger
{
// 日志类:
// 1. 日志的生成
// 2. 根据不同的策略进行刷新
public:
Logger()
{}
~Logger()
{}
void UseConsoleStrategy() {
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy() {
_strategy = std::make_unique<FileStrategy>();
}
// void Debug(const std::string &message) {
// if (_strategy != nullptr) {
// _strategy->SyncLog(message);
// }
// }
// 内部类
class LogMessage
{
public:
LogMessage(LogLevel level, std::string& filename, int line, Logger& logger)
:_curr_time(GetCurrentTime()),
_level(level),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
// 一条完整的日志信息 = 前部分固定信息 + 后面不固定信息
// 先组成前部分固定
std::stringstream ss;
ss << '[' << _curr_time << "] "
<< '[' << LogLevelToMessage(_level) << "] "
<< '[' << _pid << "] "
<< '[' << _filename << "] "
<< '[' << _line << "] "
<< " - ";
_loginfo = ss.str();
}
~LogMessage()
{
// 在Logger析构时,进行刷新信息(RAII)
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
template<typename T>
LogMessage& operator << (const T& info) {
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this; // 返回当前LogMessage,以便连续<<
}
private:
std::string _curr_time; // 时间
LogLevel _level; // 等级
pid_t _pid; // 进程id
std::string _filename; // 文件名
int _line; // 具体位置
std::string _loginfo; // 一条完整的日志信息
// 因为这是内部类LogMessage,析构时,要知道Logger的刷新策略,所以要引用外面的Logger
Logger& _logger;
};
// 不是内部类了
// 重载的是Logger类的(),只是返回类型是LogMessage
LogMessage operator()(LogLevel level, std::string filename, int line) {
return LogMessage(level, filename, line, *this);
}
// 意义:创建出那个用来实现 RAII 的临时 LogMessage 对象
private:
std::unique_ptr<LogStrategy> _strategy;
};
// 日志对象,全局使用
Logger logger;
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
#define LOG(level) logger(level, __FILE__, __LINE__)
}
mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex() {
pthread_mutex_init(&_lock, nullptr);
}
~Mutex() {
pthread_mutex_destroy(&_lock);
}
pthread_mutex_t* Ptr() {
return &_lock;
}
void Lock() {
pthread_mutex_lock(&_lock);
}
void UnLock() {
pthread_mutex_unlock(&_lock);
}
private:
pthread_mutex_t _lock;
};
// 采用RAII风格,进行锁管理
class LockGuard
{
public:
LockGuard(Mutex& lock):_lockref(lock) {
_lockref.Lock();
}
~LockGuard() {
_lockref.UnLock();
}
private:
Mutex& _lockref;
};
Makefile
makefile
.PHONY:all
all:client_udp server_udp
server_udp:EchoServerMain.cpp
g++ -o $@ $^ -std=c++17
client_udp:EchoClient.cpp
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f client_udp server_udp
8.2 项目二:UDP 词典服务
EchoServer.hpp
cpp
#pragma once
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
const static int default_fd = -1;
const static uint16_t default_port = 8888;
using callback_t = std::function<std::string(std::string)>;
enum {
SUCCESS = 0,
SOCKET_ERR,
USAGE_ERR,
BIND_ERR
};
class UdpServer
{
public:
// UdpServer(const std::string &ip, uint16_t port = default_port)
UdpServer(callback_t cb, uint16_t port = default_port)
:_sockfd(default_fd),
_port(port),
_cb(cb)
{}
~UdpServer()
{
close(_sockfd);
}
void Init() {
// 第一步:创建socket,本质:打开网络这个文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
// 第二步:填充网络属性信息
struct sockaddr_in local{}; // {}结构体清零
local.sin_family = AF_INET;
local.sin_port = htons(_port); // hton
// inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
local.sin_addr.s_addr = INADDR_ANY;
/*
一台机器(尤其云服务器)通常不止一个 IP:
地址 |说明
公网 IP |云厂商分配的对外地址
内网 IP |比如 172.x.x.x / 10.x.x.x,VPC 内网用的
回环 |127.0.0.1
如果写成 local.sin_addr.s_addr = inet_addr("某个具体IP"),
那这个 socket 就只监听那一个 IP 的端口。比如绑了公网 IP,内网 IP 来的包就收不到。
而 INADDR_ANY(0.0.0.0)会让内核把 socket 挂到所有本地地址上,
无论客户端走公网、内网还是回环,只要目标是这个端口,都能收到。
对服务器来说这是默认、最省心的选择。
*/
// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串ip->4字节ip; 2.hton
// 只是填充在了local中,并没有写入内核
// 第三步:绑定套接字socket信息(写入内核)
ssize_t r = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
if (r < 0) {
LOG(LogLevel::FATAL) << "绑定套接字失败:" << strerror(errno);
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "绑定套接字成功。" << " port:" << _port;
}
void Loop() {
char inbuffer[1024] = {0};
while (true) {
struct sockaddr_in peer;
socklen_t peerlen = sizeof(peer);
ssize_t r = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &peerlen);
if (r < 0) {
LOG(LogLevel::ERROR) << "recvfrom接收失败:" << strerror(errno);
continue;
}
else if (r == 0) {
LOG(LogLevel::WARNING) << "recvfrom接收空数据。";
}
else {
// 客户端的信息
uint16_t client_port = ntohs(peer.sin_port);
char client_ip_char[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &peer.sin_addr, client_ip_char, sizeof(client_ip_char));
std::string client_ip(client_ip_char);
std::string client_addr = "[" + client_ip + ":" + std::to_string(client_port) + "]";
// 用户发来的数据
inbuffer[r] = 0;
// LOG(LogLevel::DEBUG) << "client [" << client_addr << "] say# " << inbuffer;
// std::string echo_string = "server echo---";
// echo_string += inbuffer;
std::string result = _cb(inbuffer);
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, peerlen);
}
}
}
private:
int _sockfd;
// std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址,给人看) && 4字节IP
uint16_t _port;
callback_t _cb;
};
EchoServerMain.cpp
cpp
#include "EchoServer.hpp"
#include "Dict.hpp"
#include <memory>
static void Usage(const std::string &process) {
std::cerr << "Usage:\n\t";
std::cerr << process << " [local_port]" << std::endl;
}
// ./server_udp [ip] [port]
int main(int argc, char* argv[]) {
if (argc != 2) {
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY()
// 1. 定义字典
Dict dict;
// 2. 构建网络服务,处理IO问题
// std::string socket_ip = argv[1];
uint16_t socket_port = std::stoi(argv[1]);
// 3. 绑定上下两层
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>([&dict](std::string word){
return dict.Translate(word);
},socket_port);
usvr->Init();
usvr->Loop();
return 0;
}
EchoClient.cpp
cpp
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &proc) {
std::cout << "Usage:\n\t";
std::cout << proc << " [server_ip] [server_port]" << std::endl;
}
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage(argv[0]);
exit(1);
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "socket error:" << strerror(errno);
exit(2);
}
// 2. 不需要显示的去bind绑定addr(IP+port)信息,udp client首次发送数据的时候,OS底层会隐式自动获取随机端口并绑定IP+port
// 后面发数据的时候,只需要sockaddr就行
struct sockaddr_in server{};
server.sin_family = AF_INET;
server.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);
// 3. 循环执行客户端
while (true) {
std::string message;
// 1. 获取用户输入
std::cout << "请输入要发送的消息:";
std::getline(std::cin, message);
// 2. sendto发送数据给server
ssize_t r = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
if (r > 0) {
// recvform
char inbuffer[1024] = {0};
struct sockaddr_in tmp{}; // 避免recvfrom()内参数填NULL,可能会出问题,只是存放收到的服务端的套接字
socklen_t tmplen = sizeof(tmp);
ssize_t n = recvfrom(sockfd, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&tmp, &tmplen);
if (n > 0) {
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
}
else {
std::cerr << "sendto seeor:" << strerror(errno);
exit(3);
}
}
return 0;
}
Dict.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
static const std::string default_path = "./dict.txt";
static const std::string sep = ": ";
class Dict
{
public:
Dict(const std::string &dict_path = default_path)
:_dict_path(dict_path)
{
LoadDict();
}
~Dict()
{}
void LoadDict() {
std::ifstream in(_dict_path);
if (!in.is_open()) {
LOG(LogLevel::FATAL) << "open [" << _dict_path << "] error";
exit(1);
}
std::string line;
while (std::getline(in, line)) {
LOG(LogLevel::INFO) << "load [" << line << "] success";
// apple: 苹果
auto sep_pos = line.find(sep);
/*
std::string::npos 精简总结
本质:static const size_type npos = -1;,无符号回绕成极大值,作"无效位置"标记。
含义:find 系列找不到时返回它 → "没找到"。
判断:if (s.find("x") != std::string::npos) 找到;== 没找到。别写成 if (s.find(...))。
接收:用 auto 或 size_t,别用 int。
附带用途:substr(pos) 的第二参数默认就是 npos,表示"取到末尾"。
坑:别把 npos 当下标传给 substr(会抛 out_of_range)。
*/
if (sep_pos == std::string::npos) {
LOG(LogLevel::WARNING) << "Format: [" << line << "] error";
continue;
}
std::string k = line.substr(0, sep_pos);
std::string v = line.substr(sep_pos + sep.size());
_dict.insert({k, v});
}
LOG(LogLevel::INFO) << "load done...";
}
std::string Translate(std::string word) {
auto it = _dict.find(word);
if (it != _dict.end()) {
return it->second;
}
else {
return "None";
}
}
private:
std::string _dict_path;
std::unordered_map<std::string, std::string> _dict;
};
dict.txt
text
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
Logger.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/time.h>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
#include "mutex.hpp"
namespace NS_LOG_MODULE
{
// 日志等级信息
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 为避免打印日志时,流式输出等级是数字
std::string LogLevelToMessage(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 "UNKNOW";
}
}
// 详细的时间信息
std::string GetCurrentTime() {
struct timeval current_time;
gettimeofday(¤t_time, nullptr);
// current_time.tv_sec; current_time.tv_usec;
struct tm struct_time;
localtime_r(&(current_time.tv_sec), &struct_time);
char timestr[128];
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
struct_time.tm_year + 1900,
struct_time.tm_mon + 1,
struct_time.tm_mday,
struct_time.tm_hour,
struct_time.tm_min,
struct_time.tm_sec,
current_time.tv_usec
);
return timestr;
}
// 策略模式,策略接口
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印策略
class ConsoleStrategy : public LogStrategy
{
public:
~ConsoleStrategy()
{}
void SyncLog(const std::string &message) override
{
LockGuard lg(_mutex);
std::cout << message << std::endl;
}
private:
Mutex _mutex;
};
// 文件打印策略
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
class FileStrategy : public LogStrategy
{
public:
FileStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename)
:_logpath(path)
,_logfilename(filename)
{
LockGuard lg(_mutex);
if (std::filesystem::exists(_logpath)) return;
try
{
std::filesystem::create_directories(_logpath);
}
catch (std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << std::endl;
}
}
~FileStrategy()
{}
void SyncLog(const std::string &message) override
{
LockGuard lg(_mutex);
if (!_logpath.empty() && _logpath.back() != '/') _logpath += "/";
std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
std::ofstream out(targetlog, std::ios::app); // 追加的方式写入
if (!out.is_open())
{
std::cerr << "open " << targetlog << " failed!" << std::endl;
return;
}
out << message << '\n';
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
class Logger
{
// 日志类:
// 1. 日志的生成
// 2. 根据不同的策略进行刷新
public:
Logger()
{}
~Logger()
{}
void UseConsoleStrategy() {
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy() {
_strategy = std::make_unique<FileStrategy>();
}
// void Debug(const std::string &message) {
// if (_strategy != nullptr) {
// _strategy->SyncLog(message);
// }
// }
// 内部类
class LogMessage
{
public:
LogMessage(LogLevel level, std::string& filename, int line, Logger& logger)
:_curr_time(GetCurrentTime()),
_level(level),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
// 一条完整的日志信息 = 前部分固定信息 + 后面不固定信息
// 先组成前部分固定
std::stringstream ss;
ss << '[' << _curr_time << "] "
<< '[' << LogLevelToMessage(_level) << "] "
<< '[' << _pid << "] "
<< '[' << _filename << "] "
<< '[' << _line << "] "
<< " - ";
_loginfo = ss.str();
}
~LogMessage()
{
// 在Logger析构时,进行刷新信息(RAII)
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
template<typename T>
LogMessage& operator << (const T& info) {
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this; // 返回当前LogMessage,以便连续<<
}
private:
std::string _curr_time; // 时间
LogLevel _level; // 等级
pid_t _pid; // 进程id
std::string _filename; // 文件名
int _line; // 具体位置
std::string _loginfo; // 一条完整的日志信息
// 因为这是内部类LogMessage,析构时,要知道Logger的刷新策略,所以要引用外面的Logger
Logger& _logger;
};
// 不是内部类了
// 重载的是Logger类的(),只是返回类型是LogMessage
LogMessage operator()(LogLevel level, std::string filename, int line) {
return LogMessage(level, filename, line, *this);
}
// 意义:创建出那个用来实现 RAII 的临时 LogMessage 对象
private:
std::unique_ptr<LogStrategy> _strategy;
};
// 日志对象,全局使用
Logger logger;
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
#define LOG(level) logger(level, __FILE__, __LINE__)
}
mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex() {
pthread_mutex_init(&_lock, nullptr);
}
~Mutex() {
pthread_mutex_destroy(&_lock);
}
pthread_mutex_t* Ptr() {
return &_lock;
}
void Lock() {
pthread_mutex_lock(&_lock);
}
void UnLock() {
pthread_mutex_unlock(&_lock);
}
private:
pthread_mutex_t _lock;
};
// 采用RAII风格,进行锁管理
class LockGuard
{
public:
LockGuard(Mutex& lock):_lockref(lock) {
_lockref.Lock();
}
~LockGuard() {
_lockref.UnLock();
}
private:
Mutex& _lockref;
};
Makefile
makefile
.PHONY:all
all:client_udp server_udp
server_udp:EchoServerMain.cpp
g++ -o $@ $^ -std=c++17
client_udp:EchoClient.cpp
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f client_udp server_udp
8.3 项目三:UDP 多人聊天室
ChatServerMain.cpp
cpp
#include <memory>
#include "UdpServer.hpp" // 获取事件
#include "ThreadPool.hpp" // 执行者
#include "Route.hpp" // 任务
using task_t = std::function<void ()>;
static void Usage(const std::string &process) {
std::cerr << "Usage:\n\t";
std::cerr << process << " [local_port]" << std::endl;
}
using namespace NS_THREAD_POOL_MODULE;
// ./server_udp [ip] [port]
int main(int argc, char* argv[]) {
if (argc != 2) {
Usage(argv[0]);
exit(USAGE_ERR);
}
ENABLE_CONSOLE_LOG_STRATEGY()
uint16_t socket_port = std::stoi(argv[1]);
// 线程池模块
auto thread_pool = ThreadPool<task_t>::Instance();
// 路由模块
Route rt;
// 网络服务模块
UdpServer usvr(socket_port);
usvr.Init();
usvr.RegisterService(
[&rt](const InetAddr& addr){
rt.CheckUser(addr);
},
[&rt, thread_pool](int sockfd, std::string msg){
auto t = std::bind(&Route::Broadcast, &rt, sockfd, msg);
thread_pool->Enqueue(t);
// 等同于
// thread_pool->Enqueue([&rt, &sockfd, &msg](){
// rt.Broadcast(sockfd, msg);
// });
});
usvr.Loop();
return 0;
}
UdpServer.hpp
cpp
#pragma once
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include "Logger.hpp"
#include "InetAddr.hpp"
using namespace NS_LOG_MODULE;
const static int default_fd = -1;
const static uint16_t default_port = 8888;
// using callback_t = std::function<std::string(std::string)>;
using handler_addr_t = std::function<void (const InetAddr&)>;
using handler_msg_t = std::function<void (int, std::string)>;
enum {
SUCCESS = 0,
SOCKET_ERR,
USAGE_ERR,
BIND_ERR
};
class UdpServer
{
public:
// UdpServer(const std::string &ip, uint16_t port = default_port)
UdpServer(uint16_t port = default_port)
:_sockfd(default_fd),
_port(port)
{}
~UdpServer()
{
close(_sockfd);
}
void Init() {
// 第一步:创建socket,本质:打开网络这个文件
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0) {
LOG(LogLevel::FATAL) << "create socket error";
exit(SOCKET_ERR);
}
LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;
// 第二步:填充网络属性信息
InetAddr local(_port);
// 只是填充在了local中,并没有写入内核
// 第三步:绑定套接字socket信息(写入内核)
ssize_t r = bind(_sockfd, (struct sockaddr*)local.GetNetAddress(), local.Len());
if (r < 0) {
LOG(LogLevel::FATAL) << "绑定套接字失败:" << strerror(errno);
exit(BIND_ERR);
}
LOG(LogLevel::INFO) << "绑定套接字成功。" << " port:" << _port;
}
void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg) {
_handler_addr = handler_addr;
_handler_msg = handler_msg;
}
void Loop() {
char inbuffer[1024] = {0};
while (true) {
struct sockaddr_in peer;
socklen_t peerlen = sizeof(peer);
ssize_t r = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&peer, &peerlen);
if (r < 0) {
LOG(LogLevel::ERROR) << "recvfrom接收失败:" << strerror(errno);
continue;
}
else if (r == 0) {
LOG(LogLevel::WARNING) << "recvfrom接收空数据。";
}
else {
inbuffer[r] = 0;
// 1. 检测新用户
InetAddr client_address(peer);
std::string message = client_address.ToString() + inbuffer;
LOG(LogLevel::DEBUG) << message;
_handler_addr(client_address);
// 2. 转发消息
_handler_msg(_sockfd, message);
}
}
}
private:
int _sockfd;
// std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址,给人看) && 4字节IP
uint16_t _port;
// callback_t _cb;
handler_addr_t _handler_addr;
handler_msg_t _handler_msg;
};
ChatClient.cpp
cpp
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "thread.hpp"
#include "InetAddr.hpp"
using namespace NS_THREAD_MODULE;
int sockfd = 0;
std::string server_ip;
uint16_t server_port = 0;
std::string nickname;
void Usage(const std::string &proc) {
std::cout << "Usage:\n\t";
std::cout << proc << " [server_ip] [server_port]" << std::endl;
}
void Online(InetAddr& server_addr) {
std::cout << "请设置你的昵称:";
std::getline(std::cin, nickname);
std::string online_message = nickname + " 上线了!";
ssize_t r = sendto(sockfd, online_message.c_str(), online_message.size(), 0, (struct sockaddr*)server_addr.GetNetAddress(), server_addr.Len());
(void)r;
}
void RecvMessage() {
while (true) {
// recvform
char inbuffer[1024] = {0};
struct sockaddr_in tmp{}; // 避免recvfrom()内参数填NULL,可能会出问题,只是存放收到的服务端的套接字
socklen_t tmplen = sizeof(tmp);
ssize_t n = recvfrom(sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&tmp, &tmplen);
if (n > 0) {
inbuffer[n] = 0;
std::cout << inbuffer << std::endl;
}
}
}
void SendMessage() {
InetAddr server_addr(server_port, server_ip);
Online(server_addr);
while (true) {
std::string message;
// 1. 获取用户输入
std::cout << "请输入要发送的消息:";
std::getline(std::cin, message);
message = nickname + "$ " + message;
// 2. sendto发送数据给server
ssize_t r = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)server_addr.GetNetAddress(), server_addr.Len());
(void)r;
}
}
int main(int argc, char* argv[]) {
if (argc != 3) {
Usage(argv[0]);
exit(1);
}
server_ip = argv[1];
server_port = std::stoi(argv[2]);
// 1. 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
std::cerr << "socket error:" << strerror(errno);
exit(2);
}
// 2. 线程模块
Thread recver(RecvMessage);
Thread sender(SendMessage);
recver.Start();
sender.Start();
recver.Join();
sender.Join();
return 0;
}
Dict.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
static const std::string default_path = "./dict.txt";
static const std::string sep = ": ";
class Dict
{
public:
Dict(const std::string &dict_path = default_path)
:_dict_path(dict_path)
{
LoadDict();
}
~Dict()
{}
void LoadDict() {
std::ifstream in(_dict_path);
if (!in.is_open()) {
LOG(LogLevel::FATAL) << "open [" << _dict_path << "] error";
exit(1);
}
std::string line;
while (std::getline(in, line)) {
LOG(LogLevel::INFO) << "load [" << line << "] success";
// apple: 苹果
auto sep_pos = line.find(sep);
/*
std::string::npos 精简总结
本质:static const size_type npos = -1;,无符号回绕成极大值,作"无效位置"标记。
含义:find 系列找不到时返回它 → "没找到"。
判断:if (s.find("x") != std::string::npos) 找到;== 没找到。别写成 if (s.find(...))。
接收:用 auto 或 size_t,别用 int。
附带用途:substr(pos) 的第二参数默认就是 npos,表示"取到末尾"。
坑:别把 npos 当下标传给 substr(会抛 out_of_range)。
*/
if (sep_pos == std::string::npos) {
LOG(LogLevel::WARNING) << "Format: [" << line << "] error";
continue;
}
std::string k = line.substr(0, sep_pos);
std::string v = line.substr(sep_pos + sep.size());
_dict.insert({k, v});
}
LOG(LogLevel::INFO) << "load done...";
}
std::string Translate(std::string word) {
auto it = _dict.find(word);
if (it != _dict.end()) {
return it->second;
}
else {
return "None";
}
}
private:
std::string _dict_path;
std::unordered_map<std::string, std::string> _dict;
};
InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 对客户端进行先描述
class InetAddr
{
public:
InetAddr(const struct sockaddr_in &address)
:_address(address),
_len(sizeof(address))
{
_port = ntohs(_address.sin_port);
char _ip_char[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &_address.sin_addr, _ip_char, sizeof(_ip_char));
_ip.assign(_ip_char);
}
InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
:_ip(ip),
_port(port)
{
memset(&_address, 0, sizeof(_address));
_address.sin_family = AF_INET;
_address.sin_port = htons(_port);
inet_pton(AF_INET, _ip.c_str(), &_address.sin_addr);
_len = sizeof(_address);
}
InetAddr()
{}
~InetAddr()
{}
bool operator==(const InetAddr &addr) {
return (this->_ip == addr._ip) && (this->_port == addr._port);
}
struct sockaddr_in* GetNetAddress() {
return &_address;
}
socklen_t Len() {
return _len;
}
std::string ToString() {
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
private:
// net地址
struct sockaddr_in _address;
socklen_t _len;
// host地址
std::string _ip;
uint16_t _port;
};
Route.hpp
cpp
#pragma once
#include <string>
#include <memory>
#include "mutex.hpp"
#include "UserManager.hpp"
class Route
{
public:
Route()
:_uma(std::make_unique<UserManager>())
{}
~Route()
{}
void Broadcast(int sockfd, std::string message) {
LockGuard lg(_lock);
auto& users = _uma->Users();
for (auto& user : users) {
sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)user.GetNetAddress(), user.Len());
}
}
void CheckUser(const InetAddr& addr) {
LockGuard lg(_lock);
_uma->AddUser(addr);
}
void OffLineUser(const InetAddr& addr) {
LockGuard lg(_lock);
_uma->DelUser(addr);
}
private:
std::unique_ptr<UserManager> _uma;
Mutex _lock;
};
UserManager.hpp
cpp
#pragma once
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace NS_LOG_MODULE;
class UserManager
{
public:
UserManager()
{}
~UserManager()
{}
void AddUser(const InetAddr &addr) {
if (SearchUser(addr)) return;
_users.emplace_back(addr);
}
void DelUser(const InetAddr &addr) {
if (!SearchUser(addr)) {
LOG(LogLevel::WARNING) << "This addr Unexist.";
}
for (auto it = _users.begin(); it != _users.end(); ++it) {
if (*it == addr) {
_users.erase(it);
break;
}
}
}
bool SearchUser(const InetAddr &addr) {
for (auto& user : _users) {
if (user == addr) {
return true;
}
}
return false;
}
// bool ModUser(const InetAddr &addr) {
// }
std::vector<InetAddr>& Users() {
return _users;
}
private:
std::vector<InetAddr> _users;
};
ThreadPool.hpp
cpp
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include "mutex.hpp"
#include "Logger.hpp"
#include "thread.hpp"
#include "Cond.hpp"
namespace NS_THREAD_POOL_MODULE
{
using namespace NS_THREAD_MODULE;
using namespace NS_LOG_MODULE;
const int defaultnum = 5;
// 要对线程池进行管理------先描述再组织!
template<typename T>
class ThreadPool
{
private:
void HandlerTask() {
char name[128];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true) {
T task;
{
// 保护临界区
LockGuard lg(_mutex);
// 检测有无任务
while (_isrunning && _tasks.empty()) {
// 没有任务,休眠
++_slaver_sleep_count;
_cond.Wait(_mutex);
--_slaver_sleep_count;
}
// 若果是线程池要结束运行,正好没任务
if (!_isrunning && _tasks.empty()) break;
// 有任务,取任务(本质:将任务由公共的变为私有)
task = _tasks.front();
_tasks.pop();
}
// 处理任务不需要在临界区内部处理
LOG(LogLevel::INFO) << name << "处理任务:";
task();
}
// 线程退出
LOG(LogLevel::INFO) << name << "quit...";
}
ThreadPool(int slaver_num = defaultnum)
:_isrunning(false),
_slaver_num(slaver_num),
_slaver_sleep_count(0)
{
for (int i = 0; i < _slaver_num; ++i) {
// auto f = std::bind(&ThreadPool::HandlerTask, this);
// _slavers.emplace_back(f);
_slavers.emplace_back([this](){
this->HandlerTask();
});
}
}
ThreadPool<T>(const ThreadPool&) = delete;
ThreadPool<T>& operator=(const ThreadPool&) = delete;
public:
static ThreadPool<T>* Instance() {
{
if (_instance == nullptr) { // 双if判断
LockGuard lg(_lock);
if (_instance == nullptr) {
// 第一次调用
_instance = new ThreadPool<T>();
_instance->Start();
LOG(LogLevel::INFO) << "第一次调用线程池,创建线程池对象.";
}
}
}
return _instance;
}
~ThreadPool()
{}
void Start() {
if (_isrunning) {
LOG(LogLevel::WARNING) << "Thread Pool Is Already Running!";
return;
}
_isrunning = true;
for (auto& slave : _slavers) {
slave.Start();
}
}
void Stop() {
// -----------------------------------
// if (!_isrunning) {
// LOG(LogLevel::WARNING) << "Thread Is Not Running!";
// return;
// }
// for (auto& slave : _slavers) {
// slave.Stop();
// }
// _isrunning = false;
// -----------------------------------
// 1. _isrunning = faulse
// 2. _tasks内所有任务都要处理完
_mutex.Lock();
_isrunning = false;
if (_slaver_sleep_count > 0) {
_cond.Broadcast();
}
_mutex.UnLock();
}
void Wait() {
for (auto& slave : _slavers) {
slave.Join();
}
}
void Enqueue(T in) {
_mutex.Lock();
_tasks.push(in);
if (_slaver_sleep_count > 0) {
_cond.Signal();
}
_mutex.UnLock();
}
private:
bool _isrunning;
int _slaver_num;
std::vector<Thread> _slavers;
std::queue<T> _tasks; // 任务队列,临界资源
Mutex _mutex;
Cond _cond;
int _slaver_sleep_count;
// 添加单例模式
static ThreadPool<T>* _instance;
static Mutex _lock; // 保证单例安全
};
template<typename T>
ThreadPool<T>* ThreadPool<T>::_instance = nullptr;
template<typename T>
Mutex ThreadPool<T>::_lock;
}
thread.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <string.h>
#include <functional>
#include "Logger.hpp"
namespace NS_THREAD_MODULE
{
using namespace NS_LOG_MODULE;
static int g_number = 1;
using callback_t = std::function<void ()>;
enum class TSTATUS {
THREAD_NEW,
THREAD_RUNNING,
THREAD_STOP
};
std::string StatusToString(TSTATUS s) {
switch (s) {
case TSTATUS::THREAD_NEW:
return "THREAD_NEW";
case TSTATUS::THREAD_RUNNING:
return "THREAD_RUNNING";
case TSTATUS::THREAD_STOP:
return "THREAD_STOP";
default:
return "UNKNOWN";
}
}
std::string IsJoin(bool _joinable) {
return _joinable ? "true" : "false";
}
class Thread
{
public:
void ToRunning() {
_status = TSTATUS::THREAD_RUNNING;
}
void ToStop() {
_status = TSTATUS::THREAD_STOP;
}
static void* ThreadRoutine(void* args) {
Thread* self = static_cast<Thread*>(args);
pthread_setname_np(self->_tid, self->_name.c_str());
self->_cb();
self->ToStop();
return nullptr;
}
public:
Thread(callback_t cb)
:_tid(-1),
_status(TSTATUS::THREAD_NEW),
_joinable(true), _cb(cb), _result(nullptr)
{
_name = "New-Thread-" + std::to_string(g_number++);
}
~Thread() {}
bool Start() {
int ret = pthread_create(&_tid, nullptr, ThreadRoutine, this);
if (ret != 0) {
return false;
}
ToRunning();
return true;
}
void Join() {
if (_joinable) {
int ret = pthread_join(_tid, &_result);
if (ret != 0) {
std::cerr << "pthread_join失败:" << strerror(ret) << std::endl;
return;
}
_status = TSTATUS::THREAD_STOP;
LOG(LogLevel::DEBUG) << _name << "join success!";
}
else {
std::cerr << "不是可汇线程! join_status:" << IsJoin(_joinable) << std::endl;
}
}
void Stop() {
if (_status == TSTATUS::THREAD_RUNNING) {
pthread_cancel(_tid);
_status = TSTATUS::THREAD_STOP;
}
}
void Detach() {
if (_status == TSTATUS::THREAD_RUNNING && _joinable) {
pthread_detach(_tid);
_joinable = false;
}
else {
std::cerr << "detach" << _name << "失败" << std::endl;
}
}
void PrintInfo() {
std::cout << "thread name: " << _name << std::endl;
std::cout << "thread tid: " << _tid << std::endl;
std::cout << "thread status: " << StatusToString(_status) << std::endl;
std::cout << "thread joinable: " << IsJoin(_joinable) << std::endl;
}
private:
std::string _name;
pthread_t _tid;
TSTATUS _status;
bool _joinable;
callback_t _cb;
void* _result;
};
}
Cond.hpp
cpp
#pragma once
#include "mutex.hpp"
#include <pthread.h>
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond,nullptr);
}
~Cond()
{
pthread_cond_destroy(&_cond);
}
void Wait(Mutex &mutex) {
pthread_cond_wait(&_cond,mutex.Ptr());
}
void Signal() {
pthread_cond_signal(&_cond);
}
void Broadcast() {
pthread_cond_broadcast(&_cond);
}
private:
pthread_cond_t _cond;
};
mutex.hpp
cpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
Mutex() {
pthread_mutex_init(&_lock, nullptr);
}
~Mutex() {
pthread_mutex_destroy(&_lock);
}
pthread_mutex_t* Ptr() {
return &_lock;
}
void Lock() {
pthread_mutex_lock(&_lock);
}
void UnLock() {
pthread_mutex_unlock(&_lock);
}
private:
pthread_mutex_t _lock;
};
// 采用RAII风格,进行锁管理
class LockGuard
{
public:
LockGuard(Mutex& lock):_lockref(lock) {
_lockref.Lock();
}
~LockGuard() {
_lockref.UnLock();
}
private:
Mutex& _lockref;
};
Logger.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/time.h>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
#include "mutex.hpp"
namespace NS_LOG_MODULE
{
// 日志等级信息
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
// 为避免打印日志时,流式输出等级是数字
std::string LogLevelToMessage(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 "UNKNOW";
}
}
// 详细的时间信息
std::string GetCurrentTime() {
struct timeval current_time;
gettimeofday(¤t_time, nullptr);
// current_time.tv_sec; current_time.tv_usec;
struct tm struct_time;
localtime_r(&(current_time.tv_sec), &struct_time);
char timestr[128];
snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
struct_time.tm_year + 1900,
struct_time.tm_mon + 1,
struct_time.tm_mday,
struct_time.tm_hour,
struct_time.tm_min,
struct_time.tm_sec,
current_time.tv_usec
);
return timestr;
}
// 策略模式,策略接口
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 显示器打印策略
class ConsoleStrategy : public LogStrategy
{
public:
~ConsoleStrategy()
{}
void SyncLog(const std::string &message) override
{
LockGuard lg(_mutex);
std::cout << message << std::endl;
}
private:
Mutex _mutex;
};
// 文件打印策略
const std::string defaultpath = "./log";
const std::string defaultfilename = "log.txt";
class FileStrategy : public LogStrategy
{
public:
FileStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename)
:_logpath(path)
,_logfilename(filename)
{
LockGuard lg(_mutex);
if (std::filesystem::exists(_logpath)) return;
try
{
std::filesystem::create_directories(_logpath);
}
catch (std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << std::endl;
}
}
~FileStrategy()
{}
void SyncLog(const std::string &message) override
{
LockGuard lg(_mutex);
if (!_logpath.empty() && _logpath.back() != '/') _logpath += "/";
std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
std::ofstream out(targetlog, std::ios::app); // 追加的方式写入
if (!out.is_open())
{
std::cerr << "open " << targetlog << " failed!" << std::endl;
return;
}
out << message << '\n';
}
private:
std::string _logpath;
std::string _logfilename;
Mutex _mutex;
};
class Logger
{
// 日志类:
// 1. 日志的生成
// 2. 根据不同的策略进行刷新
public:
Logger()
{}
~Logger()
{}
void UseConsoleStrategy() {
_strategy = std::make_unique<ConsoleStrategy>();
}
void UseFileStrategy() {
_strategy = std::make_unique<FileStrategy>();
}
// void Debug(const std::string &message) {
// if (_strategy != nullptr) {
// _strategy->SyncLog(message);
// }
// }
// 内部类
class LogMessage
{
public:
LogMessage(LogLevel level, std::string& filename, int line, Logger& logger)
:_curr_time(GetCurrentTime()),
_level(level),
_pid(getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
// 一条完整的日志信息 = 前部分固定信息 + 后面不固定信息
// 先组成前部分固定
std::stringstream ss;
ss << '[' << _curr_time << "] "
<< '[' << LogLevelToMessage(_level) << "] "
<< '[' << _pid << "] "
<< '[' << _filename << "] "
<< '[' << _line << "] "
<< " - ";
_loginfo = ss.str();
}
~LogMessage()
{
// 在Logger析构时,进行刷新信息(RAII)
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
template<typename T>
LogMessage& operator << (const T& info) {
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this; // 返回当前LogMessage,以便连续<<
}
private:
std::string _curr_time; // 时间
LogLevel _level; // 等级
pid_t _pid; // 进程id
std::string _filename; // 文件名
int _line; // 具体位置
std::string _loginfo; // 一条完整的日志信息
// 因为这是内部类LogMessage,析构时,要知道Logger的刷新策略,所以要引用外面的Logger
Logger& _logger;
};
// 不是内部类了
// 重载的是Logger类的(),只是返回类型是LogMessage
LogMessage operator()(LogLevel level, std::string filename, int line) {
return LogMessage(level, filename, line, *this);
}
// 意义:创建出那个用来实现 RAII 的临时 LogMessage 对象
private:
std::unique_ptr<LogStrategy> _strategy;
};
// 日志对象,全局使用
Logger logger;
#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();
#define LOG(level) logger(level, __FILE__, __LINE__)
}
Makefile
makefile
.PHONY:all
all:client_chat server_chat
server_chat:ChatServerMain.cpp
g++ -o $@ $^ -std=c++17
client_chat:ChatClient.cpp
g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
rm -f client_chat server_chat