基于UDP协议的英汉翻译服务系统:从网络通信到字典查询的完整机制

目录

一、UdpServer.hpp

1、头文件和预处理指令

2、包含的头文件

3、类型别名和常量定义

[4、UdpServer 类定义](#4、UdpServer 类定义)

[4.1 构造函数](#4.1 构造函数)

[4.2 Init() 方法 - 初始化服务器](#4.2 Init() 方法 - 初始化服务器)

[4.3 Start() 方法 - 启动服务器](#4.3 Start() 方法 - 启动服务器)

[4.4 析构函数](#4.4 析构函数)

[4.5 成员变量](#4.5 成员变量)

5、设计特点

6、潜在改进点

总结

二、UdpServer.cc

1、头文件包含

2、测试用的默认处理器

3、主函数

[3.1 参数处理](#3.1 参数处理)

[3.2 日志初始化](#3.2 日志初始化)

4、核心功能实现

[4.1 字典初始化](#4.1 字典初始化)

[4.2 UDP服务器创建](#4.2 UDP服务器创建)

[4.3 服务器启动](#4.3 服务器启动)

5、设计特点分析

6、潜在改进点

7、完整工作流程

总结

三、InetAddr.hpp

1、头文件保护

2、头文件包含

3、类定义

4、成员详解

[4.1 构造函数](#4.1 构造函数)

[4.2 成员函数](#4.2 成员函数)

[4.3 析构函数](#4.3 析构函数)

[4.4 成员变量](#4.4 成员变量)

5、技术要点

[5.1 网络字节序转换](#5.1 网络字节序转换)

[5.2 IP地址表示](#5.2 IP地址表示)

[5.3 设计考虑](#5.3 设计考虑)

6、潜在改进

7、使用示例

8、总结

四、Dict.hpp

1、头文件和预处理指令

2、头文件包含

3、全局常量定义

4、类定义

5、成员详解

[5.1 构造函数](#5.1 构造函数)

[5.2 LoadDict() 方法](#5.2 LoadDict() 方法)

[功能:从 _dict_path 指定的文件加载词典数据到内存](#功能:从 _dict_path 指定的文件加载词典数据到内存)

详细流程:

设计特点:

[5.3 Translate() 方法](#5.3 Translate() 方法)

功能:查询单词的中文翻译

参数:

详细流程:

设计特点:

[5.4 成员变量](#5.4 成员变量)

6、技术要点

[6.1 文件格式](#6.1 文件格式)

[6.2 错误处理](#6.2 错误处理)

[6.3 日志记录](#6.3 日志记录)

[6.4 性能考虑](#6.4 性能考虑)

7、潜在改进

8、使用示例

9、总结

五、UdpClient.cc

1、代码功能概述

2、代码逐段解析

[(1) 头文件引入](#(1) 头文件引入)

[(2) 命令行参数检查](#(2) 命令行参数检查)

[(3) 创建 UDP 套接字](#(3) 创建 UDP 套接字)

[(4) 服务器地址配置](#(4) 服务器地址配置)

[(5) 主循环(发送与接收)](#(5) 主循环(发送与接收))

3、关键问题解答

[(1) 客户端是否需要 bind()?](#(1) 客户端是否需要 bind()?)

[(2) 为什么 UDP 客户端通常不绑定?](#(2) 为什么 UDP 客户端通常不绑定?)

4、潜在问题与改进

6、总结

[7、补充:struct sockaddr_in server 和 struct sockaddr_in peer](#7、补充:struct sockaddr_in server 和 struct sockaddr_in peer)

[1. server 的作用](#1. server 的作用)

[2. peer 的作用](#2. peer 的作用)

[3. 为什么不能直接用 server 代替 peer?](#3. 为什么不能直接用 server 代替 peer?)

[4. 当前代码的问题](#4. 当前代码的问题)

[5. 可以省略 peer 吗?](#5. 可以省略 peer 吗?)

总结

六、相关的必需头文件

Log.hpp

Mutex.hpp

七、运行输出结果


一、UdpServer.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"

using namespace LogModule;

using func_t = std::function<std::string(const std::string&, InetAddr&)>;

const int defaultfd = -1;

// 你是为了进行网络通信的!
class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultfd),
        //   _ip(ip),
          _port(port),
          _isrunning(false),
          _func(func)
    {
    }
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;

        // 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
        // 2.1 填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        // 我会不会把我的IP地址和端口号发送给对方?
        // IP信息和端口信息,一定要发送到网络!
        // 本地格式->网络序列
        local.sin_port = htons(_port);
        // IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
        //local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
        local.sin_addr.s_addr = INADDR_ANY;

        // 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind error";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
            ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (s > 0)
            {
                InetAddr client(peer);
                buffer[s] = 0;
                // 收到的内容,当做英文单词
                std::string result = _func(buffer, client);


                // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer; // 1. 消息内容 2. 谁发的??

                // 2. 发消息
                // std::string echo_string = "server echo@ ";
                // echo_string += buffer;
                sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);
            }
        }
    }
    ~UdpServer()
    {
    }

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip; // 用的是字符串风格,点分十进制, "192.168.1.1"
    bool _isrunning;

    func_t _func; // 服务器的回调函数,用来进行对数据进行处理
};

这是一个基于 UDP 协议的服务器实现,下面我将从各个方面详细讲解这份代码。

1、头文件和预处理指令

cpp 复制代码
#pragma once
  • #pragma once 是一个非标准但被广泛支持的预处理指令,用于防止头文件被多次包含。

2、包含的头文件

cpp 复制代码
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"
  • 标准库头文件:<iostream>, <string>, <functional>

  • 系统头文件:<strings.h> (字符串操作), <sys/types.h> (系统数据类型), <sys/socket.h> (套接字API), <netinet/in.h> (IPv4地址结构), <arpa/inet.h> (地址转换函数)

  • 自定义头文件:"Log.hpp" (日志模块), "InetAddr.hpp" (封装了IP地址操作)

3、类型别名和常量定义

cpp 复制代码
using func_t = std::function<std::string(const std::string&, InetAddr&)>;
const int defaultfd = -1;
  • 定义了一个函数类型 func_t,它是一个可调用对象,接受一个 const std::string& 和一个 InetAddr& 参数,返回 std::string

  • 定义了一个常量 defaultfd 表示默认的文件描述符值(-1表示无效)

4、UdpServer 类定义

4.1 构造函数

cpp 复制代码
UdpServer(uint16_t port, func_t func)
    : _sockfd(defaultfd),
      _port(port),
      _isrunning(false),
      _func(func)
{
}
  • 参数:

    • port: 服务器监听的端口号

    • func: 处理接收到的数据的回调函数

  • 初始化列表:

    • _sockfd: 初始化为无效值

    • _port: 设置为传入的端口号

    • _isrunning: 初始化为false

    • _func: 设置为传入的回调函数

4.2 Init() 方法 - 初始化服务器

cpp 复制代码
void Init()
{
    // 1. 创建套接字
    _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "socket error!";
        exit(1);
    }
    LOG(LogLevel::INFO) << "socket success, sockfd : " << _sockfd;

    // 2. 绑定socket信息
    struct sockaddr_in local;
    bzero(&local, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    local.sin_addr.s_addr = INADDR_ANY;

    int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
    if (n < 0)
    {
        LOG(LogLevel::FATAL) << "bind error";
        exit(2);
    }
    LOG(LogLevel::INFO) << "bind success, sockfd : " << _sockfd;
}
  1. 创建套接字:

    • 使用 socket() 函数创建 UDP 套接字 (SOCK_DGRAM)

    • 检查返回值,失败则记录日志并退出

  2. 绑定套接字:

    • 创建并初始化 sockaddr_in 结构体

    • bzero() 清零结构体

    • 设置地址族为 AF_INET (IPv4)

    • 设置端口号 (htons() 将主机字节序转换为网络字节序)

    • 设置IP地址为 INADDR_ANY (监听所有网络接口)

    • 调用 bind() 绑定套接字到指定地址和端口

    • 检查返回值,失败则记录日志并退出

4.3 Start() 方法 - 启动服务器

cpp 复制代码
void Start()
{
    _isrunning = true;
    while (_isrunning)
    {
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        
        // 1. 接收消息
        ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, 
                            (struct sockaddr *)&peer, &len);
        if (s > 0)
        {
            InetAddr client(peer);
            buffer[s] = 0;
            std::string result = _func(buffer, client);

            // 2. 发送响应
            sendto(_sockfd, result.c_str(), result.size(), 0, 
                  (struct sockaddr*)&peer, len);
        }
    }
}
  1. 主循环:

    • 设置 _isrunning 为 true,进入循环

    • 准备接收缓冲区 (buffer) 和客户端地址结构 (peer)

  2. 接收消息:

    • 使用 recvfrom() 接收数据

    • 如果成功接收 (s > 0):

      • 用接收到的地址信息创建 InetAddr 对象

      • 在缓冲区末尾添加 null 终止符

      • 调用回调函数 _func 处理接收到的数据,获取响应

  3. 发送响应: 使用 sendto() 将处理结果发送回客户端

4.4 析构函数

cpp 复制代码
~UdpServer()
{
}
  • 当前为空实现,理论上应该关闭套接字 (close(_sockfd))

4.5 成员变量

cpp 复制代码
private:
    int _sockfd;          // 套接字文件描述符
    uint16_t _port;       // 服务器监听的端口号
    bool _isrunning;      // 服务器运行状态标志
    func_t _func;         // 数据处理回调函数

5、设计特点

  • **基于事件的处理:**使用回调函数机制处理接收到的数据,使服务器逻辑与数据处理逻辑解耦

  • UDP协议特性:

    • 无连接:不需要建立连接,直接收发数据

    • 不可靠:不保证数据顺序和可靠性,但效率高

  • 可扩展性: 通过 func_t 回调函数,可以灵活定义不同的数据处理逻辑

  • 日志记录: 使用自定义的 LogModule 记录关键操作和错误

6、潜在改进点

  1. 资源管理:

    • 析构函数中应该关闭套接字

    • 考虑使用 RAII 模式管理资源

  2. 错误处理:

    • 可以添加更多错误处理逻辑

    • 考虑部分失败的情况

  3. 性能优化:

    • 可以添加缓冲区管理

    • 考虑多线程/多路复用处理高并发

  4. **配置灵活性:**可以添加设置超时、缓冲区大小等选项

总结

这是一个简洁但功能完整的UDP服务器实现,适合学习网络编程基础。它展示了UDP服务器的核心操作:创建套接字、绑定地址、接收和发送数据。通过回调函数的设计,使得数据处理逻辑可以灵活定制。


二、UdpServer.cc

cpp 复制代码
#include <iostream>
#include <memory>
#include "Dict.hpp"      // 翻译的功能
#include "UdpServer.hpp" // 网络通信的功能


// 仅仅是用来进行测试的
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}

// 需求
// 1. 翻译系统,字符串当成英文单词,把英文单词翻译成为汉语
// 2. 基于文件来做



// ./udpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    // std::string ip = argv[1];
    uint16_t port = std::stoi(argv[1]);
    Enable_Console_Log_Strategy();

    // 1. 字典对象提供翻译功能
    Dict dict;
    dict.LoadDict();
    
    // 2. 网络服务器对象,提供通信功能
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr&cli)->std::string{
        return dict.Translate(word, cli);
    });
    usvr->Init();
    usvr->Start();
    return 0;
}

这是一个基于UDP协议的翻译服务器实现,结合了网络通信和字典翻译功能。下面我将从各个方面详细讲解这份代码。

1、头文件包含

cpp 复制代码
#include <iostream>
#include <memory>
#include "Dict.hpp"      // 翻译的功能
#include "UdpServer.hpp" // 网络通信的功能
  • <iostream>: 标准输入输出流

  • <memory>: 智能指针相关功能

  • "Dict.hpp": 自定义字典类,提供翻译功能

  • "UdpServer.hpp": 自定义UDP服务器类,提供网络通信功能

2、测试用的默认处理器

cpp 复制代码
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}
  • 这是一个简单的测试用的消息处理器

  • 功能:在接收到的消息前加上"hello, "并返回

  • 虽然定义了但实际代码中并未使用

3、主函数

cpp 复制代码
int main(int argc, char *argv[])
{
    // 参数检查
    if(argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    
    // 获取端口号
    uint16_t port = std::stoi(argv[1]);
    
    // 启用控制台日志策略
    Enable_Console_Log_Strategy();

3.1 参数处理

  • 检查命令行参数数量,要求必须提供一个端口号参数

  • 如果参数数量不正确,打印用法信息并返回错误码1

  • 使用std::stoi将字符串端口号转换为整数

3.2 日志初始化

  • 调用Enable_Console_Log_Strategy()启用控制台日志输出

  • 这应该是日志模块的一个配置函数

4、核心功能实现

cpp 复制代码
// 1. 字典对象提供翻译功能
    Dict dict;
    dict.LoadDict();
    
    // 2. 网络服务器对象,提供通信功能
    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict](const std::string &word, InetAddr& cli)->std::string{
        return dict.Translate(word, cli);
    });
    usvr->Init();
    usvr->Start();

4.1 字典初始化

  • 创建Dict对象dict

  • 调用LoadDict()方法加载字典数据

  • 字典数据应该来自文件(根据注释说明)

4.2 UDP服务器创建

  • 使用std::make_unique创建UdpServer的智能指针

  • 构造函数参数:

    • port: 服务器监听端口

    • Lambda表达式作为回调函数:

      • 捕获dict对象的引用

      • 参数:要翻译的单词(word)和客户端地址(cli)

      • 返回值:翻译结果字符串

      • 调用dict.Translate()方法进行实际翻译

4.3 服务器启动

  • 调用Init()初始化服务器

  • 调用Start()启动服务器主循环

5、设计特点分析

  1. 模块化设计: 将网络通信(UdpServer)和业务逻辑(Dict)分离、通过回调函数将两者结合起来

  2. 智能指针使用: 使用std::unique_ptr管理UdpServer对象生命周期、自动内存管理,避免内存泄漏

  3. Lambda表达式: 使用Lambda作为回调函数,简洁地封装了翻译逻辑、捕获局部变量dict,使回调能访问字典对象

  4. 错误处理: 基本参数检查、但服务器初始化错误处理在UdpServer内部(通过日志和退出)

6、潜在改进点

  • **配置灵活性:**字典文件路径应该可配置、考虑添加命令行选项或配置文件

  • **错误处理:**更完善的参数验证(如端口号范围)、字典加载失败的处理

  • **日志增强:**添加更多运行日志、考虑不同日志级别

  • **性能考虑:**对于高并发,可能需要优化字典查询、考虑缓存常用翻译结果

  • **代码组织:**将主函数逻辑进一步拆分到函数中、考虑使用专门的服务器类封装

7、完整工作流程

  1. 程序启动,检查并获取端口号参数

  2. 初始化日志系统

  3. 加载字典数据到内存

  4. 创建UDP服务器,设置翻译回调

  5. 服务器初始化并开始运行:

    • 监听指定端口

    • 接收客户端UDP消息

    • 对每个收到的单词调用字典翻译

    • 将翻译结果发送回客户端

总结

这是一个结合了网络通信和字典翻译功能的UDP服务器实现。它展示了如何:

  • 使用UDP协议进行网络通信

  • 实现业务逻辑(翻译)与网络层的分离

  • 使用现代C++特性(智能指针、Lambda)简化代码

  • 通过回调函数机制实现灵活的业务处理

代码结构清晰,职责分明,是一个良好的网络服务实现示例。


三、InetAddr.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
// 网络地址和主机地址之间进行转换的类

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);           // 从网络中拿到的!网络序列
        _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IP
    }
    uint16_t Port() {return _port;}
    std::string Ip() {return _ip;}

    ~InetAddr()
    {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

这个 InetAddr 类是一个用于网络地址管理的工具类,主要功能是将网络字节序的地址信息转换为主机字节序的可读格式。下面我将从各个方面详细讲解这个类的实现。

1、头文件保护

cpp 复制代码
#pragma once
  • 使用 #pragma once 防止头文件被多次包含

  • 这是非标准但被广泛支持的预处理指令,比传统的 #ifndef 方式更简洁

2、头文件包含

cpp 复制代码
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netinet/in.h>
  • <iostream><string>: 标准C++库,用于输入输出和字符串处理

  • <sys/socket.h>, <sys/types.h>: 系统头文件,提供套接字相关定义

  • <arpa/inet.h>: 提供IP地址转换函数(如 inet_ntoa

  • <netinet/in.h>: 提供 sockaddr_in 结构体定义

3、类定义

cpp 复制代码
class InetAddr
{
public:
    // 构造函数
    InetAddr(struct sockaddr_in &addr) : _addr(addr)
    {
        _port = ntohs(_addr.sin_port);           // 网络字节序转主机字节序
        _ip = inet_ntoa(_addr.sin_addr);         // 网络IP转点分十进制字符串
    }
    
    // 获取端口号
    uint16_t Port() {return _port;}
    
    // 获取IP地址字符串
    std::string Ip() {return _ip;}

    // 析构函数
    ~InetAddr()
    {}

private:
    struct sockaddr_in _addr;  // 原始的网络地址结构
    std::string _ip;           // 存储点分十进制IP字符串
    uint16_t _port;            // 存储主机字节序的端口号
};

4、成员详解

4.1 构造函数

cpp 复制代码
InetAddr(struct sockaddr_in &addr) : _addr(addr)
{
    _port = ntohs(_addr.sin_port);
    _ip = inet_ntoa(_addr.sin_addr);
}
  • 参数:接收一个 sockaddr_in 结构体的引用

  • 初始化列表:用传入的 addr 初始化成员变量 _addr

  • 构造函数体内:

    • ntohs(_addr.sin_port): 将网络字节序的端口号转换为主机字节序

      • ntohs: Network to Host Short

      • 网络字节序是大端序,主机字节序取决于CPU架构

    • inet_ntoa(_addr.sin_addr): 将网络字节序的IP地址转换为点分十进制字符串

      • 例如:将 0x7F000001 (127.0.0.1) 转换为字符串 "127.0.0.1"

4.2 成员函数

cpp 复制代码
uint16_t Port() {return _port;}
std::string Ip() {return _ip;}
  • Port(): 返回存储的主机字节序端口号

  • Ip(): 返回点分十进制格式的IP地址字符串

  • 这两个函数都是简单的getter方法,没有参数验证或错误处理

4.3 析构函数

cpp 复制代码
~InetAddr()
{}
  • 空的析构函数

  • 由于类中没有需要手动释放的资源,所以不需要实现特殊清理逻辑

4.4 成员变量

cpp 复制代码
private:
    struct sockaddr_in _addr;  // 原始的网络地址结构
    std::string _ip;           // 存储点分十进制IP字符串
    uint16_t _port;            // 存储主机字节序的端口号
  • _addr: 存储原始的 sockaddr_in 结构体

    • 这是POSIX网络编程中表示IPv4地址的标准结构

    • 包含:地址族、端口号、IP地址等信息

  • _ip: 存储转换后的可读IP地址字符串

  • _port: 存储转换后的主机字节序端口号

5、技术要点

5.1 网络字节序转换

  • 网络传输中使用大端序(网络字节序)

  • 不同CPU架构可能使用不同字节序(x86是小端序,网络设备通常是大端序)

  • 相关函数:

    • htons(): 主机到网络短整型转换

    • ntohs(): 网络到主机短整型转换

    • htonl(), ntohl(): 长整型转换

5.2 IP地址表示

  • sockaddr_in.sin_addrin_addr 结构体,通常包含一个32位IPv4地址

  • inet_ntoa():

    • in_addr 转换为点分十进制字符串

    • 返回指向静态缓冲区的指针,后续调用会覆盖内容

    • 线程不安全(在多线程环境中需要额外处理)

5.3 设计考虑

  1. 封装性:

    • 将原始的 sockaddr_in 结构和转换后的可读格式都存储

    • 提供简单的接口获取可读信息

  2. 性能:

    • 构造函数中完成所有转换工作

    • 后续调用 Ip()Port() 直接返回存储的值

  3. 安全性:

    • 没有暴露原始 sockaddr_in 结构的修改接口

    • inet_ntoa() 的线程安全问题需要注意

6、潜在改进

  1. 线程安全:

    • 使用 inet_ntop() 替代 inet_ntoa()

    • inet_ntop() 是线程安全的,且支持IPv6

  2. IPv6支持:

    • 当前只支持IPv4 (sockaddr_in)

    • 可以扩展支持 sockaddr_in6

  3. 错误处理:

    • 添加对无效地址的处理

    • 例如 inet_ntoa() 返回NULL时的处理

  4. **移动语义:**可以添加移动构造函数,避免不必要的拷贝

  5. **常量正确性:**将getter方法标记为const:

    cpp 复制代码
    uint16_t Port() const {return _port;}
    std::string Ip() const {return _ip;}

7、使用示例

cpp 复制代码
#include "InetAddr.hpp"
#include <sys/socket.h>
#include <netinet/in.h>

int main()
{
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(8080);          // 设置端口为8080
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 设置IP为127.0.0.1
    
    InetAddr inetAddr(addr);
    
    std::cout << "IP: " << inetAddr.Ip() << std::endl;
    std::cout << "Port: " << inetAddr.Port() << std::endl;
    
    return 0;
}

输出:

8、总结

InetAddr 类是一个简单的网络地址封装类,主要功能包括:

  1. 封装 sockaddr_in 结构体

  2. 提供网络字节序到主机字节序的转换

  3. 提供IP地址从二进制到字符串的转换

  4. 提供简单的接口获取可读的IP和端口信息

这个类在网络编程中非常有用,特别是在需要记录或显示客户端/服务器地址信息时。虽然实现简单,但它隐藏了底层网络字节序转换的细节,使上层代码更简洁易读。


四、Dict.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"

const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";

using namespace LogModule;

class Dict
{
public:
    Dict(const std::string &path = defaultdict) : _dict_path(path)
    {
    }
    bool LoadDict()
    {
        std::ifstream in(_dict_path);
        if (!in.is_open())
        {
            LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
            return false;
        }
        std::string line;
        while (std::getline(in, line))
        {
            // "apple: 苹果"
            auto pos = line.find(sep);
            if (pos == std::string::npos)
            {
                LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
                continue;
            }
            std::string english = line.substr(0, pos);
            std::string chinese = line.substr(pos + sep.size());
            if (english.empty() || chinese.empty())
            {
                LOG(LogLevel::WARNING) << "没有有效内容: " << line;
                continue;
            }

            _dict.insert(std::make_pair(english, chinese));
            LOG(LogLevel::DEBUG) << "加载: " << line;
        }

        in.close();
        return true;
    }
    std::string Translate(const std::string &word, InetAddr &client)
    {
        auto iter = _dict.find(word);
        if (iter == _dict.end())
        {
            LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";
            return "None";
        }
        LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << iter->second;
        return iter->second;
    }
    ~Dict()
    {
    }

private:
    std::string _dict_path; // 路径+文件名
    std::unordered_map<std::string, std::string> _dict;
};

这个 Dict 类是一个简单的英汉词典实现,主要功能是从文件加载词典数据并提供翻译服务。下面我将从各个方面详细讲解这个类的实现。

1、头文件和预处理指令

cpp 复制代码
#pragma once
  • 使用 #pragma once 防止头文件被多次包含

  • 这是非标准但被广泛支持的预处理指令,比传统的 #ifndef 方式更简洁

2、头文件包含

cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"
  • <iostream>: 标准C++输入输出流

  • <fstream>: 文件流操作,用于读取词典文件

  • <string>: 字符串处理

  • <unordered_map>: 哈希表实现,用于存储词典数据

  • "Log.hpp": 自定义日志模块

  • "InetAddr.hpp": 前面讲解的网络地址类

3、全局常量定义

cpp 复制代码
const std::string defaultdict = "./dictionary.txt";
const std::string sep = ": ";
  • defaultdict: 默认词典文件路径

  • sep: 词典文件中英文和中文之间的分隔符

4、类定义

cpp 复制代码
using namespace LogModule;

class Dict
{
public:
    // 构造函数
    Dict(const std::string &path = defaultdict) : _dict_path(path)
    {
    }
    
    // 加载词典方法
    bool LoadDict();
    
    // 翻译方法
    std::string Translate(const std::string &word, InetAddr &client);
    
    // 析构函数
    ~Dict()
    {
    }

private:
    std::string _dict_path; // 词典文件路径
    std::unordered_map<std::string, std::string> _dict; // 词典存储结构
};

5、成员详解

5.1 构造函数

cpp 复制代码
Dict(const std::string &path = defaultdict) : _dict_path(path)
{
}
  • 参数:接收一个字符串参数表示词典文件路径,默认使用 defaultdict

  • 功能:初始化 _dict_path 成员变量

  • 设计特点:

    • 使用了默认参数,使调用更灵活

    • 初始化列表简洁高效

5.2 LoadDict() 方法

cpp 复制代码
bool LoadDict()
{
    std::ifstream in(_dict_path);
    if (!in.is_open())
    {
        LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";
        return false;
    }
    
    std::string line;
    while (std::getline(in, line))
    {
        // "apple: 苹果"
        auto pos = line.find(sep);
        if (pos == std::string::npos)
        {
            LOG(LogLevel::WARNING) << "解析: " << line << " 失败";
            continue;
        }
        
        std::string english = line.substr(0, pos);
        std::string chinese = line.substr(pos + sep.size());
        
        if (english.empty() || chinese.empty())
        {
            LOG(LogLevel::WARNING) << "没有有效内容: " << line;
            continue;
        }

        _dict.insert(std::make_pair(english, chinese));
        LOG(LogLevel::DEBUG) << "加载: " << line;
    }

    in.close();
    return true;
}
功能:从 _dict_path 指定的文件加载词典数据到内存
详细流程:
  1. 打开文件:

    • 使用 std::ifstream 打开词典文件(重点!!!std::ifstream 是 C++ 标准库中的一个类,用于从文件读取数据。它是 std::basic_ifstream 模板针对 char 类型的特化,属于 <fstream> 头文件,之前在日志系统和线程池那部分使用过;std::ifstream 实例化出来的对象代表一个已打开的文件,并通过该对象操作文件内容。不过更准确地说,它是一个文件输入流对象,封装了与文件的交互逻辑,而不仅仅是"文件"本身。)

    • 如果打开失败,记录DEBUG日志并返回false

  2. 逐行读取:

    • 使用 std::getline 逐行读取文件内容

    • 每行格式应为 "英文: 中文"

  3. 解析每行:

    • 查找分隔符 ": " 的位置

    • 如果找不到分隔符,记录WARNING日志并跳过该行

    • 分割出英文部分和中文部分

    • 检查两部分是否为空,如果为空则记录WARNING并跳过

  4. 存储词典数据:

    • 将有效的英汉对插入 _dict 哈希表

    • 记录DEBUG日志表示成功加载该行

  5. **关闭文件:**显式关闭文件流(虽然析构函数也会自动关闭)

  6. **返回结果:**成功加载返回true,文件打开失败返回false

设计特点:
  • 使用了RAII管理文件资源(通过 ifstream 的析构函数自动关闭文件)

  • 详细的日志记录,便于调试和问题排查

  • 健壮的错误处理,跳过无效行而不中断整个加载过程

  • 使用 unordered_map 存储词典,提供O(1)时间复杂度的查询

5.3 Translate() 方法

cpp 复制代码
std::string Translate(const std::string &word, InetAddr &client)
{
    auto iter = _dict.find(word);
    if (iter == _dict.end())
    {
        LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->None";
        return "None";
    }
    LOG(LogLevel::DEBUG) << "进入到了翻译模块, [" << client.Ip() << " : " << client.Port() << "]# " << word << "->" << iter->second;
    return iter->second;
}
功能:查询单词的中文翻译
参数:
  • word: 要查询的英文单词

  • client: 客户端地址信息(用于日志记录)

详细流程:
  1. 查询词典:

    • 使用 unordered_mapfind 方法查询单词

    • 如果找不到,返回 "None"

  2. 记录日志:

    • 无论是否找到,都记录DEBUG日志

    • 日志包含客户端IP、端口、查询单词和结果

    • 使用 InetAddr 类的方法获取客户端IP和端口

  3. **返回结果:**找到则返回中文翻译,否则返回 "None"

设计特点:
  • 使用了哈希表查询,效率高

  • 日志记录详细,便于追踪查询请求

  • 接口简单,只返回字符串结果

5.4 成员变量

cpp 复制代码
private:
    std::string _dict_path; // 词典文件路径
    std::unordered_map<std::string, std::string> _dict; // 词典存储结构
  • _dict_path: 存储词典文件路径

  • _dict: 使用 unordered_map 存储词典数据

    • 键:英文单词(string

    • 值:中文翻译(string

    • 选择 unordered_map 因为其查询效率高于 map(O(1) vs O(log n))

6、技术要点

6.1 文件格式

词典文件预期格式:(dictionary.txt)

cpp 复制代码
apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello: 
: 你好



run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
  • 每行一个词条

  • 英文和中文之间用 ": " 分隔

6.2 错误处理

  • 文件打开失败:返回false

  • 行格式错误:跳过并记录警告

  • 空内容:跳过并记录警告

  • 查询不到:返回"None"

6.3 日志记录

  • 使用自定义的 LogModule

  • 记录不同级别的日志:

    • DEBUG: 详细调试信息

    • WARNING: 可恢复的错误情况

  • 日志包含上下文信息(如客户端地址)

6.4 性能考虑

  • 使用 unordered_map 存储词典,查询效率高

  • 文件读取使用缓冲I/O(ifstream 默认行为)

  • 只在启动时加载词典,运行时查询不涉及I/O

7、潜在改进

  1. **词典热更新:**可以添加监视词典文件变化并自动重新加载的功能

  2. **更复杂的查询:**支持模糊查询、前缀匹配等

  3. **多词典支持:**支持加载多个词典文件

  4. **性能优化:**对于大型词典,可以考虑内存映射文件

  5. 错误处理增强:

    • 区分不同类型的错误(文件不存在 vs 权限问题)

    • 提供更详细的错误信息

  6. 线程安全: 如果需要在多线程环境中使用,需要添加互斥锁保护 _dict

  7. **持久化:**支持将词典保存回文件

  8. **编码处理:**明确处理文件编码(如UTF-8)

8、使用示例

cpp 复制代码
#include "Dict.hpp"
#include "InetAddr.hpp"

int main()
{
    // 创建词典对象(使用默认路径)
    Dict dict;
    
    // 加载词典
    if (!dict.LoadDict())
    {
        std::cerr << "加载词典失败" << std::endl;
        return 1;
    }
    
    // 模拟客户端地址
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(12345);
    addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    InetAddr client(addr);
    
    // 测试翻译
    std::cout << "apple -> " << dict.Translate("apple", client) << std::endl;
    std::cout << "banana -> " << dict.Translate("banana", client) << std::endl;
    std::cout << "unknown -> " << dict.Translate("unknown", client) << std::endl;
    
    return 0;
}

9、总结

Dict 类是一个简单但功能完整的词典实现,主要特点包括:

  • 文件加载:

    • 从文本文件加载词典数据

    • 支持自定义文件路径

    • 健壮的错误处理和日志记录

  • **高效查询:**使用哈希表存储词典数据、提供快速的单词查询接口

  • **日志集成:**与日志系统集成,便于监控和调试、记录客户端信息和查询详情

  • **简洁接口:**对外提供简单的构造、加载和查询接口、隐藏内部实现细节

这个类适合用作小型词典服务或作为更复杂翻译系统的基础组件。其设计体现了关注点分离的原则,将词典数据的存储、加载和查询功能封装在一个类中。


五、UdpClient.cc

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h> // close()

int main(int argc, char *argv[]) {
    if (argc != 3) {
        std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
        return 1;
    }

    // 解析服务器地址
    std::string server_ip = argv[1];
    uint16_t server_port = std::stoi(argv[2]);
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    if (inet_addr(server_ip.c_str()) == INADDR_NONE) {
        std::cerr << "Invalid server IP" << std::endl;
        return 1;
    }
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0) {
        perror("socket error");
        return 2;
    }

    // 主循环
    while (true) {
        std::string input;
        std::cout << "Please Enter# ";
        std::getline(std::cin, input);

        if (input == "exit") break; // 退出条件

        // 发送数据
        ssize_t n = sendto(sockfd, input.c_str(), input.size(), 0,
                          (struct sockaddr*)&server, sizeof(server));
        if (n < 0) {
            perror("sendto error");
            continue;
        }

        // 接收数据
        char buffer[1024];
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0,
                            (struct sockaddr*)&peer, &len);
        if (m < 0) {
            perror("recvfrom error");
            continue;
        } else if (m == 0) {
            std::cerr << "Server closed connection" << std::endl;
            break;
        }

        buffer[m] = '\0';
        std::cout << "Server response: " << buffer << std::endl;
    }

    close(sockfd); // 关闭套接字
    return 0;
}

这段代码实现了一个简单的 UDP 客户端,用于向指定的服务器发送消息并接收响应。以下是详细讲解,包括代码逻辑、关键函数、网络编程概念以及潜在问题。

1、代码功能概述

  • 作用:通过 UDP 协议与服务器通信,用户输入消息,客户端发送到服务器并接收回显。

  • 特点

    • 使用 UDP(无连接协议,无需建立连接)。

    • 客户端不显式绑定端口(由操作系统自动分配临时端口)。

    • 支持交互式输入(循环读取用户输入并发送)。

2、代码逐段解析

(1) 头文件引入

cpp 复制代码
#include <iostream>
#include <string>
#include <cstring>       // memset
#include <netinet/in.h>  // sockaddr_in, htons, inet_addr
#include <arpa/inet.h>   // inet_addr
#include <sys/types.h>   // 标准类型(如 socklen_t)
#include <sys/socket.h>  // socket, sendto, recvfrom
  • 关键头文件

    • <sys/socket.h>:提供套接字 API(如 socket()sendto())。

    • <netinet/in.h>:定义 IP 地址和端口的结构(如 sockaddr_in)。

    • <arpa/inet.h>:提供 IP 地址转换函数(如 inet_addr())。

(2) 命令行参数检查

cpp 复制代码
if (argc != 3) {
    std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
    return 1;
}
std::string server_ip = argv[1];
uint16_t server_port = std::stoi(argv[2]);
  • 作用:检查用户是否输入了服务器 IP 和端口。

  • 关键点

    • argc != 3:程序名 + 2 个参数(IP + 端口)。

    • std::stoi():将字符串端口转换为整数(uint16_t)。

(3) 创建 UDP 套接字

cpp 复制代码
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
    std::cerr << "socket error" << std::endl;
    return 2;
}

函数socket()

  • 参数

    • AF_INET:IPv4 协议族。

    • SOCK_DGRAM:UDP 协议(无连接,数据报)。

    • 0:默认协议(UDP 对应 IPPROTO_UDP)。

  • 返回值

    • 成功:返回套接字文件描述符(sockfd)。

    • 失败:返回 -1(此处未处理 errno,实际开发中建议用 perror())。

(4) 服务器地址配置

cpp 复制代码
struct sockaddr_in server;
memset(&server, 0, sizeof(server));  // 清零结构体
server.sin_family = AF_INET;         // IPv4
server.sin_port = htons(server_port); // 端口号(网络字节序)
server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // IP 地址
  • 关键结构sockaddr_in

    • sin_family:地址族(AF_INET)。

    • sin_port:端口号(需用 htons() 转换为网络字节序)。

    • sin_addr.s_addr:IP 地址(inet_addr() 将字符串 IP 转换为二进制)。

  • 注意

    • memset 初始化结构体,避免未定义字段。

    • htons()inet_addr() 是字节序转换函数(主机序 ↔ 网络序)。

(5) 主循环(发送与接收)

cpp 复制代码
while (true) {
    // 读取用户输入
    std::string input;
    std::cout << "Please Enter# ";
    std::getline(std::cin, input);

    // 发送数据到服务器
    int n = sendto(sockfd, input.c_str(), input.size(), 0, 
                  (struct sockaddr*)&server, sizeof(server));
    (void)n; // 忽略返回值(实际开发中应检查)

    // 接收服务器响应
    char buffer[1024];
    struct sockaddr_in peer;
    socklen_t len = sizeof(peer);
    int m = recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, 
                    (struct sockaddr*)&peer, &len);
    if (m > 0) {
        buffer[m] = 0; // 添加字符串终止符
        std::cout << buffer << std::endl;
    }
}
  • 关键函数

    • sendto()

      • 发送数据到指定地址(UDP 不需要连接)。

      • 参数:套接字、数据、长度、标志位、目标地址、地址长度。

    • recvfrom()

      • 接收数据,并记录发送方地址(此处未使用 peer,因为已知是服务器)。

      • 参数:套接字、缓冲区、长度、标志位、发送方地址、地址长度指针。

  • 缓冲区处理: buffer[m] = 0:将接收的数据转为 C 风格字符串(避免打印乱码)。

  • 无限循环: 用户输入 Ctrl+C 终止程序(实际开发中可添加退出条件)。

3、关键问题解答

(1) 客户端是否需要 bind()

  • 不需要显式绑定

    • 首次调用 sendto() 时,操作系统会自动为客户端分配一个临时端口(随机端口)。

    • 显式绑定可能引发端口冲突(因为一个端口只能被一个进程绑定)。

  • 为什么可以自动绑定?

    • UDP 是无连接的,客户端只需知道目标地址,无需维护连接状态。

    • 临时端口(Ephemeral Port)范围通常为 32768--60999(可通过 /proc/sys/net/ipv4/ip_local_port_range 配置)。

(2) 为什么 UDP 客户端通常不绑定?

  • 灵活性:避免手动管理端口。

  • 避免冲突:多个客户端实例运行时,显式绑定同一端口会导致失败。

  • 服务器需绑定:因为服务器必须监听固定端口供客户端访问。

4、潜在问题与改进

  1. 错误处理不足

    • sendto()recvfrom() 的返回值未充分检查(如 -1 表示错误)。

    • 建议使用 perror()strerror(errno) 打印错误信息。

  2. 缓冲区溢出风险

    • recvfrom() 的缓冲区固定为 1024 字节,若数据过长可能截断。

    • 改进:动态分配缓冲区或检查 m 是否等于 sizeof(buffer)-1

  3. 硬编码退出条件 :当前循环无法正常退出,可添加特定输入(如 "exit")终止程序。

  4. 服务器地址验证 :未检查 inet_addr() 是否返回 INADDR_NONE(无效 IP)。

6、总结

  • UDP 客户端特点:无需连接、无需显式绑定端口、适合低延迟场景。

  • 关键函数socket()sendto()recvfrom()

  • 注意事项 :错误处理、缓冲区管理、资源释放(如 close())。

  • 扩展学习 :对比 TCP 客户端(需 connect() 和显式 bind())。

7、补充:struct sockaddr_in serverstruct sockaddr_in peer

在这段代码中,struct sockaddr_in serverstruct sockaddr_in peer 确实有重叠的功能,但它们的用途是不同的。让我详细解释它们的区别和为什么需要 peer

1. server 的作用

  • 存储客户端要发送的目标地址(即命令行参数指定的服务器 IP 和端口)。

  • 仅用于 sendto(),表示客户端要把数据发送到哪里:

    cpp 复制代码
    sendto(sockfd, input.c_str(), input.size(), 0, 
           (struct sockaddr*)&server, sizeof(server));
  • server 是客户端主动指定的目标,但 UDP 是无连接的,服务器回复时可能不会用这个地址(例如 NAT 或代理场景)。

2. peer 的作用

  • 存储实际发送响应数据的来源地址 (由 recvfrom() 填充)。

  • 用于 recvfrom(),表示是谁发送了响应数据:

    cpp 复制代码
    recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, 
             (struct sockaddr*)&peer, &len);
  • peer 是实际回复的来源 ,可能和 server 不同(例如:

    • 服务器可能从不同的端口回复(如负载均衡)。

    • 可能有恶意第三方发送伪造响应。

    • 网络中间设备(如 NAT)可能修改地址。

3. 为什么不能直接用 server 代替 peer

  • 语义不同

    • server 是客户端信任的目标地址。

    • peer 是实际回复的来源,需要验证是否可信。

  • UDP 的无连接性

    • 即使客户端发送到 server,回复可能来自其他地址(如代理、多播、错误配置)。

    • 如果直接用 server 假设回复来源,可能处理错误的数据。

4. 当前代码的问题

这段代码 没有比较 peerserver ,所以即使收到伪造响应,也会当作合法处理。这是一个安全隐患。改进方法是验证 peer 是否匹配 server

cpp 复制代码
// 接收数据后,检查来源是否可信
if (peer.sin_addr.s_addr != server.sin_addr.s_addr || 
    peer.sin_port != server.sin_port) {
    std::cerr << "Warning: Response from unexpected source!" << std::endl;
    continue;
}

5. 可以省略 peer 吗?

  • 如果不需要知道来源 (例如完全信任网络环境),可以省略 peer,让 recvfrom() 传入 NULL

    cpp 复制代码
    recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, NULL, NULL);
  • 但通常不建议,因为:

    • 调试时可能需要知道实际来源。

    • 安全性要求高的场景必须验证来源。

总结

变量 用途 是否必须
server 客户端指定的目标地址
peer 实际回复的来源地址 视情况
  • server 是"我要发给谁"peer 是"谁实际回复了我"**。

  • UDP 的无连接性要求我们通过 peer 验证来源,否则可能处理错误或恶意数据。

  • 当前代码未验证 peer,存在安全隐患,建议添加检查逻辑。


六、相关的必需头文件

Log.hpp

cpp 复制代码
#ifndef __LOG_HPP__
#define __LOG_HPP__

#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <memory>
#include <ctime>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式,C++多态特性
    // 2. 刷新策略 a: 显示器打印 b:向指定的文件写入
    //  刷新策略基类
    class LogStrategy
    {
    public:
        ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印日志的策略 : 子类
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cout << message << gsep;
        }
        ~ConsoleLogStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    // 文件打印日志的策略 : 子类
    const std::string defaultpath = "./log";
    const std::string defaultfile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &file = defaultfile)
            : _path(path),
              _file(file)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_path))
            {
                return;
            }
            try
            {
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << '\n';
            }
        }
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);

            std::string filename = _path + (_path.back() == '/' ? "" : "/") + _file; // "./log/" + "my.log"
            std::ofstream out(filename, std::ios::app);                              // 追加写入的 方式打开
            if (!out.is_open())
            {
                return;
            }
            out << message << gsep;
            out.close();
        }
        ~FileLogStrategy()
        {
        }

    private:
        std::string _path; // 日志文件所在路径
        std::string _file; // 日志文件本身
        Mutex _mutex;
    };

    // 形成一条完整的日志&&根据上面的策略,选择不同的刷新方式

    // 1. 形成日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };
    std::string Level2Str(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }
    std::string GetTimeStamp()
    {
        time_t curr = time(nullptr);
        struct tm curr_tm;
        localtime_r(&curr, &curr_tm);
        char timebuffer[128];
        snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
            curr_tm.tm_year+1900,
            curr_tm.tm_mon+1,
            curr_tm.tm_mday,
            curr_tm.tm_hour,
            curr_tm.tm_min,
            curr_tm.tm_sec
        );
        return timebuffer;
    }

    // 1. 形成日志 && 2. 根据不同的策略,完成刷新
    class Logger
    {
    public:
        Logger()
        {
            EnableConsoleLogStrategy();
        }
        void EnableFileLogStrategy()
        {
            _fflush_strategy = std::make_unique<FileLogStrategy>();
        }
        void EnableConsoleLogStrategy()
        {
            _fflush_strategy = std::make_unique<ConsoleLogStrategy>();
        }

        // 表示的是未来的一条日志
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string &src_name, int line_number, Logger &logger)
                : _curr_time(GetTimeStamp()),
                  _level(level),
                  _pid(getpid()),
                  _src_name(src_name),
                  _line_number(line_number),
                  _logger(logger)
            {
                // 日志的左边部分,合并起来
                std::stringstream ss;
                ss << "[" << _curr_time << "] "
                   << "[" << Level2Str(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _src_name << "] "
                   << "[" << _line_number << "] "
                   << "- ";
                _loginfo = ss.str();
            }
            // LogMessage() << "hell world" << "XXXX" << 3.14 << 1234
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                // a = b = c =d;
                // 日志的右半部分,可变的
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._fflush_strategy)
                {
                    _logger._fflush_strategy->SyncLog(_loginfo);
                }
            }

        private:
            std::string _curr_time;
            LogLevel _level;
            pid_t _pid;
            std::string _src_name;
            int _line_number;
            std::string _loginfo; // 合并之后,一条完整的信息
            Logger &_logger;
        };

        // 这里故意写成返回临时对象
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }
        ~Logger()
        {
        }

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

    // 全局日志对象
    Logger logger;

    // 使用宏,简化用户操作,获取文件名和行号
    #define LOG(level) logger(level, __FILE__, __LINE__)
    #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}

#endif

Mutex.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <pthread.h>

namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            pthread_mutex_init(&_mutex, nullptr);
        }
        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }
        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }
        ~Mutex()
        {
            pthread_mutex_destroy(&_mutex);
        }
        pthread_mutex_t *Get()
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };

    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex):_mutex(mutex)
        {
            _mutex.Lock();
        }
        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex &_mutex;
    };
}

七、运行输出结果

我们使用makefile文件来进行编译操作:

bash 复制代码
.PHONY:all
all:udpclient udpserver

udpclient:UdpClient.cc
	g++ -o $@ $^ -std=c++17 #-static
udpserver:UdpServer.cc
	g++ -o $@ $^ -std=c++17

.PHONY:clean
clean:
	rm -f udpclient udpserver

首先我们运行和启动服务端:

然后我们再运行和启动客户端:

此时我们使用客户端给服务端发送用户要求翻译的单词(下面举例了三种情况(只有第二个才是正确的),也就是说只能英译中,不能中译英,否则就会输出None),回车后然后可以看到服务端给客户端回显一条对应的翻译信息,同时服务端也记录着日志信息:

相关推荐
阿巴~阿巴~2 小时前
简易回声服务器实现与网络测试指南
linux·服务器·网络·udp协议·网络测试·udp套接字编程
star_start_sky2 小时前
住宅代理网络:我最近用来数据采集和自动化的小工具
网络·爬虫·自动化
科技智驱2 小时前
误分区数据恢复:3种方法,按需选择更高效
网络·电脑·数据恢复
another heaven3 小时前
【计算机网络 HTTP 请求参数规范详解】
网络协议·计算机网络·http
凡间客4 小时前
Ansible安装与入门
linux·运维·ansible
君以思为故4 小时前
认识Linux -- 进程概念
linux·服务器
云边云科技5344 小时前
云边云科技SD-WAN解决方案 — 构建安全、高效、智能的云网基石
网络·科技·安全·架构·it·sdwan
_OP_CHEN4 小时前
Linux网络编程:(八)GCC/G++ 编译器完全指南:从编译原理到实战优化,手把手教你玩转 C/C++ 编译
linux·运维·c++·编译和链接·gcc/g++·编译优化·静态链接与动态链接
阿乐艾官5 小时前
【十一、Linux管理网络安全】
linux·运维·web安全