当前位置: 首页 > news >正文

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

目录

一、UdpServer.hpp

1、头文件和预处理指令

2、包含的头文件

3、类型别名和常量定义

4、UdpServer 类定义

4.1 构造函数

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

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

4.4 析构函数

4.5 成员变量

5、设计特点

6、潜在改进点

总结

二、UdpServer.cc

1、头文件包含

2、测试用的默认处理器

3、主函数

3.1 参数处理

3.2 日志初始化

4、核心功能实现

4.1 字典初始化

4.2 UDP服务器创建

4.3 服务器启动

5、设计特点分析

6、潜在改进点

7、完整工作流程

总结

三、InetAddr.hpp

1、头文件保护

2、头文件包含

3、类定义

4、成员详解

4.1 构造函数

4.2 成员函数

4.3 析构函数

4.4 成员变量

5、技术要点

5.1 网络字节序转换

5.2 IP地址表示

5.3 设计考虑

6、潜在改进

7、使用示例

8、总结

四、Dict.hpp

1、头文件和预处理指令

2、头文件包含

3、全局常量定义

4、类定义

5、成员详解

5.1 构造函数

5.2 LoadDict() 方法

功能:从 _dict_path 指定的文件加载词典数据到内存

详细流程:

设计特点:

5.3 Translate() 方法

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

参数:

详细流程:

设计特点:

5.4 成员变量

6、技术要点

6.1 文件格式

6.2 错误处理

6.3 日志记录

6.4 性能考虑

7、潜在改进

8、使用示例

9、总结

五、UdpClient.cc

1、代码功能概述

2、代码逐段解析

(1) 头文件引入

(2) 命令行参数检查

(3) 创建 UDP 套接字

(4) 服务器地址配置

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

3、关键问题解答

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

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

4、潜在问题与改进

6、总结

7、补充:struct sockaddr_in server 和 struct sockaddr_in peer

1. server 的作用

2. peer 的作用

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

4. 当前代码的问题

5. 可以省略 peer 吗?

总结

六、相关的必需头文件

Log.hpp

Mutex.hpp

七、运行输出结果


一、UdpServer.hpp

#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()); // TODOlocal.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、头文件和预处理指令

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

2、包含的头文件

#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、类型别名和常量定义

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 构造函数

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() 方法 - 初始化服务器

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() 方法 - 启动服务器

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 析构函数

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

4.5 成员变量

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

#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、头文件包含

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

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

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

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

2、测试用的默认处理器

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

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

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

3、主函数

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、核心功能实现

// 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

#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、头文件保护

#pragma once
  • 使用 #pragma once 防止头文件被多次包含

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

2、头文件包含

#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、类定义

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 构造函数

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 成员函数

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

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

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

4.3 析构函数

~InetAddr()
{}
  • 空的析构函数

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

4.4 成员变量

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_addr 是 in_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:

    uint16_t Port() const {return _port;}
    std::string Ip() const {return _ip;}

7、使用示例

#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);          // 设置端口为8080addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // 设置IP为127.0.0.1InetAddr 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

#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、头文件和预处理指令

#pragma once
  • 使用 #pragma once 防止头文件被多次包含

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

2、头文件包含

#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、全局常量定义

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

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

4、类定义

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 构造函数

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

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

  • 设计特点:

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

    • 初始化列表简洁高效

5.2 LoadDict() 方法

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() 方法

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_map 的 find 方法查询单词

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

  2. 记录日志:

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

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

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

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

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

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

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

5.4 成员变量

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)

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、使用示例

#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

#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) 头文件引入

#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) 命令行参数检查

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 套接字

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) 服务器地址配置

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) 主循环(发送与接收)

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 server 和 struct sockaddr_in peer

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

1. server 的作用

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

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

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

2. peer 的作用

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

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

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

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

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

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

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

  • 语义不同

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

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

  • UDP 的无连接性

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

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

4. 当前代码的问题

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

// 接收数据后,检查来源是否可信
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

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

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

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

总结

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

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

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


六、相关的必需头文件

Log.hpp

#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 << 1234template <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

#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文件来进行编译操作:

.PHONY:all
all:udpclient udpserverudpclient:UdpClient.ccg++ -o $@ $^ -std=c++17 #-static
udpserver:UdpServer.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f udpclient udpserver

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

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

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

http://www.dtcms.com/a/609161.html

相关文章:

  • 在ec2上部署indexTTS和尝试部署sparkTTS模型
  • IP种子技术:构建全球P2P网络实时监测方案
  • Kali远程桌面+cpolar:网络安全攻防的跨域协作新范式
  • 网络安全学习困扰及解决建议
  • 黑马点评学习笔记11(Redission)
  • 计算机网络复习日报18
  • 网站开发合同知识产权wordpress gettheid
  • Redis 全体系深度解析(架构原理、性能模型、使用场景、持久化机制、过期策略与最佳实践)
  • 百度世界 2025 核心看点:文心 5.0、萝卜快跑、惠博星数字人、伐谋智能体齐亮相!
  • 【百度拥抱开源】介绍ERNIE-4.5-VL-28B-A3B-Thinking:多模态AI的重大突破
  • HarmonyOS分布式输入法开发:实现多设备无缝输入体验
  • 基于GIS的智慧旅游调度指挥平台
  • 网站怎么做才美观WordPress moe acg
  • C/C++ Linux网络编程4 - 解决TCP服务器并发的方式
  • AI取名大师 | uni-app + Wot UI 跟随设备自动切换明暗主题
  • 镜像站更新
  • 《uni-app跨平台开发完全指南》- 07 - 数据绑定与事件处理
  • 福州网站建设方案咨询免费观看电视剧软件
  • 虚拟机网站建设与管理wordpress前台修改密码
  • 福州绿光网站建设工作室合肥那个公司做网站优化好
  • Java基础——方法
  • 设计模式实战篇(二):业务逻辑“随时切换招式”——策略模式(Strategy Pattern)解析
  • 从“能说会道”到“自主思考”:一文读懂AI的过去、现在与未来
  • Python语言编译器 | Python语言编译器的使用与原理解析
  • 【JAVA 进阶】Spring Boot 注解体系与工程实践
  • Effective Python 第51条:优先考虑通过类修饰器来提供可组合的扩充功能,不要使用元类
  • Rust时序数据库实现:从压缩算法到并发优化的实战之旅
  • SpringCloud-Consul服务注册与发现
  • 网站建设原因分析wordpress 页面分页
  • SSH级知识管理:通过CPolar暴露Obsidian vault构建你的知识API服务,实现跨设备无缝同步