二、Socket编程UDP

一、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 就是那把刀。

  1. 基础设施先行(Infrastructure First)

先搭建底层工具,再写业务代码。就像盖房子先打地基,而不是边盖边挖。

  1. 关注点分离(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。

所以:锁放在构造函数里加,析构函数里解。

你自己写的话,就这三个问题

  1. 构造函数干什么?→ pthread_mutex_lock

  2. 析构函数干什么?→ pthread_mutex_unlock

  3. 需要保存什么?→ 一个 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") 打了一条日志。过两天回来看,发现三个问题:

  1. 不知道什么时候打的 ------ 没有时间戳

  2. 不知道严不严重 ------ 是调试信息还是致命错误?

  3. 不知道哪个进程打的 ------ 多进程程序分不清

所以: 封装一个日志类,自动补上等级、时间、进程号,还支持 printf 风格的格式化输出。而且做成全局对象,任何文件 include 就能用,不用传参。

[Warning][2025-01-15 14:30:22][pid:1234] bind failed, errno: 98
^等级 ^时间戳 ^进程号 ^你的消息

自己写之前问自己

  1. 有哪些等级?→ Debug / Info / Warning / Error / Fatal

  2. 输出到哪里?→ 屏幕(默认),可选文件

  3. 怎么格式化?→ 等级 + 时间 + 用户消息

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)

写之前问自己

  1. 谁往队列里塞任务?→ 主线程(Push)

  2. 谁从队列里取任务?→ 工作线程(Pop)

  3. 队列空了怎么办?→ 条件变量等待(pthread_cond_wait)

  4. 塞了任务怎么办?→ 唤醒等待的线程(pthread_cond_signal)

  5. 几个线程?→ 构造时指定,默认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();         // 有了,取出来

重点理清几个可能没看懂的地方:

  1. 为什么单例要判断两次?
cpp 复制代码
  if (instance == nullptr)        // 第一次:没锁,快速判断,避免每次都加锁
      LockGuard lock(&mutex);
      if (instance == nullptr)    // 第二次:有锁了,真正确认

第一次是"偷看"(省性能),第二次是"确认"(防并发)。

  1. 为什么 while(IsEmpty()) 而不是 if?

线程可能被"虚假唤醒"------没人 Push 但线程醒了。while 醒来后再检查一次,空就继续睡。if 不检查,直接取空队列就崩了。

  1. 为什么任务在锁外面执行?

锁内执行任务:线程A干活 → 线程B等着 → 线程A干完 → 线程B才能取任务

锁外执行任务:线程A取任务 → 解锁 → 线程B也能取 → 两人同时干

锁内执行就变成单线程了,白创建那么多线程。

  1. 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();    // 一行等待

自己写之前问自己

  1. 线程函数的签名是什么?→ void*(*)(void*)(C风格)

  2. 用户想传什么?→ 一个函数 + 一份数据

  3. 怎么把 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()
{
}

完结

相关推荐
无相孤君9 小时前
我用 Docker + JunimoServer 搭了一个星露谷物语无头服,还顺手做了个本地管理面板
linux·游戏·docker·开源
谪星·阿凯9 小时前
内网渗透之横向移动实战
网络·web安全·网络安全
爱吃龙利鱼9 小时前
ubuntu2026.04部署k8s1.36版本的傻瓜式教程(注:运行时为docker,网络插件为calico)
运维·网络·笔记·docker·云原生·kubernetes
L、21810 小时前
CANN异构计算实践:CPU+NPU协同工作的最佳模式
网络·人工智能·pytorch·python·安全
汤愈韬10 小时前
IP安全 SEC VPN_1_IA阶段各种名词讲解
网络·网络协议·安全·网络安全·security
浮生若城10 小时前
Linux基础I/O(2):理解“一切皆文件”与缓冲区
linux·运维·服务器
爱吃龙利鱼10 小时前
MobaXterm连接ubuntu26.04无法在vim界面粘贴问题解决方法(粘贴会提示进入进入可视模式VISUAL))
linux·ubuntu·编辑器·vim
.柒宇.10 小时前
Zabbix7.0部署完整指南
linux·运维·zabbix·监控
learndiary10 小时前
Linux 维修案例视频12则
linux·维修