云计算百科
云计算领域专业知识百科平台

Linux网络编程入门(二):从 UDP 回显服务器到多人聊天室,三个项目的渐进式实践

从 UDP 回显服务器到多人聊天室:三个项目的渐进式实践

这三个项目是我按顺序完成的:先做一个最小的 UDP 回显服务器,再把固定回显改成可替换的业务处理,最后把服务扩展成支持多个客户端的聊天室。

它们背后是一条清晰的学习路线:

阶段项目新增内容
1 1_echo_server UDP Socket 的创建、绑定和收发
2 2_echo_server 回调函数、业务逻辑与网络收发解耦
3 3_ChatServer 用户管理、消息广播、互斥锁、条件变量和线程池

三个项目都使用 UDP。UDP 提供数据报收发接口,但不保证数据一定到达,也不保证多个数据报按发送顺序到达。项目中的“聊天室”是一次面向应用层的练习,并没有实现 TCP 那样的可靠连接。

目录

  • 先认识程序中的通信对象
  • 第一步:完成 UDP 回显
  • 第二步:把业务处理抽成回调
  • 第三步:从回显服务扩展到聊天室
  • 三个项目串起来看
  • 运行方式
  • 当前实现的边界与后续改进
  • 完整代码
  • 一、先认识程序中的通信对象

    网络通信最终要把数据交给正确的应用程序。可以先用下面这组关系理解项目里的地址:

    • IP 地址定位一台主机;
    • 端口号帮助操作系统把数据交给主机上的某个网络服务;
    • Socket是应用程序使用网络收发数据的接口;
    • UDP 数据报到达服务器后,recvfrom 不仅返回消息,也会告诉服务器消息来自哪个地址。

    因此服务器回复 UDP 客户端时,需要把收到的对端地址交给 sendto。在第一个项目里,这个地址就是客户端的 sockaddr_in。

    二、第一步:完成 UDP 回显

    2.1 服务器启动时做什么

    1_echo_server 的服务器端主要完成三件事:

  • 调用 socket 创建 UDP Socket;
  • 填写本地地址和端口,并调用 bind,让操作系统把这个 Socket 与服务器端口关联起来;
  • 进入循环,用 recvfrom 收消息,再用 sendto 把回复发回消息来源。
  • 下面是服务器收发流程的核心代码:

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    sockaddr_in local{};
    local.sin_family = AF_INET;
    local.sin_port = htons(port);
    local.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, reinterpret_cast<sockaddr*>(&local),
    sizeof(local)) < 0) {
    // 处理绑定失败
    }

    while (true) {
    char buffer[1024] = {};
    sockaddr_in peer{};
    socklen_t peer_len = sizeof(peer);

    ssize_t size = recvfrom(
    sockfd, buffer, sizeof(buffer) – 1, 0,
    reinterpret_cast<sockaddr*>(&peer), &peer_len);

    if (size <= 0) {
    continue;
    }

    buffer[size] = '\\0';
    std::string reply = "server echo—";
    reply += buffer;

    sendto(
    sockfd, reply.c_str(), reply.size(), 0,
    reinterpret_cast<sockaddr*>(&peer), peer_len);
    }

    这里有几个值得记住的细节:

    • SOCK_DGRAM 表示使用 UDP;
    • htons 把端口转换成网络字节序;
    • INADDR_ANY 表示监听本机的所有 IPv4 地址;
    • UDP 不需要像 TCP 那样调用 listen 和 accept。服务器直接通过 recvfrom 收到一个数据报,并取得发送方地址;
    • 客户端第一次发送数据时,操作系统通常会自动分配本地端口,因此示例客户端没有显式调用 bind。

    2.2 客户端的一次请求与回复

    客户端创建 UDP Socket 后,准备服务器地址。每次输入一条消息,就向该地址调用 sendto,随后用 recvfrom 等待回显:

    sockaddr_in server{};
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);

    std::string message = "hello";
    sendto(sockfd, message.c_str(), message.size(), 0,
    reinterpret_cast<sockaddr*>(&server), sizeof(server));

    char buffer[1024] = {};
    sockaddr_in peer{};
    socklen_t peer_len = sizeof(peer);
    ssize_t size = recvfrom(
    sockfd, buffer, sizeof(buffer) – 1, 0,
    reinterpret_cast<sockaddr*>(&peer), &peer_len);

    if (size > 0) {
    buffer[size] = '\\0';
    std::cout << buffer << std::endl;
    }

    到这里,已经建立了一个完整的 UDP 请求—响应闭环。服务器的业务很简单:收到什么,就在前面加上 server echo— 再发回去。 在这里插入图片描述

    三、第二步:把业务处理抽成回调

    3.1 网络收发与业务逻辑分开

    第一个项目把“收到数据后如何处理”写在服务器循环里。这样做适合验证 Socket 流程,但业务一多,网络代码和业务代码就会缠在一起。

    在 2_echo_server 中,我把处理函数作为参数传给服务器:

    using callback_t = std::function<std::string(std::string)>;

    class UdpServer {
    public:
    UdpServer(callback_t cb, uint16_t port = default_port)
    : _sockfd(default_fd), _port(port), _cb(cb) {}

    private:
    int _sockfd;
    uint16_t _port;
    callback_t _cb;
    };

    收包循环只负责接收数据、调用业务函数,再把业务函数返回的结果发回客户端:

    std::string result = _cb(buffer);

    sendto(sockfd, result.c_str(), result.size(), 0,
    reinterpret_cast<sockaddr*>(&peer), peer_len);

    这样一来,UDP 服务器不必知道具体业务是回显、翻译还是其他操作。它只约定输入和输出:给回调一段消息,拿回一段回复。

    3.2 用词典实现一个翻译服务

    第二个项目增加了 Dict 类。它从 dict.txt 读取形如 apple: 苹果 的词条,保存到 unordered_map,再通过 Translate 查询结果:

    std::string Translate(std::string word) {
    auto it = _dict.find(word);
    if (it != _dict.end()) {
    return it->second;
    }
    return "None";
    }

    主程序把词典查询绑定到服务器回调上:

    Dict dict;

    UdpServer server(
    [&dict](std::string word) {
    return dict.Translate(word);
    },
    port);

    server.Init();
    server.Loop();

    这里的 [&dict] 表示 Lambda 引用外部的 dict 对象。由于服务器循环在 main 中运行,dict 在整个服务期间都还存在。

    这一阶段的变化不是 Socket API,而是程序结构:网络层负责收发,业务层负责回答,回调把两者连接起来。 以后替换业务时,可以保留收发框架,只更换回调实现。 在这里插入图片描述

    四、第三步:从回显服务扩展到聊天室

    第三个项目 3_ChatServer 把单次请求—响应扩展成了多个客户端之间的消息转发。它仍使用 UDP,但服务器需要记录客户端地址,并把一条消息发送给所有已记录的客户端。

    4.1 先把客户端地址封装起来

    InetAddr 把网络地址 sockaddr_in 和便于程序使用的 IP、端口信息放到同一个对象里。客户端地址通过 IP 和端口比较:

    bool operator==(const InetAddr& other) {
    return _ip == other._ip && _port == other._port;
    }

    对这个 UDP 示例来说,IP 加端口可以区分一次运行中的客户端端点。它并不等于用户身份认证:用户重启客户端后,操作系统可能分配不同的临时端口。

    4.2 在线用户表与消息广播

    UserManager 用 vector<InetAddr> 保存客户端地址,并提供添加、查找和删除操作。收到客户端数据报后,服务器先检查并记录这个地址,再将带有来源地址的信息交给路由模块。

    Route 负责两件事:

    • CheckUser:把新客户端加入用户表;
    • Broadcast:遍历用户表,对每个客户端调用 sendto。

    核心广播逻辑如下:

    void Broadcast(int sockfd, std::string message) {
    LockGuard guard(_lock);

    auto& users = _uma->Users();
    for (auto& user : users) {
    sendto(sockfd, message.c_str(), message.size(), 0,
    reinterpret_cast<sockaddr*>(user.GetNetAddress()),
    user.Len());
    }
    }

    这里的互斥锁保护用户表。原因是接收线程可能添加用户,而线程池中的工作线程同时遍历用户表进行广播。若二者并发修改和读取同一个 vector,就会产生数据竞争。用 LockGuard 管理锁,可以在离开作用域时自动解锁。当前这把锁也覆盖整个 sendto 循环,所以多个广播任务仍会在同一把锁上排队;后面的改进建议会缩短这段持锁时间。

    4.3 用线程池处理广播任务

    如果每次收包后都在网络接收循环里完成整轮广播,客户端越多,接收循环花在发送上的时间就越长。第三个项目把广播封装成任务,放入线程池:

    usvr.RegisterService(
    [&route](const InetAddr& address) {
    route.CheckUser(address);
    },
    [&route, thread_pool](int sockfd, std::string message) {
    auto task = std::bind(
    &Route::Broadcast, &route, sockfd, message);
    thread_pool->Enqueue(task);
    });

    线程池的工作流程可以概括为:

  • 网络线程收到消息,创建广播任务;
  • 任务进入受互斥锁保护的队列;
  • 如果工作线程正在条件变量上等待,生产者发出通知;
  • 工作线程醒来后重新检查队列,取出任务;
  • 工作线程释放队列锁,再执行广播任务。
  • 等待时需要在循环里重新检查条件,因为线程被唤醒时,队列可能仍为空,也可能已有其他工作线程先取走了任务:

    while (true) {
    std::function<void()> task;
    {
    LockGuard guard(_mutex);
    while (_isrunning && _tasks.empty()) {
    _cond.Wait(_mutex);
    }

    if (!_isrunning && _tasks.empty()) {
    break;
    }

    task = _tasks.front();
    _tasks.pop();
    }

    // 离开临界区后执行任务
    task();
    }

    互斥锁保护共享队列,条件变量让没有任务的工作线程休眠,避免空转占用 CPU。这正是生产者—消费者模型在项目中的一个小型应用:网络线程生产任务,工作线程消费任务。

    4.4 客户端同时收消息和发消息

    聊天室客户端用两个线程分别处理接收和发送:

    • 接收线程循环调用 recvfrom,把服务器转发来的消息打印到终端;
    • 发送线程读取用户输入,并调用 sendto 把消息发给服务器。

    用户先输入昵称,客户端发送上线消息;之后每条聊天消息都会加上昵称前缀,例如 小明$ 大家好。这是一个简单的文本协议,服务器目前只负责转发,并不解析昵称或消息类型。 在这里插入图片描述

    五、三个项目串起来看

    从项目一到项目三,网络通信的基本骨架一直没有变:

    创建 Socket
    ↓
    绑定服务器地址和端口
    ↓
    recvfrom 收到数据报和来源地址
    ↓
    调用对应的业务处理
    ↓
    sendto 把数据发给目标地址

    变化发生在“业务处理”部分:

    项目收到消息后做什么程序结构上的进步
    UDP 回显 加前缀后发回原客户端 跑通最小通信闭环
    词典服务 查询词典并返回释义 通过回调分离网络与业务
    聊天室 记录发送端,再广播给多个客户端 引入用户管理、并发任务和共享数据保护

    这条路线也把几个知识点自然地连在一起:Socket 地址和端口、UDP 收发、回调函数、容器、线程、互斥锁、条件变量以及线程池。每一步都建立在前一步的基础上,先把功能跑通,再逐步拆分职责、处理并发。

    六、运行方式

    三个项目的 Makefile 都使用 g++ -std=c++17。代码依赖 Linux/POSIX Socket 和 pthread 接口,下面的命令在 Linux 或 WSL 中、分别进入对应项目目录后执行。词典项目需要从包含 dict.txt 的目录启动服务器,因为词典路径是相对路径。

    # 项目一:终端 1 启动服务端
    cd 1_echo_server
    make
    ./server_udp 8888

    # 项目一:终端 2 启动客户端
    cd 1_echo_server
    ./client_udp 127.0.0.1 8888

    # 项目二:终端 1 启动服务端
    cd 2_echo_server
    make
    ./server_udp 8888

    # 项目二:终端 2 启动客户端
    cd 2_echo_server
    ./client_udp 127.0.0.1 8888

    # 项目三:终端 1 启动服务端
    cd 3_ChatServer
    make
    ./server_chat 8888

    # 项目三:在其他终端各启动一个客户端
    cd 3_ChatServer
    ./client_chat 127.0.0.1 8888

    如果工具链要求显式链接 pthread,可在 Makefile 的编译选项中加入 -pthread。

    七、当前实现的边界与后续改进

    这些项目适合学习和演示基本原理。继续完善时,可以优先考虑以下问题:

  • UDP 不保证送达和顺序。 当前程序没有确认、重传、去重或顺序控制;如果业务需要可靠传输,可以改用 TCP,或在应用层设计相应机制。
  • 在线用户没有超时清理。 项目中有 OffLineUser,但当前收发流程没有调用它。可以增加心跳和超时检测,清理失联客户端。
  • 消息协议还比较简单。 目前把昵称和正文拼成普通字符串。后续可以加入消息类型、长度、发送者标识等字段,并明确编码规则。
  • 收发缓冲区有固定大小。 示例一次最多处理有限长度的数据报,较长消息可能被截断;实际服务需要定义最大消息长度并检查发送、接收结果。
  • 广播时持有用户表锁。 当前代码在遍历用户表并调用 sendto 时一直持锁。可以在锁内复制一份地址列表,解锁后再逐个发送,缩短其他线程等待用户表锁的时间。
  • 对我来说,这三个项目的价值不只是“做出了一个回显服务器和一个聊天室”,而是把网络程序从能收发数据,一步步推进到职责拆分和并发处理。每个阶段都不大,但能清楚地看到下一层问题是怎么出现的,也能看到新概念为什么需要被引入。

    八、完整代码

    以下按项目分别收录源码、头文件、构建文件和运行所需的词典数据。为便于单独查看每个项目,重复文件也按原目录分别保留;编译生成的可执行文件不属于源代码,因此不收录。

    8.1 项目一:UDP 回显服务器

    EchoServer.hpp

    #pragma once
    #include <string.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <string>
    #include "Logger.hpp"

    using namespace NS_LOG_MODULE;

    const static int default_fd = –1;
    const static uint16_t default_port = 8888;

    enum {
    SUCCESS = 0,
    SOCKET_ERR,
    USAGE_ERR,
    BIND_ERR
    };

    class UdpServer
    {
    public:
    // UdpServer(const std::string &ip, uint16_t port = default_port)
    UdpServer(uint16_t port = default_port)
    :_sockfd(default_fd),
    _port(port)
    {}

    ~UdpServer()
    {
    close(_sockfd);
    }

    void Init() {
    // 第一步:创建socket,本质:打开网络这个文件
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0) {
    LOG(LogLevel::FATAL) << "create socket error";
    exit(SOCKET_ERR);
    }
    LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;

    // 第二步:填充网络属性信息
    struct sockaddr_in local{}; // {}结构体清零
    local.sin_family = AF_INET;
    local.sin_port = htons(_port); // hton
    // inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
    local.sin_addr.s_addr = INADDR_ANY;

    /*
    一台机器(尤其云服务器)通常不止一个 IP:
    地址|说明
    公网 IP|云厂商分配的对外地址
    内网 IP|比如 172.x.x.x / 10.x.x.x,VPC 内网用的
    回环|127.0.0.1
    如果写成 local.sin_addr.s_addr = inet_addr("某个具体IP"),
    那这个 socket 就只监听那一个 IP 的端口。比如绑了公网 IP,内网 IP 来的包就收不到。
    而 INADDR_ANY(0.0.0.0)会让内核把 socket 挂到所有本地地址上,
    无论客户端走公网、内网还是回环,只要目标是这个端口,都能收到。
    对服务器来说这是默认、最省心的选择。
    */

    // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串ip->4字节ip; 2.hton

    // 只是填充在了local中,并没有写入内核

    // 第三步:绑定套接字socket信息(写入内核)
    ssize_t r = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
    if (r < 0) {
    LOG(LogLevel::FATAL) << "绑定套接字失败:" << strerror(errno);
    exit(BIND_ERR);
    }
    LOG(LogLevel::INFO) << "绑定套接字成功。" << " port:" << _port;
    }

    void Loop() {
    char inbuffer[1024] = {0};
    while (true) {
    struct sockaddr_in peer;
    socklen_t peerlen = sizeof(peer);
    ssize_t r = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) – 1, 0, (struct sockaddr*)&peer, &peerlen);
    if (r < 0) {
    LOG(LogLevel::ERROR) << "recvfrom接收失败:" << strerror(errno);
    continue;
    }
    else if (r == 0) {
    LOG(LogLevel::WARNING) << "recvfrom接收空数据。";
    }
    else {
    // 客户端的信息
    uint16_t client_port = ntohs(peer.sin_port);
    char client_ip_char[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &peer.sin_addr, client_ip_char, sizeof(client_ip_char));
    std::string client_ip(client_ip_char);
    std::string client_addr = "[" + client_ip + ":" + std::to_string(client_port) + "]";

    // 用户发来的数据
    inbuffer[r] = 0;
    LOG(LogLevel::DEBUG) << "client [" << client_addr << "] say# " << inbuffer;
    std::string echo_string = "server echo—";
    echo_string += inbuffer;
    sendto(_sockfd, echo_string.c_str(), echo_string.size(), 0, (struct sockaddr*)&peer, peerlen);
    }
    }
    }
    private:
    int _sockfd;
    // std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址,给人看) && 4字节IP
    uint16_t _port;
    };

    EchoServerMain.cpp

    #include "EchoServer.hpp"
    #include <memory>

    static void Usage(const std::string &process) {
    std::cerr << "Usage:\\n\\t";
    std::cerr << process << " [local_port]" << std::endl;
    }

    // ./server_udp [ip] [port]
    int main(int argc, char* argv[]) {

    if (argc != 2) {
    Usage(argv[0]);
    exit(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG_STRATEGY()

    // std::string socket_ip = argv[1];
    uint16_t socket_port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(socket_port);
    usvr->Init();
    usvr->Loop();
    return 0;
    }

    EchoClient.cpp

    #include <iostream>
    #include <string>
    #include <string.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>

    void Usage(const std::string &proc) {
    std::cout << "Usage:\\n\\t";
    std::cout << proc << " [server_ip] [server_port]" << std::endl;
    }

    int main(int argc, char* argv[]) {

    if (argc != 3) {
    Usage(argv[0]);
    exit(1);
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
    std::cerr << "socket error:" << strerror(errno);
    exit(2);
    }

    // 2. 不需要显示的去bind绑定addr(IP+port)信息,udp client首次发送数据的时候,OS底层会隐式自动获取随机端口并绑定IP+port

    // 后面发数据的时候,只需要sockaddr就行
    struct sockaddr_in server{};
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);

    // 3. 循环执行客户端
    while (true) {

    std::string message;
    // 1. 获取用户输入
    std::cout << "请输入要发送的消息:";
    std::getline(std::cin, message);

    // 2. sendto发送数据给server
    ssize_t r = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
    if (r > 0) {
    // recvform
    char inbuffer[1024] = {0};
    struct sockaddr_in tmp{}; // 避免recvfrom()内参数填NULL,可能会出问题,只是存放收到的服务端的套接字
    socklen_t tmplen = sizeof(tmp);
    ssize_t n = recvfrom(sockfd, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&tmp, &tmplen);
    if (n > 0) {
    inbuffer[n] = 0;
    std::cout << inbuffer << std::endl;
    }
    }
    else {
    std::cerr << "sendto seeor:" << strerror(errno);
    exit(3);
    }

    }

    return 0;
    }

    Logger.hpp

    #pragma once
    #include <iostream>
    #include <string>
    #include <sys/time.h>
    #include <ctime>
    #include <cstdio>
    #include <unistd.h>
    #include <filesystem>
    #include <fstream>
    #include <memory>
    #include <sstream>
    #include "mutex.hpp"

    namespace NS_LOG_MODULE
    {

    // 日志等级信息
    enum class LogLevel
    {
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
    };

    // 为避免打印日志时,流式输出等级是数字
    std::string LogLevelToMessage(LogLevel level) {
    switch (level)
    {
    case LogLevel::DEBUG: return "DEBUG";
    case LogLevel::INFO: return "INFO";
    case LogLevel::WARNING: return "WARNING";
    case LogLevel::ERROR: return "ERROR";
    case LogLevel::FATAL: return "FATAL";
    default: return "UNKNOW";
    }
    }

    // 详细的时间信息
    std::string GetCurrentTime() {
    struct timeval current_time;
    gettimeofday(&current_time, nullptr);
    // current_time.tv_sec; current_time.tv_usec;

    struct tm struct_time;
    localtime_r(&(current_time.tv_sec), &struct_time);
    char timestr[128];
    snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
    struct_time.tm_year + 1900,
    struct_time.tm_mon + 1,
    struct_time.tm_mday,
    struct_time.tm_hour,
    struct_time.tm_min,
    struct_time.tm_sec,
    current_time.tv_usec
    );
    return timestr;
    }

    // 策略模式,策略接口
    class LogStrategy
    {
    public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印策略
    class ConsoleStrategy : public LogStrategy
    {
    public:
    ~ConsoleStrategy()
    {}
    void SyncLog(const std::string &message) override
    {
    LockGuard lg(_mutex);
    std::cout << message << std::endl;
    }

    private:
    Mutex _mutex;
    };

    // 文件打印策略
    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";
    class FileStrategy : public LogStrategy
    {
    public:
    FileStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename)
    :_logpath(path)
    ,_logfilename(filename)
    {
    LockGuard lg(_mutex);
    if (std::filesystem::exists(_logpath)) return;
    try
    {
    std::filesystem::create_directories(_logpath);
    }
    catch (std::filesystem::filesystem_error &e)
    {
    std::cerr << e.what() << std::endl;
    }
    }
    ~FileStrategy()
    {}
    void SyncLog(const std::string &message) override
    {
    LockGuard lg(_mutex);
    if (!_logpath.empty() && _logpath.back() != '/') _logpath += "/";
    std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
    std::ofstream out(targetlog, std::ios::app); // 追加的方式写入
    if (!out.is_open())
    {
    std::cerr << "open " << targetlog << " failed!" << std::endl;
    return;
    }
    out << message << '\\n';
    }
    private:
    std::string _logpath;
    std::string _logfilename;
    Mutex _mutex;
    };

    class Logger
    {
    // 日志类:
    // 1. 日志的生成
    // 2. 根据不同的策略进行刷新
    public:
    Logger()
    {}
    ~Logger()
    {}

    void UseConsoleStrategy() {
    _strategy = std::make_unique<ConsoleStrategy>();
    }
    void UseFileStrategy() {
    _strategy = std::make_unique<FileStrategy>();
    }
    // void Debug(const std::string &message) {
    // if (_strategy != nullptr) {
    // _strategy->SyncLog(message);
    // }
    // }

    // 内部类
    class LogMessage
    {
    public:
    LogMessage(LogLevel level, std::string& filename, int line, Logger& logger)
    :_curr_time(GetCurrentTime()),
    _level(level),
    _pid(getpid()),
    _filename(filename),
    _line(line),
    _logger(logger)
    {
    // 一条完整的日志信息 = 前部分固定信息 + 后面不固定信息

    // 先组成前部分固定
    std::stringstream ss;
    ss << '[' << _curr_time << "] "
    << '[' << LogLevelToMessage(_level) << "] "
    << '[' << _pid << "] "
    << '[' << _filename << "] "
    << '[' << _line << "] "
    << " – ";

    _loginfo = ss.str();
    }
    ~LogMessage()
    {
    // 在Logger析构时,进行刷新信息(RAII)
    if (_logger._strategy)
    {
    _logger._strategy->SyncLog(_loginfo);
    }
    }

    template<typename T>
    LogMessage& operator << (const T& info) {
    std::stringstream ss;
    ss << info;
    _loginfo += ss.str();
    return *this; // 返回当前LogMessage,以便连续<<
    }

    private:
    std::string _curr_time; // 时间
    LogLevel _level; // 等级
    pid_t _pid; // 进程id
    std::string _filename; // 文件名
    int _line; // 具体位置

    std::string _loginfo; // 一条完整的日志信息

    // 因为这是内部类LogMessage,析构时,要知道Logger的刷新策略,所以要引用外面的Logger
    Logger& _logger;
    };

    // 不是内部类了
    // 重载的是Logger类的(),只是返回类型是LogMessage
    LogMessage operator()(LogLevel level, std::string filename, int line) {
    return LogMessage(level, filename, line, *this);
    }
    // 意义:创建出那个用来实现 RAII 的临时 LogMessage 对象

    private:
    std::unique_ptr<LogStrategy> _strategy;
    };

    // 日志对象,全局使用
    Logger logger;

    #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
    #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

    #define LOG(level) logger(level, __FILE__, __LINE__)
    }

    mutex.hpp

    #pragma once
    #include <iostream>
    #include <pthread.h>

    class Mutex
    {
    public:
    Mutex() {
    pthread_mutex_init(&_lock, nullptr);
    }
    ~Mutex() {
    pthread_mutex_destroy(&_lock);
    }

    pthread_mutex_t* Ptr() {
    return &_lock;
    }
    void Lock() {
    pthread_mutex_lock(&_lock);
    }
    void UnLock() {
    pthread_mutex_unlock(&_lock);
    }
    private:
    pthread_mutex_t _lock;
    };

    // 采用RAII风格,进行锁管理
    class LockGuard
    {
    public:
    LockGuard(Mutex& lock):_lockref(lock) {
    _lockref.Lock();
    }
    ~LockGuard() {
    _lockref.UnLock();
    }
    private:
    Mutex& _lockref;
    };

    Makefile

    .PHONY:all
    all:client_udp server_udp
    server_udp:EchoServerMain.cpp
    g++ -o $@ $^ -std=c++17
    client_udp:EchoClient.cpp
    g++ -o $@ $^ -std=c++17

    .PHONY:clean
    clean:
    rm -f client_udp server_udp

    8.2 项目二:UDP 词典服务

    EchoServer.hpp

    #pragma once
    #include <string.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <string>
    #include <functional>
    #include "Logger.hpp"

    using namespace NS_LOG_MODULE;

    const static int default_fd = –1;
    const static uint16_t default_port = 8888;

    using callback_t = std::function<std::string(std::string)>;

    enum {
    SUCCESS = 0,
    SOCKET_ERR,
    USAGE_ERR,
    BIND_ERR
    };

    class UdpServer
    {
    public:
    // UdpServer(const std::string &ip, uint16_t port = default_port)
    UdpServer(callback_t cb, uint16_t port = default_port)
    :_sockfd(default_fd),
    _port(port),
    _cb(cb)
    {}

    ~UdpServer()
    {
    close(_sockfd);
    }

    void Init() {
    // 第一步:创建socket,本质:打开网络这个文件
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0) {
    LOG(LogLevel::FATAL) << "create socket error";
    exit(SOCKET_ERR);
    }
    LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;

    // 第二步:填充网络属性信息
    struct sockaddr_in local{}; // {}结构体清零
    local.sin_family = AF_INET;
    local.sin_port = htons(_port); // hton
    // inet_pton(AF_INET, _ip.c_str(), &local.sin_addr);
    local.sin_addr.s_addr = INADDR_ANY;

    /*
    一台机器(尤其云服务器)通常不止一个 IP:
    地址|说明
    公网 IP|云厂商分配的对外地址
    内网 IP|比如 172.x.x.x / 10.x.x.x,VPC 内网用的
    回环|127.0.0.1
    如果写成 local.sin_addr.s_addr = inet_addr("某个具体IP"),
    那这个 socket 就只监听那一个 IP 的端口。比如绑了公网 IP,内网 IP 来的包就收不到。
    而 INADDR_ANY(0.0.0.0)会让内核把 socket 挂到所有本地地址上,
    无论客户端走公网、内网还是回环,只要目标是这个端口,都能收到。
    对服务器来说这是默认、最省心的选择。
    */

    // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串ip->4字节ip; 2.hton

    // 只是填充在了local中,并没有写入内核

    // 第三步:绑定套接字socket信息(写入内核)
    ssize_t r = bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
    if (r < 0) {
    LOG(LogLevel::FATAL) << "绑定套接字失败:" << strerror(errno);
    exit(BIND_ERR);
    }
    LOG(LogLevel::INFO) << "绑定套接字成功。" << " port:" << _port;
    }

    void Loop() {
    char inbuffer[1024] = {0};
    while (true) {
    struct sockaddr_in peer;
    socklen_t peerlen = sizeof(peer);
    ssize_t r = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) – 1, 0, (struct sockaddr*)&peer, &peerlen);
    if (r < 0) {
    LOG(LogLevel::ERROR) << "recvfrom接收失败:" << strerror(errno);
    continue;
    }
    else if (r == 0) {
    LOG(LogLevel::WARNING) << "recvfrom接收空数据。";
    }
    else {
    // 客户端的信息
    uint16_t client_port = ntohs(peer.sin_port);
    char client_ip_char[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &peer.sin_addr, client_ip_char, sizeof(client_ip_char));
    std::string client_ip(client_ip_char);
    std::string client_addr = "[" + client_ip + ":" + std::to_string(client_port) + "]";

    // 用户发来的数据
    inbuffer[r] = 0;
    // LOG(LogLevel::DEBUG) << "client [" << client_addr << "] say# " << inbuffer;
    // std::string echo_string = "server echo—";
    // echo_string += inbuffer;

    std::string result = _cb(inbuffer);

    sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, peerlen);
    }
    }
    }
    private:
    int _sockfd;
    // std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址,给人看) && 4字节IP
    uint16_t _port;
    callback_t _cb;
    };

    EchoServerMain.cpp

    #include "EchoServer.hpp"
    #include "Dict.hpp"
    #include <memory>

    static void Usage(const std::string &process) {
    std::cerr << "Usage:\\n\\t";
    std::cerr << process << " [local_port]" << std::endl;
    }

    // ./server_udp [ip] [port]
    int main(int argc, char* argv[]) {

    if (argc != 2) {
    Usage(argv[0]);
    exit(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG_STRATEGY()

    // 1. 定义字典
    Dict dict;

    // 2. 构建网络服务,处理IO问题
    // std::string socket_ip = argv[1];
    uint16_t socket_port = std::stoi(argv[1]);

    // 3. 绑定上下两层
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>([&dict](std::string word){
    return dict.Translate(word);
    },socket_port);
    usvr->Init();
    usvr->Loop();
    return 0;
    }

    EchoClient.cpp

    #include <iostream>
    #include <string>
    #include <string.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>

    void Usage(const std::string &proc) {
    std::cout << "Usage:\\n\\t";
    std::cout << proc << " [server_ip] [server_port]" << std::endl;
    }

    int main(int argc, char* argv[]) {

    if (argc != 3) {
    Usage(argv[0]);
    exit(1);
    }

    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);

    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
    std::cerr << "socket error:" << strerror(errno);
    exit(2);
    }

    // 2. 不需要显示的去bind绑定addr(IP+port)信息,udp client首次发送数据的时候,OS底层会隐式自动获取随机端口并绑定IP+port

    // 后面发数据的时候,只需要sockaddr就行
    struct sockaddr_in server{};
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);

    // 3. 循环执行客户端
    while (true) {

    std::string message;
    // 1. 获取用户输入
    std::cout << "请输入要发送的消息:";
    std::getline(std::cin, message);

    // 2. sendto发送数据给server
    ssize_t r = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
    if (r > 0) {
    // recvform
    char inbuffer[1024] = {0};
    struct sockaddr_in tmp{}; // 避免recvfrom()内参数填NULL,可能会出问题,只是存放收到的服务端的套接字
    socklen_t tmplen = sizeof(tmp);
    ssize_t n = recvfrom(sockfd, inbuffer, sizeof(inbuffer), 0, (struct sockaddr*)&tmp, &tmplen);
    if (n > 0) {
    inbuffer[n] = 0;
    std::cout << inbuffer << std::endl;
    }
    }
    else {
    std::cerr << "sendto seeor:" << strerror(errno);
    exit(3);
    }

    }

    return 0;
    }

    Dict.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include <fstream>
    #include <unordered_map>

    #include "Logger.hpp"

    using namespace NS_LOG_MODULE;

    static const std::string default_path = "./dict.txt";
    static const std::string sep = ": ";

    class Dict
    {
    public:
    Dict(const std::string &dict_path = default_path)
    :_dict_path(dict_path)
    {
    LoadDict();
    }
    ~Dict()
    {}

    void LoadDict() {
    std::ifstream in(_dict_path);
    if (!in.is_open()) {
    LOG(LogLevel::FATAL) << "open [" << _dict_path << "] error";
    exit(1);
    }

    std::string line;
    while (std::getline(in, line)) {
    LOG(LogLevel::INFO) << "load [" << line << "] success";
    // apple: 苹果
    auto sep_pos = line.find(sep);
    /*
    std::string::npos 精简总结
    本质:static const size_type npos = -1;,无符号回绕成极大值,作"无效位置"标记。
    含义:find 系列找不到时返回它 → "没找到"。
    判断:if (s.find("x") != std::string::npos) 找到;== 没找到。别写成 if (s.find(…))。
    接收:用 auto 或 size_t,别用 int。
    附带用途:substr(pos) 的第二参数默认就是 npos,表示"取到末尾"。
    坑:别把 npos 当下标传给 substr(会抛 out_of_range)。
    */

    if (sep_pos == std::string::npos) {
    LOG(LogLevel::WARNING) << "Format: [" << line << "] error";
    continue;
    }

    std::string k = line.substr(0, sep_pos);
    std::string v = line.substr(sep_pos + sep.size());
    _dict.insert({k, v});
    }

    LOG(LogLevel::INFO) << "load done…";
    }

    std::string Translate(std::string word) {
    auto it = _dict.find(word);
    if (it != _dict.end()) {
    return it->second;
    }
    else {
    return "None";
    }
    }
    private:
    std::string _dict_path;
    std::unordered_map<std::string, std::string> _dict;
    };

    dict.txt

    apple: 苹果
    banana: 香蕉
    cat: 猫
    dog: 狗
    book: 书
    pen: 笔
    happy: 快乐的
    sad: 悲伤的
    run: 跑
    jump: 跳
    teacher: 老师
    student: 学生
    car: 汽车
    bus: 公交车
    love: 爱
    hate: 恨
    hello: 你好
    goodbye: 再见
    summer: 夏天
    winter: 冬天

    Logger.hpp

    #pragma once
    #include <iostream>
    #include <string>
    #include <sys/time.h>
    #include <ctime>
    #include <cstdio>
    #include <unistd.h>
    #include <filesystem>
    #include <fstream>
    #include <memory>
    #include <sstream>
    #include "mutex.hpp"

    namespace NS_LOG_MODULE
    {

    // 日志等级信息
    enum class LogLevel
    {
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
    };

    // 为避免打印日志时,流式输出等级是数字
    std::string LogLevelToMessage(LogLevel level) {
    switch (level)
    {
    case LogLevel::DEBUG: return "DEBUG";
    case LogLevel::INFO: return "INFO";
    case LogLevel::WARNING: return "WARNING";
    case LogLevel::ERROR: return "ERROR";
    case LogLevel::FATAL: return "FATAL";
    default: return "UNKNOW";
    }
    }

    // 详细的时间信息
    std::string GetCurrentTime() {
    struct timeval current_time;
    gettimeofday(&current_time, nullptr);
    // current_time.tv_sec; current_time.tv_usec;

    struct tm struct_time;
    localtime_r(&(current_time.tv_sec), &struct_time);
    char timestr[128];
    snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
    struct_time.tm_year + 1900,
    struct_time.tm_mon + 1,
    struct_time.tm_mday,
    struct_time.tm_hour,
    struct_time.tm_min,
    struct_time.tm_sec,
    current_time.tv_usec
    );
    return timestr;
    }

    // 策略模式,策略接口
    class LogStrategy
    {
    public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印策略
    class ConsoleStrategy : public LogStrategy
    {
    public:
    ~ConsoleStrategy()
    {}
    void SyncLog(const std::string &message) override
    {
    LockGuard lg(_mutex);
    std::cout << message << std::endl;
    }

    private:
    Mutex _mutex;
    };

    // 文件打印策略
    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";
    class FileStrategy : public LogStrategy
    {
    public:
    FileStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename)
    :_logpath(path)
    ,_logfilename(filename)
    {
    LockGuard lg(_mutex);
    if (std::filesystem::exists(_logpath)) return;
    try
    {
    std::filesystem::create_directories(_logpath);
    }
    catch (std::filesystem::filesystem_error &e)
    {
    std::cerr << e.what() << std::endl;
    }
    }
    ~FileStrategy()
    {}
    void SyncLog(const std::string &message) override
    {
    LockGuard lg(_mutex);
    if (!_logpath.empty() && _logpath.back() != '/') _logpath += "/";
    std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
    std::ofstream out(targetlog, std::ios::app); // 追加的方式写入
    if (!out.is_open())
    {
    std::cerr << "open " << targetlog << " failed!" << std::endl;
    return;
    }
    out << message << '\\n';
    }
    private:
    std::string _logpath;
    std::string _logfilename;
    Mutex _mutex;
    };

    class Logger
    {
    // 日志类:
    // 1. 日志的生成
    // 2. 根据不同的策略进行刷新
    public:
    Logger()
    {}
    ~Logger()
    {}

    void UseConsoleStrategy() {
    _strategy = std::make_unique<ConsoleStrategy>();
    }
    void UseFileStrategy() {
    _strategy = std::make_unique<FileStrategy>();
    }
    // void Debug(const std::string &message) {
    // if (_strategy != nullptr) {
    // _strategy->SyncLog(message);
    // }
    // }

    // 内部类
    class LogMessage
    {
    public:
    LogMessage(LogLevel level, std::string& filename, int line, Logger& logger)
    :_curr_time(GetCurrentTime()),
    _level(level),
    _pid(getpid()),
    _filename(filename),
    _line(line),
    _logger(logger)
    {
    // 一条完整的日志信息 = 前部分固定信息 + 后面不固定信息

    // 先组成前部分固定
    std::stringstream ss;
    ss << '[' << _curr_time << "] "
    << '[' << LogLevelToMessage(_level) << "] "
    << '[' << _pid << "] "
    << '[' << _filename << "] "
    << '[' << _line << "] "
    << " – ";

    _loginfo = ss.str();
    }
    ~LogMessage()
    {
    // 在Logger析构时,进行刷新信息(RAII)
    if (_logger._strategy)
    {
    _logger._strategy->SyncLog(_loginfo);
    }
    }

    template<typename T>
    LogMessage& operator << (const T& info) {
    std::stringstream ss;
    ss << info;
    _loginfo += ss.str();
    return *this; // 返回当前LogMessage,以便连续<<
    }

    private:
    std::string _curr_time; // 时间
    LogLevel _level; // 等级
    pid_t _pid; // 进程id
    std::string _filename; // 文件名
    int _line; // 具体位置

    std::string _loginfo; // 一条完整的日志信息

    // 因为这是内部类LogMessage,析构时,要知道Logger的刷新策略,所以要引用外面的Logger
    Logger& _logger;
    };

    // 不是内部类了
    // 重载的是Logger类的(),只是返回类型是LogMessage
    LogMessage operator()(LogLevel level, std::string filename, int line) {
    return LogMessage(level, filename, line, *this);
    }
    // 意义:创建出那个用来实现 RAII 的临时 LogMessage 对象

    private:
    std::unique_ptr<LogStrategy> _strategy;
    };

    // 日志对象,全局使用
    Logger logger;

    #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
    #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

    #define LOG(level) logger(level, __FILE__, __LINE__)
    }

    mutex.hpp

    #pragma once
    #include <iostream>
    #include <pthread.h>

    class Mutex
    {
    public:
    Mutex() {
    pthread_mutex_init(&_lock, nullptr);
    }
    ~Mutex() {
    pthread_mutex_destroy(&_lock);
    }

    pthread_mutex_t* Ptr() {
    return &_lock;
    }
    void Lock() {
    pthread_mutex_lock(&_lock);
    }
    void UnLock() {
    pthread_mutex_unlock(&_lock);
    }
    private:
    pthread_mutex_t _lock;
    };

    // 采用RAII风格,进行锁管理
    class LockGuard
    {
    public:
    LockGuard(Mutex& lock):_lockref(lock) {
    _lockref.Lock();
    }
    ~LockGuard() {
    _lockref.UnLock();
    }
    private:
    Mutex& _lockref;
    };

    Makefile

    .PHONY:all
    all:client_udp server_udp
    server_udp:EchoServerMain.cpp
    g++ -o $@ $^ -std=c++17
    client_udp:EchoClient.cpp
    g++ -o $@ $^ -std=c++17

    .PHONY:clean
    clean:
    rm -f client_udp server_udp

    8.3 项目三:UDP 多人聊天室

    ChatServerMain.cpp

    #include <memory>
    #include "UdpServer.hpp" // 获取事件
    #include "ThreadPool.hpp" // 执行者
    #include "Route.hpp" // 任务

    using task_t = std::function<void ()>;

    static void Usage(const std::string &process) {
    std::cerr << "Usage:\\n\\t";
    std::cerr << process << " [local_port]" << std::endl;
    }

    using namespace NS_THREAD_POOL_MODULE;

    // ./server_udp [ip] [port]
    int main(int argc, char* argv[]) {

    if (argc != 2) {
    Usage(argv[0]);
    exit(USAGE_ERR);
    }
    ENABLE_CONSOLE_LOG_STRATEGY()
    uint16_t socket_port = std::stoi(argv[1]);

    // 线程池模块
    auto thread_pool = ThreadPool<task_t>::Instance();

    // 路由模块
    Route rt;

    // 网络服务模块
    UdpServer usvr(socket_port);
    usvr.Init();

    usvr.RegisterService(
    [&rt](const InetAddr& addr){
    rt.CheckUser(addr);
    },
    [&rt, thread_pool](int sockfd, std::string msg){
    auto t = std::bind(&Route::Broadcast, &rt, sockfd, msg);
    thread_pool->Enqueue(t);

    // 等同于
    // thread_pool->Enqueue([&rt, &sockfd, &msg](){
    // rt.Broadcast(sockfd, msg);
    // });
    });
    usvr.Loop();

    return 0;
    }

    UdpServer.hpp

    #pragma once
    #include <string.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <string>
    #include <functional>
    #include "Logger.hpp"
    #include "InetAddr.hpp"

    using namespace NS_LOG_MODULE;

    const static int default_fd = –1;
    const static uint16_t default_port = 8888;

    // using callback_t = std::function<std::string(std::string)>;
    using handler_addr_t = std::function<void (const InetAddr&)>;
    using handler_msg_t = std::function<void (int, std::string)>;

    enum {
    SUCCESS = 0,
    SOCKET_ERR,
    USAGE_ERR,
    BIND_ERR
    };

    class UdpServer
    {
    public:
    // UdpServer(const std::string &ip, uint16_t port = default_port)
    UdpServer(uint16_t port = default_port)
    :_sockfd(default_fd),
    _port(port)
    {}

    ~UdpServer()
    {
    close(_sockfd);
    }

    void Init() {
    // 第一步:创建socket,本质:打开网络这个文件
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0) {
    LOG(LogLevel::FATAL) << "create socket error";
    exit(SOCKET_ERR);
    }
    LOG(LogLevel::INFO) << "create socket success, sockfd: " << _sockfd;

    // 第二步:填充网络属性信息

    InetAddr local(_port);

    // 只是填充在了local中,并没有写入内核

    // 第三步:绑定套接字socket信息(写入内核)
    ssize_t r = bind(_sockfd, (struct sockaddr*)local.GetNetAddress(), local.Len());
    if (r < 0) {
    LOG(LogLevel::FATAL) << "绑定套接字失败:" << strerror(errno);
    exit(BIND_ERR);
    }
    LOG(LogLevel::INFO) << "绑定套接字成功。" << " port:" << _port;
    }

    void RegisterService(handler_addr_t handler_addr, handler_msg_t handler_msg) {
    _handler_addr = handler_addr;
    _handler_msg = handler_msg;
    }

    void Loop() {
    char inbuffer[1024] = {0};
    while (true) {
    struct sockaddr_in peer;
    socklen_t peerlen = sizeof(peer);
    ssize_t r = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) – 1, 0, (struct sockaddr*)&peer, &peerlen);
    if (r < 0) {
    LOG(LogLevel::ERROR) << "recvfrom接收失败:" << strerror(errno);
    continue;
    }
    else if (r == 0) {
    LOG(LogLevel::WARNING) << "recvfrom接收空数据。";
    }
    else {
    inbuffer[r] = 0;

    // 1. 检测新用户
    InetAddr client_address(peer);
    std::string message = client_address.ToString() + inbuffer;
    LOG(LogLevel::DEBUG) << message;
    _handler_addr(client_address);

    // 2. 转发消息
    _handler_msg(_sockfd, message);
    }
    }
    }
    private:
    int _sockfd;
    // std::string _ip; // "192.168.2.2"(字符串风格的点分十进制IP地址,给人看) && 4字节IP
    uint16_t _port;
    // callback_t _cb;
    handler_addr_t _handler_addr;
    handler_msg_t _handler_msg;
    };

    ChatClient.cpp

    #include <iostream>
    #include <string>
    #include <string.h>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>
    #include "thread.hpp"
    #include "InetAddr.hpp"

    using namespace NS_THREAD_MODULE;

    int sockfd = 0;
    std::string server_ip;
    uint16_t server_port = 0;
    std::string nickname;

    void Usage(const std::string &proc) {
    std::cout << "Usage:\\n\\t";
    std::cout << proc << " [server_ip] [server_port]" << std::endl;
    }

    void Online(InetAddr& server_addr) {
    std::cout << "请设置你的昵称:";
    std::getline(std::cin, nickname);
    std::string online_message = nickname + " 上线了!";
    ssize_t r = sendto(sockfd, online_message.c_str(), online_message.size(), 0, (struct sockaddr*)server_addr.GetNetAddress(), server_addr.Len());
    (void)r;
    }

    void RecvMessage() {
    while (true) {
    // recvform
    char inbuffer[1024] = {0};
    struct sockaddr_in tmp{}; // 避免recvfrom()内参数填NULL,可能会出问题,只是存放收到的服务端的套接字
    socklen_t tmplen = sizeof(tmp);
    ssize_t n = recvfrom(sockfd, inbuffer, sizeof(inbuffer) – 1, 0, (struct sockaddr*)&tmp, &tmplen);
    if (n > 0) {
    inbuffer[n] = 0;
    std::cout << inbuffer << std::endl;
    }
    }
    }

    void SendMessage() {
    InetAddr server_addr(server_port, server_ip);
    Online(server_addr);

    while (true) {
    std::string message;
    // 1. 获取用户输入
    std::cout << "请输入要发送的消息:";
    std::getline(std::cin, message);
    message = nickname + "$ " + message;

    // 2. sendto发送数据给server
    ssize_t r = sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)server_addr.GetNetAddress(), server_addr.Len());
    (void)r;
    }
    }

    int main(int argc, char* argv[]) {

    if (argc != 3) {
    Usage(argv[0]);
    exit(1);
    }

    server_ip = argv[1];
    server_port = std::stoi(argv[2]);

    // 1. 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
    std::cerr << "socket error:" << strerror(errno);
    exit(2);
    }

    // 2. 线程模块
    Thread recver(RecvMessage);
    Thread sender(SendMessage);
    recver.Start();
    sender.Start();

    recver.Join();
    sender.Join();

    return 0;
    }

    Dict.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include <fstream>
    #include <unordered_map>

    #include "Logger.hpp"

    using namespace NS_LOG_MODULE;

    static const std::string default_path = "./dict.txt";
    static const std::string sep = ": ";

    class Dict
    {
    public:
    Dict(const std::string &dict_path = default_path)
    :_dict_path(dict_path)
    {
    LoadDict();
    }
    ~Dict()
    {}

    void LoadDict() {
    std::ifstream in(_dict_path);
    if (!in.is_open()) {
    LOG(LogLevel::FATAL) << "open [" << _dict_path << "] error";
    exit(1);
    }

    std::string line;
    while (std::getline(in, line)) {
    LOG(LogLevel::INFO) << "load [" << line << "] success";
    // apple: 苹果
    auto sep_pos = line.find(sep);
    /*
    std::string::npos 精简总结
    本质:static const size_type npos = -1;,无符号回绕成极大值,作"无效位置"标记。
    含义:find 系列找不到时返回它 → "没找到"。
    判断:if (s.find("x") != std::string::npos) 找到;== 没找到。别写成 if (s.find(…))。
    接收:用 auto 或 size_t,别用 int。
    附带用途:substr(pos) 的第二参数默认就是 npos,表示"取到末尾"。
    坑:别把 npos 当下标传给 substr(会抛 out_of_range)。
    */

    if (sep_pos == std::string::npos) {
    LOG(LogLevel::WARNING) << "Format: [" << line << "] error";
    continue;
    }

    std::string k = line.substr(0, sep_pos);
    std::string v = line.substr(sep_pos + sep.size());
    _dict.insert({k, v});
    }

    LOG(LogLevel::INFO) << "load done…";
    }

    std::string Translate(std::string word) {
    auto it = _dict.find(word);
    if (it != _dict.end()) {
    return it->second;
    }
    else {
    return "None";
    }
    }
    private:
    std::string _dict_path;
    std::unordered_map<std::string, std::string> _dict;
    };

    InetAddr.hpp

    #pragma once

    #include <iostream>
    #include <string>
    #include <cstring>
    #include <sys/socket.h>
    #include <arpa/inet.h>
    #include <netinet/in.h>

    // 对客户端进行先描述
    class InetAddr
    {
    public:
    InetAddr(const struct sockaddr_in &address)
    :_address(address),
    _len(sizeof(address))
    {
    _port = ntohs(_address.sin_port);
    char _ip_char[INET_ADDRSTRLEN];
    inet_ntop(AF_INET, &_address.sin_addr, _ip_char, sizeof(_ip_char));
    _ip.assign(_ip_char);
    }
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
    :_ip(ip),
    _port(port)
    {
    memset(&_address, 0, sizeof(_address));
    _address.sin_family = AF_INET;
    _address.sin_port = htons(_port);
    inet_pton(AF_INET, _ip.c_str(), &_address.sin_addr);
    _len = sizeof(_address);
    }
    InetAddr()
    {}
    ~InetAddr()
    {}

    bool operator==(const InetAddr &addr) {
    return (this->_ip == addr._ip) && (this->_port == addr._port);
    }

    struct sockaddr_in* GetNetAddress() {
    return &_address;
    }

    socklen_t Len() {
    return _len;
    }

    std::string ToString() {
    return "[" + _ip + ":" + std::to_string(_port) + "]";
    }

    private:
    // net地址
    struct sockaddr_in _address;
    socklen_t _len;

    // host地址
    std::string _ip;
    uint16_t _port;
    };

    Route.hpp

    #pragma once

    #include <string>
    #include <memory>
    #include "mutex.hpp"
    #include "UserManager.hpp"

    class Route
    {
    public:
    Route()
    :_uma(std::make_unique<UserManager>())
    {}
    ~Route()
    {}

    void Broadcast(int sockfd, std::string message) {
    LockGuard lg(_lock);
    auto& users = _uma->Users();
    for (auto& user : users) {
    sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)user.GetNetAddress(), user.Len());
    }
    }

    void CheckUser(const InetAddr& addr) {
    LockGuard lg(_lock);
    _uma->AddUser(addr);
    }

    void OffLineUser(const InetAddr& addr) {
    LockGuard lg(_lock);
    _uma->DelUser(addr);
    }

    private:
    std::unique_ptr<UserManager> _uma;
    Mutex _lock;
    };

    UserManager.hpp

    #pragma once

    #include <string>
    #include <vector>

    #include "InetAddr.hpp"
    #include "Logger.hpp"

    using namespace NS_LOG_MODULE;

    class UserManager
    {
    public:
    UserManager()
    {}
    ~UserManager()
    {}

    void AddUser(const InetAddr &addr) {
    if (SearchUser(addr)) return;
    _users.emplace_back(addr);
    }

    void DelUser(const InetAddr &addr) {
    if (!SearchUser(addr)) {
    LOG(LogLevel::WARNING) << "This addr Unexist.";
    }

    for (auto it = _users.begin(); it != _users.end(); ++it) {
    if (*it == addr) {
    _users.erase(it);
    break;
    }
    }
    }

    bool SearchUser(const InetAddr &addr) {
    for (auto& user : _users) {
    if (user == addr) {
    return true;
    }
    }
    return false;
    }

    // bool ModUser(const InetAddr &addr) {

    // }

    std::vector<InetAddr>& Users() {
    return _users;
    }

    private:
    std::vector<InetAddr> _users;
    };

    ThreadPool.hpp

    #pragma once
    #include <iostream>
    #include <vector>
    #include <queue>
    #include "mutex.hpp"
    #include "Logger.hpp"
    #include "thread.hpp"
    #include "Cond.hpp"

    namespace NS_THREAD_POOL_MODULE
    {
    using namespace NS_THREAD_MODULE;
    using namespace NS_LOG_MODULE;

    const int defaultnum = 5;

    // 要对线程池进行管理——先描述再组织!
    template<typename T>
    class ThreadPool
    {
    private:
    void HandlerTask() {
    char name[128];
    pthread_getname_np(pthread_self(), name, sizeof(name));
    while (true) {
    T task;
    {
    // 保护临界区
    LockGuard lg(_mutex);
    // 检测有无任务
    while (_isrunning && _tasks.empty()) {
    // 没有任务,休眠
    ++_slaver_sleep_count;
    _cond.Wait(_mutex);
    —_slaver_sleep_count;
    }

    // 若果是线程池要结束运行,正好没任务
    if (!_isrunning && _tasks.empty()) break;

    // 有任务,取任务(本质:将任务由公共的变为私有)
    task = _tasks.front();
    _tasks.pop();
    }
    // 处理任务不需要在临界区内部处理
    LOG(LogLevel::INFO) << name << "处理任务:";
    task();
    }
    // 线程退出
    LOG(LogLevel::INFO) << name << "quit…";
    }

    ThreadPool(int slaver_num = defaultnum)
    :_isrunning(false),
    _slaver_num(slaver_num),
    _slaver_sleep_count(0)
    {

    for (int i = 0; i < _slaver_num; ++i) {

    // auto f = std::bind(&ThreadPool::HandlerTask, this);
    // _slavers.emplace_back(f);

    _slavers.emplace_back([this](){
    this->HandlerTask();
    });
    }
    }
    ThreadPool<T>(const ThreadPool&) = delete;
    ThreadPool<T>& operator=(const ThreadPool&) = delete;

    public:
    static ThreadPool<T>* Instance() {
    {
    if (_instance == nullptr) { // 双if判断
    LockGuard lg(_lock);
    if (_instance == nullptr) {
    // 第一次调用
    _instance = new ThreadPool<T>();
    _instance->Start();
    LOG(LogLevel::INFO) << "第一次调用线程池,创建线程池对象.";
    }
    }
    }
    return _instance;
    }
    ~ThreadPool()
    {}
    void Start() {
    if (_isrunning) {
    LOG(LogLevel::WARNING) << "Thread Pool Is Already Running!";
    return;
    }
    _isrunning = true;
    for (auto& slave : _slavers) {
    slave.Start();
    }
    }

    void Stop() {
    // ———————————–
    // if (!_isrunning) {
    // LOG(LogLevel::WARNING) << "Thread Is Not Running!";
    // return;
    // }
    // for (auto& slave : _slavers) {
    // slave.Stop();
    // }
    // _isrunning = false;
    // ———————————–

    // 1. _isrunning = faulse
    // 2. _tasks内所有任务都要处理完
    _mutex.Lock();
    _isrunning = false;
    if (_slaver_sleep_count > 0) {
    _cond.Broadcast();
    }
    _mutex.UnLock();
    }

    void Wait() {
    for (auto& slave : _slavers) {
    slave.Join();
    }
    }

    void Enqueue(T in) {
    _mutex.Lock();
    _tasks.push(in);
    if (_slaver_sleep_count > 0) {
    _cond.Signal();
    }
    _mutex.UnLock();
    }
    private:
    bool _isrunning;
    int _slaver_num;
    std::vector<Thread> _slavers;
    std::queue<T> _tasks; // 任务队列,临界资源
    Mutex _mutex;
    Cond _cond;
    int _slaver_sleep_count;

    // 添加单例模式
    static ThreadPool<T>* _instance;
    static Mutex _lock; // 保证单例安全
    };

    template<typename T>
    ThreadPool<T>* ThreadPool<T>::_instance = nullptr;

    template<typename T>
    Mutex ThreadPool<T>::_lock;
    }

    thread.hpp

    #pragma once
    #include <iostream>
    #include <pthread.h>
    #include <string>
    #include <string.h>
    #include <functional>
    #include "Logger.hpp"

    namespace NS_THREAD_MODULE
    {
    using namespace NS_LOG_MODULE;
    static int g_number = 1;
    using callback_t = std::function<void ()>;

    enum class TSTATUS {
    THREAD_NEW,
    THREAD_RUNNING,
    THREAD_STOP
    };

    std::string StatusToString(TSTATUS s) {
    switch (s) {
    case TSTATUS::THREAD_NEW:
    return "THREAD_NEW";
    case TSTATUS::THREAD_RUNNING:
    return "THREAD_RUNNING";
    case TSTATUS::THREAD_STOP:
    return "THREAD_STOP";
    default:
    return "UNKNOWN";
    }
    }

    std::string IsJoin(bool _joinable) {
    return _joinable ? "true" : "false";
    }

    class Thread
    {
    public:
    void ToRunning() {
    _status = TSTATUS::THREAD_RUNNING;
    }

    void ToStop() {
    _status = TSTATUS::THREAD_STOP;
    }

    static void* ThreadRoutine(void* args) {
    Thread* self = static_cast<Thread*>(args);
    pthread_setname_np(self->_tid, self->_name.c_str());
    self->_cb();
    self->ToStop();
    return nullptr;
    }

    public:
    Thread(callback_t cb)
    :_tid(–1),
    _status(TSTATUS::THREAD_NEW),
    _joinable(true), _cb(cb), _result(nullptr)
    {
    _name = "New-Thread-" + std::to_string(g_number++);
    }
    ~Thread() {}

    bool Start() {
    int ret = pthread_create(&_tid, nullptr, ThreadRoutine, this);
    if (ret != 0) {
    return false;
    }
    ToRunning();
    return true;
    }

    void Join() {
    if (_joinable) {
    int ret = pthread_join(_tid, &_result);
    if (ret != 0) {
    std::cerr << "pthread_join失败:" << strerror(ret) << std::endl;
    return;
    }
    _status = TSTATUS::THREAD_STOP;
    LOG(LogLevel::DEBUG) << _name << "join success!";
    }
    else {
    std::cerr << "不是可汇线程! join_status:" << IsJoin(_joinable) << std::endl;
    }
    }

    void Stop() {
    if (_status == TSTATUS::THREAD_RUNNING) {
    pthread_cancel(_tid);
    _status = TSTATUS::THREAD_STOP;
    }
    }

    void Detach() {
    if (_status == TSTATUS::THREAD_RUNNING && _joinable) {
    pthread_detach(_tid);
    _joinable = false;
    }
    else {
    std::cerr << "detach" << _name << "失败" << std::endl;
    }
    }

    void PrintInfo() {
    std::cout << "thread name: " << _name << std::endl;
    std::cout << "thread tid: " << _tid << std::endl;
    std::cout << "thread status: " << StatusToString(_status) << std::endl;
    std::cout << "thread joinable: " << IsJoin(_joinable) << std::endl;
    }
    private:
    std::string _name;
    pthread_t _tid;
    TSTATUS _status;
    bool _joinable;
    callback_t _cb;
    void* _result;
    };
    }

    Cond.hpp

    #pragma once

    #include "mutex.hpp"
    #include <pthread.h>

    class Cond
    {
    public:
    Cond()
    {
    pthread_cond_init(&_cond,nullptr);
    }
    ~Cond()
    {
    pthread_cond_destroy(&_cond);
    }

    void Wait(Mutex &mutex) {
    pthread_cond_wait(&_cond,mutex.Ptr());
    }
    void Signal() {
    pthread_cond_signal(&_cond);
    }
    void Broadcast() {
    pthread_cond_broadcast(&_cond);
    }
    private:
    pthread_cond_t _cond;
    };

    mutex.hpp

    #pragma once
    #include <iostream>
    #include <pthread.h>

    class Mutex
    {
    public:
    Mutex() {
    pthread_mutex_init(&_lock, nullptr);
    }
    ~Mutex() {
    pthread_mutex_destroy(&_lock);
    }

    pthread_mutex_t* Ptr() {
    return &_lock;
    }
    void Lock() {
    pthread_mutex_lock(&_lock);
    }
    void UnLock() {
    pthread_mutex_unlock(&_lock);
    }
    private:
    pthread_mutex_t _lock;
    };

    // 采用RAII风格,进行锁管理
    class LockGuard
    {
    public:
    LockGuard(Mutex& lock):_lockref(lock) {
    _lockref.Lock();
    }
    ~LockGuard() {
    _lockref.UnLock();
    }
    private:
    Mutex& _lockref;
    };

    Logger.hpp

    #pragma once
    #include <iostream>
    #include <string>
    #include <sys/time.h>
    #include <ctime>
    #include <cstdio>
    #include <unistd.h>
    #include <filesystem>
    #include <fstream>
    #include <memory>
    #include <sstream>
    #include "mutex.hpp"

    namespace NS_LOG_MODULE
    {

    // 日志等级信息
    enum class LogLevel
    {
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
    };

    // 为避免打印日志时,流式输出等级是数字
    std::string LogLevelToMessage(LogLevel level) {
    switch (level)
    {
    case LogLevel::DEBUG: return "DEBUG";
    case LogLevel::INFO: return "INFO";
    case LogLevel::WARNING: return "WARNING";
    case LogLevel::ERROR: return "ERROR";
    case LogLevel::FATAL: return "FATAL";
    default: return "UNKNOW";
    }
    }

    // 详细的时间信息
    std::string GetCurrentTime() {
    struct timeval current_time;
    gettimeofday(&current_time, nullptr);
    // current_time.tv_sec; current_time.tv_usec;

    struct tm struct_time;
    localtime_r(&(current_time.tv_sec), &struct_time);
    char timestr[128];
    snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
    struct_time.tm_year + 1900,
    struct_time.tm_mon + 1,
    struct_time.tm_mday,
    struct_time.tm_hour,
    struct_time.tm_min,
    struct_time.tm_sec,
    current_time.tv_usec
    );
    return timestr;
    }

    // 策略模式,策略接口
    class LogStrategy
    {
    public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印策略
    class ConsoleStrategy : public LogStrategy
    {
    public:
    ~ConsoleStrategy()
    {}
    void SyncLog(const std::string &message) override
    {
    LockGuard lg(_mutex);
    std::cout << message << std::endl;
    }

    private:
    Mutex _mutex;
    };

    // 文件打印策略
    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";
    class FileStrategy : public LogStrategy
    {
    public:
    FileStrategy(const std::string &path = defaultpath, const std::string &filename = defaultfilename)
    :_logpath(path)
    ,_logfilename(filename)
    {
    LockGuard lg(_mutex);
    if (std::filesystem::exists(_logpath)) return;
    try
    {
    std::filesystem::create_directories(_logpath);
    }
    catch (std::filesystem::filesystem_error &e)
    {
    std::cerr << e.what() << std::endl;
    }
    }
    ~FileStrategy()
    {}
    void SyncLog(const std::string &message) override
    {
    LockGuard lg(_mutex);
    if (!_logpath.empty() && _logpath.back() != '/') _logpath += "/";
    std::string targetlog = _logpath + _logfilename; // "./log/log.txt"
    std::ofstream out(targetlog, std::ios::app); // 追加的方式写入
    if (!out.is_open())
    {
    std::cerr << "open " << targetlog << " failed!" << std::endl;
    return;
    }
    out << message << '\\n';
    }
    private:
    std::string _logpath;
    std::string _logfilename;
    Mutex _mutex;
    };

    class Logger
    {
    // 日志类:
    // 1. 日志的生成
    // 2. 根据不同的策略进行刷新
    public:
    Logger()
    {}
    ~Logger()
    {}

    void UseConsoleStrategy() {
    _strategy = std::make_unique<ConsoleStrategy>();
    }
    void UseFileStrategy() {
    _strategy = std::make_unique<FileStrategy>();
    }
    // void Debug(const std::string &message) {
    // if (_strategy != nullptr) {
    // _strategy->SyncLog(message);
    // }
    // }

    // 内部类
    class LogMessage
    {
    public:
    LogMessage(LogLevel level, std::string& filename, int line, Logger& logger)
    :_curr_time(GetCurrentTime()),
    _level(level),
    _pid(getpid()),
    _filename(filename),
    _line(line),
    _logger(logger)
    {
    // 一条完整的日志信息 = 前部分固定信息 + 后面不固定信息

    // 先组成前部分固定
    std::stringstream ss;
    ss << '[' << _curr_time << "] "
    << '[' << LogLevelToMessage(_level) << "] "
    << '[' << _pid << "] "
    << '[' << _filename << "] "
    << '[' << _line << "] "
    << " – ";

    _loginfo = ss.str();
    }
    ~LogMessage()
    {
    // 在Logger析构时,进行刷新信息(RAII)
    if (_logger._strategy)
    {
    _logger._strategy->SyncLog(_loginfo);
    }
    }

    template<typename T>
    LogMessage& operator << (const T& info) {
    std::stringstream ss;
    ss << info;
    _loginfo += ss.str();
    return *this; // 返回当前LogMessage,以便连续<<
    }

    private:
    std::string _curr_time; // 时间
    LogLevel _level; // 等级
    pid_t _pid; // 进程id
    std::string _filename; // 文件名
    int _line; // 具体位置

    std::string _loginfo; // 一条完整的日志信息

    // 因为这是内部类LogMessage,析构时,要知道Logger的刷新策略,所以要引用外面的Logger
    Logger& _logger;
    };

    // 不是内部类了
    // 重载的是Logger类的(),只是返回类型是LogMessage
    LogMessage operator()(LogLevel level, std::string filename, int line) {
    return LogMessage(level, filename, line, *this);
    }
    // 意义:创建出那个用来实现 RAII 的临时 LogMessage 对象

    private:
    std::unique_ptr<LogStrategy> _strategy;
    };

    // 日志对象,全局使用
    Logger logger;

    #define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
    #define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

    #define LOG(level) logger(level, __FILE__, __LINE__)
    }

    Makefile

    .PHONY:all
    all:client_chat server_chat
    server_chat:ChatServerMain.cpp
    g++ -o $@ $^ -std=c++17
    client_chat:ChatClient.cpp
    g++ -o $@ $^ -std=c++17

    .PHONY:clean
    clean:
    rm -f client_chat server_chat

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux网络编程入门(二):从 UDP 回显服务器到多人聊天室,三个项目的渐进式实践
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!