
🎬 个人主页 :艾莉丝努力练剑
❄专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录》
《Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享》
⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平
🎬 艾莉丝的简介:

文章目录
- [1 ~> UDP编程:三个代码案例搞定](#1 ~> UDP编程:三个代码案例搞定)
- [2 ~> 先搞透基础:UDP Socket核心原理全景](#2 ~> 先搞透基础:UDP Socket核心原理全景)
-
- [2.1 Socket的本质:操作系统提供的网络抽象](#2.1 Socket的本质:操作系统提供的网络抽象)
- [2.2 核心数据结构:sockaddr\_in与地址转换](#2.2 核心数据结构:sockaddr_in与地址转换)
-
- [2.2.1 地址转换函数](#2.2.1 地址转换函数)
- [2.3 字节序:为什么必须处理大小端?](#2.3 字节序:为什么必须处理大小端?)
- [2.4 bind函数深度解析](#2.4 bind函数深度解析)
- [2.5 客户端为什么不需要显式bind?](#2.5 客户端为什么不需要显式bind?)
- [3 ~> V1版本:Echo回显服务器](#3 ~> V1版本:Echo回显服务器)
-
- [3.1 完整代码实现](#3.1 完整代码实现)
-
- [3.1.1 Mutex\.hpp(互斥锁封装)](#3.1.1 Mutex.hpp(互斥锁封装))
- [3.1.2 Logger\.hpp(日志模块)](#3.1.2 Logger.hpp(日志模块))
- [3.1.3 UdpEchoServer\.hpp(核心逻辑)](#3.1.3 UdpEchoServer.hpp(核心逻辑))
- [3.1.4 main\.cpp(程序入口)](#3.1.4 main.cpp(程序入口))
- [3.2 编译与运行](#3.2 编译与运行)
-
- [3.2.1 Makefile](#3.2.1 Makefile)
- [3.2.2 运行命令](#3.2.2 运行命令)
- [3.2.3 客户端测试](#3.2.3 客户端测试)
- [3.3 V1版本的局限性](#3.3 V1版本的局限性)
- [4 ~> V2版本:增强版英译汉字典服务器(生产级解耦)](#4 ~> V2版本:增强版英译汉字典服务器(生产级解耦))
-
- [4.1 核心改进:生产级工程化能力](#4.1 核心改进:生产级工程化能力)
- [4.2 完整代码实现](#4.2 完整代码实现)
-
- [4.2.1 Mutex\.hpp(复用V1版本)](#4.2.1 Mutex.hpp(复用V1版本))
- [4.2.2 Logger\.hpp(复用V1版本)](#4.2.2 Logger.hpp(复用V1版本))
- [4.2.3 字典业务模块:Dictionary\.hpp](#4.2.3 字典业务模块:Dictionary.hpp)
- [4.2.4 字典数据文件:Dict\.txt](#4.2.4 字典数据文件:Dict.txt)
- [4.2.5 通用UDP服务器:UdpServer\.hpp](#4.2.5 通用UDP服务器:UdpServer.hpp)
- [4.2.6 服务器主函数:Main\.cc](#4.2.6 服务器主函数:Main.cc)
- [4.3 客户端代码(通用版)](#4.3 客户端代码(通用版))
- [4.4 编译与运行](#4.4 编译与运行)
-
- [4.4.1 Makefile](#4.4.1 Makefile)
- [4.4.2 运行步骤](#4.4.2 运行步骤)
- [4.5 V2版本优势总结](#4.5 V2版本优势总结)
- [5 ~> V3版本:多人聊天室(生产级并发)](#5 ~> V3版本:多人聊天室(生产级并发))
-
- [5.1 整体架构:生产者 \- 消费者模型](#5.1 整体架构:生产者 - 消费者模型)
- [5.2 核心模块补充](#5.2 核心模块补充)
-
- [5.2.1 Cond\.hpp(条件变量封装)](#5.2.1 Cond.hpp(条件变量封装))
- [5.2.2 Thread\.hpp(线程封装)](#5.2.2 Thread.hpp(线程封装))
- [5.2.3 InetAddr\.hpp(地址封装)](#5.2.3 InetAddr.hpp(地址封装))
- [5.2.4 Route\.hpp(消息路由)](#5.2.4 Route.hpp(消息路由))
- [5.2.5 UdpServer\.hpp(聊天室专用版)](#5.2.5 UdpServer.hpp(聊天室专用版))
- [5.2.6 ThreadPool\.hpp(懒汉单例线程池)](#5.2.6 ThreadPool.hpp(懒汉单例线程池))
- [5.3 主函数整合](#5.3 主函数整合)
- [5.4 编译与运行](#5.4 编译与运行)
-
- [5.4.1 Makefile](#5.4.1 Makefile)
- [5.4.2 运行步骤](#5.4.2 运行步骤)
- [5.5 客户端实现:多线程收发分离](#5.5 客户端实现:多线程收发分离)
-
- [5.5.1 Linux客户端](#5.5.1 Linux客户端)
- [6 ~> 工程化实践与踩坑总结](#6 ~> 工程化实践与踩坑总结)
-
- [6.1 日志系统使用指南](#6.1 日志系统使用指南)
- [6.2 UDP编程踩坑总结](#6.2 UDP编程踩坑总结)
-
- [6.2.1 基础API踩坑](#6.2.1 基础API踩坑)
- [6.2.2 多线程踩坑](#6.2.2 多线程踩坑)
- [6.2.3 业务逻辑踩坑](#6.2.3 业务逻辑踩坑)
- [6.3 客户端访问服务端,云服务器需要开放一些端口](#6.3 客户端访问服务端,云服务器需要开放一些端口)
- [7 ~> 总结与后续优化方向](#7 ~> 总结与后续优化方向)
-
- [7.1 本文总结](#7.1 本文总结)
- [7.2 后续优化方向](#7.2 后续优化方向)
- 附录:完整项目文件结构
- 结尾

1 ~> UDP编程:三个代码案例搞定
很多人学UDP编程只停留在"能发能收"的Echo服务器阶段,但真实的网络服务需要解决多用户并发、业务解耦、性能瓶颈、跨平台兼容四大核心问题。本文将带你走完UDP编程的完整学习路线:
| 版本 | 功能 | 解决的核心问题 | 核心技术点 |
|---|---|---|---|
| V1 | Echo回显服务器 | 基础Socket收发、地址转换、端口绑定 | socket()/bind()/recvfrom()/sendto() |
| V2 | 增强版英译汉字典服务器 | 网络层与业务层解耦、生产级日志、配置文件加载 | 回调函数、策略模式日志、文件IO |
| V3 | 多人聊天室 | 多用户管理、并发优化、跨平台通信 | 生产者-消费者模型、线程池、多线程收发分离 |
本文所有代码均基于C++17标准,兼容Linux与Windows平台,所有代码均来自真实生产实践,可直接编译运行,如果有问题,简单调试,通过修改参数、调整文件名、检查构造函数初始化顺序等方式可以矫正。
2 ~> 先搞透基础:UDP Socket核心原理全景
2.1 Socket的本质:操作系统提供的网络抽象
-
定义:Socket是应用层与TCP/IP协议栈之间的编程接口,本质是一个特殊的文件描述符
-
类比:Socket = 网络电话,IP = 手机号,端口 = 分机号
-
核心特性 :UDP是无连接、不可靠、面向数据报的协议,支持全双工通信
2.2 核心数据结构:sockaddr_in与地址转换
cpp
// IPv4地址结构体
struct sockaddr_in {
short int sin_family; // 地址族,固定为AF_INET
unsigned short int sin_port; // 16位端口号(网络字节序)
struct in_addr sin_addr; // 32位IP地址(网络字节序)
unsigned char sin_zero[8]; // 填充字节
};
2.2.1 地址转换函数
| 函数 | 功能 | 线程安全 | 推荐场景 |
|---|---|---|---|
inet\_addr() |
点分十进制 → 网络序IP | 是 | 简单场景 |
inet\_ntoa() |
网络序IP → 点分十进制 | 否 | 单线程环境 |
inet\_pton() |
点分十进制 → 网络序IP(支持IPv6) | 是 | 多线程/生产环境 |
inet\_ntop() |
网络序IP → 点分十进制(支持IPv6) | 是 | 多线程/生产环境 |
坑人点 :
inet_ntoa()使用静态缓冲区存储结果,多线程调用会互相覆盖,生产环境必须使用inet_ntop()。
2.3 字节序:为什么必须处理大小端?
-
大端序 :高位字节存低地址,网络字节序统一使用大端
-
小端序:低位字节存低地址,x86/x86_64架构CPU默认使用小端
-
核心转换函数:
-
htons():主机序 → 网络序(短整型,用于端口) -
ntohs():网络序 → 主机序(短整型) -
htonl()/ntohl():长整型转换(用于IP地址)
-
2.4 bind函数深度解析
-
作用:将Socket与指定的IP和端口绑定,告诉操作系统"这个Socket负责接收发往该地址的数据包"
-
为什么服务器必须显式bind?:服务器端口必须是固定且众所周知的,客户端才能连接
-
为什么推荐绑定
0.0.0.0?:-
0.0.0.0表示监听本机所有网卡的所有IP地址 -
云服务器网卡上没有配置公网IP(公网IP是运营商NAT映射的),直接绑定公网IP会失败
-
-
常见bind失败原因:端口被占用、权限不足(1024以下端口需要root)、绑定不存在的IP
2.5 客户端为什么不需要显式bind?
-
客户端调用
sendto()时,操作系统会自动为其分配一个随机的未被占用的端口 -
客户端数量众多,不需要固定端口,自动分配可以避免端口冲突
3 ~> V1版本:Echo回显服务器
3.1 完整代码实现
3.1.1 Mutex.hpp(互斥锁封装)
cpp
#ifndef __MUTEX_HPP
#define __MUTEX_HPP
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
pthread_mutex_t *Orgin()
{
return &_lock;
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
// 自动加解锁的Guard(RAII风格)
class LockGuard
{
public:
LockGuard(Mutex *lockp): _lockp(lockp)
{
_lockp->Lock();
}
~LockGuard()
{
_lockp->Unlock();
}
private:
Mutex *_lockp;
};
#endif
3.1.2 Logger.hpp(日志模块)
cpp
#ifndef __LOGGER_HPP
#define __LOGGER_HPP
#include <iostream>
#include <cstdio>
#include <string>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <memory>
#include <unistd.h>
#include <cstdlib>
#include "Mutex.hpp"
namespace LogModule
{
std::string GetTimeStamp()
{
time_t timestamp = time(nullptr);
struct tm data_time;
localtime_r(×tamp, &data_time);
char data_time_str[128];
snprintf(data_time_str, sizeof(data_time_str), "%4d-%02d-%02d %02d:%02d:%02d",
data_time.tm_year + 1900,
data_time.tm_mon + 1,
data_time.tm_mday,
data_time.tm_hour,
data_time.tm_min,
data_time.tm_sec);
return data_time_str;
}
enum class LogLevel
{
DEBUG,
INFO,
WARNING,
ERROR,
FATAL
};
std::string LogLevel2String(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";
}
}
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &logmessage) = 0;
};
class ConsoleLogStrategy : public LogStrategy
{
public:
void SyncLog(const std::string &logmessage) override
{
LockGuard lockguard(&_mutex);
std::cout << logmessage << std::endl;
}
private:
Mutex _mutex;
};
static const std::string glogdir = "./log/";
static const std::string glogfilename = "log.txt";
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &dir = glogdir, const std::string &filename = glogfilename)
: _logdir(dir), _logfilename(filename)
{
LockGuard lockguard(&_mutex);
if (!std::filesystem::exists(_logdir))
{
try
{
std::filesystem::create_directories(_logdir);
}
catch (const std::filesystem::filesystem_error &e)
{
std::cerr << "创建日志目录失败: " << e.what() << '\n';
}
}
}
void SyncLog(const std::string &logmessage) override
{
LockGuard lockguard(&_mutex);
std::string target = _logdir + _logfilename;
std::ofstream out(target, std::ios::app);
if (!out.is_open())
{
std::cerr << "打开日志文件失败: " << target << std::endl;
return;
}
out << logmessage << "\n";
out.close();
}
private:
std::string _logdir;
std::string _logfilename;
Mutex _mutex;
};
class Logger
{
public:
Logger()
{
UseConsoleLogStrategy();
}
void UseConsoleLogStrategy()
{
_strategy = std::make_unique<ConsoleLogStrategy>();
}
void UseFileLogStrategy()
{
_strategy = std::make_unique<FileLogStrategy>();
}
class LogMessage
{
public:
LogMessage(LogLevel level, std::string filename, int line, Logger &self)
: _level(level),
_curr_time(GetTimeStamp()),
_pid(getpid()),
_filename(std::move(filename)),
_line(line),
_logger(self)
{
std::stringstream ss;
ss << "[" << _curr_time << "] "
<< "[" << LogLevel2String(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << ":" << _line << "] "
<< "- ";
_loginfo = ss.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
if (_level == LogLevel::FATAL)
{
exit(EXIT_FAILURE);
}
}
private:
LogLevel _level;
std::string _curr_time;
pid_t _pid;
std::string _filename;
int _line;
std::string _loginfo;
Logger &_logger;
};
LogMessage operator()(LogLevel level, std::string filename, int line)
{
return LogMessage(level, std::move(filename), line, *this);
}
private:
std::unique_ptr<LogStrategy> _strategy;
};
Logger logger;
#define LOG(level) logger(level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG() logger.UseConsoleLogStrategy()
#define ENABLE_FILE_LOG() logger.UseFileLogStrategy()
}
#endif
3.1.3 UdpEchoServer.hpp(核心逻辑)
cpp
#ifndef __UDP_ECHOSERVER_HPP
#define __UDP_ECHOSERVER_HPP
#include <iostream>
#include <string>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include "Logger.hpp"
using namespace LogModule;
class UdpEchoServer
{
public:
UdpEchoServer(uint16_t port)
: _sockfd(-1), _port(port)
{
}
void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "创建Socket失败!";
exit(EXIT_FAILURE);
}
LOG(LogLevel::INFO) << "创建Socket成功,fd: " << _sockfd;
struct sockaddr_in local;
std::memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
{
LOG(LogLevel::FATAL) << "绑定端口[" << _port << "]失败!";
exit(EXIT_FAILURE);
}
LOG(LogLevel::INFO) << "绑定端口[" << _port << "]成功!";
}
void Start()
{
LOG(LogLevel::INFO) << "UDP Echo Server 启动成功,监听端口: " << _port;
char buffer[1024] = {0};
struct sockaddr_in peer;
socklen_t peer_len = sizeof(peer);
while (true)
{
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0,
(struct sockaddr *)&peer, &peer_len);
if (n < 0)
{
LOG(LogLevel::WARNING) << "接收数据失败!";
continue;
}
buffer[n] = '\0';
std::string client_ip = inet_ntoa(peer.sin_addr);
uint16_t client_port = ntohs(peer.sin_port);
LOG(LogLevel::INFO) << "收到客户端[" << client_ip << ":" << client_port
<< "]消息: " << buffer;
std::string echo_msg = "[Echo] " + std::string(buffer);
sendto(_sockfd, echo_msg.c_str(), echo_msg.size(), 0,
(struct sockaddr *)&peer, peer_len);
}
}
~UdpEchoServer()
{
if (_sockfd >= 0)
{
close(_sockfd);
LOG(LogLevel::INFO) << "关闭Socket,fd: " << _sockfd;
}
}
private:
int _sockfd;
uint16_t _port;
};
#endif
3.1.4 main.cpp(程序入口)
cpp
#include "UdpEchoServer.hpp"
#include <iostream>
void Usage(const char *progname)
{
std::cout << "Usage: " << progname << " <port>" << std::endl;
std::cout << "Example: " << progname << " 8080" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return EXIT_FAILURE;
}
uint16_t port = atoi(argv[1]);
if (port <= 1024 || port > 65535)
{
std::cerr << "端口必须在 1025~65535 之间!" << std::endl;
return EXIT_FAILURE;
}
// ENABLE_FILE_LOG(); // 可选:切换到文件日志
UdpEchoServer server(port);
server.Init();
server.Start();
return EXIT_SUCCESS;
}
3.2 编译与运行
3.2.1 Makefile
Makefile
CC = g++
STD = -std=c++17
CFLAGS = -Wall -g -lpthread
all: udp_echo_server
udp_echo_server: main.cpp
$(CC) -o $@ $^ $(STD) $(CFLAGS)
.PHONY: clean
clean:
rm -f udp_echo_server
rm -rf log/
3.2.2 运行命令
Bash
make
./udp_echo_server 8080
3.2.3 客户端测试
Bash
nc -u 127.0.0.1 8080
# 输入任意字符,会收到服务器回显
3.3 V1版本的局限性
-
网络层与业务逻辑强耦合,无法复用
-
只能处理单客户端,不支持多用户
-
基础日志,无生产级错误处理
-
单线程模型,性能瓶颈明显
4 ~> V2版本:增强版英译汉字典服务器(生产级解耦)
4.1 核心改进:生产级工程化能力
相比基础版字典服务器,本版本增加了:
-
策略模式日志系统:支持控制台/文件双日志输出,线程安全
-
配置文件加载:从外部txt文件加载字典数据,支持动态扩展
-
完善的错误处理:文件打开失败、格式错误等异常处理
-
增强字典内容:每个单词包含中文翻译+英文例句+中文例句
4.2 完整代码实现
4.2.1 Mutex.hpp(复用V1版本)
代码同V1版本,此处省略。
4.2.2 Logger.hpp(复用V1版本)
代码同V1版本,此处省略。
4.2.3 字典业务模块:Dictionary.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Logger.hpp"
std::string defaultdict = "./Dict.txt";
std::string gsep = ": ";
using namespace LogModule;
class Dictionary
{
private:
void LoadConf()
{
std::ifstream in(_dictfilename);
if (!in.is_open())
{
LOG(LogLevel::FATAL) << "Open dictionary file error: " << _dictfilename;
exit(1);
}
std::string line;
int line_num = 0;
while (std::getline(in, line))
{
line_num++;
if (line.empty()) continue;
auto pos = line.find(gsep);
if (pos == std::string::npos)
{
LOG(LogLevel::WARNING) << "Line " << line_num << " format error, skip: " << line;
continue;
}
std::string key = line.substr(0, pos);
std::string value = line.substr(pos + gsep.size());
_dictmap.insert({key, value});
}
in.close();
LOG(LogLevel::INFO) << "Load dictionary success, total " << _dictmap.size() << " words";
}
public:
Dictionary(const std::string &dictfilename = defaultdict)
: _dictfilename(dictfilename)
{
LoadConf();
}
std::string Translate(const std::string &word)
{
auto iter = _dictmap.find(word);
if(iter == _dictmap.end())
{
return "未知单词";
}
return iter->second;
}
~Dictionary() {}
private:
std::string _dictfilename;
std::unordered_map<std::string, std::string> _dictmap;
};
4.2.4 字典数据文件:Dict.txt
Plaintext
apple: 苹果 - I eat an apple every day. / 我每天吃一个苹果。
banana: 香蕉 - The monkey is eating a banana. / 猴子正在吃香蕉。
cat: 猫 - My cat likes to sleep on the sofa. / 我的猫喜欢在沙发上睡觉。
dog: 狗 - She takes her dog for a walk every morning. / 她每天早上带她的狗去散步。
book: 书 - This book is very interesting. / 这本书非常有趣。
pen: 笔 - May I use your pen? / 我可以用一下你的笔吗?
happy: 快乐的 - She looks very happy today. / 她今天看起来很快乐。
sad: 悲伤的 - He felt sad when he lost his watch. / 他丢了手表时感到很悲伤。
run: 跑 - I run fast in the park. / 我在公园里跑得很快。
jump: 跳 - The child can jump high. / 这个孩子能跳得很高。
teacher: 老师 - Our teacher is very kind. / 我们的老师非常和蔼。
student: 学生 - He is a hardworking student. / 他是一个勤奋的学生。
car: 汽车 - His car is very fast. / 他的汽车非常快。
bus: 公交车 - I go to school by bus. / 我乘公交车上学。
love: 爱 - I love my family. / 我爱我的家人。
hate: 恨 - I hate getting up early. / 我讨厌早起。
hello: 你好 - Hello, nice to meet you! / 你好,很高兴认识你!
goodbye: 再见 - Goodbye, see you tomorrow! / 再见,明天见!
summer: 夏天 - Summer is my favorite season. / 夏天是我最喜欢的季节。
winter: 冬天 - It is very cold in winter. / 冬天非常冷。
4.2.5 通用UDP服务器:UdpServer.hpp
cpp
#ifndef __UDP_SERVER_HPP
#define __UDP_SERVER_HPP
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "Logger.hpp"
using namespace LogModule;
using callback_t = std::function<std::string(const std::string &)>;
class UdpServer
{
public:
UdpServer(callback_t cb, uint16_t port)
: _sockfd(-1),
_port(port),
_cb(cb)
{
}
void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "Create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "Create socket success, fd: " << _sockfd;
struct sockaddr_in local;
bzero(&local, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
if (n < 0)
{
LOG(LogLevel::FATAL) << "Bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "Bind socket success, port: " << _port;
}
void Start()
{
LOG(LogLevel::INFO) << "Udp Dictionary Server start running...";
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (n < 0)
{
LOG(LogLevel::WARNING) << "Recvfrom error, errno: " << errno;
continue;
}
inbuffer[n] = '\0';
std::string clientip = inet_ntoa(peer.sin_addr);
uint16_t clientport = ntohs(peer.sin_port);
LOG(LogLevel::INFO) << "Received from [" << clientip << ":" << clientport
<< "]: " << inbuffer;
std::string result;
if(_cb)
{
result = _cb(inbuffer);
}
sendto(_sockfd, result.c_str(), result.size(), 0,
(struct sockaddr *)&peer, len);
LOG(LogLevel::DEBUG) << "Sent response to [" << clientip << ":" << clientport
<< "]: " << result;
}
}
~UdpServer()
{
if (_sockfd >= 0)
{
close(_sockfd);
}
}
private:
int _sockfd;
uint16_t _port;
callback_t _cb;
};
#endif
4.2.6 服务器主函数:Main.cc
cpp
#include "UdpServer.hpp"
#include "Dictionary.hpp"
#include <memory>
void Usage(const std::string &proc)
{
std::cout << "Usage: \n\t" << proc << " local_port\n" << std::endl;
}
int main(int argc, char *argv[])
{
if(argc != 2)
{
Usage(argv[0]);
return 1;
}
ENABLE_CONSOLE_LOG_STRATEGY();
// ENABLE_FILE_LOG_STRATEGY();
uint16_t port = std::stoi(argv[1]);
std::unique_ptr<Dictionary> dict = std::make_unique<Dictionary>();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(
[&dict](const std::string &word) -> std::string {
return dict->Translate(word);
},
port
);
usvr->Init();
usvr->Start();
return 0;
}
4.3 客户端代码(通用版)
cpp
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "Create socket error: " << strerror(errno) << std::endl;
return 2;
}
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = htons(serverport);
server.sin_addr.s_addr = inet_addr(serverip.c_str());
std::cout << "=== UDP Dictionary Client ===" << std::endl;
std::cout << "Enter 'quit' to exit" << std::endl;
std::string word;
while (true)
{
std::cout << "\nPlease enter a word: ";
std::getline(std::cin, word);
if (word == "quit" || word == "exit")
{
std::cout << "Goodbye!" << std::endl;
break;
}
if (word.empty()) continue;
sendto(sock, word.c_str(), word.size(), 0,
(struct sockaddr*)&server, sizeof(server));
char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t n = recvfrom(sock, buffer, sizeof(buffer)-1, 0,
(struct sockaddr*)&temp, &len);
if (n > 0)
{
buffer[n] = '\0';
std::cout << "Result: " << buffer << std::endl;
}
else
{
std::cerr << "Recvfrom error" << std::endl;
}
}
close(sock);
return 0;
}
4.4 编译与运行
4.4.1 Makefile
Makefile
CC = g++
STD = -std=c++17
CFLAGS = -Wall -g -lpthread -lstdc++fs
all: dict_server dict_client
dict_server: Main.cc
$(CC) -o $@ $^ $(STD) $(CFLAGS)
dict_client: DictClient.cc
$(CC) -o $@ $^ $(STD) $(CFLAGS)
.PHONY: clean
clean:
rm -f dict_server dict_client
rm -rf log/
4.4.2 运行步骤
-
确保所有文件在同一目录下
-
执行
make编译 -
运行服务器:
\./dict\_server 8888 -
运行客户端:
\./dict\_client 127\.0\.0\.1 8888 -
输入单词查询翻译,输入
quit退出
4.5 V2版本优势总结
-
完全解耦:网络层与业务层完全分离,UdpServer可复用
-
生产级日志:支持控制台/文件双输出,线程安全
-
可扩展性强:字典数据从外部文件加载,无需修改代码即可扩展
-
完善的错误处理:文件错误、网络错误、格式错误均有处理
-
线程安全:所有共享资源访问均加锁保护
5 ~> V3版本:多人聊天室(生产级并发)
5.1 整体架构:生产者 - 消费者模型
我们采用经典的生产者-消费者分层架构,解决单线程性能瓶颈:
-
生产者层:UdpServer主线程,专门负责接收客户端UDP数据包(IO密集型,阻塞在recvfrom)
-
缓冲层:线程池内部的线程安全任务队列,解耦生产和消费速度,削峰填谷
-
消费者层:线程池中的多个工作线程,负责用户管理、消息路由和广播(CPU密集型)
5.2 核心模块补充
5.2.1 Cond.hpp(条件变量封装)
cpp
#ifndef __COND_HPP
#define __COND_HPP
#include <pthread.h>
#include "Mutex.hpp"
class Cond
{
public:
Cond()
{
pthread_cond_init(&_cond, nullptr);
}
void Wait(Mutex &mutex)
{
pthread_cond_wait(&_cond, mutex.Orgin());
}
void NotifyOne()
{
pthread_cond_signal(&_cond);
}
void NotifyAll()
{
pthread_cond_broadcast(&_cond);
}
~Cond()
{
pthread_cond_destroy(&_cond);
}
private:
pthread_cond_t _cond;
};
#endif
5.2.2 Thread.hpp(线程封装)
cpp
#ifndef __THREAD_HPP
#define __THREAD_HPP
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#include "Logger.hpp"
using namespace LogModule;
using func_t = std::function<void()>;
enum class TSTATUS
{
THREAD_NEW,
THREAD_RUNNING,
THREAD_STOP
};
static int gcnt = 1;
class Thread
{
private:
void getprocessid()
{
_pid = getpid();
}
void getlwp()
{
_lwpid = syscall(SYS_gettid);
}
static void *routine(void *args)
{
Thread *ts = static_cast<Thread *>(args);
ts->getprocessid();
ts->getlwp();
pthread_setname_np(pthread_self(), ts->Name().c_str());
ts->_func();
return nullptr;
}
public:
Thread(func_t f) : _joinable(true), _status(TSTATUS::THREAD_NEW), _func(f)
{
_name = "Worker-" + std::to_string(gcnt++);
}
void start()
{
if (_status == TSTATUS::THREAD_RUNNING)
{
LOG(LogLevel::INFO) << "thread is already running";
return;
}
int n = pthread_create(&_tid, nullptr, routine, this);
(void)n;
_status = TSTATUS::THREAD_RUNNING;
}
void stop()
{
if (_status == TSTATUS::THREAD_RUNNING)
{
int n = pthread_cancel(_tid);
(void)n;
_status = TSTATUS::THREAD_STOP;
}
else
{
LOG(LogLevel::WARNING) << "thread status is : THREAD_NEW or THREAD_STOP! stop error";
}
}
void join()
{
if (_joinable)
{
int n = pthread_join(_tid, nullptr);
(void)n;
LOG(LogLevel::INFO) << "lwp : " << _lwpid << ", name: " << _name << ", join success";
}
else
{
LOG(LogLevel::WARNING) << "lwp : " << _lwpid << ", name: " << _name << ", join failed, because thread is detach";
}
}
void detach()
{
if (_joinable && _status == TSTATUS::THREAD_RUNNING)
{
_joinable = false;
int n = pthread_detach(_tid);
(void)n;
}
}
std::string Name()
{
return _name;
}
~Thread() {}
private:
pthread_t _tid;
pid_t _pid;
pid_t _lwpid;
std::string _name;
bool _joinable;
TSTATUS _status;
func_t _func;
};
#endif
5.2.3 InetAddr.hpp(地址封装)
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define CONV(addr) ((struct sockaddr*)(addr))
class InetAddr
{
public:
InetAddr(struct sockaddr_in &addr) : _net_addr(addr)
{
_port = ntohs(_net_addr.sin_port);
_ip = inet_ntoa(_net_addr.sin_addr);
}
InetAddr(uint16_t port, std::string ip = "0.0.0.0")
: _port(port), _ip(ip)
{
_net_addr.sin_family = AF_INET;
_net_addr.sin_port = htons(_port);
_net_addr.sin_addr.s_addr = inet_addr(_ip.c_str());
}
uint16_t Port() { return _port; }
std::string Ip() { return _ip; }
struct sockaddr *Addr()
{
return CONV(&_net_addr);
}
bool operator==(const InetAddr &addr)
{
return (_ip == addr._ip) && (_port == addr._port);
}
socklen_t AddrLen()
{
return sizeof(_net_addr);
}
std::string StringAddress()
{
return "[" + _ip + ":" + std::to_string(_port) + "]";
}
~InetAddr()
{
}
private:
uint16_t _port;
std::string _ip;
struct sockaddr_in _net_addr;
};
5.2.4 Route.hpp(消息路由)
cpp
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include "InetAddr.hpp"
#include "Mutex.hpp"
#include "Logger.hpp"
using namespace LogModule;
class Route
{
private:
bool IsOnline(const InetAddr &who)
{
for (auto &user : _users)
{
if (user == who)
return true;
}
return false;
}
void AddUser(const InetAddr &who)
{
_users.push_back(who);
LOG(LogLevel::INFO) << "New user online: " << who.StringAddress();
}
void DeleteUser(const InetAddr &who)
{
auto iter = std::remove_if(_users.begin(), _users.end(),
[&who](const InetAddr &user)
{ return user == who; });
if (iter != _users.end())
{
_users.erase(iter, _users.end());
LOG(LogLevel::INFO) << "User offline: " << who.StringAddress();
}
}
public:
Route() {}
void RouteMessage(std::string message, InetAddr who, int sockfd)
{
LOG(LogLevel::DEBUG) << "3. RouteMessage";
std::vector<InetAddr> temp_users;
{
LockGuard lockguard(&_lock);
if (!IsOnline(who))
{
AddUser(who);
}
temp_users = _users;
};
std::string send_message = who.StringAddress() + "# " + message;
for (auto &user : temp_users)
{
ssize_t ret = sendto(sockfd, send_message.c_str(), send_message.size(), 0, user.Addr(), user.AddrLen());
if (ret < 0)
{
LOG(LogLevel::WARNING) << "sendto error: " << user.StringAddress();
}
}
if (message == "QUIT")
{
LockGuard lockguard(&_lock);
DeleteUser(who);
}
}
~Route() {}
private:
std::vector<InetAddr> _users;
Mutex _lock;
};
5.2.5 UdpServer.hpp(聊天室专用版)
cpp
#ifndef __UDP_SERVER_HPP
#define __UDP_SERVER_HPP
#include <iostream>
#include <string>
#include <strings.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include "InetAddr.hpp"
#include "Logger.hpp"
using namespace LogModule;
using callback_t = std::function<void(std::string message, InetAddr who, int sockfd)>;
class UdpServer
{
public:
UdpServer(callback_t cb, uint16_t port)
: _sockfd(-1),
_port(port),
_cb(cb)
{
}
void Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
LOG(LogLevel::FATAL) << "create socket error";
exit(1);
}
LOG(LogLevel::INFO) << "create socket fd success: " << _sockfd;
InetAddr local(_port);
int n = bind(_sockfd, local.Addr(), local.AddrLen());
if (n < 0)
{
LOG(LogLevel::FATAL) << "bind socket error";
exit(2);
}
LOG(LogLevel::INFO) << "bind socket fd success: " << _sockfd;
}
void Start()
{
char inbuffer[1024];
while (true)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&peer, &len);
if (n < 0)
{
LOG(LogLevel::WARNING) << "recvfrom error";
break;
}
inbuffer[n] = 0;
InetAddr clientaddr(peer);
if (_cb)
{
_cb(inbuffer, clientaddr, _sockfd);
}
}
}
~UdpServer()
{
if (_sockfd >= 0)
{
close(_sockfd);
_sockfd = -1;
}
}
private:
int _sockfd;
uint16_t _port;
callback_t _cb;
};
#endif
5.2.6 ThreadPool.hpp(懒汉单例线程池)
cpp
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include "Thread.hpp"
#include "Logger.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
static const int gnum = 5;
using namespace LogModule;
template <typename T>
class ThreadPool
{
private:
bool IsTaskQueueEmpty()
{
return _queue.empty();
}
T PopHelper()
{
T t = _queue.front();
_queue.pop();
return t;
}
void ThreadRoutine()
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof(name));
while (true)
{
T task;
{
LockGuard lockguard(&_lock);
while (IsTaskQueueEmpty() && _isrunning)
{
_sleeper_cnt++;
LOG(LogLevel::DEBUG) << "没有任务, 线程休眠: |" << name << "|";
_cond.Wait(_lock);
LOG(LogLevel::DEBUG) << "有任务, 线程唤醒: |" << name << "|";
_sleeper_cnt--;
}
if (IsTaskQueueEmpty() && !_isrunning)
{
LOG(LogLevel::INFO) << "Thread: " << name << " quit";
break;
}
task = PopHelper();
}
task();
}
}
ThreadPool(int num = gnum) : _num(num), _isrunning(false), _sleeper_cnt(0)
{
for (int i = 0; i < _num; i++)
{
_threads.emplace_back([this]()
{
this->ThreadRoutine();
});
}
}
ThreadPool(const ThreadPool<T> &) = delete;
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
public:
static ThreadPool<T> *GetInstance()
{
if (_instance == nullptr)
{
LockGuard lockguard(&_singleton_lock);
if (_instance == nullptr)
{
LOG(LogLevel::DEBUG) << "首次使用,创建线程池对象";
_instance = new ThreadPool<T>();
_instance->Start();
}
}
return _instance;
}
void Start()
{
LockGuard lockguard(&_lock);
if (_isrunning)
return;
_isrunning = true;
for (auto &thread : _threads)
thread.start();
}
void Enqueue(T task)
{
LockGuard lockguard(&_lock);
if (!_isrunning)
return;
_queue.push(task);
if (_sleeper_cnt > 0)
_cond.NotifyOne();
LOG(LogLevel::DEBUG) << "2. Enqueue task";
}
void Stop()
{
LockGuard lockguard(&_lock);
if (_isrunning)
{
LOG(LogLevel::DEBUG) << "关闭线程池";
_isrunning = false;
if (_sleeper_cnt > 0)
_cond.NotifyAll();
}
}
void Wait()
{
for (auto &thread : _threads)
thread.join();
}
~ThreadPool() {}
private:
std::vector<Thread> _threads;
int _num;
bool _isrunning;
int _sleeper_cnt;
std::queue<T> _queue;
Mutex _lock;
Cond _cond;
static ThreadPool<T> *_instance;
static Mutex _singleton_lock;
};
template <typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;
template <typename T>
Mutex ThreadPool<T>::_singleton_lock;
5.3 主函数整合
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include "UdpServer.hpp"
#include "Route.hpp"
#include "ThreadPool.hpp"
#include "Logger.hpp"
using namespace LogModule;
static Route g_route;
void StopHandler(int signo)
{
LOG(LogLevel::INFO) << "recv signal: " << signo << ", stop chatroom...";
ThreadPool<std::function<void()>>::GetInstance()->Stop();
ThreadPool<std::function<void()>>::GetInstance()->Wait();
LOG(LogLevel::INFO) << "chatroom stop success!";
exit(0);
}
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cerr << "Usage: " << argv[0] << " <port>" << std::endl;
return 1;
}
ENABLE_CONSOLE_LOG_STRATEGY();
// ENABLE_FILE_LOG_STRATEGY();
signal(SIGINT, StopHandler);
signal(SIGTERM, StopHandler);
uint16_t port = std::stoi(argv[1]);
auto *tp = ThreadPool<std::function<void()>>::GetInstance();
UdpServer server
(
[&g_route, tp](std::string message, InetAddr who, int sockfd)
{
LOG(LogLevel::INFO) << "1. get a message: " << message
<< ", client addr: " << who.Ip() << ":" << who.Port();
auto task = [message, who, sockfd, &g_route]()
{
g_route.RouteMessage(message, who, sockfd);
};
tp->Enqueue(task);
},
port
);
server.Init();
LOG(LogLevel::INFO) << "chatroom start success! port: " << port;
server.Start();
return 0;
}
5.4 编译与运行
5.4.1 Makefile
Bash
CC = g++
STD = -std=c++17
CFLAGS = -Wall -g -lpthread -lstdc++fs
all: chat_server chat_client
chat_server: ChatMain.cc
$(CC) -o $@ $^ $(STD) $(CFLAGS)
chat_client: ChatClient.cc
$(CC) -o $@ $^ $(STD) $(CFLAGS)
.PHONY: clean
clean:
rm -f chat_server chat_client
rm -rf log/
5.4.2 运行步骤
Bash
make
./chat_server 8888
nc -u 127.0.0.1 8888
5.5 客户端实现:多线程收发分离
5.5.1 Linux客户端
cpp
#include <iostream>
#include <string>
#include <thread>
#include <cstring>
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <unistd.h>
int sockfd = -1;
std::string server_ip;
uint16_t server_port = 0;
void Usage(const std::string& name)
{
std::cerr << "Usage: " << name << " server_ip server_port" << std::endl;
}
void RecvMessage()
{
char buffer[4096];
while (true)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,
(struct sockaddr*)&temp, &len);
if (n > 0)
{
buffer[n] = '\0';
std::cerr << buffer << std::endl;
}
}
}
void SendMessage()
{
InetAddr server_addr(server_port, server_ip);
std::string line;
while (true)
{
std::cout << "Please Enter# ";
std::getline(std::cin, line);
if (line.empty()) continue;
sendto(sockfd, line.c_str(), line.size(), 0,
server_addr.Addr(), server_addr.AddrLen());
if (line == "QUIT")
{
std::cout << "Goodbye!" << std::endl;
close(sockfd);
exit(0);
}
}
}
int main(int argc, char* argv[])
{
if (argc != 3)
{
Usage(argv[0]);
exit(0);
}
server_ip = argv[1];
server_port = std::stoi(argv[2]);
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
std::cerr << "Create socket error" << std::endl;
exit(1);
}
std::thread recv_thread(RecvMessage);
std::thread send_thread(SendMessage);
recv_thread.join();
send_thread.join();
close(sockfd);
return 0;
}
6 ~> 工程化实践与踩坑总结
6.1 日志系统使用指南
-
默认控制台日志:程序启动时自动启用
-
切换到文件日志 :在main函数开头调用
ENABLE\_FILE\_LOG\_STRATEGY\(\) -
日志等级:从低到高为DEBUG < INFO < WARNING < ERROR < FATAL
-
日志格式 :
\[时间\] \[等级\] \[进程ID\] \[文件名\] \[行号\] \- 日志内容
6.2 UDP编程踩坑总结
6.2.1 基础API踩坑
-
bind公网IP失败 :云服务器网卡上没有配置公网IP,只能绑定
0.0.0.0 -
端口被占用 :使用
netstat -tulpn | grep 端口号查看占用进程 -
recvfrom返回-1:检查sockfd是否有效、len参数是否正确初始化
-
sendto返回-1:检查目标地址是否正确、防火墙是否放行
6.2.2 多线程踩坑
-
线程池未启动 :懒汉单例线程池必须在
GetInstance()中启动线程 -
vector迭代器失效:多线程遍历vector时必须加锁,避免同时扩容
-
inet_ntoa线程不安全 :多线程环境下必须使用
inet\_ntop
6.2.3 业务逻辑踩坑
-
用户身份标识错误 :必须用
IP\+端口唯一标识用户,不能只用IP -
锁粒度过大:加锁期间只操作共享资源,转发逻辑放在锁外
-
中文乱码:Linux默认UTF-8,Windows默认GBK,需要统一编码

6.3 客户端访问服务端,云服务器需要开放一些端口
为保障云服务器安全,厂商已通过防火墙对所有端口进行了限制处理,导致客户端无法正常访问服务器端口。若需解决该问题,可在防火墙或安全组中开放云服务器的端口(建议开放8000~9000端口范围),相关操作有对应视频教程,完成端口开放后,客户端即可正常访问服务器。
7 ~> 总结与后续优化方向
7.1 本文总结
我们完成了UDP编程从入门到生产级的完整演进:
-
从V1的Echo服务器掌握了基础Socket收发
-
从V2的增强版字典服务器学会了生产级工程化实践(日志、配置、解耦)
-
从V3的聊天室掌握了多用户管理、并发优化和跨平台通信
7.2 后续优化方向
-
用户下线检测:实现心跳机制,定期清理超时用户
-
消息可靠性:实现ACK确认和重传机制
-
大消息传输:实现消息分片与重组
-
用户体系:实现登录注册,支持用户名密码
-
私聊功能:支持一对一消息发送
-
性能优化:使用无锁队列、epoll非阻塞IO
附录:完整项目文件结构
Plaintext
udp_project/
├── V1-Echo/
│ ├── Mutex.hpp
│ ├── Logger.hpp
│ ├── UdpEchoServer.hpp
│ ├── main.cpp
│ └── Makefile
├── V2-Dictionary/
│ ├── Mutex.hpp
│ ├── Logger.hpp
│ ├── Dictionary.hpp
│ ├── UdpServer.hpp
│ ├── Main.cc
│ ├── DictClient.cc
│ ├── Dict.txt
│ └── Makefile
├── V3-ChatRoom/
│ ├── Mutex.hpp
│ ├── Logger.hpp
│ ├── Cond.hpp
│ ├── Thread.hpp
│ ├── InetAddr.hpp
│ ├── Route.hpp
│ ├── UdpServer.hpp
│ ├── ThreadPool.hpp
│ ├── ChatMain.cc
│ ├── ChatClient.cc
│ └── Makefile
└── README.md
结尾
uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!"
"技术之路难免有困惑,但同行的人会让前进更有方向。" |
结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!
往期回顾:
【Linux网络】Linux 网络编程入门:UDP Socket 编程(上)
🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა
