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

socket套接字-UDP(中)

socket套接字-UDP(上)https://blog.csdn.net/Small_entreprene/article/details/147465441?fromshare=blogdetail&sharetype=blogdetail&sharerId=147465441&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link

UDP服务器的搭建

在之前的博客中,我们已经完成了一个功能完整的UDP服务器基础架构。通过UdpServer类的实现,我们能够轻松创建一个UDP服务器。该服务器会监听指定端口,接收客户端发送的消息,并通过回调函数对消息进行处理,最后将处理结果返回给客户端。

代码解析

UdpServer.hpp文件中,我们定义了UDP服务器的核心逻辑。我们通过socket系统调用创建套接字,并使用bind将套接字与指定端口绑定。在Start方法中,服务器进入消息循环,不断接收客户端的消息,并调用回调函数处理消息。

void Start()
{_isrunning = true;while (_isrunning){char buffer[1024];struct sockaddr_in peer;socklen_t len = sizeof(peer);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);sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);}}
}

这一部分的代码实现了服务器的基础功能,但此时的服务器功能比较单一,只能对消息进行简单的回显处理。

回调机制的引入

在最初的版本中,服务器的功能是固定的,只能对消息进行简单的回显处理。这存在一个很大的局限性——服务器的功能是固定的,如果想增加新的功能,就必须修改服务器的内部代码。

我思考了一下,如果我想要在将来给服务器增加新的功能,比如翻译功能、计算功能或者其他什么功能,那是不是每次都要修改服务器的内部代码呢?这显然不符合我们追求的模块化、可扩展的设计理念。

于是,我灵机一动,想出了一个好办法——引入回调机制。这个想法其实来源于我们平时使用的很多软件库,它们通过回调函数允许用户自定义行为。

在我们的UDP服务器中,我定义了一个回调函数类型using func_t = std::function<std::string(const std::string &)>,这个函数类型表示我们的回调函数将接收一个字符串作为输入,并返回一个字符串作为输出。然后,我在UdpServer类的构造函数中增加了一个func_t类型的参数,这样在创建服务器的时候,就可以传入我们想要的处理逻辑了。

在服务器的消息循环中,每当我接收到客户端发送的消息时,我就可以直接调用这个回调函数,将消息交给它处理,然后把处理结果发送回客户端。

std::string result = _func(buffer); // 调用回调函数进行处理
sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);

这样一来,我们的UDP就服务器变得非常灵活了。只要实现一个符合func_t类型的回调函数,就可以给服务器增加新的功能,而不用再去修改服务器的核心代码了。

翻译功能的实现

有了回调机制之后,我就可以开始实现翻译功能了。这个功能的想法其实来源于我平时学习英语的时候,经常会遇到不认识的单词,需要查字典。我就想,要是能有个服务器,可以让我把不认识的单词发给它,它就能直接返回单词的中文意思,那该多好啊!

于是,我开始构思这个翻译功能的实现。首先,我需要一个字典来存储单词和对应的中文翻译。我决定用一个简单的文本文件来作为字典文件,文件的每一行就是一个单词和它的翻译,中间用特定的分隔符隔开,比如apple: 苹果

然后,我创建了一个Dict类来管理这个字典。这个类有一个方法LoadDict,用来从文件中加载字典数据。在加载的时候,我会逐行读取文件内容,然后按照分隔符把单词和翻译分开,存到一个unordered_map中,方便后续查询。

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)){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;
}

接着,我实现了一个Translate方法,它接收一个单词作为输入,然后在字典中查找对应的翻译。如果找到了,就返回翻译结果;如果没有找到,就返回“None”。

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;
}

最后,在main函数中,我创建了Dict对象,并调用LoadDict方法加载字典。然后,我创建了UDP服务器对象,并将DictTranslate方法作为回调函数传递给服务器。

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();Dict dict;dict.LoadDict();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;
}

这样,当客户端发送一个单词给服务器时,服务器就会调用Translate方法,查找单词的翻译,并将结果返回给客户端。

网络地址的封装

在实现翻译功能的过程中,我遇到了一个小问题。我想在服务器的日志中记录每个客户端的IP地址和端口号,这样我就可以知道是谁发来的单词。但是,我发现每次处理客户端消息的时候,都要从sockaddr_in结构体中提取IP和端口号,然后再转换为字符串格式,这样显得有点麻烦。

问题的提出

在早期的代码中,每次收到客户端的消息后,我们需要手动从sockaddr_in结构体中提取IP地址和端口号,并将其转换为便于打印和记录的字符串形式。例如:

int peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);

这种做法存在以下问题:

  • 代码重复 :每次处理客户端消息时,都需要重复这段提取和转换代码,导致代码冗余,增加了维护成本。

  • 可读性差 :直接操作sockaddr_in结构体的成员变量,使得代码的可读性降低,对于不熟悉网络编程的开发者来说,理解起来有一定难度。

  • 扩展性差 :如果后续需要增加与网络地址相关的其他功能,例如地址验证、地址转换等,这种分散的处理方式会使代码难以扩展和维护。

封装InetAddr

为了解决上述问题,我决定封装一个InetAddr类来管理网络地址信息。这个类的构造函数接收一个sockaddr_in结构体,然后在内部将IP地址和端口号提取出来,并转换为方便使用的格式。

InetAddr(struct sockaddr_in &addr) : _addr(addr)
{_port = ntohs(_addr.sin_port);_ip = inet_ntoa(_addr.sin_addr);
}

然后,我为这个类提供了PortIp两个方法,用来获取端口号和IP地址。

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

封装后的优势

通过封装InetAddr类,我们获得了以下优势:

  • 代码简化 :在处理客户端消息时,只需创建一个InetAddr对象,即可方便地获取客户端的IP地址和端口号,无需重复编写提取和转换代码。例如:

封装前:

int peer_port = ntohs(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);

封装后:

InetAddr client(peer);
std::string ip = client.Ip();
uint16_t port = client.Port();
  • 提高可读性 :封装后的代码更加直观和清晰,开发者可以更容易地理解代码的意图,减少了理解成本。

  • 增强扩展性 :如果后续需要增加与网络地址相关的功能,只需在InetAddr类中进行扩展,而无需修改其他业务逻辑代码,大大提高了代码的可维护性和可扩展性。

回调机制的优化

在最初的设计中,我的回调函数只接收一个参数,那就是客户端发送的消息。但是,在实现翻译功能的时候,我发现我还想在回调函数中使用客户端的IP地址和端口号,比如在日志中记录这些信息。

变化动机

  • 增加信息利用率 :在最初的回调机制中,回调函数只能获取到客户端发送的消息内容,但无法获取到发送该消息的客户端的网络地址信息。这意味着在处理消息时,我们无法根据客户端的地址进行个性化的处理或记录,限制了功能的灵活性和丰富度。

  • 满足功能需求 :以翻译功能为例,我们希望能够记录是哪个客户端发送了哪个单词进行查询,这需要在回调函数中同时获取消息内容和客户端地址信息。此外,像访问统计、基于客户端地址的权限控制等功能的实现,也都需要在回调函数中获取客户端的地址信息。

优化过程

于是,我决定对回调机制进行优化,让回调函数可以接收更多的参数。我修改了回调函数的类型定义,让它可以接收一个InetAddr对象作为第二个参数。

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

然后,在服务器的Start方法中,当调用回调函数的时候,我将InetAddr对象作为参数传递进去。

InetAddr client(peer);
buffer[s] = 0;
std::string result = _func(buffer, client);

这样,在回调函数中,我就可以同时获取到客户端发送的消息以及客户端的网络地址信息了。

代码注释与详细解释

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;// UDP 服务器类
class UdpServer
{
public:// 构造函数,初始化服务器端口和消息处理回调函数UdpServer(uint16_t port, func_t func): _sockfd(defaultfd), // 初始化套接字文件描述符为默认值_port(port),        // 设置服务器端口_isrunning(false),  // 初始化运行状态为停止_func(func)         // 设置消息处理回调函数{}// 初始化服务器,创建套接字并绑定端口void Init(){// 1. 创建套接字// 使用 socket 函数创建一个 UDP 套接字// AF_INET 表示使用 IPv4 地址族// SOCK_DGRAM 表示使用 UDP 协议_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. 绑定套接字信息(IP 和端口)// 2.1 填充 sockaddr_in 结构体,用于指定绑定的地址信息struct sockaddr_in local;bzero(&local, sizeof(local)); // 清零结构体,避免未定义行为local.sin_family = AF_INET;   // 设置地址族为 IPv4// 将本地端口号转换为网络字节序(大端字节序)local.sin_port = htons(_port);// 设置本地 IP 地址为 INADDR_ANY,表示监听所有网络接口上的连接// 这样服务器可以接收来自任何 IP 地址的客户端请求local.sin_addr.s_addr = INADDR_ANY;// 调用 bind 函数将套接字绑定到指定的地址和端口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. 接收消息// 使用 recvfrom 函数接收 UDP 消息// 参数包括套接字文件描述符、缓冲区、缓冲区大小、消息标志、发送端地址结构体指针和地址结构体长度指针ssize_t s = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (s > 0){// 创建 InetAddr 对象,封装发送端的地址信息InetAddr client(peer);// 在缓冲区末尾添加字符串终止符,确保数据以 C 风格字符串形式存储buffer[s] = 0;// 调用回调函数处理消息,并获取处理结果// 回调函数接收消息内容和客户端地址作为参数std::string result = _func(buffer, client);// 2. 发送响应消息// 使用 sendto 函数将处理结果发送回客户端// 参数包括套接字文件描述符、消息内容、消息长度、消息标志、发送端地址结构体指针和地址结构体长度sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, len);}}}// 析构函数~UdpServer(){}private:int _sockfd; // 套接字文件描述符uint16_t _port; // 服务器端口号bool _isrunning; // 服务器运行状态标志func_t _func; // 消息处理回调函数
};
  • 回调函数的定义与使用 :通过定义func_t作为回调函数类型,并在UdpServer类中使用,实现了将消息处理逻辑与服务器通信逻辑的分离。这样,用户可以通过传入不同的回调函数,轻松地为服务器增加不同的功能。

  • 网络地址的封装 :通过InetAddr类对网络地址信息进行封装,使得在处理客户端消息时,能够更加方便地获取和使用客户端的IP地址和端口号。

Dict.hpp 文件

#pragma once#include <iostream>
#include <fstream>
#include <string>
#include <unordered_map>
#include "Log.hpp"
#include "InetAddr.hpp"// 定义字典文件的默认路径为当前目录下的 dictionary.txt 文件
const std::string defaultdict = "./dictionary.txt";
// 定义字典文件中单词和翻译之间的分隔符为 ": "
const std::string sep = ": ";// 引入 LogModule 命名空间,便于使用日志功能
using namespace LogModule;class Dict
{
public:// 构造函数,初始化字典文件路径,默认为 defaultdictDict(const std::string &path = defaultdict) : _dict_path(path){}// 加载字典文件的方法bool LoadDict(){// 打开字典文件std::ifstream in(_dict_path);// 如果文件打开失败,输出错误日志并返回 falseif (!in.is_open()){LOG(LogLevel::DEBUG) << "打开字典: " << _dict_path << " 错误";return false;}// 定义一个字符串变量,用于逐行读取文件内容std::string line;// 循环逐行读取文件while (std::getline(in, line)){// 查找分隔符在当前行中的位置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();// 返回 true,表示字典加载成功return true;}// 翻译方法,根据输入的单词和客户端地址返回翻译结果std::string Translate(const std::string &word, InetAddr &client){// 在字典中查找输入的单词auto iter = _dict.find(word);// 如果未找到该单词,输出调试日志并返回 "None"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;// 使用 unordered_map 存储单词和翻译的键值对,键为单词,值为翻译std::unordered_map<std::string, std::string> _dict;
};

dictionary.txt

apple: 苹果
banana: 香蕉
cat: 猫
dog: 狗
book: 书
pen: 笔
happy: 快乐的
sad: 悲伤的
hello: 
: 你好run: 跑
jump: 跳
teacher: 老师
student: 学生
car: 汽车
bus: 公交车
love: 爱
hate: 恨
hello: 你好
goodbye: 再见
summer: 夏天
winter: 冬天
  • 字典文件的加载 :在LoadDict方法中,通过读取字典文件并解析每一行的内容,将单词及其对应的翻译存储到unordered_map中,实现了字典数据的快速加载和高效查询。

  • 翻译功能的实现Translate方法通过在字典中查找指定的单词,返回对应的翻译结果。如果找不到,则返回“None”。

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:// 构造函数,接收一个 sockaddr_in 结构体作为参数// 该结构体通常由 socket API 返回,包含网络地址信息InetAddr(struct sockaddr_in &addr) : _addr(addr){// 使用 ntohs 将网络字节序的端口号转换为主机字节序// 网络字节序通常是大端字节序(big-endian),而主机字节序可能是小端(little-endian)_port = ntohs(_addr.sin_port);// 使用 inet_ntoa 将 4 字节的网络字节序 IP 地址转换为点分十进制的字符串形式_ip = inet_ntoa(_addr.sin_addr);}// 获取端口号的方法uint16_t Port() { return _port; }// 获取 IP 地址字符串的方法std::string Ip() { return _ip; }// 析构函数,目前无特殊清理操作~InetAddr() {}private:// 存储原始的 sockaddr_in 结构体,包含完整的网络地址信息struct sockaddr_in _addr;// 存储转换后的 IP 地址字符串,格式为 "xxx.xxx.xxx.xxx"std::string _ip;// 存储转换后的端口号,为主机字节序uint16_t _port;
};

网络地址信息的封装 :构造函数接收一个sockaddr_in结构体,并从中提取出IP地址和端口号,将其转换为便于使用的格式。通过PortIp方法,可以方便地获取客户端的端口号和IP地址。

测试代码:UdpServer.cc

#include <iostream>
#include <memory>
#include "Dict.hpp"      // 翻译的功能
#include "UdpServer.hpp" // 网络通信的功能// 测试用的默认消息处理函数
// 用于演示服务器的基本功能,将接收到的消息前面加上 "hello, " 后返回
std::string defaulthandler(const std::string &message)
{std::string hello = "hello, ";hello += message;return hello;
}// 程序入口函数
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();// 创建字典对象,用于提供翻译功能Dict dict;// 加载字典文件,准备翻译所需的数据dict.LoadDict();// 创建 UDP 服务器对象,并指定端口号和消息处理回调函数// 这里使用 lambda 表达式捕获字典对象,以便在回调函数中调用其翻译方法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;
}

测试代码:UdpClient.cc

#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>// UDP 客户端程序入口
int main(int argc, char *argv[])
{// 检查命令行参数是否正确,需要提供服务器 IP 和端口号if (argc != 3){std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}// 获取服务器 IP 和端口号std::string server_ip = argv[1];uint16_t server_port = std::stoi(argv[2]);// 1. 创建 UDP 套接字int sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "socket error" << std::endl;return 2;}// 2. 填写服务器地址信息struct sockaddr_in server;memset(&server, 0, sizeof(server)); // 初始化内存为零server.sin_family = AF_INET;        // 设置地址族为 IPv4server.sin_port = htons(server_port); // 将端口号转换为网络字节序server.sin_addr.s_addr = inet_addr(server_ip.c_str()); // 设置服务器 IP 地址// 3. 与服务器进行通信的循环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; // 确保接收缓冲区以 null 字符结尾std::cout << buffer << std::endl; // 输出服务器返回的消息}}return 0;
}

测试结果

总结与展望

通过这一系列的改进,我们的UDP服务器已经从一个简单的消息收发工具,进化成了一个具有实用翻译功能的应用程序。这个过程让我深刻体会到了模块化设计和回调机制的强大之处。它们不仅让我们的代码更加清晰和易于维护,还极大地提高了代码的可扩展性和复用性。

在未来的开发中,我计划继续优化这个服务器。比如,增加对更多语言的支持,或者让服务器能够同时处理多个客户端的请求。另外,我还想尝试将这个服务器部署到云平台上,让更多的用户能够使用这个翻译服务。

如果你对这个项目感兴趣,或者有任何建议和想法,欢迎随时与我交流。让我们一起在编程的世界里不断探索,创造更多有趣的作品!

下期预告:群聊实现及补充收尾!!!😝

相关文章:

socket套接字-UDP(中)

socket套接字-UDP&#xff08;上&#xff09;https://blog.csdn.net/Small_entreprene/article/details/147465441?fromshareblogdetail&sharetypeblogdetail&sharerId147465441&sharereferPC&sharesourceSmall_entreprene&sharefromfrom_link UDP服务器…...

Android源码编译命令详解

一、引言 先看下面几条指令,相信编译过Android源码的人都再熟悉不过的。 source setenv.sh lunch make -j8记得最初刚接触Android时,同事告诉我用上面的指令就可以编译Android源码,指令虽短但过几天就记不全或者忘记顺序,每次编译时还需要看看自己的云笔记,冰冷的指令总…...

AI 发展历史与关键里程碑_附AI 模型清单及典型应用场景以及物流自动化适合的模型选择

AI 发展历史与关键里程碑_附AI 模型清单及典型应用场景以及物流自动化适合的模型选择 下面分三部分进行介绍: 1. AI 发展历史与关键里程碑 1950 年:图灵测试 1950 年,艾伦图灵提出“图灵测试”(Turing Test),首次以可检验的方式讨论机器能否“思考”。# 图灵测试示意:…...

MVCC(多版本并发控制)

MVCC&#xff08;多版本并发控制&#xff09;是数据库实现高并发事务的核心技术之一&#xff0c;其核心是通过数据多版本解决读写冲突。以下从‌技术原理、实现细节、应用场景、优缺点‌四个方面深入解析。 ‌一、技术原理‌ 1. ‌核心思想‌ ‌数据多版本化‌&#xff1a;每…...

可以隐藏列的表格

今天积累一个可以隐藏列的表格的实现方法 需求&#xff1a; 表格中有一部分列可以隐藏&#xff0c;在列名右侧有一个复选框&#xff0c;点击勾选展示&#xff0c;否则隐藏另有一个小工具栏&#xff0c;其中有每一列对应的复选框&#xff0c;点击可以将隐藏的列再次展示 思路…...

学习MySQL的第十二天

夕阳西下 云霞满天 一、存储过程概述 1.1 理解 含义:存储过程的英文是 Stored Procedure。它的思想很简单,就是一组经过预先编译的SQL语句的封装。 执行过程:存储过程预先存储在MySQL服务器上,需要执行的时候,客户端只需要向服务器端发出调用存储过程的命令,服…...

用Python做有趣的AI项目4:AI 表情识别助手

本项目将使用 计算机视觉 CNN 模型来识别人脸表情&#xff0c;例如&#xff1a; 开心 &#x1f60a; | 生气 &#x1f620; | 悲伤 &#x1f622; | 惊讶 &#x1f632; | 厌恶 &#x1f612; | 害怕 &#x1f631; | 中性 &#x1f610; &#x1f9e0; 项目目标 实时摄像头…...

2005-2020年 各省-绿色信贷水平原始数据及测算

各省-绿色信贷水平原始数据及测算&#xff08;2005-2020年&#xff09;.ziphttps://download.csdn.net/download/2401_84585615/90259771 https://download.csdn.net/download/2401_84585615/90259771 绿色信贷是指金融机构向符合环保要求的企业或项目提供的贷款&#xff0c;旨…...

STM32F103_HAL库+寄存器学习笔记21 - CAN接收过滤器:CPU减负神器,提升系统效率的第一道防线

在STM32F103的CAN总线应用中&#xff0c;硬件过滤器&#xff08;Filter&#xff09;承担着关键角色。 本章将从寄存器层面深入剖析CAN接收过滤器的工作机制与配置方法&#xff0c;帮助理解如何高效筛选关键信息&#xff0c;减轻CPU负担。 通过合理使用过滤器&#xff0c;不仅能…...

java_基础Java 转义字符学习笔记

Java 转义字符学习笔记 在Java编程中&#xff0c;转义字符用于表示那些无法直接在代码中表示的字符。以下是一些常用的Java转义字符&#xff1a; \t - 制表符&#xff1a;用于实现对齐功能。\n - 换行符&#xff1a;用于在文本中换行。\ - 反斜杠&#xff1a;表示一个反斜杠字…...

JavaScript基础(七)之web APIs

第二部分:Web APIs 目录 第二部分:Web APIs 五、DOM-节点操作 5.1 日期对象 5.1.1 实例化 5.1.2 时间对象方法 5.1.3 时间戳 5.2 节点操作 5.2.1 DOM节点 5.2.2 查找节点 父节点查找: 子节点查找: 兄弟关系查找: 5.2.3 增加节点 创建节点 5.2.4 删除节点 …...

强化学习机器人路径规划——Sparrow复现

强化学习机器人路径规划——Sparrow-v1.1复现教程 Sparrow是一个开源的移动机器人路径规划模拟器,重视模拟速度和轻量化,使用DDQN强化学习方法进行训练。本文在其基础上,增加了绘制训练曲线教程,并给出了自制地图文件,以实现在自己的地图上进行训练。 模型示意图 源码地…...

怎样给MP3音频重命名?是时候管理下电脑中的音频文件名了

在处理大量音频文件时&#xff0c;给这些文件起一个有意义的名字可以帮助我们更高效地管理和查找所需的内容。通过使用专业的文件重命名工具如简鹿文件批量重命名工具&#xff0c;可以极大地简化这一过程。本文将详细介绍如何利用该工具对 MP3 音频文件进行重命名。 步骤一&am…...

【Nova UI】十二、打造组件库之按钮组件(上):迈向功能构建的关键一步

序言 在上一篇文章中&#xff0c;我们深入探索了 icon 组件从测试到全局注册的全过程&#x1f3af;&#xff0c;成功为其在项目中稳定运行筑牢了根基。此刻&#xff0c;组件库的建设之旅仍在继续&#xff0c;我们将目光聚焦于另一个关键组件 —— 按钮组件。按钮作为用户与界面…...

C++初阶-STL简介

目录 1.什么是STL 2.STL的版本 3.STL的六大组件 4.STL的重要性 4.1在笔试中 4.2在面试中 4.3.在公司中 5.如何学习STL 6.总结和之后的规划 1.什么是STL STL&#xff08;standard template library-标准模板库&#xff09;&#xff1b;是C标准库的重要组成部分&#xf…...

(最短路)洛谷 P6880 JOI2020 奥运公交 题解

题意 给定一个 n n n 点 m m m 边的有向图&#xff0c;每条边从 u u u 指向 v v v&#xff0c;经过这条边的代价为 c c c。点编号为 1 1 1 到 n n n&#xff0c;无自环。 我们可以翻转一条边&#xff0c;即让他从 u u u 指向 v v v 变为从 v v v 指向 u u u&#…...

动态规划算法题1

动态规划做题步骤 确定状态表示&#xff1a;dp表中某一个位置中的值所表示的含义就是状态表示根据状态表示推导状态转移方程&#xff1a;dp[i]等于什么状态转移方程就是什么&#xff0c;用之前或者之后的状态&#xff0c;推导出dp[i]的值初始化(防止越界)&#xff1a;根据状态…...

π0.5:带开放世界泛化的视觉-语言-动作模型

25年4月来自具身机器人创业公司 PI 公司的论文“π0.5: a Vision-Language-Action Model with Open-World Generalization”。 为了使机器人发挥作用&#xff0c;它们必须在实验室之外的现实世界中执行实际相关的任务。虽然视觉-语言-动作 (VLA) 模型在端到端机器人控制方面已…...

ESP32开发入门(四):ESP32-s3多串口开发实践

摘要 本文详细介绍ESP32-S3芯片的UART外设开发方法&#xff0c;涵盖UART0(默认调试串口)、UART1和UART2的配置与使用技巧&#xff0c;并提供完整示例代码&#xff0c;帮助开发者快速实现多设备串口通信。 一、ESP32-S3串口硬件资源 ESP32-S3芯片提供3个UART控制器&#xff1…...

树莓派学习专题<10>:使用V4L2驱动获取摄像头数据--申请和管理缓冲区

树莓派学习专题&#xff1c;10&#xff1e;&#xff1a;使用V4L2驱动获取摄像头数据--申请和管理缓冲区 1. 申请和管理缓冲区代码2. 代码解析3. 实测结果 1. 申请和管理缓冲区代码 /* 数据缓冲区 */ typedef struct tag_BufDesc {void *pvBufPtr ;size_t szBuf…...

Android10.0 Android.bp文件详解,以及内置app编写Android.bp文件

1.前言 在10.0的系统rom定制化开发中,在内置app的时候都是常用的用法,用Android.mk的常用,但是某些时候,会 使用Android.bp的方式来内置app,接下来就来使用常用的方式来写内置so aar jar等文件 2.Android.bp文件详解,以及内置app编写Android.bp文件的介绍 根据设计,An…...

git回退commit

在Git中回退提交(commit)主要有两种方法:使用 `git reset` 或 `git revert`,具体取决于是否需要保留提交历史或是否已推送到远程仓库。以下是详细步骤: 一、使用 `git reset`(适合本地未推送的提交) `git reset` 会移动分支的 HEAD 指针到指定提交,可选择是否保留修改。…...

arcpy列表函数的应用(4)

动态获取字段信息 在处理要素类或表时&#xff0c;可能需要动态获取字段信息&#xff0c;以便根据字段类型或名称进行特定操作。可以使用arcpy.ListFields()函数获取字段列表&#xff0c;并根据需要筛选字段。 示例&#xff1a; python # 获取指定要素类的所有字段 fields …...

02 业务流程架构

业务流程架构提供了自上而下的组织鸟瞰图&#xff0c;是业务流程的全景图。根据所采用的方法不同&#xff0c;有时被称为流程全景图或高层级流程图&#xff0c;提供了业务运营中所有业务流程的整体视图。 这样有助于理解企业内部各个业务流程之间的相互关系以及它们如何共同工…...

「Mac畅玩AIGC与多模态01」架构篇01 - 展示层到硬件层的架构总览

一、概述 AIGC&#xff08;AI Generated Content&#xff09;系统由多个结构层级组成&#xff0c;自上而下涵盖交互界面、API 通信、模型推理、计算框架、底层驱动与硬件支持。本篇梳理 AIGC 应用的六层体系结构&#xff0c;明确各组件在系统中的职责与上下游关系&#xff0c;…...

如何有效防止 SQL 注入攻击?

&#x1f512; 如何有效防止 SQL 注入攻击&#xff1f; SQL 注入&#xff08;SQL Injection&#xff09;是黑客通过构造恶意输入&#xff0c;篡改 SQL 查询语句的攻击方式。以下是 7 大防御策略&#xff0c;涵盖开发、测试和运维全流程。 ✅ 1. 使用参数化查询&#xff08;Pre…...

路由交换网络专题 | 第九章 | NAT地址转换 | NAT回流

拓扑图 &#xff08;1&#xff09;配置实现内网用户可以通过 NAT 转换地址访问外网。 // 配置一条静态路由通往PC2 [AR1]ip route-static 0.0.0.0 0 60.1.1.10 // 配置ACL匹配网段 [AR1]acl 2000 [AR1-acl-basic-2000]rule permit source 192.168.1.10 0.0.0.0 // 设置地址池(不…...

DFPatternFunctor遍历计算图

文件&#xff1a;include/tvm/relay/dataflow_pattern_functor.h 功能&#xff1a;定义 DFPatternFunctor 基类&#xff0c;为 DFPattern 提供访问者模式(Visitor Pattern)的实现框架&#xff0c;支持对不同类型的模式节点进行差异化处理。 继承关系&#xff1a; template &…...

Spring Boot中@RequestParam、@RequestBody、@PathVariable的区别与使用

Spring Boot中RequestParam、RequestBody、PathVariable的区别与使用 前言 在当今的Web开发领域&#xff0c;Spring Boot凭借其简洁、高效和强大的功能&#xff0c;成为了Java开发者构建Web应用的首选框架。在开发过程中&#xff0c;处理来自客户端的请求参数是一项常见且关键…...

大模型 SFT 中的关键技术总结学习

文章目录 微调策略LoRA 微调核心思想具体实现过程超参数与技巧实现步骤​ QLoRA 相关技术1. 核心原理2. 技术优势​3. 实现流程​4. 应用场景​ P-tuning核心思想关键技术点训练流程优点应用场景 P-tuning v2Prefix Tuning一、关键概念前缀&#xff08;Prefix&#xff09;虚拟标…...

AI如何重塑DDoS防护行业?六大变革与未来展望

一、AI驱动的攻击与防御&#xff1a;攻防博弈的全面升级 AI技术的引入使DDoS攻防进入“智能对抗”时代&#xff0c;攻击者与防御方均借助AI提升效率&#xff0c;形成新的技术平衡。 1. 攻击端&#xff1a;AI赋能攻击的智能进化 动态流量生成&#xff1a;攻击者利用生成对抗网…...

电池的寿命

思路&#xff1a; 首先&#xff0c;我们观察发现&#xff1a;由于每枚电池的使用时间不同&#xff0c;而我们又要减少浪费才能使所有电池加起来用得最久&#xff0c;不难发现&#xff1a;当n2时&#xff0c;输出较小值。 第一步&#xff1a;将电池分为两组&#xff0c;使两组…...

Android完整开发环境搭建/Studio安装/NDK/本地Gradle下载配置/创建AVD/运行一个Android项目/常用插件

目录 安装Android Studio 修改sdk位置 配置 HTTP 代理 安装 NDK 设置快捷键 Gradle 说明 setting.gradle init.gradle build.gradle 下载 相关设置 创建项目 阿里云加速 清理缓存并同步 创建AVD 实用插件 ADB Idea Android Drawable Importer GsonFormat …...

【KWDB 创作者计划】_KWDB引领数据库技术革新的璀璨之星

【KWDB 创作者计划】_KWDB引领数据库技术革新的璀璨之星 &#x1f31f;嗨&#xff0c;我是LucianaiB&#xff01; &#x1f30d; 总有人间一两风&#xff0c;填我十万八千梦。 &#x1f680; 路漫漫其修远兮&#xff0c;吾将上下而求索。 在当今数字化浪潮汹涌澎湃的时代&…...

设计模式--桥接模式详解

桥接模式&#xff08;bridge pattern&#xff09; 桥接模式时将抽象部分与它的实现部分分离&#xff0c;使他们可以独立的变化。它是一种对象结构型模式&#xff0c;又称为柄体&#xff08;Handle and Body&#xff09;模式或者接口&#xff08;interface&#xff09;模式&…...

Python+Selenium+Pytest+Allure PO模式UI自动化框架

一、框架结构 allure-report&#xff1a;测试报告base&#xff1a;定位元素封装data&#xff1a;数据log&#xff1a;日志文件page&#xff1a;页面封装文件夹report&#xff1a;缓存报告testcases&#xff1a;测试用例层utils&#xff1a;工具类run.py&#xff1a;执行文件 二…...

【C语言操作符详解(一)】--进制转换,原反补码,移位操作符,位操作符,逗号表达式,下标访问及函数调用操作符

目录 一.操作符的分类 二.二进制和进制转换 2.1--2进制转10进制 ​编辑 2.1.1--10进制转2进制数字 2.2--2进制转8进制和16进制 2.2.1--2进制转8进制 2.2.2--2进制转16进制 三.原码&#xff0c;反码&#xff0c;补码 四.移位操作符 4.1--左移操作符 4.2--右移操作符…...

回顾|Apache Cloudberry™ (Incubating) Meetup·2025 杭州站

2025 年 4 月 19 日&#xff0c;由酷克数据与中启乘数联合举办的 Apache Cloudberry™ (Incubating) Meetup 杭州站在浙江省杭州市滨江区滨江会展中心成功举办。本次活动邀请了 Cloudberry PPMC 团队成员、活跃内核贡献者以及中兴 EBASE-A、阿里云 ADB-PG、网易、中启乘数等多…...

使用 Autofac 实现依赖注入

前言&#xff1a;接上一篇文章&#xff0c;有了微软官方的依赖注入组件Microsoft.Extensions.DependencyInjection&#xff0c; 那么今天介绍一个新的开源的依赖注入组件Autofac 一、二者的差异Autofac和微软官方的依赖注入组件&#xff08;Microsoft.Extensions.DependencyIn…...

HTTP:十二.HTTPS

HTTPS 概述 超文本传输安全协议(英语:HyperText Transfer Protocol Secure,缩写:HTTPS;常称为HTTP over TLS、HTTP over SSL或HTTP Secure)是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,利用TLS加密数据包。 HTTPS的主要目的是提供对网站服务器…...

《代码整洁之道》第12章 迭进 - 笔记

好的设计是如何形成的&#xff1f; 章节核心&#xff1a; 好的软件设计不是完全靠前期庞大的设计方案来完成的&#xff0c;而更多地是在持续的编码、测试和重构过程中&#xff0c;“涌现”或“演进”出来的。 设计不是一次性的前期活动 大白话&#xff1a; 作者认为&#x…...

数字巴别塔:全栈多模态开发框架如何用自然语言重构软件生产关系?

一、自然语言编程的范式革命 1. 从代码行数到语义密度 开发效率对比&#xff08;某金融 SaaS 案例&#xff09;&#xff1a; 开发方式代码量&#xff08;行&#xff09;开发时间&#xff08;天&#xff09;维护成本&#xff08;$/年&#xff09;传统 React5,2004512,000低代码…...

【C语言极简自学笔记】C 语言数组详解:一维数组与二维数组

在 C 语言中&#xff0c;数组是一种非常重要的数据结构&#xff0c;它可以将多个相同类型的元素组织在一起&#xff0c;以便于我们进行批量处理和操作。本文将详细介绍 C 语言中的一维数组和二维数组&#xff0c;包括它们的定义、初始化、元素访问以及内存存储等方面的内容。 …...

从零构建云原生秒杀系统——后端架构与实战

📝个人主页🌹:一ge科研小菜鸡-CSDN博客 🌹🌹期待您的关注 🌹🌹 一、引言:秒杀系统的挑战与机遇 在电商、票务、抢购等业务场景中,“秒杀”系统扮演着至关重要的角色。 秒杀活动通常会在极短时间内爆发出数十倍至数百倍的平时流量,这对后端系统的承载能力、响应…...

Linux Socket编程:从API到实战

Linux Socket编程完全指南&#xff1a;从API到实战 概述 Socket&#xff08;套接字&#xff09;是网络编程的基础&#xff0c;它允许不同主机或同一主机上的不同进程之间进行通信。在Linux系统中&#xff0c;Socket编程主要通过一系列系统调用来实现&#xff0c;这些API提供了…...

德州仪器(TI)—TDA4VM芯片详解(1)—产品特性

写在前面 本系列文章主要讲解德州仪器&#xff08;TI&#xff09;TDA4VM芯片的相关知识&#xff0c;希望能帮助更多的同学认识和了解德州仪器&#xff08;TI&#xff09;TDA4VM芯片。 若有相关问题&#xff0c;欢迎评论沟通&#xff0c;共同进步。(*^▽^*) 错过其他章节的同学…...

增强版wps-plugin-deepseek开源插件是DeepSeek 支持的 WPS 插件,在您的办公工作流程中提供智能文档自动化和 AI 驱动的生产力增强

一、软件介绍 文末提供程序和源码下载学习 增强版wps-plugin-deepseek开源插件专为WPS Office插件开发打造的Vue模板&#xff0c;搭配Vite构建工具&#xff0c;提供丰富的WPS API实操示例。虽然官方提供了TypeScript扩展包&#xff0c;但支持程度有限&#xff0c;因此本项目选…...

在 Cursor 中 配置 GitHub MCP Server

文章目录 1、简单回顾 sequentialthinking 的安装2、提出问题:如何在 cursor 配置 github mcp 呢3、结果如下How to Configure GitHub MCP in CursorPrerequisitesStep 1: Update Cursor (if needed)Step 2: Generate a GitHub Personal Access TokenStep 3: Open Cursor MCP S…...

uniapp-商城-40-shop 购物车 选好了 进行订单确认4 配送方式3 地址编辑

前面说了配送 和地址页面 当地址页面为空或需要添加地址时&#xff0c;需要添加地址。 我的地址页面有个按钮 就是添加地址 点击 添加地址 按钮 后&#xff0c;就会跳转到地址添加的页面 1、添加地址页面 2、添加地址文件夹以及文件的创建 3、添加地址的代码 <template…...

初步自定义layui的table(laravel 12)

layui的table是非常好的表格&#xff0c;有美观的样式&#xff0c;对接起来也很便捷。使用后端翻页传过来的数据&#xff0c;本地测试是好的&#xff0c;部署到服务器时&#xff0c;翻页不起作用。故而暂时采用一次性读取全部数据&#xff0c;发送给table&#xff0c;界面如下所…...