【Linux网络】构建UDP服务器与字典翻译系统

📢博客主页:https://blog.csdn.net/2301_779549673
📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!
📢本文由 JohnKi 原创,首发于 CSDN🙉
📢未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 🏳️‍🌈一、服务端更新
    • 1.1 函数对象声明
    • 1.2 UdpServer 类成员更新
    • 1.3 构造函数更新
    • 1.4 开始 - Start() 更新
  • 🏳️‍🌈二、Dictionary 字典类设计
    • 2.1 基本结构
    • 2.2 加载字典文件 - LoadDictionary(const std::string& path)
    • 2.3 构造函数
    • 2.4 翻译函数
    • 2.5 服务端运行更新
  • 🏳️‍🌈三、整体代码
  • 👥总结


上一篇文章中,我们实现了回显客户端输入的功能,这功能往往是不够的,为了更好地模拟现实需求,我们现在多增加一个功能 - 字典翻译功能

🏳️‍🌈一、服务端更新

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个大的步骤

    1. 读方式打开文件
    2. 按行读取内容[需要考虑中间有空格情况,一行中没找到分隔符情况]
    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); // 时间戳,获取可读性较强的时间信息5char buffer[1024];// bugsnprintf(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.txtstd::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;            // 进程pidstd::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_udpserver_udp:UdpServer.cppg++ -o $@ $^ -std=c++17client_udp:UdpClient.cpp g++ -o $@ $^ -std=c++17.PHONY: clean
clean:rm -f server_udp client_udp

👥总结

本篇博文对 【Linux网络】构建Udp服务器与字典翻译系统 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/76627.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【项目管理】成本类计算 笔记

项目管理-相关文档&#xff0c;希望互相学习&#xff0c;共同进步 风123456789&#xff5e;-CSDN博客 &#xff08;一&#xff09;知识总览 项目管理知识域 知识点&#xff1a; &#xff08;项目管理概论、立项管理、十大知识域、配置与变更管理、绩效域&#xff09; 对应&…

div(HTML标准元素)和view(微信小程序专用组件)的主要区别体

div&#xff08;HTML标准元素&#xff09;和view&#xff08;微信小程序专用组件&#xff09;的主要区别体现在以下方面&#xff1a; 一、应用场景与开发框架 ‌适用平台不同‌ div是HTML/CSS开发中通用的块级元素&#xff0c;用于Web页面布局‌&#xff1b;view是微信小程序专…

【C++软件实战问题排查经验分享】UI界面卡顿 | CPU占用高 | GDI对象泄漏 | 线程堵塞 系列问题排查总结

目录 1、UI界面卡顿问题排查 2、软件CPU占用高问题排查 3、UI界面显示异常&#xff08;GDI对象泄漏导致窗口绘制异常&#xff09;问题排查 4、软件线程堵塞&#xff08;包含线程死锁&#xff09;问题排查 5、最后 C软件异常排查从入门到精通系列教程&#xff08;核心精品专…

管理杂谈——采石矶大捷的传奇与启示

南宋抗金史上&#xff0c;岳飞与岳家军的铁血传奇家喻户晓&#xff0c;但另一位力挽狂澜的“文官战神”却常被忽视——他从未掌兵&#xff0c;却在南宋存亡之际整合溃军&#xff0c;以少胜多&#xff0c;缔造采石矶大捷。此人正是虞允文。一介书生何以扭转乾坤&#xff1f;他的…

动态规划-零钱兑换

332.零钱兑换 给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount &#xff0c;表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额&#xff0c;返回 -1 。你可以认为每种硬币的数量是无…

SpringAI+DeepSeek大模型应用开发——4 对话机器人

目录​​​​​​​ ​​​​​​​​​​​​​​项目初始化 pom文件 配置模型 ChatClient 同步调用 流式调用 日志功能 对接前端 解决跨域 会话记忆功能 ChatMemory 添加会话记忆功能 会话历史 管理会话id 保存会话id 查询会话历史 完善会话记忆 定义可序列…

Java 关键字

本章列出了Java 语言的所有关键字和“类关键字的单词”。 “受限关键字”是指&#xff0c;它们旨在模块声明中是关键字&#xff0c;在其他情况下则是标识符。 “受限标识符”是指&#xff0c;除非用在某些特定位置&#xff0c;否则他们只是标识符。例如&#xff0c;var一般都…

AI重塑网络安全:机遇与威胁并存的“双刃剑”时代

一、引言 人工智能&#xff08;AI&#xff09;技术的迅猛发展&#xff0c;正在深刻改变网络安全行业的格局。从ChatGPT生成钓鱼邮件到AI驱动的漏洞挖掘&#xff0c;从零信任架构的普及到安全大模型的实战应用&#xff0c;AI既是攻击者的“新武器”&#xff0c;也是防御者的“新…

网络原理——UDP

1、 与TCP的关键区别 特性UDPTCP连接方式无连接面向连接可靠性不可靠可靠数据顺序不保证顺序保证顺序传输速度更快相对较慢头部开销8字节20-60字节流量控制无有拥塞控制无有适用场景实时应用、广播/多播可靠性要求高的应用 2、UDP 报文结构 报文结构大致可以分为首部和载荷&a…

STM32——新建工程并使用寄存器以及库函数进行点灯

本文是根据江协科技提供的教学视频所写&#xff0c;旨在便于日后复习&#xff0c;同时供学习嵌入式的朋友们参考&#xff0c;文中涉及到的所有资料也均来源于江协科技&#xff08;资料下载&#xff09;。 新建工程并使用寄存器以及库函数进行点灯操作 新建工程步骤1.建立工程2.…

Unocss 类名基操, tailwindcss 类名

这里只列出 unocss 的可实现类名&#xff0c;tailwindcss 可以拿去试试用 1. 父元素移入&#xff0c;子元素改样式 <!-- 必须是 group 类名 --> <div class"group"><div class"group-hover:color-red">Text</div> </div>2…

深度学习入门(一)

一、简介 深度学习是机器学习领域新兴且关键的研究方向。机器学习重点在于让计算机从数据中挖掘规律以预测未知&#xff0c;而深度学习借助构建多层神经网络&#xff0c;自动学习数据的复杂特征&#xff0c;从而实现更精准的模式识别&#xff0c;在图像、语音等众多领域广泛应…

element-plus中,Steps 步骤条组件的使用

目录 一.基本使用 1.代码 2.效果展示 3.代码解读 二.案例&#xff1a;修改用户的密码 1.期望效果 2.代码 3.展示效果 结语 一.基本使用 1.代码 从官网复制如下代码&#xff1a; <template><div><el-stepsstyle"max-width: 600px":space&quo…

jax 备忘录

https://zhuanlan.zhihu.com/p/532504225 https://docs.jax.dev/en/latest/index.html

NLTK 基础入门:用 Python 解锁自然语言处理

自然语言处理&#xff08;NLP&#xff09;是人工智能领域的重要分支&#xff0c;它让计算机能够理解、处理和生成人类语言。而 NLTK&#xff08;Natural Language Toolkit&#xff09; 作为 Python 生态中最经典的 NLP 库之一&#xff0c;提供了丰富的工具和资源&#xff0c;是…

ElementUI中checkbox v-model绑定值为布尔、字符串或数字类型

这篇博客介绍了在Vue.js中使用El-Checkbox组件时&#xff0c;如何设置和处理v-model的布尔值和类型转换。通过示例代码展示了如何设置true-label和false-label属性来改变选中状态的值&#xff0c;适用于需要特定类型&#xff08;如字符串或整数&#xff09;的场景。v-model不能…

JBoss 项目修复笔记:绕开 iframe 安全问题,JSF 与 Angular 最小代价共存方案

JBoss 项目修复笔记&#xff1a;绕开 iframe 安全问题&#xff0c;JSF 与 Angular 最小代价共存方案 本篇笔记衔接的内容为&#xff1a;JBoss WildFly 本地开发环境完全指南&#xff0c;里面简单的描述了一下怎么配置 docker&#xff0c;在本地启动一个可以运行的 JBoss 和 W…

Linux文件时间戳详解:Access、Modify、Change时间的区别与作用

在 Linux 系统中&#xff0c;文件的这三个时间戳&#xff08;Access、Modify、Change&#xff09;分别表示不同的文件状态变更时间&#xff0c;具体含义如下&#xff1a; 1. Access Time (Access) 含义&#xff1a;文件最后一次被访问的时间&#xff08;读取内容或执行&#xf…

SpringBoot项目打包为window安装包

SpringBoot项目打包为window安装包 通过jpackage及maven插件的方式将springboot项目打包为exe或msi pom.xml 添加插件 <plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>3.1.0</vers…

pip永久换镜像地址

要将 pip 永久设置为阿里云镜像源&#xff0c;可以通过修改 pip 的全局配置文件来实现。以下是具体步骤&#xff1a; 步骤 1&#xff1a;创建或修改 pip 配置文件 根据你的操作系统&#xff0c;配置文件的路径略有不同&#xff1a; Linux/macOS 配置文件路径&#xff1a;~/.…