目录
[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 成员变量)
[3.1 参数处理](#3.1 参数处理)
[3.2 日志初始化](#3.2 日志初始化)
[4.1 字典初始化](#4.1 字典初始化)
[4.2 UDP服务器创建](#4.2 UDP服务器创建)
[4.3 服务器启动](#4.3 服务器启动)
[4.1 构造函数](#4.1 构造函数)
[4.2 成员函数](#4.2 成员函数)
[4.3 析构函数](#4.3 析构函数)
[4.4 成员变量](#4.4 成员变量)
[5.1 网络字节序转换](#5.1 网络字节序转换)
[5.2 IP地址表示](#5.2 IP地址表示)
[5.3 设计考虑](#5.3 设计考虑)
[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.1 文件格式](#6.1 文件格式)
[6.2 错误处理](#6.2 错误处理)
[6.3 日志记录](#6.3 日志记录)
[6.4 性能考虑](#6.4 性能考虑)
[(1) 头文件引入](#(1) 头文件引入)
[(2) 命令行参数检查](#(2) 命令行参数检查)
[(3) 创建 UDP 套接字](#(3) 创建 UDP 套接字)
[(4) 服务器地址配置](#(4) 服务器地址配置)
[(5) 主循环(发送与接收)](#(5) 主循环(发送与接收))
[(1) 客户端是否需要 bind()?](#(1) 客户端是否需要 bind()?)
[(2) 为什么 UDP 客户端通常不绑定?](#(2) 为什么 UDP 客户端通常不绑定?)
[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 吗?)
一、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;
}
-
创建套接字:
-
使用
socket()函数创建 UDP 套接字 (SOCK_DGRAM) -
检查返回值,失败则记录日志并退出
-
-
绑定套接字:
-
创建并初始化
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);
}
}
}
-
主循环:
-
设置
_isrunning为 true,进入循环 -
准备接收缓冲区 (
buffer) 和客户端地址结构 (peer)
-
-
接收消息:
-
使用
recvfrom()接收数据 -
如果成功接收 (s > 0):
-
用接收到的地址信息创建
InetAddr对象 -
在缓冲区末尾添加 null 终止符
-
调用回调函数
_func处理接收到的数据,获取响应
-
-
-
发送响应: 使用
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、潜在改进点
-
资源管理:
-
析构函数中应该关闭套接字
-
考虑使用 RAII 模式管理资源
-
-
错误处理:
-
可以添加更多错误处理逻辑
-
考虑部分失败的情况
-
-
性能优化:
-
可以添加缓冲区管理
-
考虑多线程/多路复用处理高并发
-
-
**配置灵活性:**可以添加设置超时、缓冲区大小等选项
总结
这是一个简洁但功能完整的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、设计特点分析
-
模块化设计: 将网络通信(
UdpServer)和业务逻辑(Dict)分离、通过回调函数将两者结合起来 -
智能指针使用: 使用
std::unique_ptr管理UdpServer对象生命周期、自动内存管理,避免内存泄漏 -
Lambda表达式: 使用Lambda作为回调函数,简洁地封装了翻译逻辑、捕获局部变量
dict,使回调能访问字典对象 -
错误处理: 基本参数检查、但服务器初始化错误处理在
UdpServer内部(通过日志和退出)
6、潜在改进点
-
**配置灵活性:**字典文件路径应该可配置、考虑添加命令行选项或配置文件
-
**错误处理:**更完善的参数验证(如端口号范围)、字典加载失败的处理
-
**日志增强:**添加更多运行日志、考虑不同日志级别
-
**性能考虑:**对于高并发,可能需要优化字典查询、考虑缓存常用翻译结果
-
**代码组织:**将主函数逻辑进一步拆分到函数中、考虑使用专门的服务器类封装
7、完整工作流程
-
程序启动,检查并获取端口号参数
-
初始化日志系统
-
加载字典数据到内存
-
创建UDP服务器,设置翻译回调
-
服务器初始化并开始运行:
-
监听指定端口
-
接收客户端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_addr是in_addr结构体,通常包含一个32位IPv4地址 -
inet_ntoa():-
将
in_addr转换为点分十进制字符串 -
返回指向静态缓冲区的指针,后续调用会覆盖内容
-
线程不安全(在多线程环境中需要额外处理)
-
5.3 设计考虑
-
封装性:
-
将原始的
sockaddr_in结构和转换后的可读格式都存储 -
提供简单的接口获取可读信息
-
-
性能:
-
构造函数中完成所有转换工作
-
后续调用
Ip()和Port()直接返回存储的值
-
-
安全性:
-
没有暴露原始
sockaddr_in结构的修改接口 -
但
inet_ntoa()的线程安全问题需要注意
-
6、潜在改进
-
线程安全:
-
使用
inet_ntop()替代inet_ntoa() -
inet_ntop()是线程安全的,且支持IPv6
-
-
IPv6支持:
-
当前只支持IPv4 (
sockaddr_in) -
可以扩展支持
sockaddr_in6
-
-
错误处理:
-
添加对无效地址的处理
-
例如
inet_ntoa()返回NULL时的处理
-
-
**移动语义:**可以添加移动构造函数,避免不必要的拷贝
-
**常量正确性:**将getter方法标记为const:
cppuint16_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 类是一个简单的网络地址封装类,主要功能包括:
-
封装
sockaddr_in结构体 -
提供网络字节序到主机字节序的转换
-
提供IP地址从二进制到字符串的转换
-
提供简单的接口获取可读的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 指定的文件加载词典数据到内存
详细流程:
-
打开文件:
-
使用
std::ifstream打开词典文件(重点!!!std::ifstream是 C++ 标准库中的一个类,用于从文件读取数据。它是std::basic_ifstream模板针对char类型的特化,属于<fstream>头文件,之前在日志系统和线程池那部分使用过;std::ifstream实例化出来的对象代表一个已打开的文件,并通过该对象操作文件内容。不过更准确地说,它是一个文件输入流对象,封装了与文件的交互逻辑,而不仅仅是"文件"本身。) -
如果打开失败,记录DEBUG日志并返回false
-
-
逐行读取:
-
使用
std::getline逐行读取文件内容 -
每行格式应为 "英文: 中文"
-
-
解析每行:
-
查找分隔符
": "的位置 -
如果找不到分隔符,记录WARNING日志并跳过该行
-
分割出英文部分和中文部分
-
检查两部分是否为空,如果为空则记录WARNING并跳过
-
-
存储词典数据:
-
将有效的英汉对插入
_dict哈希表 -
记录DEBUG日志表示成功加载该行
-
-
**关闭文件:**显式关闭文件流(虽然析构函数也会自动关闭)
-
**返回结果:**成功加载返回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: 客户端地址信息(用于日志记录)
详细流程:
-
查询词典:
-
使用
unordered_map的find方法查询单词 -
如果找不到,返回 "None"
-
-
记录日志:
-
无论是否找到,都记录DEBUG日志
-
日志包含客户端IP、端口、查询单词和结果
-
使用
InetAddr类的方法获取客户端IP和端口
-
-
**返回结果:**找到则返回中文翻译,否则返回 "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、潜在改进
-
**词典热更新:**可以添加监视词典文件变化并自动重新加载的功能
-
**更复杂的查询:**支持模糊查询、前缀匹配等
-
**多词典支持:**支持加载多个词典文件
-
**性能优化:**对于大型词典,可以考虑内存映射文件
-
错误处理增强:
-
区分不同类型的错误(文件不存在 vs 权限问题)
-
提供更详细的错误信息
-
-
线程安全: 如果需要在多线程环境中使用,需要添加互斥锁保护
_dict -
**持久化:**支持将词典保存回文件
-
**编码处理:**明确处理文件编码(如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、潜在问题与改进
-
错误处理不足:
-
sendto()和recvfrom()的返回值未充分检查(如-1表示错误)。 -
建议使用
perror()或strerror(errno)打印错误信息。
-
-
缓冲区溢出风险:
-
recvfrom()的缓冲区固定为 1024 字节,若数据过长可能截断。 -
改进:动态分配缓冲区或检查
m是否等于sizeof(buffer)-1。
-
-
硬编码退出条件 :当前循环无法正常退出,可添加特定输入(如
"exit")终止程序。 -
服务器地址验证 :未检查
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(),表示客户端要把数据发送到哪里:cppsendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr*)&server, sizeof(server)); -
server是客户端主动指定的目标,但 UDP 是无连接的,服务器回复时可能不会用这个地址(例如 NAT 或代理场景)。
2. peer 的作用
-
存储实际发送响应数据的来源地址 (由
recvfrom()填充)。 -
用于
recvfrom(),表示是谁发送了响应数据:cpprecvfrom(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:
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:cpprecvfrom(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),回车后然后可以看到服务端给客户端回显一条对应的翻译信息,同时服务端也记录着日志信息:
