📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨
文章目录
上一篇文章中,我们实现了回显客户端输入的功能,这功能往往是不够的,为了更好地模拟现实需求,我们现在多增加一个功能 - 字典翻译功能
🏳️🌈一、服务端更新
1.1 函数对象声明
别的功能、成员名保持不变,为了新增字典翻译功能,我们需要引入函数对象类型
// 回调函数对象声明
using func_t = std::function<std::string(std::string)>;
1.2 UdpServer 类成员更新
class UdpServer : public nocopy{
public:
UdpServer(func_t func,uint16_t localport = glocalport);
void InitServer();
void Start();
~UdpServer();
private:
int _sockfd; // 文件描述符
uint16_t _localport; // 端口号
std::string _localip; // 本地IP地址
bool _isrunning; // 运行状态
func_t _func; // 回调函数
};
1.3 构造函数更新
- 构造函数只需增加一个函数对象参数,初始化列表初始化变量即可!!!
UdpServer(uint16_t localport = gdefaultport, func_t func = nullptr)
: _sockfd(gsockfd), _localport(localport), _isrunning(false), _func(func) {}
1.4 开始 - Start() 更新
- 之前只需要回显的时候,我们直接接收客户端信息,将网络字节序的客户端ip和端口号转换为主机字节序,再返回就行了
- 现在我们要在这之间添加一个环节,使收到的客户端信息,先通过字典翻译回调函数,将处理后的值传回去
void Start() {
_isrunning = true;
while (true) {
char inbuffer[1024]; // 接收缓冲区
struct sockaddr_in peer; // 接收客户端地址
socklen_t peerlen = sizeof(peer); // 计算接收的客户端地址长度
// 接收数据报
// recvfrom(int sockfd, void* buf, size_t len, int flags, struct
// sockaddr* src_addr, socklen_t* addrlen)
// 从套接字接收数据,并存入buf指向的缓冲区中,返回实际接收的字节数
// 参数sockfd:套接字文件描述符
// 参数buf:指向接收缓冲区的指针,c_str()函数可以将字符串转换为char*,以便存入缓冲区
// 参数len:接收缓冲区的长度
// 参数flags:接收标志,一般设为0
// 参数src_addr:指向客户端地址的指针,若不为NULL,函数返回时,该指针指向客户端的地址,是网络字节序
// 参数addrlen:客户端地址长度的指针,若不为NULL,函数返回时,该指针指向实际的客户端地址长度
ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0,
CONV(&peer), &peerlen);
if (n > 0) {
// 将英文单词 转换为 中文
std::string result = _func(inbuffer);
::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer),
peerlen);
}
}
}
🏳️🌈二、Dictionary 字典类设计
字典类执行加载字典文件 和 执行翻译的功能
2.1 基本结构
class Dictionary{
private:
// 加载字典文件
void LoadDictionary(const std::string& path);
public:
// 构造函数
Dictionary(const std::string& path);
// 翻译
std::string Translate(const std::string& word);
// 析构函数
~Dictionary();
private:
std::unordered_map<std::string, std::string> _dict; // 字典结构
std::string _dict_path; // 文件路径
};
2.2 加载字典文件 - LoadDictionary(const std::string& path)
我们以
": "
一个冒号加一个空格的形式,进行翻译
加载字典文件的本质是以KV的形式将英文单词和中文翻译插入到_dict哈希表中!
加载文件包含3个大的步骤:
- 读方式打开文件
- 按行读取内容[需要考虑中间有空格情况,一行中没找到分隔符情况]
- 关闭文件
// 加载字典文件
void LoadDictionary(const std::string& path) {
// 1. 读方式打开文件
std::ifstream in(path);
if (!in.is_open()) {
LOG(LogLevel::FATAL) << "open " << path.c_str() << " failed";
Die(1);
}
std::string line;
// 2. 按行读取内容
while (std::getline(in, line)) {
LOG(LogLevel::DEBUG) << line.c_str() << "load success";
if (line.empty())
continue; // 中间有空格情况
auto pos = line.find(sep); // 使用find找到分隔符位置,返回迭代器位置
if (pos == std::string::npos)
continue; // 找不到分隔符,跳过该行
std::string key = line.substr(0, pos); // 前闭后开
if (key.empty())
continue; // 键为空,跳过该行
std::string value = line.substr(pos + sep.size());
if (value.empty())
continue; // 值为空,跳过该行
_dict.insert(std::make_pair(key, value));
}
LOG(LogLevel::INFO) << path.c_str() << " load success";
// 3. 关闭文件
in.close();
}
2.3 构造函数
初始化字典文件,并将键值对加载到本地保存
// 构造函数
Dictionary(const std::string& path = gpath + gdictname) { LoadDictionary(path); }
2.4 翻译函数
在键值对中查找是否有该单词,有单词就返回值,没有返回None
// 翻译
std::string Translate(const std::string& word) {
auto iter = _dict.find(word);
if (iter == _dict.end())
return "None";
return iter->second;
}
2.5 服务端运行更新
因为我们现在需要将字典类的查找方法 作为回调函数传给服务端 ,所以需要进行一些变化
#include "UdpServer.hpp"
#include "Dictionary.hpp"
int main(int argc, char *argv[])
{
if(argc != 2){
std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
Die(1);
}
uint16_t port = std::stoi(argv[1]);
ENABLE_CONSOLE_LOG(); // 日期类方法,使日志在控制台输出
std::shared_ptr<Dictionary> dict_ptr = std::make_shared<Dictionary>();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict_ptr]( const std::string& word){
std::cout << "|" << word << "|" << std::endl;
return dict_ptr->Translate(word);
});
usvr->InitServer(); // 初始化服务端
usvr->Start(); // 启动服务端
return 0;
}
🏳️🌈三、整体代码
UdpServer.hpp
#include <iostream>
#include <string>
#include <memory>
#include <cstring>
#include <functional>
#include <cerrno> // 这个头文件包含了errno定义,用于存放系统调用的返回值
#include <strings.h> // 属于POSIX扩展(非标准C/C++),常见于Unix/Linux系统,提供额外字符串函数(如 bcopy, bzero)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "InetAddr.hpp"
#include "Log.hpp"
#include "Common.hpp"
using namespace LogModule;
const static int gsockfd = -1;
const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;
// 回调函数对象声明
using func_t = std::function<std::string(std::string)>;
class nocopy{
public:
nocopy(){}
~nocopy(){}
nocopy(const nocopy&) = delete; // 禁止拷贝构造函数
const nocopy& operator=(const nocopy&) = delete; // 禁止拷贝赋值运算符
};
class UdpServer : public nocopy{
public:
UdpServer(uint16_t localport = gdefaultport, func_t func = nullptr)
: _sockfd(gsockfd),
_localport(localport),
_isrunning(false),
_func(func)
{}
void InitServer(){
// 1. 创建套接字
// socket(int domain, int type, int protocol)
// 返回一个新的套接字文件描述符,或者在出错时返回-1
// 参数domain:协议族,AF_INET,表示IPv4协议族
// 参数type:套接字类型,SOCK_DGRAM,表示UDP套接字
// 参数protocol:协议,0,表示默认协议
_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);
if(_sockfd < 0){
LOG(LogLevel::FATAL) << "socket: " << strerror(errno);
// exit(SOCKET_ERR) 表示程序运行失败,并返回指定的错误码
exit(SOCKET_ERR);
}
LOG(LogLevel::DEBUG) << "socket success, sockfd is: " << _sockfd;
// 2. bind
// sockaddr_in
struct sockaddr_in local;
// 将local全部置零,以便后面设置
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET; // IPv4协议族
local.sin_port = htons(_localport); // 端口号,网络字节序
local.sin_addr.s_addr = htonl(INADDR_ANY); // 本地IP地址,网络字节序
// 将套接字绑定到本地地址
// bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen)
// 绑定一个套接字到一个地址,使得套接字可以接收来自该地址的数据报
// 参数sockfd:套接字文件描述符
// 参数addr:指向sockaddr_in结构体的指针,表示要绑定的地址
// 参数addrlen:地址长度,即sizeof(sockaddr_in)
// 返回0表示成功,-1表示出错
int n = ::bind(_sockfd, (struct sockaddr* )&local, sizeof(local));
if(n < 0){
LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
exit(BIND_ERR);
}
LOG(LogLevel::DEBUG) << "bind success";
}
void Start(){
_isrunning = true;
while(true){
char inbuffer[1024]; // 接收缓冲区
struct sockaddr_in peer; // 接收客户端地址
socklen_t peerlen = sizeof(peer); // 计算接收的客户端地址长度
// 接收数据报
// recvfrom(int sockfd, void* buf, size_t len, int flags, struct sockaddr* src_addr, socklen_t* addrlen)
// 从套接字接收数据,并存入buf指向的缓冲区中,返回实际接收的字节数
// 参数sockfd:套接字文件描述符
// 参数buf:指向接收缓冲区的指针,c_str()函数可以将字符串转换为char*,以便存入缓冲区
// 参数len:接收缓冲区的长度
// 参数flags:接收标志,一般设为0
// 参数src_addr:指向客户端地址的指针,若不为NULL,函数返回时,该指针指向客户端的地址,是网络字节序
// 参数addrlen:客户端地址长度的指针,若不为NULL,函数返回时,该指针指向实际的客户端地址长度
ssize_t n = ::recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, CONV(&peer), &peerlen);
if(n > 0){
// 将英文单词 转换为 中文
std::string result = _func(inbuffer);
::sendto(_sockfd, result.c_str(), result.size(), 0, CONV(&peer), peerlen);
}
}
}
~UdpServer(){
// 判断 _sockfd 是否是一个有效的套接字文件描述符
// 有效的文件描述符(如套接字、打开的文件等)是非负整数(>= 0)
if(_sockfd > -1) ::close(_sockfd);
}
private:
int _sockfd; // 文件描述符
uint16_t _localport; // 端口号
std::string _localip; // 本地IP地址
bool _isrunning; // 运行状态
func_t _func; // 回调函数
};
UdpServer.cpp
#include "UdpServer.hpp"
#include "Dictionary.hpp"
int main(int argc, char *argv[])
{
if(argc != 2){
std::cerr << "Usage: " << argv[0] << " localport" << std::endl;
Die(1);
}
uint16_t port = std::stoi(argv[1]);
ENABLE_CONSOLE_LOG(); // 日期类方法,使日志在控制台输出
std::shared_ptr<Dictionary> dict_ptr = std::make_shared<Dictionary>();
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&dict_ptr]( const std::string& word){
std::cout << "|" << word << "|" << std::endl;
return dict_ptr->Translate(word);
});
usvr->InitServer(); // 初始化服务端
usvr->Start(); // 启动服务端
return 0;
}
UdpClient.hpp
#pragma once
#include "Common.hpp"
#include <iostream>
#include <cstring>
#include <string>
#include <cstdlib>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
UdpClient.cpp
#include "UdpClient.hpp"
int main(int argc, char* argv[]){
if(argc != 3){
std::cerr << argv[0] << " serverip server" << std::endl;
Die(USAGE_ERR);
}
std::string serverip = argv[1];
uint16_t serverport = std::stoi(argv[2]);
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if(sockfd < 0){
std::cerr << "create socket error" << std::endl;
Die(SOCKET_ERR);
}
// 1. 填充 server 信息
struct sockaddr_in server;
memset(&server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_port = ::htons(serverport);
server.sin_addr.s_addr = ::inet_addr(serverip.c_str());
// 2. 发送数据
while(true){
std::cout << "Please Enter# ";
std::string msg;
std::getline(std::cin, msg);
// client 必须自己的ip和端口。但是客户端,不需要显示调用bind
// 客户端首次 sendto 消息的时候,由OS自动bind
// 1. 如何理解 client 自动随机bind端口号? 一个端口号,只能读一个进程bind
// 2. 如何理解 server 要显示地bind? 必须稳定!必须是众所周知且不能轻易改变的
int n = ::sendto(sockfd, msg.c_str(), msg.size(), 0, CONV(&server), sizeof(server));
(void)n;
struct sockaddr_in temp;
socklen_t len = sizeof(temp);
char buffer[1024];
n = ::recvfrom(sockfd, buffer,sizeof(buffer) - 1, 0, CONV(&temp), &len);
if(n > 0){
buffer[n] = 0;
std::cout << buffer << std::endl;
}
}
return 0;
}
Common.hpp
#pragma once
#include <iostream>
#define Die(code) \
do \
{ \
exit(code); \
} while (0)
#define CONV(v) (struct sockaddr *)(v)
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR
};
InetAddr.hpp
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Common.hpp"
class InetAddr
{
private:
void PortNet2Host()
{
_port = ::ntohs(_net_addr.sin_port);
}
void IpNet2Host()
{
char ipbuffer[64];
const char *ip = ::inet_ntop(AF_INET, &_net_addr.sin_addr, ipbuffer, sizeof(ipbuffer));
(void)ip;
}
public:
InetAddr(){}
InetAddr(const struct sockaddr_in &addr) : _net_addr(addr)
{
PortNet2Host();
IpNet2Host();
}
InetAddr(uint16_t port) : _port(port), _ip("")
{
_net_addr.sin_family = AF_INET;
_net_addr.sin_port = htons(_port);
_net_addr.sin_addr.s_addr = INADDR_ANY;
}
struct sockaddr *NetAddr() { return CONV(&_net_addr); }
socklen_t NetAddrLen() { return sizeof(_net_addr); }
std::string Ip() { return _ip; }
uint16_t Port() { return _port; }
~InetAddr()
{
}
private:
struct sockaddr_in _net_addr;
std::string _ip;
uint16_t _port;
};
Dictionary.hpp
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
#include "Common.hpp"
using namespace LogModule;
const static std::string sep = ": ";
const static std::string gpath = "./";
const static std::string gdictname = "dict.txt";
class Dictionary{
private:
// 加载字典文件
void LoadDictionary(const std::string& path){
// 1. 读方式打开文件
std::ifstream in(path);
if(!in.is_open()){
LOG(LogLevel::FATAL) << "open " << path.c_str() << " failed";
Die(1);
}
std::string line;
// 2. 按行读取内容
while(std::getline(in, line)){
LOG(LogLevel::DEBUG) << line.c_str() << "load success";
if(line.empty())
continue; // 中间有空格情况
auto pos = line.find(sep); // 使用find找到分隔符位置,返回迭代器位置
if(pos == std::string::npos)
continue; // 找不到分隔符,跳过该行
std::string key = line.substr(0, pos); // 前闭后开
if(key.empty())
continue; // 键为空,跳过该行
std::string value = line.substr(pos + sep.size());
if(value.empty())
continue; // 值为空,跳过该行
_dict.insert(std::make_pair(key, value));
}
LOG(LogLevel::INFO) << path.c_str() << " load success";
// 3. 关闭文件
in.close();
}
public:
// 构造函数
Dictionary(const std::string& path = gpath + gdictname){
LoadDictionary(path);
}
// 翻译
std::string Translate(const std::string& word){
auto iter = _dict.find(word);
if(iter == _dict.end()) return "None";
return iter->second;
}
// 析构函数
~Dictionary(){}
private:
std::unordered_map<std::string, std::string> _dict; // 字典结构
std::string _dict_path; // 文件路径
};
Log.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <string>
#include <fstream>
#include <sstream>
#include <memory>
#include <filesystem> //C++17
#include <unistd.h>
#include <time.h>
#include "Mutex.hpp"
namespace LogModule
{
using namespace LockModule;
// 获取一下当前系统的时间
std::string CurrentTime()
{
time_t time_stamp = ::time(nullptr);
struct tm curr;
localtime_r(&time_stamp, &curr); // 时间戳,获取可读性较强的时间信息5
char buffer[1024];
// bug
snprintf(buffer, sizeof(buffer), "%4d-%02d-%02d %02d:%02d:%02d",
curr.tm_year + 1900,
curr.tm_mon + 1,
curr.tm_mday,
curr.tm_hour,
curr.tm_min,
curr.tm_sec);
return buffer;
}
// 构成: 1. 构建日志字符串 2. 刷新落盘(screen, file)
// 1. 日志文件的默认路径和文件名
const std::string defaultlogpath = "./log/";
const std::string defaultlogname = "log.txt";
// 2. 日志等级
enum class LogLevel
{
DEBUG = 1,
INFO,
WARNING,
ERROR,
FATAL
};
std::string Level2String(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 "None";
}
}
// 3. 刷新策略.
class LogStrategy
{
public:
virtual ~LogStrategy() = default;
virtual void SyncLog(const std::string &message) = 0;
};
// 3.1 控制台策略
class ConsoleLogStrategy : public LogStrategy
{
public:
ConsoleLogStrategy()
{
}
~ConsoleLogStrategy()
{
}
void SyncLog(const std::string &message)
{
LockGuard lockguard(_lock);
std::cout << message << std::endl;
}
private:
Mutex _lock;
};
// 3.2 文件级(磁盘)策略
class FileLogStrategy : public LogStrategy
{
public:
FileLogStrategy(const std::string &logpath = defaultlogpath, const std::string &logname = defaultlogname)
: _logpath(logpath),
_logname(logname)
{
// 确认_logpath是存在的.
LockGuard lockguard(_lock);
if (std::filesystem::exists(_logpath))
{
return;
}
try
{
std::filesystem::create_directories(_logpath);
}
catch (std::filesystem::filesystem_error &e)
{
std::cerr << e.what() << "\n";
}
}
~FileLogStrategy()
{
}
void SyncLog(const std::string &message)
{
LockGuard lockguard(_lock);
std::string log = _logpath + _logname; // ./log/log.txt
std::ofstream out(log, std::ios::app); // 日志写入,一定是追加
if (!out.is_open())
{
return;
}
out << message << "\n";
out.close();
}
private:
std::string _logpath;
std::string _logname;
// 锁
Mutex _lock;
};
// 日志类: 构建日志字符串, 根据策略,进行刷新
class Logger
{
public:
Logger()
{
// 默认采用ConsoleLogStrategy策略
_strategy = std::make_shared<ConsoleLogStrategy>();
}
void EnableConsoleLog()
{
_strategy = std::make_shared<ConsoleLogStrategy>();
}
void EnableFileLog()
{
_strategy = std::make_shared<FileLogStrategy>();
}
~Logger() {}
// 一条完整的信息: [2024-08-04 12:27:03] [DEBUG] [202938] [main.cc] [16] + 日志的可变部分(<< "hello world" << 3.14 << a << b;)
class LogMessage
{
public:
LogMessage(LogLevel level, const std::string &filename, int line, Logger &logger)
: _currtime(CurrentTime()),
_level(level),
_pid(::getpid()),
_filename(filename),
_line(line),
_logger(logger)
{
std::stringstream ssbuffer;
ssbuffer << "[" << _currtime << "] "
<< "[" << Level2String(_level) << "] "
<< "[" << _pid << "] "
<< "[" << _filename << "] "
<< "[" << _line << "] - ";
_loginfo = ssbuffer.str();
}
template <typename T>
LogMessage &operator<<(const T &info)
{
std::stringstream ss;
ss << info;
_loginfo += ss.str();
return *this;
}
~LogMessage()
{
if (_logger._strategy)
{
_logger._strategy->SyncLog(_loginfo);
}
}
private:
std::string _currtime; // 当前日志的时间
LogLevel _level; // 日志等级
pid_t _pid; // 进程pid
std::string _filename; // 源文件名称
int _line; // 日志所在的行号
Logger &_logger; // 负责根据不同的策略进行刷新
std::string _loginfo; // 一条完整的日志记录
};
// 就是要拷贝,故意的拷贝
LogMessage operator()(LogLevel level, const std::string &filename, int line)
{
return LogMessage(level, filename, line, *this);
}
private:
std::shared_ptr<LogStrategy> _strategy; // 日志刷新的策略方案
};
Logger logger;
#define LOG(Level) logger(Level, __FILE__, __LINE__)
#define ENABLE_CONSOLE_LOG() logger.EnableConsoleLog()
#define ENABLE_FILE_LOG() logger.EnableFileLog()
}
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
namespace LockModule
{
class Mutex
{
public:
Mutex(const Mutex&) = delete;
const Mutex& operator = (const Mutex&) = delete;
Mutex()
{
int n = ::pthread_mutex_init(&_lock, nullptr);
(void)n;
}
~Mutex()
{
int n = ::pthread_mutex_destroy(&_lock);
(void)n;
}
void Lock()
{
int n = ::pthread_mutex_lock(&_lock);
(void)n;
}
pthread_mutex_t *LockPtr()
{
return &_lock;
}
void Unlock()
{
int n = ::pthread_mutex_unlock(&_lock);
(void)n;
}
private:
pthread_mutex_t _lock;
};
class LockGuard
{
public:
LockGuard(Mutex &mtx):_mtx(mtx)
{
_mtx.Lock();
}
~LockGuard()
{
_mtx.Unlock();
}
private:
Mutex &_mtx;
};
}
Makefile
.PHONY: all
all:server_udp client_udp
server_udp:UdpServer.cpp
g++ -o $@ $^ -std=c++17
client_udp:UdpClient.cpp
g++ -o $@ $^ -std=c++17
.PHONY: clean
clean:
rm -f server_udp client_udp
👥总结
本篇博文对 【Linux网络】构建Udp服务器与字典翻译系统 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~