一、Socket编程UDP
二、UDP 网络编程大体流程
服务器
第1步:socket() ← 创建一个"通信管道"(文件描述符) ↓ 第2步:bind() ← 把管道绑定到 IP+端口(让客户端能找到你) ↓ 第3步:recvfrom() ← 阻塞等待,收客户端消息(同时拿到客户端地址) ↓ 第4步:sendto() ← 给客户端回消息(用第3步拿到的地址) ↓ 重复第3、4步(服务器永远不退出)
客户端
第1步:socket() ← 创建通信管道 ↓ 第2步:(不需要 bind,OS 首次 sendto 时自动分配随机端口) ↓ 第3步:sendto() ← 给服务器发消息(需要知道服务器的 IP+端口) ↓ 第4步:recvfrom() ← 收服务器的回复 ↓ 重复第3、4步
为什么服务器要 bind,客户端不用?
-
服务器端口必须固定(众所周知),客户端才知道往哪发
-
客户端数量很多,OS 随机分配端口就行
三、系统调用详解
1. socket() --- 创建套接字
cpp
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
| 参数 | 含义 | 常用值 |
|---|---|---|
| domain | 协议族 | AF_INET(IPv4) |
| type | 套接字类型 | SOCK_DGRAM(UDP) |
| protocol | 协议 | 0(自动选 UDP) |
返回值:成功返回文件描述符(如 3),失败返回 -1。
本质:socket 就是一个文件描述符,和 open() 文件返回的 fd 一样,后面所有操作都围绕这个 fd。
2. bind() --- 绑定地址
cpp
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
| 参数 | 含义 |
|---|---|
| sockfd | socket() 返回的文件描述符 |
| addr | 指向地址结构体的指针(IP + 端口) |
| addrlen | 地址结构体大小 |
返回值:成功返回 0,失败返回 -1(常见:端口被占用)。
为什么要强转成 sockaddr*?因为 bind 是通用接口,IPv4 用 sockaddr_in,IPv6 用 sockaddr_in6,统一转成 sockaddr* 一套接口兼容所有协议。
3. recvfrom() --- 接收数据
cpp
#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
| 参数 | 含义 |
|---|---|
| sockfd | socket 文件描述符 |
| buf | 接收缓冲区(数据存到这里) |
| len | 缓冲区大小 |
| flags | 标志位,填 0 |
| src_addr | 输出参数,拿到发送方地址 |
| addrlen | 输入输出参数,必须初始化为 sizeof(peer) |
返回值:成功返回收到的字节数,失败返回 -1。
cpp
char buffer[1024];
struct sockaddr_in peer;
socklen_t len = sizeof(peer); // 必须初始化!
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,
(struct sockaddr*)&peer, &len);
if (n > 0) {
buffer[n] = 0; // 手动加 \0,当字符串用
}
4. sendto() --- 发送数据
cpp
#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
| 参数 | 含义 |
|---|---|
| sockfd | socket 文件描述符 |
| buf | 要发送的数据 |
| len | 数据长度 |
| flags | 标志位,填 0 |
| dest_addr | 目标地址(IP + 端口) |
| addrlen | 地址结构体大小 |
返回值:成功返回实际发送的字节数,失败返回 -1。
四、sockaddr_in 结构体
cpp
struct sockaddr_in {
sa_family_t sin_family; // 协议族,填 AF_INET
in_port_t sin_port; // 端口号(网络字节序)
struct in_addr sin_addr; // IP地址(网络字节序)
char sin_zero[8]; // 填充,和 sockaddr 一样大
};
struct in_addr {
in_addr_t s_addr; // 32位IP(网络字节序)
};
使用流程:
cpp
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(8888); // 端口转网络字节序
addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // IP转网络字节序
// 或者:
addr.sin_addr.s_addr = INADDR_ANY; // 绑定任意网卡
五、字节序 --- 为什么需要转换
什么是字节序
一个 16 位整数 0x1234 在内存中怎么存?
大端序(网络用的):12 在前,34 在后(高位在前) 小端序(x86电脑):34 在前,12 在后(低位在前)
网络传输统一用大端序,你的电脑可能是小端序,所以要转换。
端口转换函数(16位,short)
cpp
#include <arpa/inet.h>
uint16_t htons(uint16_t hostshort); // 主机序 → 网络序(发数据前用)
uint16_t ntohs(uint16_t netshort); // 网络序 → 主机序(收数据后用)
| 函数 | 记忆 | 用途 |
|---|---|---|
| htons | host to network short | 端口填入 sockaddr_in 前转换 |
| ntohs | network to host short | 从 sockaddr_in 取出端口后转换 |
六、IP 地址转换 --- 字符串和二进制互转
字符串 → 二进制(发数据前用)
cpp
#include <arpa/inet.h>
// 点分十进制 → 32位网络字节序整数
in_addr_t inet_addr(const char *strptr);
// 例:inet_addr("127.0.0.1") → 0x0100007F
// 更安全的版本,支持 IPv4 和 IPv6
int inet_pton(int family, const char *strptr, void *addrptr);
二进制 → 字符串(收数据后用)
cpp
#include <arpa/inet.h>
// 32位网络字节序整数 → 点分十进制字符串
char *inet_ntoa(struct in_addr inaddr);
// 例:inet_ntoa(addr.sin_addr) → "127.0.0.1"
// 注意:返回值指向静态内存,下次调用会覆盖,要立刻拷贝
// 更安全的版本,调用者提供缓冲区
const char *inet_ntop(int family, const void *addrptr, char *strptr, size_t len);
完整例子
cpp
// 发数据前:字符串 → 二进制
server.sin_addr.s_addr = inet_addr("127.0.0.1");
// 收数据后:二进制 → 字符串
char *ip = inet_ntoa(peer.sin_addr); // IP转字符串
uint16_t port = ntohs(peer.sin_port); // 端口转主机序
七、INADDR_ANY --- 绑定任意网卡
cpp
local.sin_addr.s_addr = INADDR_ANY; // 等价于 0.0.0.0
一台服务器可能有多个网卡(有线、无线、回环),用 INADDR_ANY 表示"接受任意网卡发来的连接",省去确定具体用哪个 IP 的麻烦。
正式开始前,我们要先了解一个思想:
八、【工程思想】基础设施先行
原则:先造工具,再写业务
好处:
-
业务代码干净,只关注核心逻辑
-
工具复用,不重复造轮子
-
改bug只改一处
写项目之前先想:我会反复用到什么?
→ 日志打印 → 封装 Log
→ 加锁解锁 → 封装 LockGuard
→ 创建线程 → 封装 Thread
把这些放到 common/ 目录,业务代码直接 #include 用
一句话总结:磨刀不误砍柴工。 common 就是那把刀。
- 基础设施先行(Infrastructure First)
先搭建底层工具,再写业务代码。就像盖房子先打地基,而不是边盖边挖。
- 关注点分离(Separation of Concerns)
每个模块只管一件事:
-
Log 只管日志
-
LockGuard 只管锁
-
Thread 只管线程
-
业务代码只管业务逻辑
互不干扰,各司其职。
第一步:Log.hpp + Comm.hpp + nopy.hpp
↓ (先有工具,后面不重复造)
第二步:InetAddr.hpp
↓ (地址转换,后面都要用)
第三步:UdpServer.hpp(只写 Init)
↓ (先让服务器能启动)
第四步:UdpClient.cpp
↓ (能收发数据了再往下走)
第五步:UdpServer.hpp 补上 Start
↓ (回显跑通)
先写服务器,不是先写客户端。 因为客户端依赖服务器的地址,而且服务器是被动方,逻辑更简单清晰。
在写网络服务器时会遇到这些问题:

common/ --- 公共工具库
LockGuard.hpp --- 自动加锁解锁
你写多线程代码,要保护共享变量,得手动加锁解锁:
cpp
pthread_mutex_lock(&mutex);
// ... 做事情 ...
// 如果这里 return 了、抛异常了、忘记 unlock 了
pthread_mutex_unlock(&mutex); // 永远执行不到 → 死锁
解决思路:RAII
C++ 的核心思想:构造函数申请资源,析构函数释放资源。 对象离开作用域时,析构函数一定会被调用,不管你有没有 return。
所以:锁放在构造函数里加,析构函数里解。
你自己写的话,就这三个问题
-
构造函数干什么?→ pthread_mutex_lock
-
析构函数干什么?→ pthread_mutex_unlock
-
需要保存什么?→ 一个 pthread_mutex_t*
cpp
#pragma once
#include <pthread.h>
// 作用:RAII 风格的互斥锁封装,防止忘记解锁导致死锁
// LockGuard 的思路(RAII):
// 构造函数里加锁,析构函数里解锁
// C++ 保证:离开作用域时析构函数一定会被调用
// 所以不管你怎么离开(return、异常、正常结束),锁一定会被释放
class LockGuard
{
public:
// 构造函数:拿到锁的指针,立刻加锁
// 为什么是指针?因为 pthread_mutex_t 不能拷贝,只能传地址
LockGuard(pthread_mutex_t *mutex) : _mutex(mutex)
{
pthread_mutex_lock(_mutex); // 构造时加锁
}
// 析构函数:自动解锁
// 不管临界区代码怎么退出,只要这个对象被销毁,就一定会执行这里
~LockGuard()
{
pthread_mutex_unlock(_mutex); // 析构时解锁
}
private:
pthread_mutex_t *_mutex; // 保存锁的指针,构造和析构时用
};
使用时:
cpp
{
LockGuard lock(&_mutex); // 加锁
// ... 临界区代码 ...
} // 离开作用域,自动解锁
你写这个类之前要问自己:
-
我为什么需要它?→ 防止忘记 unlock
-
它的核心机制是什么?→ RAII(构造=申请,析构=释放)
-
有几个成员?→ 就一个指针
想清楚这三个问题,代码自然就出来了。
Log.hpp --- 带等级和时间戳的日志系统
它解决什么问题?
你调试程序,用 printf("error\n") 打了一条日志。过两天回来看,发现三个问题:
-
不知道什么时候打的 ------ 没有时间戳
-
不知道严不严重 ------ 是调试信息还是致命错误?
-
不知道哪个进程打的 ------ 多进程程序分不清
所以: 封装一个日志类,自动补上等级、时间、进程号,还支持 printf 风格的格式化输出。而且做成全局对象,任何文件 include 就能用,不用传参。
[Warning][2025-01-15 14:30:22][pid:1234] bind failed, errno: 98
^等级 ^时间戳 ^进程号 ^你的消息
自己写之前问自己
-
有哪些等级?→ Debug / Info / Warning / Error / Fatal
-
输出到哪里?→ 屏幕(默认),可选文件
-
怎么格式化?→ 等级 + 时间 + 用户消息
cpp
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <ctime>
#include <cstdarg> // va_list, va_start, va_end, vsnprintf
#include <fstream>
// 第一步:定义日志等级
// 从轻到重排列,后面用数字大小来判断输出到哪里
enum LogLevel
{
Debug = 0, // 调试信息,开发时用,上线后关掉
Info, // 普通信息,比如"服务器启动成功"
Warning, // 警告,不影响运行但要注意
Error, // 错误,某个操作失败了
Fatal // 致命错误,程序必须退出
};
// 等级转字符串,打印时用
// 比如 level=3 → "Error"
// static 让每个 .cpp 文件各有一份,避免多个 .cpp include 时链接器报"重复定义"
static std::string LevelToString(int level)
{
switch (level)
{
case Debug:
return "Debug";
case Info:
return "Info";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "Unknown";
}
}
// 第二步:获取当前时间字符串
// 返回格式:"2025-01-15 14:30:22"
static std::string GetTimeString()
{
time_t now = time(nullptr); // 获取当前时间戳(秒数)
struct tm *t = localtime(&now); // 转成年月日时分秒的结构体
char buffer[64];
// 格式化输出,%04d 表示4位数字,不足补0
snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
t->tm_year + 1900, // tm_year 是从1900开始算的,所以要+1900
t->tm_mon + 1, // tm_mon 是从0开始的(0=一月),所以要+1
t->tm_mday,
t->tm_hour, t->tm_min, t->tm_sec);
return buffer;
}
// 第三步:日志类
// 作用:格式化输出日志,自动带上等级、时间、进程号
//
// 输出效果:
// [Info][2025-01-15 14:30:22][pid:1234] socket success, sockfd: 3
// [Fatal][2025-01-15 14:30:22][pid:1234] socket errrr, 98: Address already in use
//
// 为什么用可变参数 ... ?
// 这样你可以像 printf 一样传格式化字符串:
// lg.LogMessage(Fatal, "socket errrr, %d : %s\n", errno, strerror(errno));
// 而不是拼接字符串:
// lg.LogMessage(Fatal, "socket errrr, " + to_string(errno) + ": " + strerror(errno));
// printf 风格更简洁,也更常用
class Log
{
public:
// 构造函数
// logname: 日志文件路径,默认 "./log.txt"
Log(const std::string &logname = "./log.txt")
: _logname(logname), _enable_file(false)
{
}
// 开启文件日志(默认只输出到屏幕)
void EnableFileLog()
{
_enable_file = true;
}
// 核心方法:打印日志
// level: 日志等级(Debug/Info/Warning/Error/Fatal)
// format: printf 风格的格式化字符串
// ...: 可变参数,对应 format 里的 %d %s 等
//
// 调用示例:
// lg.LogMessage(Info, "socket success, sockfd: %d\n", sockfd);
// lg.LogMessage(Fatal, "bind errrr, %d : %s\n", errno, strerror(errno));
// ============================================================
void LogMessage(int level, const char *format, ...)
{
// ---- 第一步:把可变参数格式化成字符串 ----
// va_list: 可变参数列表的"游标"
// va_start(ap, format): 从 format 参数之后开始读取可变参数
// vsnprintf: 和 snprintf 一样,但从 va_list 读参数(不是 ...)
// va_end(ap): 结束,释放资源
char user_msg[1024];
va_list ap;
va_start(ap, format);
vsnprintf(user_msg, sizeof(user_msg), format, ap);
va_end(ap);
// ---- 第二步:拼接完整日志 ----
// [等级][时间][pid] 用户消息
// getpid() 获取当前进程ID
char log_buffer[2048];
snprintf(log_buffer, sizeof(log_buffer), "[%s][%s][pid:%d] %s",
LevelToString(level).c_str(),
GetTimeString().c_str(),
getpid(),
user_msg);
// ---- 第三步:输出到屏幕 ----
// Warning 及以上 → stderr(错误流,红色高亮,立即刷新)
// 其他等级 → stdout(标准输出,可能有缓冲)
if (level >= Warning)
{
std::cerr << log_buffer << std::endl;
}
else
{
std::cout << log_buffer << std::endl;
}
// ---- 第四步(可选):输出到文件 ----
// 默认关闭,调用 EnableFileLog() 后开启
// ios::app 表示追加模式,不会覆盖之前的日志
if (_enable_file)
{
std::ofstream out(_logname, std::ios::app);
if (out.is_open())
{
out << log_buffer << std::endl;
out.close();
}
}
}
~Log()
{
}
private:
std::string _logname; // 日志文件路径
bool _enable_file; // 是否同时写入文件
};
// 全局日志对象
// 放在头文件里,所有 include Log.hpp 的文件都能直接用:
// lg.LogMessage(Info, "xxx");
// 不用传参,不用单例,include 就能用
//
// static 让每个 .cpp 文件各有一份自己的 lg 对象
// 虽然不是同一个对象,但都是默认构造,行为一致,都能正常打日志
static Log lg;
关键点:
-
va_list + vsnprintf 让你像 printf 一样传可变参数
-
全局对象 lg 让你哪里都能直接用,不用传参
ThreadPool.hpp(最难)
为什么需要线程池
来一个请求 → 创建线程 → 处理 → 销毁线程
来一个请求 → 创建线程 → 处理 → 销毁线程
...
创建/销毁线程很贵。如果一秒来1000个请求,你就创建销毁1000次。
线程池的思路: 提前创建好 N 个线程,来了任务就丢进队列,线程从队列里取任务执行。
工作流程
主线程:recvfrom 收到请求 → Push(任务到队列) → signal 唤醒一个工作线程
工作线程1:等待 → 被唤醒 → 从队列取任务 → 执行 → 继续等下一个
工作线程2:等待 → 被唤醒 → 从队列取任务 → 执行 → 继续等下一个
工作线程3:等待 → 被唤醒 → 从队列取任务 → 执行 → 继续等下一个
需要三个东西

-
一个任务队列(std::queue)
-
一把锁(保护队列,多线程抢着取)
-
一个条件变量(队列空时让线程等着,别死循环转 CPU)
写之前问自己
-
谁往队列里塞任务?→ 主线程(Push)
-
谁从队列里取任务?→ 工作线程(Pop)
-
队列空了怎么办?→ 条件变量等待(pthread_cond_wait)
-
塞了任务怎么办?→ 唤醒等待的线程(pthread_cond_signal)
-
几个线程?→ 构造时指定,默认5个
cpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include "LockGuard.hpp"
// 线程池:提前创建一批线程,来了任务直接分配,不用反复创建销毁
// 工作流程:
// 主线程: recvfrom收到请求 → Push(任务) → 丢进队列 → 唤醒工作线程
// 工作线程: 等待 → 被唤醒 → 从队列取任务 → 执行 → 继续等待下一个任务
// 模板参数 T 就是任务类型,一般是 std::function<void()>
template <class T>
class ThreadPool
{
public:
// 单例模式:整个程序只创建一个线程池实例
// 为什么用单例?线程池是全局资源,创建多个会浪费线程
static ThreadPool<T> *GetInstance()
{
// static 局部变量:第一次调用时初始化,之后再调用直接返回
static ThreadPool<T> *instance = nullptr;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 第一层判断:如果已经创建了,直接返回(不用加锁,提高效率)
if (instance == nullptr)
{
// 没创建,加锁准备创建
// LockGuard:构造时加锁,离开这个if作用域时自动解锁
LockGuard lockguard(&mutex);
// 第二层判断:为什么加锁后还要再判断一次?
// 因为可能有多个线程同时通过了第一层判断
// 线程A拿到锁创建了instance,线程B拿到锁时instance已经不是nullptr了
// 如果不判断,线程B会再创建一个,就不是单例了
if (instance == nullptr)
{
instance = new ThreadPool<T>();
}
}
return instance;
}
// 启动线程池:创建 _thread_num 个工作线程
// 每个工作线程都会执行 Worker 函数(后面定义的)
void Start()
{
for (int i = 0; i < _thread_num; i++)
{
pthread_t tid;
// 参数说明:
// &tid --- 输出参数,拿到线程ID
// nullptr --- 用默认线程属性
// Worker --- 线程要执行的函数(必须是 static)
// this --- 把当前对象指针传进去,Worker里要用
pthread_create(&tid, nullptr, Worker, this);
}
}
// 往任务队列里塞一个任务(主线程调用)
void Push(const T &task)
{
// 加锁:因为 _queue 是共享资源,多个线程会同时访问
LockGuard lockguard(&_mutex);
_queue.push(task); // 把任务塞进队列
// 唤醒一个正在等待的线程:"有活干了!"
// 如果没有线程在等待,这个信号会被忽略(不会丢任务)
pthread_cond_signal(&_cond);
}
private:
// 私有构造函数:只有 GetInstance() 能调用,外部不能 new
ThreadPool(int thread_num = 5) : _thread_num(thread_num)
{
// 初始化互斥锁和条件变量
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
// 禁止拷贝(单例不能被复制)
ThreadPool(const ThreadPool &) = delete;
const ThreadPool &operator=(const ThreadPool &) = delete;
// 工作线程的入口函数(所有工作线程都执行这个函数)
// 必须是 static,因为 pthread_create 只接受 void*(*)(void*) 签名
static void *Worker(void *arg)
{
// 把 void* 转回 ThreadPool*,这样就能访问成员变量了
ThreadPool *tp = static_cast<ThreadPool *>(arg);
// 工作线程永远不会退出,一直循环取任务
while (true)
{
// 加锁保护任务队列
LockGuard lockguard(tp->GetMutex());
// 重点来了!为什么用 while 而不是 if?
// pthread_cond_wait 被唤醒时可能"虚假唤醒"(spurious wakeup)
// 就是说队列可能还是空的,但线程被唤醒了
// 用 while:醒来后重新检查,如果还是空的,继续等
// 用 if:醒来就直接往下走,可能取到空队列 → 崩溃
while (tp->IsEmpty())
{
// 队列空了,没事做,线程在这里"睡觉"
// 注意:wait 会自动释放锁,让别的线程能 Push 任务进来
// 被 signal 唤醒后,会自动重新获取锁,然后继续往下走
tp->Wait();
}
// 走到这里说明队列里有任务了
T task = tp->Pop(); // 取出队头任务
// LockGuard 离开这个作用域时会自动解锁(析构函数调用 unlock)
// 也就是说:任务是在锁外面执行的
// 为什么?如果在锁里面执行任务:
// - 任务可能很耗时
// - 其他线程就一直等着锁,没法取任务
// - 线程池就变成单线程了,失去意义
task(); // 执行任务!(在锁外面执行,不阻塞其他线程)
}
return nullptr;
}
// 以下几个是 Worker 函数需要调用的接口
// 为什么要封装?因为 _mutex 和 _queue 是私有的,Worker 是 static 函数
// static 函数不能直接访问非 static 成员,所以通过这些接口间接访问
pthread_mutex_t *GetMutex() { return &_mutex; }
bool IsEmpty() { return _queue.empty(); }
void Wait()
{
// pthread_cond_wait 做了三件事:
// 1. 释放锁(让 Push 能拿到锁塞任务)
// 2. 让当前线程睡觉(不占CPU)
// 3. 被唤醒后,重新获取锁,然后从这里返回
pthread_cond_wait(&_cond, &_mutex);
}
private:
int _thread_num; // 工作线程数量(默认5个)
std::queue<T> _queue; // 任务队列(所有线程共享,要加锁保护)
pthread_mutex_t _mutex; // 互斥锁:同一时刻只有一个线程能操作队列
pthread_cond_t _cond; // 条件变量:用于"队列空→等待"和"有任务→唤醒"
};
最核心的三行代码:
cpp
while (pool->_queue.empty()) // 没任务?
pthread_cond_wait(...); // 等着
task = pool->_queue.front(); // 有了,取出来
重点理清几个可能没看懂的地方:
- 为什么单例要判断两次?
cpp
if (instance == nullptr) // 第一次:没锁,快速判断,避免每次都加锁
LockGuard lock(&mutex);
if (instance == nullptr) // 第二次:有锁了,真正确认
第一次是"偷看"(省性能),第二次是"确认"(防并发)。
- 为什么 while(IsEmpty()) 而不是 if?
线程可能被"虚假唤醒"------没人 Push 但线程醒了。while 醒来后再检查一次,空就继续睡。if 不检查,直接取空队列就崩了。
- 为什么任务在锁外面执行?
锁内执行任务:线程A干活 → 线程B等着 → 线程A干完 → 线程B才能取任务
锁外执行任务:线程A取任务 → 解锁 → 线程B也能取 → 两人同时干
锁内执行就变成单线程了,白创建那么多线程。
- pthread_cond_wait 到底干了什么?
三步:释放锁 → 睡觉 → 被唤醒后重新拿锁。所以 wait 期间其他线程能拿到锁去 Push 任务。
nopy.hpp --- 禁止拷贝
它解决什么问题?
你写了一个 UdpServer,里面有一个 _sockfd(socket 文件描述符)。如果有人写了 UdpServer b = a;,b 和 a 的 _sockfd 是同一个数字。a 析构时 close(_sockfd),b 的 socket 也跟着没了 ------ 程序就崩了。
所以: 管理"独占资源"的类(服务器、socket、锁),不允许被复制。这个头文件就是干这件事的 ------ 在编译期拦住拷贝操作。
cpp
#pragma once
#include <iostream>
// class UdpServer : public nopy { ... };
// UdpServer b = a; // 编译报错!
class nopy
{
public:
nopy() {}
// = delete:告诉编译器,这个函数不要生成
// 如果有人写 UdpServer b(a); 编译器会报错
// nopy(const nopy &) 是拷贝构造函数的签名
nopy(const nopy &) = delete;
// = delete:同理,禁止赋值操作
// 如果有人写 b = a; 编译器会报错
// operator= 是赋值运算符的重载
const nopy &operator=(const nopy &) = delete;
~nopy() {}
};
Comm.hpp --- 错误码枚举
它解决什么问题?
程序出错要退出,你写 exit(1)。过两周你自己都忘了 1 是什么意思。写 exit(2),2 又是啥?
所以: 给每个错误类型起个名字。exit(Socket_Err) 一看就知道是 socket 创建失败。而且用 enum 的话,值会自动递增,不用手动编号。
cpp
#pragma once
// 作用:定义程序中用到的错误码
// 为什么用 enum 而不是 #define?
// enum 里的值会自动递增:
// Usage_Err = 1(手动指定从1开始)
// Socket_Err = 2(自动+1)
// Bind_Err = 3(自动+1)
// 如果中间插入一个新错误码,后面的数字会自动调整
// #define 需要手动写每个数字,容易写错
enum
{
Usage_Err = 1, // 用户命令行参数错误(比如少传了端口号)
Socket_Err, // socket() 创建失败,自动等于 2
Bind_Err // bind() 绑定失败,自动等于 3
};
InetAddr.hpp --- 网络地址翻译官
它解决什么问题?
你的服务器收到一个客户端消息,recvfrom 会告诉你"这个消息是谁发的",但它给你的是一坨二进制:
peer.sin_addr.s_addr = 0x0100007F ← 这是啥?
peer.sin_port = 0xA350 ← 这又是啥?
你看不懂。你想要的是 "127.0.0.1:41832" 这种人能看懂的格式。
所以: InetAddr 帮你做这个翻译。构造时自动把二进制转成字符串,后面直接用。
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 作用:把网络地址翻译成人能看懂的格式
class InetAddr
{
public:
// 构造函数:拿到 sockaddr_in 后立刻翻译
// 参数是引用,不拷贝,直接用
InetAddr(struct sockaddr_in &addr) : _addr(addr)
{
// ---- 端口翻译 ----
// ntohs = Network TO Host Short
// 网络传输用大端序(高位在前),你的x86电脑用小端序(低位在前)
// 举例:端口 8888 在内存里是 0x22B8
// 大端序(网络):22 在前,B8 在后
// 小端序(你的电脑):B8 在前,22 在后
// 不转换的话,你的电脑会把 0x22B8 读成 47138,不是 8888
_port = ntohs(_addr.sin_port);
// ---- IP翻译 ----
// inet_ntoa = Internet Number TO ASCII
// 把 32位二进制IP → "127.0.0.1" 字符串
// 注意:inet_ntoa 返回的指针指向一块静态内存
// 下次调用 inet_ntoa 会覆盖掉上次的结果
// 所以我们用 std::string 拷贝一份保存起来,安全
_ip = inet_ntoa(_addr.sin_addr);
}
// 只返回IP字符串,比如 "192.168.1.100"
std::string Ip() { return _ip; }
// 只返回端口号(已经转成主机字节序了),比如 8888
uint16_t Port() { return _port; }
// 返回 "ip:port" 格式,比如 "192.168.1.100:8888"
// 用于日志打印,一眼看出是哪个客户端发的消息
std::string PrintDebug()
{
std::string info = _ip;
info += ":";
info += std::to_string(_port); // 端口号转字符串,拼接在IP后面
return info;
}
// 获取原始 sockaddr_in 结构体的引用
// 什么时候用?服务器要给这个客户端回消息时:
// sendto(sock, msg, len, 0, (sockaddr*)&addr.GetAddr(), sizeof(...));
// 不用再把 IP 和端口转回二进制,直接用保存的原始结构体
const struct sockaddr_in &GetAddr()
{
return _addr;
}
// 重载 == 运算符:判断两个地址是不是同一个客户端
// 什么时候用?聊天室往在线列表加人时:
// if (addr == user) return; // 已经在列表里了,不重复添加
// 比较逻辑:IP相同 且 端口相同 → 同一个客户端
bool operator==(const InetAddr &addr)
{
return this->_ip == addr._ip && this->_port == addr._port;
}
~InetAddr() {}
private:
std::string _ip; // 翻译后的IP,如 "127.0.0.1"
uint16_t _port; // 翻译后的端口,如 8888
struct sockaddr_in _addr; // 保存原始结构体,给 sendto 用
};
Thread.hpp --- 线程封装
遇到的问题
cpp
// 每次创建线程都要写这么一坨
pthread_create(&tid, nullptr, [](void *arg) -> void* {
// ...
return nullptr;
}, &data);
你想要什么
cpp
Thread<MyData> t("worker", my_func, data);
t.Start(); // 一行启动
t.Join(); // 一行等待
自己写之前问自己
-
线程函数的签名是什么?→ void*(*)(void*)(C风格)
-
用户想传什么?→ 一个函数 + 一份数据
-
怎么把 C++ 函数适配到 C 风格?→ 静态函数 + this 指针
它解决什么问题?
pthread_create 是 C 语言的 API,它要的函数签名是 void*(*)(void*) ------ 一个 C 风格的函数指针。但你想传的是 C++的函数(std::function、lambda、成员函数)。
签名不匹配,编译不过。
所以需要一个"中间人":写一个 C 风格的 static 函数给 pthread_create 调用,在里面转调你的 C++ 函数。这个套路叫 trampoline(蹦床函数)。
cpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
// 第一步:定义线程函数的类型
// T 是你要传给线程的数据类型(比如 ThreadData)
// func_t<T> 表示:接收一个 T& 参数,没有返回值的函数
// 你可以传普通函数、lambda、std::bind 的结果,都能匹配
template <class T>
using func_t = std::function<void(T &)>;
// 作用:把 C++ 函数适配进 C 风格的 pthread_create
// pthread_create 只接受 C 风格函数指针:void*(*)(void*)
// 你想传的是 C++ 函数:void(T&)
// 签名不匹配,编译不过
//
// 原始写法很丑:
// void* my_func(void* arg) {
// ThreadArg* data = (ThreadArg*)arg;
// // ...
// return nullptr;
// }
// pthread_create(&tid, nullptr, my_func, &data);
// pthread_join(tid, nullptr);
//
// 封装后很干净:
// Thread<ThreadData> t("worker", my_func, data);
// t.Start();
// t.Join();
//
// 核心技巧?
// static 函数做"蹦床":C风格入口 → 转回 this → 调用 C++ 函数
template <class T>
class Thread
{
public:
// name: 线程名(调试用,打印出来知道是哪个线程在跑)
// func: 线程要执行的函数
// data: 传给线程函数的数据,用引用传递,不拷贝
Thread(const std::string &name, func_t<T> func, T &data)
: _name(name), _func(func), _data(data)
{
}
// 启动线程
void Start()
{
// pthread_create 的四个参数:
// &_tid --- 输出参数,函数会把线程ID写到这里
// nullptr --- 用默认线程属性,不用管
// Routine --- 线程入口函数(必须是static,后面解释为什么)
// this --- 把当前对象的指针传进去,Routine 里要用
pthread_create(&_tid, nullptr, Routine, this);
}
// 等待线程结束
// 如果不调 Join,main 函数结束时线程会被强制杀掉,可能数据还没处理完
void Join()
{
pthread_join(_tid, nullptr);
}
~Thread() {}
private:
// 蹦床函数(trampoline)------ 整个类的核心
// 为什么必须是 static?
// pthread_create 要的签名: void*(*)(void*)
// 普通成员函数的签名: void*(Thread::*)(void*)
// 多了一个 Thread:: 的隐式参数,签名不匹配,编译不过
// static 函数没有隐式 this 参数,签名就是 void*(*)(void*),匹配了
//
// 那 static 函数怎么访问 _func 和 _data?
// Start() 里把 this 传给了 pthread_create 的第四个参数
// 这个 this 会原封不动传到 Routine 的 arg 参数里
// 把 void* 转回 Thread*,就能访问成员了
//
// 流程:
// Start() → pthread_create(..., this)
// ↓
// Routine(void* arg) ← pthread 调用这里
// ↓
// Thread* ts = (Thread*)arg ← 转回 this
// ↓
// ts->_func(ts->_data) ← 调用你真正想执行的函数
static void *Routine(void *arg)
{
// void* → Thread*,C++用 static_cast 做安全转换
Thread *ts = static_cast<Thread *>(arg);
// 调用用户注册的函数,把数据传进去
// 这一步就是"蹦床":从 C 风格回调跳转到 C++ 函数
ts->_func(ts->_data);
return nullptr; // pthread 要求返回 void*
}
private:
std::string _name; // 线程名称(调试用)
pthread_t _tid; // 线程ID
func_t<T> _func; // 用户注册的线程函数
T &_data; // 传给线程函数的数据(引用,不拷贝)
};
总览

v1_echo/ --- UDP 回显服务器


UdpServer.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "../common/nopy.hpp"
#include "../common/Log.hpp"
#include "../common/Comm.hpp"
#include "../common/InetAddr.hpp"
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;
// UDP回显服务器(声明)
class UdpServer : public nopy
{
public:
UdpServer(uint16_t port = defaultport);
void Init(); // 创建socket并绑定端口
void Start(); // 启动事件循环
~UdpServer();
private:
uint16_t _port;
int _sockfd;
};
UdpServer.cpp
cpp
#include "UdpServer.hpp"
// 构造函数:初始化端口和sockfd
UdpServer::UdpServer(uint16_t port)
: _port(port), _sockfd(defaultfd)
{
}
// 初始化:创建socket → 绑定端口
void UdpServer::Init()
{
// 1. 创建socket,SOCK_DGRAM表示UDP协议
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
lg.LogMessage(Fatal, "socket errrr, %d : %s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);
// 2. 绑定网络信息(IP + 端口)
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)
{
lg.LogMessage(Fatal, "bind errrr, %d : %s\n", errno, strerror(errno));
exit(Bind_Err);
}
}
// 启动服务器:循环接收消息并原样回显
void UdpServer::Start()
{
char buffer[defaultsize];
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (n > 0)
{
InetAddr addr(peer);
buffer[n] = 0;
std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;
// 原样发回给客户端(回显)
sendto(_sockfd, buffer, strlen(buffer), 0,
(struct sockaddr *)&peer, len);
}
}
}
UdpServer::~UdpServer()
{
}
UdpClient.hpp
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
// ./udp_client server_ip server_port
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]);
// 1. 创建socket,UDP协议
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
std::cout << "create socket success: " << sock << std::endl;
// 2. client要不要进行bind?
// 答:一定要bind!但是不需要显式bind。
// client会在首次发送数据时由操作系统自动bind一个随机端口。
// 原因:server的端口必须众所周知且固定,而client数量很多,
// 让OS随机分配端口即可,无需用户手动指定。
// 2.1 填充server地址信息
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()); // 点分十进制转网络字节序
while (true)
{
// 从标准输入读取用户要发送的数据
std::string inbuffer;
std::cout << "Please Enter# ";
std::getline(std::cin, inbuffer);
// 发送数据给server
ssize_t n = sendto(sock, inbuffer.c_str(), inbuffer.size(), 0,
(struct sockaddr *)&server, sizeof(server));
if (n > 0)
{
char buffer[1024];
// 接收server的回显消息
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t m = recvfrom(sock, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&temp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << "server echo# " << buffer << std::endl;
}
else
break;
}
else
break;
}
close(sock);
return 0;
}
main.cpp
cpp
#include "UdpServer.hpp"
#include "../common/Comm.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}
// ./udp_server 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]);
// 创建UdpServer对象,初始化并启动
std::unique_ptr<UdpServer> usvr(new UdpServer(port));
usvr->Init();
usvr->Start();
return 0;
}
makefile
cpp
CC=g++
CFLAGS=-std=c++11 -Wall -g
LDFLAGS=-lpthread
.PHONY: all clean
all: udp_server udp_client
# 服务器:Main.cpp + UdpServer.cpp
udp_server: Main.cpp UdpServer.cpp UdpServer.hpp
$(CC) $(CFLAGS) -o $@ Main.cpp UdpServer.cpp $(LDFLAGS)
# 客户端
udp_client: UdpClient.cpp
$(CC) $(CFLAGS) -o $@ UdpClient.cpp $(LDFLAGS)
clean:
rm -f udp_server udp_client
这里简单介绍一下内容,具体makefile的学习,可以去看我主页的文章:二、Linux基础开发工具(2)-CSDN博客
bash
# Makefile 的本质:告诉 make 工具"谁依赖谁,怎么编译"
#
# 基本语法:
# 目标: 依赖文件
# 编译命令(必须用 Tab 缩进,不能用空格)
#
# make 的工作方式:
# 1. 你敲 make,它找第一个目标(这里是 all)
# 2. all 依赖 udp_server 和 udp_client
# 3. udp_server 依赖 Main.cpp 和 UdpServer.hpp
# 4. 如果 Main.cpp 或 UdpServer.hpp 比 udp_server 新(被改过)
# → 重新编译生成 udp_server
# 5. 如果 udp_server 已经是最新的 → 跳过,不重新编译
# ---- 变量定义 ----
# CC: 用什么编译器
CC=g++
# CFLAGS: 编译选项
# -std=c++11 --- 用 C++11 标准(支持 lambda、auto 等)
# -Wall --- 打开所有警告(养成好习惯,警告也要修)
# -g --- 生成调试信息(用 gdb 调试时需要)
CFLAGS=-std=c++11 -Wall -g
# LDFLAGS: 链接选项
# -lpthread --- 链接 pthread 库(线程相关函数在这个库里)
# 不加的话,pthread_create 等函数会报"未定义引用"
LDFLAGS=-lpthread
# ---- 伪目标 ----
# .PHONY 声明 all 和 clean 不是真实文件名
# 为什么需要?
# 如果你目录下碰巧有个文件叫 "all" 或 "clean"
# make 会认为"目标已经存在",不执行命令
# .PHONY 告诉 make:这两个是命令,不是文件,每次都执行
.PHONY: all clean
# ---- 默认目标 ----
# all 是第一个目标,敲 make 不加参数就执行它
# 它依赖 udp_server 和 udp_client
# make 会先检查 udp_server 需不需要编译,再检查 udp_client
all: udp_server udp_client
# ---- 编译规则:生成 udp_server ----
# udp_server 是目标(要生成的可执行文件)
# Main.cpp 和 UdpServer.hpp 是依赖(源文件)
#
# make 的判断逻辑:
# - 如果 udp_server 不存在 → 执行命令
# - 如果 Main.cpp 或 UdpServer.hpp 比 udp_server 新 → 执行命令
# - 否则跳过("已经是最新的")
#
# 编译命令拆解:
# $(CC) → g++
# $(CFLAGS) → -std=c++11 -Wall -g
# -o $@ → -o udp_server($@ 是自动变量,表示"目标名")
# Main.cpp → 要编译的源文件
# $(LDFLAGS) → -lpthread
#
# 为什么不写 g++ 而写 $(CC)?
# 万一以后要换编译器(比如 clang++),只改第一行 CC=clang++ 就行
# 不用每个规则都改
udp_server: Main.cpp UdpServer.hpp
$(CC) $(CFLAGS) -o $@ Main.cpp $(LDFLAGS)
# ---- 编译规则:生成 udp_client ----
# 同理,UdpClient.cpp 变了才重新编译
udp_client: UdpClient.cpp
$(CC) $(CFLAGS) -o $@ UdpClient.cpp $(LDFLAGS)
# ---- 清理规则 ----
# 敲 make clean 删除编译产物,重新编译
# rm -f:强制删除,文件不存在也不报错
clean:
rm -f udp_server udp_client
v2_dict/ --- 英译汉网络字典(回调版)


UdpServer.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include <functional>
#include "../common/nopy.hpp"
#include "../common/Log.hpp"
#include "../common/Comm.hpp"
#include "../common/InetAddr.hpp"
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;
// 回调函数类型
using func_t = std::function<void(const std::string &req, std::string *resp)>;
// 带回调的UDP服务器(声明)
class UdpServer : public nopy
{
public:
UdpServer(func_t func, uint16_t port = defaultport);
void Init(); // 创建socket并绑定端口
void Start(); // 启动事件循环
~UdpServer();
private:
func_t _func; // 业务回调函数
uint16_t _port;
int _sockfd;
};
UdpServer.cpp
cpp
#include "UdpServer.hpp"
// 构造函数
UdpServer::UdpServer(func_t func, uint16_t port)
: _func(func), _port(port), _sockfd(defaultfd)
{
}
// 初始化:创建socket → 绑定端口
void UdpServer::Init()
{
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
lg.LogMessage(Fatal, "socket errrr, %d : %s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Info, "socket success, sockfd: %d\n", _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)
{
lg.LogMessage(Fatal, "bind errrr, %d : %s\n", errno, strerror(errno));
exit(Bind_Err);
}
}
// 启动服务器:收消息 → 调用回调处理 → 发回响应
void UdpServer::Start()
{
char buffer[defaultsize];
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (n > 0)
{
InetAddr addr(peer);
buffer[n] = 0;
std::cout << "[" << addr.PrintDebug() << "]# " << buffer << std::endl;
// 调用回调函数处理业务逻辑(翻译)
std::string value;
_func(buffer, &value);
// 将处理结果发回客户端
sendto(_sockfd, value.c_str(), value.size(), 0,
(struct sockaddr *)&peer, len);
}
}
}
UdpServer::~UdpServer() {}
UdpClient.cpp
cpp
#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
// ./dict_client server_ip server_port
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]);
// 1. 创建socket
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
std::cout << "create socket success: " << sock << std::endl;
// 2. 填充server地址信息
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());
while (true)
{
// 用户输入要查询的单词
std::string word;
std::cout << "请输入您要查的单词: ";
std::cin >> word;
if (!std::cin) // 检测输入流是否结束(Ctrl+D)
{
std::cout << "Good Bye" << std::endl;
break;
}
// 发送查询请求给server
ssize_t n = sendto(sock, word.c_str(), word.size(), 0,
(struct sockaddr *)&server, sizeof(server));
if (n > 0)
{
char buffer[1024];
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
// 接收server返回的翻译结果
ssize_t m = recvfrom(sock, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&temp, &len);
if (m > 0)
{
buffer[m] = 0;
std::cout << word << " 意思是 " << buffer << std::endl;
}
else
break;
}
else
break;
}
close(sock);
return 0;
}
Makefile
cpp
CC=g++
CFLAGS=-std=c++11 -Wall -g
LDFLAGS=-lpthread
.PHONY: all clean
all: dict_server dict_client
# 服务器:Main.cpp + UdpServer.cpp + Dict.cpp
dict_server: Main.cpp UdpServer.cpp Dict.cpp UdpServer.hpp Dict.hpp
$(CC) $(CFLAGS) -o $@ Main.cpp UdpServer.cpp Dict.cpp $(LDFLAGS)
# 客户端
dict_client: UdpClient.cpp
$(CC) $(CFLAGS) -o $@ UdpClient.cpp $(LDFLAGS)
clean:
rm -f dict_server dict_client
Main.cpp
cpp
#include "UdpServer.hpp"
#include "Dict.hpp"
#include "../common/Comm.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}
// 全局字典对象,加载词典文件
Dict gdict("./dict.txt");
// 回调函数:调用字典的翻译方法
void Execute(const std::string &req, std::string *resp)
{
*resp = gdict.Translate(req);
}
// ./udp_server 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]);
// 创建服务器,注入翻译回调函数
std::unique_ptr<UdpServer> usvr(new UdpServer(Execute, port));
usvr->Init();
usvr->Start();
return 0;
}
Dict.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
// 用 const char* 而不是 const std::string
// 因为 const std::string 是运行时初始化,跨编译单元的全局变量构造顺序不确定
// 可能 Dict 构造时 sep 还没初始化 → 空字符串 → 解析失败
// const char* 是编译期常量,不存在这个问题
const char *const sep = ": ";
// 网络字典类(声明)
class Dict
{
public:
Dict(const std::string &confpath); // 构造时加载词典
std::string Translate(const std::string &key); // 查询单词
~Dict();
private:
void LoadDict(); // 从文件加载词典到内存
std::string _confpath;
std::unordered_map<std::string, std::string> _dict;
};
Dict.cpp
cpp
#include "Dict.hpp"
#include <cstring> // strlen
// 构造函数:保存词典文件路径,立即加载
Dict::Dict(const std::string &confpath) : _confpath(confpath)
{
LoadDict();
}
// 从文件逐行读取,按 ": " 分割成 key-value 存入哈希表
void Dict::LoadDict()
{
std::ifstream in(_confpath);
if (!in.is_open())
{
std::cerr << "open file error" << std::endl;
return;
}
std::string line;
while (std::getline(in, line))
{
if (line.empty())
continue;
auto pos = line.find(sep);
if (pos == std::string::npos)
continue;
std::string key = line.substr(0, pos); // 英文单词
std::string value = line.substr(pos + strlen(sep)); // 中文翻译
_dict.insert(std::make_pair(key, value));
}
in.close();
}
// 查询:找到返回翻译,未找到返回 "Unknown"
std::string Dict::Translate(const std::string &key)
{
auto iter = _dict.find(key);
if (iter == _dict.end())
return std::string("Unknown");
else
return iter->second;
}
Dict::~Dict()
{
}
v2_dict_wrap/ --- 英译汉网络字典(封装版)


udp_socket.hpp
cpp
#pragma once
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <cassert>
#include <string>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
// UDP socket封装类(声明)
class UdpSocket
{
public:
UdpSocket();
bool Socket(); // 创建socket
bool Close(); // 关闭socket
bool Bind(const std::string &ip, uint16_t port); // 绑定地址
bool RecvFrom(std::string *buf, std::string *ip = NULL, uint16_t *port = NULL); // 接收数据
bool SendTo(const std::string &buf, const std::string &ip, uint16_t port); // 发送数据
private:
int fd_;
};
udp_socket.cpp
cpp
#include "udp_socket.hpp"
UdpSocket::UdpSocket() : fd_(-1) {}
// 创建UDP socket
bool UdpSocket::Socket()
{
fd_ = socket(AF_INET, SOCK_DGRAM, 0);
if (fd_ < 0)
{
perror("socket");
return false;
}
return true;
}
// 关闭socket
bool UdpSocket::Close()
{
close(fd_);
return true;
}
// 绑定IP和端口
bool UdpSocket::Bind(const std::string &ip, uint16_t port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
int ret = bind(fd_, (sockaddr *)&addr, sizeof(addr));
if (ret < 0)
{
perror("bind");
return false;
}
return true;
}
// 接收数据,同时获取发送方的IP和端口
bool UdpSocket::RecvFrom(std::string *buf, std::string *ip, uint16_t *port)
{
char tmp[1024 * 10] = {0};
sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t read_size = recvfrom(fd_, tmp, sizeof(tmp) - 1, 0,
(sockaddr *)&peer, &len);
if (read_size < 0)
{
perror("recvfrom");
return false;
}
buf->assign(tmp, read_size);
if (ip != NULL)
{
*ip = inet_ntoa(peer.sin_addr);
}
if (port != NULL)
{
*port = ntohs(peer.sin_port);
}
return true;
}
// 发送数据到指定地址
bool UdpSocket::SendTo(const std::string &buf, const std::string &ip, uint16_t port)
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(ip.c_str());
addr.sin_port = htons(port);
ssize_t write_size = sendto(fd_, buf.data(), buf.size(), 0,
(sockaddr *)&addr, sizeof(addr));
if (write_size < 0)
{
perror("sendto");
return false;
}
return true;
}
udp_server.hpp
cpp
#pragma once
#include "udp_socket.hpp"
#include <functional>
typedef std::function<void(const std::string &, std::string *)> Handler;
// 通用UDP服务器(声明)
class UdpServer
{
public:
UdpServer();
~UdpServer();
bool Start(const std::string &ip, uint16_t port, Handler handler);
private:
UdpSocket sock_;
};
udp_server.cpp
cpp
#include "udp_server.hpp"
UdpServer::UdpServer()
{
assert(sock_.Socket());
}
UdpServer::~UdpServer()
{
sock_.Close();
}
// 启动服务器:绑定端口 → 事件循环(收请求 → 调回调 → 发响应)
bool UdpServer::Start(const std::string &ip, uint16_t port, Handler handler)
{
bool ret = sock_.Bind(ip, port);
if (!ret)
{
return false;
}
for (;;)
{
std::string req;
std::string remote_ip;
uint16_t remote_port = 0;
bool ret = sock_.RecvFrom(&req, &remote_ip, &remote_port);
if (!ret)
{
continue;
}
std::string resp;
handler(req, &resp);
sock_.SendTo(resp, remote_ip, remote_port);
printf("[%s:%d] req: %s, resp: %s\n",
remote_ip.c_str(), remote_port,
req.c_str(), resp.c_str());
}
sock_.Close();
return true;
}
udp_client.hpp
cpp
#pragma once
#include "udp_socket.hpp"
// 通用UDP客户端(声明)
class UdpClient
{
public:
UdpClient(const std::string &ip, uint16_t port);
~UdpClient();
bool RecvFrom(std::string *buf); // 从服务器接收数据
bool SendTo(const std::string &buf); // 发送数据到服务器
private:
UdpSocket sock_;
std::string ip_; // 服务器IP
uint16_t port_; // 服务器端口
};
udp_client.cpp
cpp
#include "udp_client.hpp"
UdpClient::UdpClient(const std::string &ip, uint16_t port) : ip_(ip), port_(port)
{
assert(sock_.Socket());
}
UdpClient::~UdpClient()
{
sock_.Close();
}
bool UdpClient::RecvFrom(std::string *buf)
{
return sock_.RecvFrom(buf);
}
bool UdpClient::SendTo(const std::string &buf)
{
return sock_.SendTo(buf, ip_, port_);
}
Makefile
cpp
CC=g++
CFLAGS=-std=c++11 -Wall -g
LDFLAGS=-lpthread
.PHONY: all clean
all: dict_server dict_client
# 服务器:dict_server.cc + udp_server.cpp + udp_socket.cpp
dict_server: dict_server.cc udp_server.cpp udp_socket.cpp udp_server.hpp udp_socket.hpp
$(CC) $(CFLAGS) -o $@ dict_server.cc udp_server.cpp udp_socket.cpp $(LDFLAGS)
# 客户端:dict_client.cc + udp_client.cpp + udp_socket.cpp
dict_client: dict_client.cc udp_client.cpp udp_socket.cpp udp_client.hpp udp_socket.hpp
$(CC) $(CFLAGS) -o $@ dict_client.cc udp_client.cpp udp_socket.cpp $(LDFLAGS)
clean:
rm -f dict_server dict_client
dict_server.cc
cpp
#include "udp_server.hpp"
#include <unordered_map>
#include <iostream>
// 全局词典:英文 -> 中文
std::unordered_map<std::string, std::string> g_dict;
// 翻译回调函数:查找词典并返回结果
void Translate(const std::string &req, std::string *resp)
{
auto it = g_dict.find(req);
if (it == g_dict.end())
{
*resp = "未查到!";
return;
}
*resp = it->second;
}
// ./dict_server [ip] [port]
int main(int argc, char *argv[])
{
if (argc != 3)
{
printf("Usage ./dict_server [ip] [port]\n");
return 1;
}
// 1. 数据初始化:向词典中插入单词
g_dict.insert(std::make_pair("hello", "你好"));
g_dict.insert(std::make_pair("world", "世界"));
g_dict.insert(std::make_pair("c++", "最好的编程语言"));
g_dict.insert(std::make_pair("bit", "特别NB"));
// 2. 启动服务器,传入翻译回调函数
UdpServer server;
server.Start(argv[1], atoi(argv[2]), Translate);
return 0;
}
dict_client.cc
cpp
#include "udp_client.hpp"
#include <iostream>
// ./dict_client [ip] [port]
int main(int argc, char *argv[])
{
if (argc != 3)
{
printf("Usage ./dict_client [ip] [port]\n");
return 1;
}
// 创建客户端,连接到指定的服务器地址
UdpClient client(argv[1], atoi(argv[2]));
for (;;)
{
// 用户输入要查询的单词
std::string word;
std::cout << "请输入您要查的单词: ";
std::cin >> word;
if (!std::cin) // Ctrl+D结束输入
{
std::cout << "Good Bye" << std::endl;
break;
}
// 发送查询请求
client.SendTo(word);
// 接收翻译结果
std::string result;
client.RecvFrom(&result);
std::cout << word << " 意思是 " << result << std::endl;
}
return 0;
}
v3_chat/ --- UDP 聊天室(线程池广播)


UdpServer.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <functional>
#include <pthread.h>
#include <vector>
#include "../common/nopy.hpp"
#include "../common/Log.hpp"
#include "../common/Comm.hpp"
#include "../common/ThreadPool.hpp"
#include "../common/LockGuard.hpp"
#include "InetAddr.hpp"
const static uint16_t defaultport = 8888;
const static int defaultfd = -1;
const static int defaultsize = 1024;
using task_t = std::function<void()>; // 线程池任务类型
// UDP聊天室服务器(声明)
// 功能:接收任意客户端消息,广播给所有在线用户
// 使用线程池处理广播任务,避免在主线程中阻塞发送
class UdpServer : public nopy
{
public:
UdpServer(uint16_t port = defaultport); // 构造函数
void Init(); // 初始化socket、绑定端口、启动线程池
void AddOnlineUser(InetAddr addr); // 添加在线用户(去重)
void Route(int sock, const std::string &message); // 广播消息给所有在线用户
void Start(); // 启动服务器事件循环
~UdpServer(); // 析构函数
private:
uint16_t _port; // 监听端口
int _sockfd; // socket文件描述符
std::vector<InetAddr> _online_user; // 在线用户列表(多线程共享)
pthread_mutex_t _user_mutex; // 保护在线用户列表的互斥锁
};
UdpServer.cpp
cpp
#include "UdpServer.hpp"
// 构造函数:初始化端口、sockfd、互斥锁
UdpServer::UdpServer(uint16_t port)
: _port(port), _sockfd(defaultfd)
{
pthread_mutex_init(&_user_mutex, nullptr);
}
// 初始化:创建socket → 绑定端口 → 启动线程池
void UdpServer::Init()
{
// 1. 创建UDP socket
_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (_sockfd < 0)
{
lg.LogMessage(Fatal, "socket errrr, %d : %s\n", errno, strerror(errno));
exit(Socket_Err);
}
lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);
// 2. 绑定地址(INADDR_ANY 表示接受任意网卡的连接)
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)
{
lg.LogMessage(Fatal, "bind errrr, %d : %s\n", errno, strerror(errno));
exit(Bind_Err);
}
// 3. 启动线程池
ThreadPool<task_t>::GetInstance()->Start();
}
// 将新用户添加到在线列表(加锁去重)
void UdpServer::AddOnlineUser(InetAddr addr)
{
LockGuard lockguard(&_user_mutex);
for (auto &user : _online_user)
{
if (addr == user) // 已在列表中,不重复添加
return;
}
_online_user.push_back(addr);
lg.LogMessage(Debug, "%s:%d is add to onlinuser list...\n",
addr.Ip().c_str(), addr.Port());
}
// 广播消息给所有在线用户(加锁遍历发送)
void UdpServer::Route(int sock, const std::string &message)
{
LockGuard lockguard(&_user_mutex);
for (auto &user : _online_user)
{
sendto(sock, message.c_str(), message.size(), 0,
(struct sockaddr *)&user.GetAddr(), sizeof(user.GetAddr()));
lg.LogMessage(Debug, "server send message to %s:%d, message: %s\n",
user.Ip().c_str(), user.Port(), message.c_str());
}
}
// 启动服务器:循环接收消息,提交广播任务到线程池
void UdpServer::Start()
{
char buffer[defaultsize];
for (;;)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&peer, &len);
if (n > 0)
{
InetAddr addr(peer);
AddOnlineUser(addr);
buffer[n] = 0;
// 拼接带发送者信息的消息:[ip:port]# 消息内容
std::string message = "[";
message += addr.Ip();
message += ":";
message += std::to_string(addr.Port());
message += "]# ";
message += buffer;
// 提交广播任务到线程池
task_t task = std::bind(&UdpServer::Route, this, _sockfd, message);
ThreadPool<task_t>::GetInstance()->Push(task);
}
}
}
// 析构函数:销毁互斥锁
UdpServer::~UdpServer()
{
pthread_mutex_destroy(&_user_mutex);
}
UdpClient.hpp
cpp
#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "../common/Thread.hpp"
#include "InetAddr.hpp"
// 打印用法提示
void Usage(const std::string &process)
{
std::cout << "Usage: " << process << " server_ip server_port" << std::endl;
}
// 线程数据类:封装socket和服务器地址,传递给收发线程
class ThreadData
{
public:
ThreadData(int sock, struct sockaddr_in &server)
: _sockfd(sock), _serveraddr(server)
{
}
~ThreadData() {}
public:
int _sockfd; // socket文件描述符
InetAddr _serveraddr; // 服务器地址
};
// 接收线程函数声明
void RecverRoutine(ThreadData &td);
// 发送线程函数声明
void SenderRoutine(ThreadData &td);
UdpClient.cpp
cpp
#include "UdpClient.hpp"
// 接收线程函数:持续从服务器接收消息并打印到stderr
// 用stderr输出,避免与stdout的输入提示混在一起
void RecverRoutine(ThreadData &td)
{
char buffer[4096];
while (true)
{
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
ssize_t n = recvfrom(td._sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr *)&temp, &len);
if (n > 0)
{
buffer[n] = 0;
std::cerr << buffer << std::endl;
}
else
break;
}
}
// 发送线程函数:持续从标准输入读取用户消息并发送给服务器
void SenderRoutine(ThreadData &td)
{
while (true)
{
std::string inbuffer;
std::cout << "Please Enter# ";
std::getline(std::cin, inbuffer);
auto server = td._serveraddr.GetAddr();
ssize_t n = sendto(td._sockfd, inbuffer.c_str(), inbuffer.size(), 0,
(struct sockaddr *)&server, sizeof(server));
if (n <= 0)
std::cout << "send error" << std::endl;
}
}
// 主函数:创建socket,启动收发两个线程
// ./chat_client server_ip server_port
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]);
// 1. 创建socket
// UDP是全双工的:一个sockfd可以同时读写
int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0)
{
std::cerr << "socket error: " << strerror(errno) << std::endl;
return 2;
}
std::cout << "create socket success: " << sock << std::endl;
// 2. 填充server地址信息(client不需要显式bind,首次sendto时OS自动分配随机端口)
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());
// 3. 创建收发两个线程
ThreadData td(sock, server);
Thread<ThreadData> recver("recver", RecverRoutine, td);
Thread<ThreadData> sender("sender", SenderRoutine, td);
// 4. 启动线程并等待
recver.Start();
sender.Start();
recver.Join();
sender.Join();
close(sock);
return 0;
}
Makefile
cpp
CC=g++
CFLAGS=-std=c++11 -Wall -g
LDFLAGS=-lpthread
.PHONY: all clean
all: chat_server chat_client
# 服务器:Main.cpp + UdpServer.cpp + InetAddr.cpp
chat_server: Main.cpp UdpServer.cpp InetAddr.cpp UdpServer.hpp InetAddr.hpp
$(CC) $(CFLAGS) -o $@ Main.cpp UdpServer.cpp InetAddr.cpp $(LDFLAGS)
# 客户端:UdpClient.cpp + InetAddr.cpp
chat_client: UdpClient.cpp UdpClient.hpp InetAddr.cpp InetAddr.hpp
$(CC) $(CFLAGS) -o $@ UdpClient.cpp InetAddr.cpp $(LDFLAGS)
clean:
rm -f chat_server chat_client
Main.cpp
cpp
#include "UdpServer.hpp"
#include "../common/Comm.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}
// ./chat_server 8888
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
return Usage_Err;
}
uint16_t port = std::stoi(argv[1]);
// 创建聊天服务器,初始化并启动
std::unique_ptr<UdpServer> usvr(new UdpServer(port));
usvr->Init();
usvr->Start();
return 0;
}
InetAddr.hpp
cpp
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
// 网络地址封装类(声明)
// 把 sockaddr_in 翻译成人能看懂的 "ip:port" 格式
class InetAddr
{
public:
InetAddr(struct sockaddr_in &addr); // 构造时自动翻译
std::string Ip(); // 返回IP字符串,如 "127.0.0.1"
uint16_t Port(); // 返回端口号,如 8888
std::string PrintDebug(); // 返回 "ip:port" 格式
const struct sockaddr_in &GetAddr(); // 获取原始结构体(给sendto用)
bool operator==(const InetAddr &addr); // 判断两个地址是否相同
~InetAddr();
private:
std::string _ip; // 翻译后的IP字符串
uint16_t _port; // 翻译后的端口号
struct sockaddr_in _addr; // 保存原始结构体
};
InetAddr.cpp
cpp
#include "InetAddr.hpp"
// 构造函数:拿到 sockaddr_in 后立刻翻译成字符串
InetAddr::InetAddr(struct sockaddr_in &addr) : _addr(addr)
{
// ntohs: 网络字节序 → 主机字节序(端口)
_port = ntohs(_addr.sin_port);
// inet_ntoa: 32位二进制IP → "127.0.0.1" 字符串
// 返回值指向静态内存,所以用 std::string 拷贝保存
_ip = inet_ntoa(_addr.sin_addr);
}
std::string InetAddr::Ip()
{
return _ip;
}
uint16_t InetAddr::Port()
{
return _port;
}
// 返回 "ip:port" 格式,用于日志打印
std::string InetAddr::PrintDebug()
{
std::string info = _ip;
info += ":";
info += std::to_string(_port);
return info;
}
// 获取原始结构体,给 sendto 发送数据时用
const struct sockaddr_in &InetAddr::GetAddr()
{
return _addr;
}
// 重载 ==:IP相同 且 端口相同 → 同一个客户端
bool InetAddr::operator==(const InetAddr &addr)
{
return this->_ip == addr._ip && this->_port == addr._port;
}
InetAddr::~InetAddr()
{
}
完结