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

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

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

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

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

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

目录

  1. 先认识程序中的通信对象
  2. [第一步:完成 UDP 回显](#第一步:完成 UDP 回显)
  3. 第二步:把业务处理抽成回调
  4. 第三步:从回显服务扩展到聊天室
  5. 三个项目串起来看
  6. 运行方式
  7. 当前实现的边界与后续改进
  8. 完整代码

一、先认识程序中的通信对象

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

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

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

二、第一步:完成 UDP 回显

2.1 服务器启动时做什么

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

  1. 调用 socket 创建 UDP Socket;
  2. 填写本地地址和端口,并调用 bind,让操作系统把这个 Socket 与服务器端口关联起来;
  3. 进入循环,用 recvfrom 收消息,再用 sendto 把回复发回消息来源。

下面是服务器收发流程的核心代码:

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);

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

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

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

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

    if (size <= 0) {
        continue;
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

cpp 复制代码
using callback_t = std::function<std::string(std::string)>;

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

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

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

cpp 复制代码
std::string result = _cb(buffer);

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

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

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

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

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

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

cpp 复制代码
Dict dict;

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

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

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

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

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

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

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

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

cpp 复制代码
bool operator==(const InetAddr& other) {
    return _ip == other._ip && _port == other._port;
}

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

4.2 在线用户表与消息广播

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

Route 负责两件事:

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

核心广播逻辑如下:

cpp 复制代码
void Broadcast(int sockfd, std::string message) {
    LockGuard guard(_lock);

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

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

4.3 用线程池处理广播任务

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

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

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

  1. 网络线程收到消息,创建广播任务;
  2. 任务进入受互斥锁保护的队列;
  3. 如果工作线程正在条件变量上等待,生产者发出通知;
  4. 工作线程醒来后重新检查队列,取出任务;
  5. 工作线程释放队列锁,再执行广播任务。

等待时需要在循环里重新检查条件,因为线程被唤醒时,队列可能仍为空,也可能已有其他工作线程先取走了任务:

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

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

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

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

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

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

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

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

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

五、三个项目串起来看

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

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

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

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

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

六、运行方式

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

bash 复制代码
# 项目一:终端 1 启动服务端
cd 1_echo_server
make
./server_udp 8888

# 项目一:终端 2 启动客户端
cd 1_echo_server
./client_udp 127.0.0.1 8888
bash 复制代码
# 项目二:终端 1 启动服务端
cd 2_echo_server
make
./server_udp 8888

# 项目二:终端 2 启动客户端
cd 2_echo_server
./client_udp 127.0.0.1 8888
bash 复制代码
# 项目三:终端 1 启动服务端
cd 3_ChatServer
make
./server_chat 8888

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

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

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

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

  1. UDP 不保证送达和顺序。 当前程序没有确认、重传、去重或顺序控制;如果业务需要可靠传输,可以改用 TCP,或在应用层设计相应机制。
  2. 在线用户没有超时清理。 项目中有 OffLineUser,但当前收发流程没有调用它。可以增加心跳和超时检测,清理失联客户端。
  3. 消息协议还比较简单。 目前把昵称和正文拼成普通字符串。后续可以加入消息类型、长度、发送者标识等字段,并明确编码规则。
  4. 收发缓冲区有固定大小。 示例一次最多处理有限长度的数据报,较长消息可能被截断;实际服务需要定义最大消息长度并检查发送、接收结果。
  5. 广播时持有用户表锁。 当前代码在遍历用户表并调用 sendto 时一直持锁。可以在锁内复制一份地址列表,解锁后再逐个发送,缩短其他线程等待用户表锁的时间。

对我来说,这三个项目的价值不只是"做出了一个回显服务器和一个聊天室",而是把网络程序从能收发数据,一步步推进到职责拆分和并发处理。每个阶段都不大,但能清楚地看到下一层问题是怎么出现的,也能看到新概念为什么需要被引入。

八、完整代码

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

8.1 项目一:UDP 回显服务器

EchoServer.hpp
cpp 复制代码
#pragma once
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include "Logger.hpp"


using namespace NS_LOG_MODULE;

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

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

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

    ~UdpServer()
    {
        close(_sockfd);
    }

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

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

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

        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串ip->4字节ip; 2.hton
        
        
        // 只是填充在了local中,并没有写入内核

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

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


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

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

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

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

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

    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(socket_port);
    usvr->Init();
    usvr->Loop();
    return 0;
}
EchoClient.cpp
cpp 复制代码
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>


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


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

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

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

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

    // 2. 不需要显示的去bind绑定addr(IP+port)信息,udp client首次发送数据的时候,OS底层会隐式自动获取随机端口并绑定IP+port
    
    // 后面发数据的时候,只需要sockaddr就行
    struct sockaddr_in server{};
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);

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

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

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

    }


    
    return 0;
}
Logger.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/time.h>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
#include "mutex.hpp"


namespace NS_LOG_MODULE
{

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

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


    // 详细的时间信息
    std::string GetCurrentTime() {
        struct timeval current_time;
        gettimeofday(&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
cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

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

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

// 采用RAII风格,进行锁管理
class LockGuard
{
public:
    LockGuard(Mutex& lock):_lockref(lock) {
        _lockref.Lock();
    }
    ~LockGuard() {
        _lockref.UnLock();
    }
private:
    Mutex& _lockref;
};
Makefile
makefile 复制代码
.PHONY:all
all:client_udp server_udp
server_udp:EchoServerMain.cpp
	g++ -o $@ $^ -std=c++17
client_udp:EchoClient.cpp
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f client_udp server_udp

8.2 项目二:UDP 词典服务

EchoServer.hpp
cpp 复制代码
#pragma once
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include "Logger.hpp"


using namespace NS_LOG_MODULE;

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

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

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

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

    ~UdpServer()
    {
        close(_sockfd);
    }

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

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

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

        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.字符串ip->4字节ip; 2.hton
        
        
        // 只是填充在了local中,并没有写入内核

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

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


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

                std::string result = _cb(inbuffer);
                
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, peerlen);
            }
        }
    }
private:
    int _sockfd;
    // std::string _ip;    // "192.168.2.2"(字符串风格的点分十进制IP地址,给人看) && 4字节IP
    uint16_t _port;
    callback_t _cb;
};
EchoServerMain.cpp
cpp 复制代码
#include "EchoServer.hpp"
#include "Dict.hpp"
#include <memory>

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

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

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

    // 1. 定义字典
    Dict dict;

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

    // 3. 绑定上下两层
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>([&dict](std::string word){
        return dict.Translate(word);
    },socket_port);
    usvr->Init();
    usvr->Loop();
    return 0;
}
EchoClient.cpp
cpp 复制代码
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>


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


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

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

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

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

    // 2. 不需要显示的去bind绑定addr(IP+port)信息,udp client首次发送数据的时候,OS底层会隐式自动获取随机端口并绑定IP+port
    
    // 后面发数据的时候,只需要sockaddr就行
    struct sockaddr_in server{};
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    inet_pton(AF_INET, server_ip.c_str(), &server.sin_addr);

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

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

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

    }


    
    return 0;
}
Dict.hpp
cpp 复制代码
#pragma once

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

#include "Logger.hpp"

using namespace NS_LOG_MODULE;

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

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

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

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

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

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

    std::string Translate(std::string word) {
        auto it = _dict.find(word);
        if (it != _dict.end()) {
            return it->second;
        }
        else {
            return "None";
        }
    }
private:
    std::string _dict_path;
    std::unordered_map<std::string, std::string> _dict;
};
dict.txt
text 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
Logger.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/time.h>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
#include "mutex.hpp"


namespace NS_LOG_MODULE
{

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

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


    // 详细的时间信息
    std::string GetCurrentTime() {
        struct timeval current_time;
        gettimeofday(&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
cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

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

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

// 采用RAII风格,进行锁管理
class LockGuard
{
public:
    LockGuard(Mutex& lock):_lockref(lock) {
        _lockref.Lock();
    }
    ~LockGuard() {
        _lockref.UnLock();
    }
private:
    Mutex& _lockref;
};
Makefile
makefile 复制代码
.PHONY:all
all:client_udp server_udp
server_udp:EchoServerMain.cpp
	g++ -o $@ $^ -std=c++17
client_udp:EchoClient.cpp
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f client_udp server_udp

8.3 项目三:UDP 多人聊天室

ChatServerMain.cpp
cpp 复制代码
#include <memory>
#include "UdpServer.hpp"   // 获取事件
#include "ThreadPool.hpp"   // 执行者
#include "Route.hpp"        // 任务


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

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

using namespace NS_THREAD_POOL_MODULE;

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

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

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

    // 路由模块
    Route rt;

    // 网络服务模块
    UdpServer usvr(socket_port);
    usvr.Init();
    
    usvr.RegisterService(
        [&rt](const InetAddr& addr){
            rt.CheckUser(addr);
        },
        [&rt, thread_pool](int sockfd, std::string msg){
            auto t = std::bind(&Route::Broadcast, &rt, sockfd, msg);
            thread_pool->Enqueue(t);

            // 等同于
            // thread_pool->Enqueue([&rt, &sockfd, &msg](){
            //     rt.Broadcast(sockfd, msg);
            // });
        });
    usvr.Loop();
    
    return 0;
}
UdpServer.hpp
cpp 复制代码
#pragma once
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string>
#include <functional>
#include "Logger.hpp"
#include "InetAddr.hpp"


using namespace NS_LOG_MODULE;

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

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

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

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

    ~UdpServer()
    {
        close(_sockfd);
    }

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

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

        InetAddr local(_port);
        
        // 只是填充在了local中,并没有写入内核

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

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

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

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

                // 2. 转发消息
                _handler_msg(_sockfd, message);
            }
        }
    }
private:
    int _sockfd;
    // std::string _ip;    // "192.168.2.2"(字符串风格的点分十进制IP地址,给人看) && 4字节IP
    uint16_t _port;
    // callback_t _cb;
    handler_addr_t _handler_addr;
    handler_msg_t _handler_msg;
};
ChatClient.cpp
cpp 复制代码
#include <iostream>
#include <string>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "thread.hpp"
#include "InetAddr.hpp"


using namespace NS_THREAD_MODULE;

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

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

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

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


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


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

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


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

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

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

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

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

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

    return 0;
}
Dict.hpp
cpp 复制代码
#pragma once

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

#include "Logger.hpp"

using namespace NS_LOG_MODULE;

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

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

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

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

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

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

    std::string Translate(std::string word) {
        auto it = _dict.find(word);
        if (it != _dict.end()) {
            return it->second;
        }
        else {
            return "None";
        }
    }
private:
    std::string _dict_path;
    std::unordered_map<std::string, std::string> _dict;
};
InetAddr.hpp
cpp 复制代码
#pragma once

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

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

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

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

    socklen_t Len() {
        return _len;
    }

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

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

    // host地址
    std::string _ip;
    uint16_t _port;
};
Route.hpp
cpp 复制代码
#pragma once

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

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

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

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

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

private:
    std::unique_ptr<UserManager> _uma;
    Mutex _lock;
};
UserManager.hpp
cpp 复制代码
#pragma once

#include <string>
#include <vector>

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

using namespace NS_LOG_MODULE;


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

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

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

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

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

    // bool ModUser(const InetAddr &addr) {

    // }

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

private:
    std::vector<InetAddr> _users;
};
ThreadPool.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include "mutex.hpp"
#include "Logger.hpp"
#include "thread.hpp"
#include "Cond.hpp"

namespace NS_THREAD_POOL_MODULE
{
    using namespace NS_THREAD_MODULE;
    using namespace NS_LOG_MODULE;

    const int defaultnum = 5;



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

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

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

        ThreadPool(int slaver_num = defaultnum)
        :_isrunning(false),
         _slaver_num(slaver_num),
         _slaver_sleep_count(0)
        {
            
            for (int i = 0; i < _slaver_num; ++i) {

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

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

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

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

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

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

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

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

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

    template<typename T>
    Mutex ThreadPool<T>::_lock;
}
thread.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <string.h>
#include <functional>
#include "Logger.hpp"

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

    enum class TSTATUS {
        THREAD_NEW,
        THREAD_RUNNING,
        THREAD_STOP
    };

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

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

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

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

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

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

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

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

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

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

        void PrintInfo() {
            std::cout << "thread name: " << _name << std::endl;
            std::cout << "thread tid: " << _tid << std::endl;
            std::cout << "thread status: " << StatusToString(_status) << std::endl;
            std::cout << "thread joinable: " << IsJoin(_joinable) << std::endl;
        }
    private:
        std::string _name;
        pthread_t _tid;
        TSTATUS _status;
        bool _joinable;
        callback_t _cb;
        void* _result;
    };
}
Cond.hpp
cpp 复制代码
#pragma once

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

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

    void Wait(Mutex &mutex) {
        pthread_cond_wait(&_cond,mutex.Ptr());
    }
    void Signal() {
        pthread_cond_signal(&_cond);
    }
    void Broadcast() {
        pthread_cond_broadcast(&_cond);
    }
private:
    pthread_cond_t _cond;
};
mutex.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

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

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

// 采用RAII风格,进行锁管理
class LockGuard
{
public:
    LockGuard(Mutex& lock):_lockref(lock) {
        _lockref.Lock();
    }
    ~LockGuard() {
        _lockref.UnLock();
    }
private:
    Mutex& _lockref;
};
Logger.hpp
cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/time.h>
#include <ctime>
#include <cstdio>
#include <unistd.h>
#include <filesystem>
#include <fstream>
#include <memory>
#include <sstream>
#include "mutex.hpp"


namespace NS_LOG_MODULE
{

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

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


    // 详细的时间信息
    std::string GetCurrentTime() {
        struct timeval current_time;
        gettimeofday(&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
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
相关推荐
Brilliantwxx1 小时前
【STM32】RTC 完结篇:日期操作、闹钟中断与附录(时间戳 / 闹钟 / NTP)
服务器·c语言·stm32·单片机·嵌入式硬件·实时音视频
论文复现现场1 小时前
RTX 3090 云GPU租赁价格对比:8家平台配置、计费方式与环境测试
linux·运维·人工智能·pytorch·深度学习·云计算·3090
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】Trae生成代码介绍
网络·c++·人工智能·学习·架构
Eloudy1 小时前
IB Specification Vol 1 Chapter 3 体系结构与 rdma-core-2410mlnx54 实现关联分析
网络·gpu·rdma
贵州诚鑫致达科技有限公司1 小时前
接入汇聚都是环怎么防环路?华为SEP加RRPP混合环
服务器·华为·php
syagain_zsx1 小时前
BRpc 搭建简单 rpc 服务器
服务器·c++·网络协议·rpc·protobuf·库
云飞云共享云桌面1 小时前
一台SolidWorks设计服务器多人共享落地方案和配置
运维·服务器·3d·电脑·制造
Funlang1 小时前
HTTP 服务端,能写业务了
网络·网络协议·http
Pointer Pursuit1 小时前
进程优先级
linux·运维·服务器