socket套接字-UDP(下)
socket套接字-UDP(中)https://blog.csdn.net/Small_entreprene/article/details/147567115?fromshare=blogdetail&sharetype=blogdetail&sharerId=147567115&sharerefer=PC&sharesource=Small_entreprene&sharefrom=from_link在我之前搭建的翻译服务基础上,下一步计划开发一个简单的 UDP 群聊系统。之前实现的翻译服务器,主要是接收客户端消息,调用翻译函数处理后返回结果,实现了服务器与单个客户端的简单交互。现在,我打算在此基础上进行功能拓展,实现一个简单的群聊功能。
前言:UDP群聊功能的实现原理
在实现翻译功能时,服务器通过 sockfd
文件描述符接收客户端的消息,同时也能通过这个文件描述符向客户端发送消息。这意味着服务器可以收集并保存所有访问客户端的信息。当有新的消息到达时,服务器就可以将保存的客户端信息用于消息的转发。这其实就是群聊功能的基本实现原理。
但如果仅用一个 UDP 服务器来实现群聊,就会出现问题。因为服务器同时承担接收和发送消息的任务,这可能影响收发效率,尤其是在高并发情况下。为了解决这个问题,我们可以在服务器收到消息后,将消息和客户端信息放入一个队列中。然后,提前创建一批线程,这些线程专门负责从队列中取出消息和客户端信息进行处理。在取出消息的同时,也将 sockfd
文件描述符传递给线程。这样,当客户端向服务器发送消息时,服务器会将消息构建成一个任务放入队列,后端线程再从队列中取出任务,通过 sockfd
将消息转发给所有在线用户。基于线程池的这种实现方式,就可以搭建一个聊天室。在这种架构下,UDP 服务器扮演生产者的角色,负责将消息入队;而后端线程则是消费者,它们取出消息并完成转发。这种转发消息的服务器本质上就是一个典型的生产者 - 消费者模型。
换句话说就是:
对于翻译功能,我们既可以通过sockfd文件描述符读,又可以通过文件描述符写,这样就服务端就可以将这些访问对象的信息收集起来,然后服务端将对应的客户端信息保存起来,等再有人发消息,服务端将这些信息发送出去,这不就是群聊的实现原理了嘛!不过实现群聊,只拿一个udp_Server服务端来做的话,不太好,因为服务器既是用来接收的,也是用来发送的,收发效率上有点不太好,udp_server将来收到信息message和clientinfo,放入到对应的一个队列当中,然后提前创建处出一批线程,udp_server收到了对应的消息,就将对应的消息直接入队列,然后由于后端所对应的线程来拿取消费message+clientinfo,于此同时将sockfd也顺带给该线程。这样,就是将来一个客户端给服务器发送消息,服务端收到消息就将消息构建成一个任务,放到队列当中,然后由一个线程把消息,通过sockfd来将信息转发给所有的在线用户,此时我们就可以基于一个线程池实现聊天室,图像右边也就是实现了一个转发消息的服务器Server了,这里udp_server就是一个典型的生产者,后端的这一批线程就是典型的消费者,我们多对应的转发消息的服务器Server本质就是一个典型的生产者消费者模型!
下面有几个我们应该提前知道的知识点:
我们所对应的文件描述符,在一个进程中的所用线程是共享的!
UDP是支持全双工的,那么支持多线程同时读写呢?
可以的!
我们线程池中线程要执行的任务是消息转发,也就是消息路由,我们就需要先实现route功能!
那么下面,我们就开始UDP群聊的实现叭!
一、群聊系统实现:单线程的初步尝试
群聊系统的核心在于消息的广播。服务器需要将一个客户端发送的消息转发给所有在线的客户端。为了实现这一功能,服务器需要维护一个在线客户端列表,记录每个客户端的网络地址信息。
1.1 单线程实现的探索与挑战
在最初的版本中,Route
类实现了群聊的核心功能,包括管理在线客户端列表和消息的广播:
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp"
#include "Log.hpp"using namespace LogModule;
class Route
{
private:bool IsExist(InetAddr &peer){for (auto &user : _online_user){if (user == peer){return true;}}return false;}void AddUser(InetAddr &peer){LOG(LogLevel::INFO) << "新增一个在线用户: " << peer.StringAddr();_online_user.push_back(peer);}void DeleteUser(InetAddr &peer){for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++){if (*iter == peer){LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr() << "成功";_online_user.erase(iter);break;}}}public:Route(){}void MessageRoute(int sockfd, const std::string &message, InetAddr &peer){if (!IsExist(peer)){AddUser(peer);}std::string send_message = peer.StringAddr() + "# " + message; // 127.0.0.1:8080# 你好// TODOfor (auto &user : _online_user){sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));}// 这个用户一定已经在线了if (message == "QUIT"){LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr();DeleteUser(peer);}}~Route(){}private:// 首次给我发消息,等同于登录std::vector<InetAddr> _online_user; // 在线用户
};
我们实现Route.hpp基本框架之后,我们可以测试
单线程的致命缺陷:阻塞导致消息延迟
单线程客户端的现象:UdpClient.cc
关键问题分析:
-
输入阻塞接收:用户输入时(
std::cin
),无法处理接收的消息。 -
接收阻塞输入:等待消息时(
recvfrom
),用户无法发送新消息。 -
实时性丧失:在输入长消息或网络延迟时,其他用户的消息会严重延迟显示。
实际场景模拟:
# 用户A在终端输入长消息(耗时10秒)
输入> 这是一个非常非常长的消息,需要慢慢输入...# 在此期间:
# - 用户B发送了3条消息
# - 用户C退出群聊# 用户A只有在完成输入后,才能看到其他消息
[群消息] 用户B: 紧急通知!
[群消息] 用户B: 会议取消了!
[群消息] [系统] 用户C 退出群聊
[群消息] 用户B: 有人看到吗?
阻塞导致消息延迟的解决
我们如果将客户端实现多线程的话:
关键代码对比:UdpClient.cc
单线程:
// 输入与接收互相阻塞
while (true) {std::string input;std::cout << "输入> ";std::getline(std::cin, input); // 阻塞点1sendto(...);char buffer[1024];recvfrom(...); // 阻塞点2std::cout << buffer << "\n";
}
这会导致消息要cout输出,但是被getline阻塞等待用户输出,其他用户发送的消息要getline后才被接收recvfrom,这就导致串行式的结果!
多线程:
// 发送线程(专注输入)
void* SendThread(void*) {while (running) {std::string input;std::getline(std::cin, input); // 独立阻塞sendto(...);}
}// 接收线程(专注输出)
void* RecvThread(void*) {while (running) {int n = recvfrom(...); // 独立阻塞std::cout << buffer << "\n";}
}
这个时候我们客户端进程为什么使用多线程是因为:
-
输入阻塞不可接受:用户需要随时中断输入查看新消息
-
网络延迟不可控:
recvfrom
可能因网络抖动长时间阻塞 -
实时性是群聊的生命线:消息必须立即显示,不能等待任何操作
为了能够实现消息的分开打印和发送,我们可以利用文件操作,一个通过cin和cout来实现消息的输入,然后群聊天按照cerr的文件中显示,使测试更加美观,更好观察(后面相关协议学习后,我们就不需要这样通过cerr和cout来实现了)
// 接收线程(专注输出)
void* RecvThread(void*) {while (running) {int n = recvfrom(...); // 独立阻塞std::cerr << buffer << "\n";//cout--->>>cerr}
}
我们可以通过重定向的方式来实现输出输入到指定文件:
查看终端设备:
$ tty
/dev/pts/num # 当前终端设备路径:我们也可以通过echo '1' > /dev/pts/num 来测试
启动客户端B(显示终端):将标准错误重定向到/dev/pts/num的对应显示终端上终端
# 将消息显示到独立终端
./udpclient 113.45.250.155 8080 2>/dev/pts/num
-
接收线程 :负责从服务器接收消息并将其输出到控制台。通过
recvfrom
函数不断监听套接字,接收服务器发送的消息。 -
发送线程 :负责从标准输入读取用户输入的消息,并将其发送到服务器。通过
sendto
函数将消息发送到服务器的地址和端口。
通过这种多线程改造,客户端能够同时处理消息的发送和接收,提供更加流畅的用户体验。
自此,我们就解决了第一张图的左边部分!!!
1.2单线程实现的探索与挑战---效率问题
当让,这个单进程带来的问题是由于客户端的单线程造成的!对于我们实现转发消息的服务器Server,我们前面知道单线程带来的是效率低,为了实现简单,我们是先从单线程入手,后续在加入多线程线程池,对应代码会有相应的修改。
二、群聊系统优化:引入线程池
2.1 引入线程池的动机
为了提高服务器的并发处理能力和响应速度,我们决定引入线程池技术。线程池可以预先创建一批线程,这些线程在服务器启动时就处于等待状态,随时准备处理任务。当服务器接收到客户端的消息时,将消息和客户端信息构建成一个任务,放入任务队列中。线程池中的线程从队列中取出任务并执行,实现消息的异步处理。从udp_server到Server(转发消息的服务器)的进化:
增加线程池之前:
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port,
[&r](int sockfd, const std::string &message, InetAddr &peer)
{ r.MessageRoute(sockfd, message, peer); });
增加线程池之后:
// 1. 路由服务
Route r;// 2. 线程池
auto tp = ThreadPool<task_t>::GetInstance();// 3. 网络服务器对象,提供通信功能
std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port,
[&r, &tp](int sockfd, const std::string &message, InetAddr &peer){task_t t = std::bind(&Route::MessageRoute, &r, sockfd, message, peer);tp->Enqueue(t); }
);
三、多线程的引入与挑战
在引入多线程后,我们面临了一些新的挑战,比如线程安全问题和任务调度问题。
3.1 多线程环境下的线程安全问题
在引入多线程后,Route
类的 MessageRoute
方法可能被多个线程同时调用。这会导致对 _online_user
列表的并发访问,从而引发数据不一致的问题。例如,一个线程可能正在遍历列表发送消息,而另一个线程可能同时添加或删除用户,这会导致不可预测的行为。
3.2 解决方案:引入互斥锁
为了解决这个问题,我们在 Route
类中引入了互斥锁(Mutex
),确保对共享资源的访问是线程安全的:
#include "Mutex.hpp"class Route
{
private:Mutex _mutex; // 互斥锁,保护对在线用户列表的访问public:void MessageRoute(int sockfd, const std::string &message, InetAddr &peer){LockGuard lockguard(_mutex); // 使用RAII方式管理锁,确保在函数退出时自动释放if (!IsExist(peer)){AddUser(peer);}std::string send_message = peer.StringAddr() + "# " + message;for (auto &user : _online_user){sendto(sockfd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));}if (message == "QUIT"){LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr();DeleteUser(peer);}}
};
引入互斥锁:
- 在
Route
类中添加了一个Mutex
成员变量_mutex
。 - 在
MessageRoute
方法的入口处,使用LockGuard
获取锁,确保在方法执行期间对_online_user
列表的独占访问。 LockGuard
使用RAII机制,在其生命周期结束时自动释放锁,避免死锁。
线程安全的用户管理:
在添加和删除用户时,同样需要在 AddUser
和 DeleteUser
方法中使用锁来保护对 _online_user
列表的访问,防止并发修改导致的问题。
代码健壮性提升:
加锁后,即使在多线程环境下,也能保证对在线用户列表的操作是原子的和一致的,避免了竞态条件和数据损坏的风险。
通过这些修改,我们确保了 Route
类在多线程环境下的线程安全性。这不仅解决了高并发场景下的数据一致性问题,还为系统的进一步扩展和优化奠定了基础。
四、代码注释与详细解释
4.1 Route.hpp 路由管理模块
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include "InetAddr.hpp" // 自定义网络地址类头文件
#include "Log.hpp" // 自定义日志记录类头文件
#include "Mutex.hpp" // 自定义互斥锁类头文件using namespace LogModule; // 引入日志模块命名空间
using namespace MutexModule; // 引入互斥锁模块命名空间// 消息路由类 - 管理在线用户并负责消息分发
class Route
{
private:// 检查指定的网络地址是否已存在于在线用户列表中bool IsExist(InetAddr &peer){// 遍历在线用户列表for (auto &user : _online_user){// 如果找到匹配的网络地址,返回true表示存在if (user == peer){return true;}}// 如果遍历完整个列表都未找到匹配项,返回false表示不存在return false;}// 向在线用户列表中添加新用户void AddUser(InetAddr &peer){// 记录用户登录的日志信息LOG(LogLevel::INFO) << "新增一个在线用户: " << peer.StringAddr();// 将新用户的网络地址添加到在线用户列表_online_user.push_back(peer);}// 从在线用户列表中删除指定用户void DeleteUser(InetAddr &peer){// 遍历在线用户列表的迭代器for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++){// 如果找到匹配的网络地址if (*iter == peer){// 记录用户退出的日志信息LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr() << "成功";// 从列表中移除该用户_online_user.erase(iter);break; // 跳出循环}}}public:// 构造函数 - 初始化路由对象Route(){}// 消息路由处理函数// 参数:sockfd(套接字描述符)、message(消息内容)、peer(发送方网络地址)void MessageRoute(int sockfd, const std::string &message, InetAddr &peer){// 创建互斥锁守护对象,确保线程安全LockGuard lockguard(_mutex);// 检查发送方是否已存在于在线用户列表if (!IsExist(peer)){// 如果不存在,则添加为新用户AddUser(peer);}// 格式化要发送的消息,包含发送方地址和原始消息std::string send_message = peer.StringAddr() + "# " + message;// 向所有在线用户广播消息for (auto &user : _online_user){// 使用套接字发送消息给每个在线用户sendto(sockfd, send_message.c_str(), send_message.size(), 0,(const struct sockaddr *)&(user.NetAddr()), sizeof(user.NetAddr()));}// 如果收到的消息是"QUIT",处理用户退出逻辑if (message == "QUIT"){// 记录用户退出的日志信息LOG(LogLevel::INFO) << "删除一个在线用户: " << peer.StringAddr();// 从在线用户列表中移除该用户DeleteUser(peer);}}// 析构函数 - 释放路由对象资源~Route(){}private:// 在线用户列表 - 存储所有当前在线用户的网络地址std::vector<InetAddr> _online_user;// 互斥锁 - 用于保护对在线用户列表的并发访问Mutex _mutex;
};
4.2 InetAddr.hpp 网络地址封装
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#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 地址的缓冲区char ipbuffer[64];// 将网络字节序的 IPv4 地址转换为点分十进制字符串inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(ipbuffer));// 存储转换后的 IP 地址字符串_ip = ipbuffer;}// 构造函数 - 从主机字节序的 IP 和端口初始化InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port){// 清除地址结构体memset(&_addr, 0, sizeof(_addr));// 设置地址族为 IPv4_addr.sin_family = AF_INET;// 将点分十进制 IP 地址字符串转换为网络字节序的 IPv4 地址inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);// 将主机字节序的端口号转换为网络字节序_addr.sin_port = htons(_port);}// 获取存储的端口号(主机字节序)uint16_t Port() { return _port; }// 获取存储的 IP 地址字符串std::string Ip() { return _ip; }// 获取内部的网络地址结构体引用const struct sockaddr_in &NetAddr() { return _addr; }// 重载等于运算符,用于比较两个网络地址是否相同bool operator==(const InetAddr &addr){// 比较 IP 地址字符串和端口号是否都相同return addr._ip == _ip && addr._port == _port;}// 生成包含 IP 和端口的字符串表示形式std::string StringAddr(){// 拼接 IP 地址和端口号return _ip + ":" + std::to_string(_port);}// 析构函数~InetAddr(){}private:// 内部存储的网络地址结构体(网络字节序)struct sockaddr_in _addr;// 存储的 IP 地址字符串(点分十进制格式)std::string _ip;// 存储的端口号(主机字节序)uint16_t _port;
};
4.3 UdpServer.hpp UDP服务端封装
#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<void(int sockfd, const std::string&, InetAddr&)>;// 默认无效的文件描述符值
const int defaultfd = -1;// UDP服务器类 - 负责创建UDP服务器并处理客户端消息
class UdpServer
{
public:// 构造函数 - 初始化UDP服务器对象UdpServer(uint16_t port, func_t func): _sockfd(defaultfd), // 初始化套接字描述符为无效值_port(port), // 设置服务器端口_isrunning(false), // 初始化运行状态为停止_func(func) // 设置消息处理回调函数{}// 初始化UDP服务器void Init(){// 1. 创建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地址为通配符,表示监听所有网络接口local.sin_addr.s_addr = INADDR_ANY;// 2.2 调用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;}// 启动UDP服务器并开始接收和处理客户端消息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对象InetAddr client(peer);// 确保接收到的消息以null结尾,形成有效的C风格字符串buffer[s] = 0;// 调用回调函数处理消息_func(_sockfd, buffer, client);// 下面是原始的回显逻辑(被回调函数替代)// LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port<< "]# " << buffer;// 2. 发送回显消息给客户端(已被回调函数替代)// std::string echo_string = "server echo@ ";// echo_string += buffer;// sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr*)&peer, len);}}}// 析构函数 - 释放UDP服务器资源~UdpServer(){}private:int _sockfd; // UDP套接字文件描述符uint16_t _port; // 服务器监听的端口号// std::string _ip; // 服务器监听的IP地址(本例中使用INADDR_ANY代替)bool _isrunning; // 服务器运行状态标志func_t _func; // 消息处理回调函数(由用户提供一个处理逻辑)
};
4.4 UdpServer.cc UDP服务端实现
#include <iostream>
#include <memory>
#include "Route.hpp" // 消息路由功能
#include "UdpServer.hpp" // UDP网络通信功能
#include "ThreadPool.hpp" // 线程池功能using namespace ThreadPoolModule; // 引入线程池模块命名空间
using task_t = std::function<void()>; // 线程池任务类型定义// 测试用的默认消息处理函数
std::string defaulthandler(const std::string &message)
{std::string hello = "hello, ";hello += message; // 简单的字符串拼接作为示例处理return hello;
}// 主函数 - 程序入口点
// 编译指令:./udpserver port
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();// 1. 创建消息路由服务对象Route r;// 2. 获取线程池实例(单例模式)auto tp = ThreadPool<task_t>::GetInstance();// 3. 创建UDP服务器对象,并设置消息处理逻辑std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r, &tp](int sockfd, const std::string &message, InetAddr &peer) {// 创建一个任务,将消息路由逻辑包装为可在线程池中执行的任务task_t t = std::bind(&Route::MessageRoute, &r, sockfd, message, peer);// 将任务提交到线程池中执行tp->Enqueue(t);});// 初始化UDP服务器,包括创建套接字和绑定端口等操作usvr->Init();// 启动UDP服务器,开始监听并接收客户端消息usvr->Start();return 0; // 程序正常结束
}
4.5 UdpClient.cc 客户端实现
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Thread.hpp"// 全局变量 - 套接字描述符
int sockfd = 0;
// 全局变量 - 服务器IP地址
std::string server_ip;
// 全局变量 - 服务器端口号
uint16_t server_port = 0;
// 全局变量 - 接收线程的线程ID
pthread_t id;using namespace ThreadModlue; // 引入线程模块命名空间// 接收消息线程函数
void Recv()
{while (true){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){// 确保接收到的消息以null结尾,形成有效的C风格字符串buffer[m] = 0;// 在控制台输出接收到的消息std::cerr << buffer << std::endl; // 2}}
}// 发送消息线程函数
void Send()
{// 填充服务器端的网络地址结构体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地址// 发送"online"消息,表示客户端上线const std::string online = "inline";sendto(sockfd, online.c_str(), online.size(), 0,(struct sockaddr *)&server, sizeof(server));while (true){std::string input;std::cout << "Please Enter# "; // 1 - 提示用户输入消息std::getline(std::cin, input); // 0 - 从标准输入读取用户输入的消息// 向服务器发送用户输入的消息int n = sendto(sockfd, input.c_str(), input.size(), 0,(struct sockaddr *)&server, sizeof(server));(void)n; // 忽略发送操作的返回结果// 如果用户输入的是"QUIT",则退出发送循环并终止接收线程if (input == "QUIT"){pthread_cancel(id); // 终止接收线程break;}}
}// UDP客户端主函数
int main(int argc, char *argv[])
{// 检查命令行参数数量是否正确if (argc != 3){// 如果参数数量不正确,输出使用说明并退出程序std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;return 1;}// 从命令行参数获取服务器IP地址和端口号server_ip = argv[1];server_port = std::stoi(argv[2]);// 1. 创建UDP套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){// 如果创建套接字失败,输出错误信息并退出程序std::cerr << "socket error" << std::endl;return 2;}// 2. 创建并启动接收和发送线程Thread recver(Recv); // 创建接收消息的线程对象Thread sender(Send); // 创建发送消息的线程对象recver.Start(); // 启动接收线程sender.Start(); // 启动发送线程// 获取接收线程的线程ID,用于后续可能的线程操作id = recver.Id();// 等待接收线程和发送线程完成(通常不会到达这里,除非客户端主动退出)recver.Join();sender.Join();// 关于客户端是否需要显式绑定本地地址的说明:// 客户端不需要显式调用bind函数。首次调用sendto发送消息时,// 操作系统会自动为客户端套接字分配一个随机的可用端口号,// 并将客户端的IP地址和端口号绑定到套接字上。这样可以避免客户端端口冲突,// 因为每个客户端进程都会被分配一个唯一的随机端口号。return 0; // 程序正常结束
}
4.6 效果展示
操作 | 客户端A(输入) | 客户端B(显示) |
---|---|---|
发送"Hello" | 输入> Hello | [新消息] 127.0.0.1:5567# Hello |
发送"QUIT" | 输入> QUIT | [通知] 127.0.0.1:5567 已退出 |
服务端日志 | 无 | 记录用户加入/退出事件 |
关键设计说明
线程分工明确》》》发送线程:专注处理阻塞式用户输入;接收线程:非阻塞轮询+实时消息显示;服务线程池:并行处理消息路由
终端输出分离:std::cout
用于输入提示(保留在原始终端);std::cerr
用于消息显示(可重定向到其他终端)
并发安全保证:Route类使用Mutex
保护在线用户列表;原子变量running
控制线程生命周期;发送接收线程完全解耦
网络字节序处理:InetAddr自动处理htons/ntohs
转换;统一使用sockaddr_in
网络原始结构
补充说明
IP地址表示方式
在IPv4网络编程中,IP地址有两种核心表示方式:
-
点分十进制字符串:如"192.168.1.1",便于人类阅读。
-
二进制结构体in_addr:32位无符号整数,存储于sockaddr_in中,用于网络传输。
字符串与in_addr的转换函数
1. 传统转换函数
#include <arpa/inet.h>// 字符串转in_addr(已废弃,不推荐使用)
in_addr_t inet_addr(const char *cp); // 字符串转in_addr(推荐替代inet_addr)
int inet_aton(const char *cp, struct in_addr *inp);// in_addr转字符串(线程不安全)
char *inet_ntoa(struct in_addr in);
函数对比:
函数 | 输入 | 输出 | 线程安全 | 支持IPv6 |
---|---|---|---|---|
inet_addr | 字符串(如"192.168.1.1") | 32位网络字节序整数 | 是 | ❌ |
inet_aton | 字符串 | 填充in_addr结构体 | 是 | ❌ |
inet_ntoa | in_addr结构体 | 静态存储区字符串指针 | ❌ | ❌ |
示例代码:
struct sockaddr_in addr;
inet_aton("192.168.1.1", &addr.sin_addr); // 字符串转二进制
printf("IP: %s\n", inet_ntoa(addr.sin_addr)); // 二进制转字符串
2. 现代转换函数(推荐使用)
#include <arpa/inet.h>// 字符串转二进制(支持IPv4/IPv6)
int inet_pton(int af, const char *src, void *dst);// 二进制转字符串(支持IPv4/IPv6)
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);
-
af
:地址族(AF_INET或AF_INET6) -
src
:输入数据(字符串或二进制地址) -
dst
:输出缓冲区 -
size
:缓冲区大小(推荐INET_ADDRSTRLEN或INET6_ADDRSTRLEN)
示例代码:
// IPv4转换示例
struct in_addr ipv4_addr;
char str[INET_ADDRSTRLEN];inet_pton(AF_INET, "192.168.1.1", &ipv4_addr); // 字符串转二进制
inet_ntop(AF_INET, &ipv4_addr, str, INET_ADDRSTRLEN); // 二进制转字符串
inet_ntoa的线程安全问题
1. 问题根源
-
静态存储区:inet_ntoa内部使用静态缓冲区存储结果,多次调用会覆盖之前的值。
-
非线程安全:多线程并发调用时,可能产生数据竞争。
验证代码:
#include <stdio.h>
#include <pthread.h>
#include <arpa/inet.h>void* thread_func1(void* arg) {struct in_addr addr = { .s_addr = 0x0100007F }; // 127.0.0.1while (1) {char* ip = inet_ntoa(addr);printf("Thread1: %s\n", ip);}return NULL;
}void* thread_func2(void* arg) {struct in_addr addr = { .s_addr = 0x0101A8C0 }; // 192.168.1.1while (1) {char* ip = inet_ntoa(addr);printf("Thread2: %s\n", ip);}return NULL;
}int main() {pthread_t t1, t2;pthread_create(&t1, NULL, thread_func1, NULL);pthread_create(&t2, NULL, thread_func2, NULL);pthread_join(t1, NULL);pthread_join(t2, NULL);return 0;
}
运行结果示例:
Thread1: 192.168.1.1 // 预期127.0.0.1
Thread2: 192.168.1.1
Thread1: 192.168.1.1 // 数据被覆盖
┌───────────────────────────inet_ntoa 线程安全问题────────────────────────────┐
│ │
│ ╭──────────────❌ 线程不安全场景─────────────╮ ╭──────────────✅ 线程安全解决方案─────────────╮ │
│ │ │ │ │ │
│ │ [Thread 1] [Thread 2] │ │ [Thread 1] [Thread 2] │ │
│ │ ┌────────────┐ ┌────────────┐ │ │ ┌────────────┐ ┌────────────┐ │ │
│ │ │ 调用 │ │ 调用 │ │ │ │ 调用 │ │ 调用 │ │ │
│ │ │ inet_ntoa │ │ inet_ntoa │ │ │ │ inet_ntop │ │ inet_ntop │ │ │
│ │ │ (ip1) │ │ (ip2) │ │ │ │ (ip1, buf1)│ │ (ip2, buf2)│ │ │
│ │ └─────┬──────┘ └─────┬──────┘ │ │ └─────┬──────┘ └─────┬──────┘ │ │
│ │ ▼ ▼ │ │ ▼ ▼ │ │
│ │ ┌──────────────────────────────────┐ │ │ ┌────────────┐ ┌────────────┐ │ │
│ │ │ 静态缓冲区(共享) │ │ │ │ 私有缓冲区 │ │ 私有缓冲区 │ │ │
│ │ │ │ │ │ │ buf1[16] │ │ buf2[16] │ │ │
│ │ │ 初始值: 192.168.1.1 │ │ │ │ "192.168.1.1" "10.0.0.1" │ │ │
│ │ │ 被Thread 2覆盖为: 10.0.0.1 ────╮ │ │ │ └────────────┘ └────────────┘ │ │
│ │ └──────────────────────────────────┘ │ │ │ │
│ │ ▲ ▲ │ │ 无冲突,结果独立 │ │
│ │ ╰─────────┬──────────╯ │ │ │ │
│ │ ╰── 冲突!两个线程均输出 "10.0.0.1" │ │
│ ╰───────────────────────────────────────────╯ ╰───────────────────────────────────────────╯ │
└───────────────────────────────────────────────────────────────────────────────────────┘
展示多线程环境下inet_ntoa函数由于使用静态缓冲区导致的数据覆盖问题。可以使用流程图或对比图形式,直观展示线程1和线程2同时调用inet_ntoa时,如何互相覆盖结果。
2. 系统差异
-
CentOS 7:某些版本通过加锁实现伪线程安全,但不可依赖。
-
其他系统:通常存在线程安全问题。
四、inet_ntop的线程安全解决方案
1. 核心优势
-
栈缓冲区:避免使用静态存储区。
-
支持IPv4/IPv6:统一接口更灵活。
正确用法:
struct in_addr ipv4_addr;
char buffer[INET_ADDRSTRLEN]; // 专用缓冲区// 转换操作
inet_ntop(AF_INET, &ipv4_addr, buffer, INET_ADDRSTRLEN);
printf("IP: %s\n", buffer);
+---------------------- inet_ntoa 线程安全问题 vs inet_ntop 解决方案 ----------------------+
| |
| ╭───────────────────────────┬───────────────────────────╮ |
| | ❌ 线程不安全 (inet_ntoa) | ✅ 线程安全 (inet_ntop) | |
| | | | |
| | [Thread 1] [Thread 2] | [Thread 1] [Thread 2] | |
| | +---------+ +---------+ | +---------+ +---------+ | |
| | |调用 | |调用 | | |调用 | |调用 | | |
| | |inet_ntoa| |inet_ntoa| | |inet_ntop| |inet_ntop| | |
| | |(ip1) | |(ip2) | | |(ip1,buf1)|(ip2,buf2)| | |
| | +----┬----+ +----┬----+ | +----┬----+ +----┬----+ | |
| | ▼ ▼ | ▼ ▼ | |
| | +---------------------+ | +---------+ +---------+ | |
| | | 静态缓冲区(共享) | | | buf1 | | buf2 | | |
| | | 初始值: 192.168.1.1 | | | 192.168 | | 10.0.0.1| | |
| | | 被覆盖为: 10.0.0.1 ←┼───┼─┤ (独立) | | (独立) | | |
| | +---------------------+ | +---------+ +---------+ | |
| | ▲ ▲ | | | | |
| | └─────┬─────┘ | ▼ ▼ | |
| | ╰── 冲突! | +---------+ +---------+ | |
| | 输出均为 10.0.0.1 | | 正确结果 | | 正确结果 | | |
| | | | 192.168 | | 10.0.0.1| | |
| ╰───────────────────────────┴───────────────────────────╯ |
+-----------------------------------------------------------------------------------------+
展示inet_ntop函数如何通过使用独立的输出缓冲区避免线程安全问题。可以使用对比图形式,与inet_ntoa的静态缓冲区方式进行对比,突出inet_ntop的优势。
函数对比与选型建议
函数对比
特性 | inet_ntoa | inet_ntop |
---|---|---|
线程安全性 | ❌ 非线程安全 | ✔️ 线程安全 |
内存管理 | 静态缓冲区 | 输出缓冲区 |
IPv6支持 | ❌ | ✔️ |
返回值稳定性 | 多次调用结果被覆盖 | 每次调用结果独立 |
推荐使用场景 | 单线程简单测试 | 生产环境、多线程程序 |
最佳实践总结
弃用inet_ntoa:尤其在多线程环境中。
优先使用inet_pton/ntop:
显式指定地址族(AF_INET或AF_INET6);为字符串转换预留足够缓冲区
缓冲区大小常量:
-
IPv4:INET_ADDRSTRLEN(16字节)
-
IPv6:INET6_ADDRSTRLEN(46字节)
安全转换示例代码
struct sockaddr_in addr;
char ip_str[INET_ADDRSTRLEN];inet_pton(AF_INET, "192.168.1.1", &addr.sin_addr);
inet_ntop(AF_INET, &addr.sin_addr, ip_str, INET_ADDRSTRLEN);
┌─────────────────────────── IP地址转换流程示意图 ───────────────────────────┐
│ │
│ [IPv4结构体] [IPv6结构体] │
│ struct in_addr struct in6_addr │
│ (二进制格式) (二进制格式) │
│ │ │ │
│ │ inet_ntoa │ inet_ntop │
│ ▼ ▼ │
│ ┌───────────┐ ┌───────────┐ │
│ │ 点分十进制 │ │ 字符串格式 │ │
│ │ 字符串 │ │ (IPv4/IPv6)│ │
│ │ e.g. │ │ e.g. │ │
│ │ "192.168.1.1" │ "2001:db8::1" │
│ └───────────┘ └───────────┘ │
│ ▲ ▲ │
│ │ inet_aton │ inet_pton │
│ │ │ │
│ ┌───────────┐ ┌───────────┐ │
│ │ 旧函数 │ │ 新函数 │ │
│ │ 线程不安全 │ │ 线程安全 │ │
│ │ (静态缓冲区) │ │ (需手动分配缓冲区)│ │
│ └───────────┘ └───────────┘ │
│ │
│ ╭─────────────────────────────┬───────────────────────────────╮ │
│ │ ❌ 不安全的旧函数 │ ✅ 安全的新函数 │ │
│ │ - `inet_ntoa` │ - `inet_ntop` │ │
│ │ - 依赖静态缓冲区 │ - 需手动分配缓冲区 │ │
│ │ - 仅支持IPv4 │ - 支持IPv4/IPv6 │ │
│ ╰─────────────────────────────┴───────────────────────────────╯ │
└──────────────────────────────────────────────────────────────────────────┘
展示从点分十进制字符串到二进制结构体,再通过inet_ntop转换回字符串的完整流程。可以使用流程图形式,标注每个步骤的关键操作和数据流向。
通过合理选择IP地址转换函数,可以显著提升程序的健壮性和跨平台兼容性,尤其在多线程网络服务中,inet_ntop是确保线程安全的不二之选。
相关文章:
socket套接字-UDP(下)
socket套接字-UDP(中)https://blog.csdn.net/Small_entreprene/article/details/147567115?fromshareblogdetail&sharetypeblogdetail&sharerId147567115&sharereferPC&sharesourceSmall_entreprene&sharefromfrom_link在我之前搭建…...
使用Docker操作MySQL
在Docker中操作MySQL可以简化数据库的部署和管理过程。以下是详细的步骤,包括如何拉取MySQL镜像、创建容器以及配置远程访问权限。 拉取MySQL镜像 首先,使用以下命令从Docker Hub拉取MySQL镜像: docker pull mysql你也可以指定版本&#x…...
OpenGL ES 3.0 第二章总结:你好,三角形(Hello Triangle)
—— 从“画出第一个三角形”理解现代图形渲染流程 🔰 写在前面 OpenGL 是一个状态机型的图形 API。第二章《你好,三角形》是整个图形开发的起点,它帮助我们掌握从「准备绘制数据」到「渲染出第一个像素」的完整流程。 这一章最核心的任务是…...
neo4j vs python
1.将库中已经存在的两个节点,创建关系。 查询库中只有2个独立的节点。 方式一,python,使用py2neo库 #coding:utf-8 from py2neo import Graph,Node,Relationship,NodeMatcher##连接neo4j数据库,输入地址、用户名、密码 graph G…...
MIT6.S081-lab7前置
MIT6.S081-lab7前置 这部分包含了设备中断和锁的内容 设备中断 之前系统调用的时候提过 usertrap ,而我们的设备中断,比如计时器中断也会在这里执行,我们可以看看具体的逻辑: void usertrap(void) {int which_dev 0;if((r_sst…...
通过漂移-扩散仿真研究钙钛矿-硅叠层太阳能电池中的电流匹配和滞后行为
引言 卤化物钙钛矿作为光活性半导体的出现,为光伏技术的发展开辟了令人振奋的新方向。[1] 除了在单结太阳能电池中的优异表现,目前研究的重点在于将钙钛矿吸收层整合到叠层器件中。在硅-钙钛矿叠层太阳能电池中,将高效的钙钛矿吸收层与成熟的…...
IIC小记
SCL 时钟同步线,由主机发出。 当SCL为高电平(逻辑1)时是工作状态,低电平(逻辑0)时是休息状态。SCL可以控制通信的速度。 SDA 数据收发线 应答位:前八个工作区间是一个字节,在SCL…...
使用 ECharts 在 Vue3 中柱状图的完整配置解析
一、初始化图表实例 const chart echarts.init(chartRef.value);二、Tooltip 提示配置 tooltip: {trigger: axis,axisPointer: {type: line // 支持 line 或 shadow 类型,指示器样式},backgroundColor: rgba(0,0,0,0.7),textStyle: { color: #fff },formatter: {…...
Ubuntu实现远程文件传输
目录 安装 FileZillaUbuntu 配套设置实现文件传输 在Ubuntu系统中,实现远程文件传输的方法有多种,常见的包括使用SSH(Secure Shell)的SCP(Secure Copy Protocol)命令、SFTP(SSH File Transfer P…...
AI驱动软件工程:SoftEngine 方法论与 Lynx 平台实践分析
引言 在过去数十年中,软件开发领域历经了从瀑布模型到敏捷开发,再到DevOps的深刻变革。然而,面对当今快速变化的市场需求和复杂的软件系统,这些方法仍然显露出明显的局限性。近年来,基于大语言模型(LLM&am…...
Vue基础(一) 基础用法
1.取消生产提示 Vue.config.productionTip false; Vue.config.devtools true; //运行开发调试 2.hello小案例 需要注意如下几点: 1.必须要有一个模板,其实就是一个html组件 2.新建一个Vue实例,并且通过el与容器建立绑定关系࿰…...
文心一言开发指南08——千帆大模型平台推理服务API
版权声明 本文原创作者:谷哥的小弟作者博客地址:http://blog.csdn.net/lfdfhl 推理服务API概述 百度智能云千帆平台提供了丰富的推理服务API,包括对话Chat、续写Completions、向量Embeddings、批量预测等API能力。 对话Chat:支…...
矩阵区域和 --- 前缀和
目录 一:题目 二:算法原理 三:代码 一:题目 题目链接:1314. 矩阵区域和 - 力扣(LeetCode) 二:算法原理 三:代码 class Solution { public:vector<vector<int…...
全局id生成器生产方案
1.只要求不重复版本(常用于分布式确定一个实体的id) uuid( MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素,计算机基于这些规则生成的 UUID 是肯定不会重复的。) UUID 作…...
DES与AES算法深度解析:原理、流程与实现细节
DES与AES算法深度解析:原理、流程与实现细节 1. DES算法详解 1.1 算法架构 DES采用16轮Feistel网络结构,核心处理流程如下: 输入64位明文 → IP初始置换 → 16轮迭代处理 → 左右交换 → IP⁻末置换 → 输出64位密文 1.2 核心处理流程 …...
大厂Java面试深度解析:Dubbo服务治理、WebSocket实时通信、RESTEasy自定义注解与C3P0连接池配置实践
第一轮基础问答 面试官:请解释Dubbo服务注册发现的完整流程,以及Sentinel如何实现流量控制? xbhog:Dubbo通过Registry协议将服务地址注册到ZooKeeper,消费者订阅服务节点变更。Sentinel通过ResourceRegistry注册资源…...
【Qt】Qt换肤,使用QResource动态加载资源文件
【Qt】使用QResource动态加载资源文件 0.前言 对于简单的应用,我们可以直接读取 QSS 样式表文件来实现换肤。但一般样式里还带有图片等资源的路径,如果通过相对路径来加载,不便于管理,不过好处是替换图片方便。我们也可以使用 Q…...
五种机器学习方法深度比较与案例实现(以手写数字识别为例)
正如人们有各种各样的学习方法一样,机器学习也有多种学习方法。若按学习时所用的方法进行分类,则机器学习可分为机械式学习、指导式学习、示例学习、类比学习、解释学习等。这是温斯顿在1977年提出的一种分类方法。 有关机器学习的基本概念,…...
【18】爬虫神器 Pyppeteer 的使用
目录 一、Pyppeteer 介绍 二、安装库 三、快速上手 Python爬虫案例 | Scrape Center 在前面我们学习了 Selenium 的基本用法,它功能的确非常强大,但很多时候我们会发现 Selenium 有一些不太方便的地方,比如环境的配置,得安装好…...
封装js方法 构建树结构和扁平化树结构
在JavaScript中,构建树结构和将树结构扁平化是常见的操作。下面我将提供两个方法,一个用于从扁平化的数据中构建树结构,另一个用于将树结构扁平化。 构建树结构 假设我们有一个扁平化的数据列表,每个节点对象包含id和parentId属…...
服务器和数据库哪一个更重要
在当今数字化的时代,服务器和数据库都是构建和运行各种应用系统的关键组成部分,要说哪一个更重要,其实很难简单地给出定论。 服务器就像是一个强大的引擎,为应用程序提供了稳定的运行环境和高效的计算能力。它负责接收和处理来自…...
Nginx 核心功能与 LNMP 架构部署
一、基于授权的访问控制 1.1 功能概述 Nginx 的基于授权的访问控制通过用户名和密码验证机制,限制用户对特定资源的访问。其实现逻辑与 Apache 类似,但配置更简洁,适用于需保护敏感目录或页面的场景(如管理后台)。 …...
Python程序开发,麒麟系统模拟电脑打开文件实现
在Python开发中,模拟电脑打开文件操作(即用默认程序打开文件),可以使用os.system()方法或subprocess模块来执行系统命令。以下是使用os库实现模拟打开文件的代码示例: 使用os.system()方法 import osfile_path &quo…...
打造惊艳的渐变色下划线动画:CSS实现详解
引言:为什么需要动态下划线效果? 在现代网页设计中,微妙的交互效果可以显著提升用户体验。动态下划线特效作为一种常见的视觉反馈方式,不仅能够引导用户注意力,还能为页面增添活力。本文将深入解析如何使用纯CSS实现一…...
gitmodule怎么维护
目录 ci-cd脚本 豆包文档 ci-cd脚本 git submodule init git submodule update cd /var/lib/jenkins/workspace/wvp-server-Dji/wvp-server git checkout Dji2 cd /var/lib/jenkins/workspace/wvp-server-Dji/cloud-sdk git checkout master 豆包文档...
企业战略管理(设计与工程师类)-2-战略规划及管理过程-2-外部环境分析-PESTEL模型实践
PESTEL在AFI框架中的作用 AFI 战略框架(Analyze, Formulate, Implement——哈佛大学商学院的教授 Michael Porter)是企业战略管理中的一个重要理论模型,帮助企业系统性地分析和制定战略。 作为第一阶段Analyze的第一步,PESTEL…...
基于arduino的温湿度传感器应用
温湿度传感器深度解析与多平台开发实战 一、温湿度传感器代码实现(Arduino平台) 1. 基础传感器驱动(DHT11) #include <DHT.h> #define DHTPIN 2 #define DHTTYPE DHT11DHT dht(DHTPIN, DHTTYPE);void setup() {Serial.begin(9600);dht.begin(); }void loop() {del…...
【AI提示词】机会成本决策分析师
提示说明 具备经济学思维的决策架构师,擅长通过机会成本模型分析复杂选择场景 提示词 # Role: 机会成本决策分析师## Profile - language: 中文 - description: 具备经济学思维的决策架构师,擅长通过机会成本模型分析复杂选择场景 - background: 经济…...
基于Springboot + vue实现的列书单读书平台
项目描述 本系统包含管理员和用户两个角色。 管理员角色: 用户管理:管理系统中所有用户的信息,包括添加、删除和修改用户。 书单信息管理:管理书单信息,包括新增、查看、修改、删除和查看评论。 在线书店管理&…...
「Mac畅玩AIGC与多模态07」开发篇03 - 开发第一个 Agent 插件调用应用
一、概述 本篇介绍如何在 macOS 环境下,基于 Dify 平台自带的网页爬虫插件工具,开发一个可以提取网页内容并作答的 Agent 应用。通过使用内置插件,无需自定义开发,即可实现基本的网页信息提取与智能体回答整合。 二、环境准备 1. 确认本地部署环境 确保以下环境已搭建并…...
Headers池技术在Python爬虫反反爬中的应用
1. 引言 在当今互联网环境中,许多网站都部署了反爬虫机制,以防止数据被大规模抓取。常见的反爬手段包括: User-Agent检测(检查请求头是否来自浏览器)IP频率限制(短时间内同一IP请求过多会被封禁ÿ…...
端到端电力电子建模、仿真与控制及AI推理
在当今世界,电力电子不再仅仅是一个专业的利基领域——它几乎是每一项重大技术变革的支柱。从可再生能源到电动汽车,从工业自动化到航空航天,对电力转换领域创新的需求正以前所未有的速度增长。而这项创新的核心在于一项关键技能:…...
Java云原生+quarkus
一、Java如何实现云原生应用? 传统的 Java 框架(如 Spring Boot)虽然功能强大,但在云原生场景下可能显得笨重。以下是一些更适合云原生的轻量级框架: Quarkus(推荐) 专为云原生和 Kubernetes 设计的 Java 框架。支持…...
在yolo中Ultralytics是什么意思呢?超越分析的智能
在YOLO(You Only Look Once)目标检测框架中,Ultralytics 是一家专注于计算机视觉和机器学习技术的公司,同时也是YOLO系列模型(如YOLOv5、YOLOv8等)的官方开发和维护团队。以下是关键点解析: 1. …...
TRAE历史版本下载参考
https://lf-cdn.trae.com.cn/obj/trae-com-cn/pkg/app/releases/stable/{此处替换为版本号}/win32/Trae%20CN-Setup-x64.exe 比如版本号为1.0.11939 那么链接为https://lf-cdn.trae.com.cn/obj/trae-com-cn/pkg/app/releases/stable/1.0.11939/win32/Trae%20CN-Setup-x64.exe …...
C++类与对象基础
目录 1.取地址运算符重载 2.初始化列表 3.类型转换 既前面所讲的C类与对象知识,C类与对象——基础知识-CSDN博客 C类与对象——构造函数与析构函数-CSDN博客 C类与对象——拷贝构造与运算符重载_c拷贝对象和对象调用同一函数的输出区别怎么实现-CSDN博客本章我们…...
C# 继承详解
继承是面向对象程序设计(OOP)中的核心概念之一,它极大地增强了代码的重用性、扩展性和维护性。本篇文章将详细讲解C#中的继承机制,包括基础概念、语法特法、多重继承(通过接口实现)、继承的规则和实际应用示…...
多源数据整合与数据虚拟化:构建灵活、高效的数据架构
多源数据整合与数据虚拟化:构建灵活、高效的数据架构 引言 随着大数据时代的到来,数据的多样性和复杂性已经成为了企业面临的一大挑战。不同来源的数据在格式、结构以及存储方式上各不相同,传统的单一数据源管理方法难以应对海量且多样化的数据需求。多源数据整合与数据虚拟…...
代码随想录第39天|leetcode198.打家劫舍、leetcode213.打家劫舍II 、leetcode337.打家劫舍III
1.198. 打家劫舍 - 力扣(LeetCode) 当前房屋偷与不偷取决于前一个房屋和前两个房屋是否被偷,所以就可以得到相应的dp数组。 即,dp[i] max(dp[i-2]nums[i],dp[i-1]); int rob(vector<int>& nums) {//dp[i]:…...
C++ 如何计算两个gps 的距离
C 完全可以计算 三维空间中的 WGS84 坐标点之间的精确欧氏距离。关键是: 要先把经纬度 海拔 (lat, lon, alt) 转换成 ECEF(地心地固坐标系),然后计算欧氏距离即可。 ✅ 使用 GeographicLib::Geocentric 实现三维距离计算…...
通过全局交叉注意力机制和距离感知训练从多模态数据中识别桥本氏甲状腺炎|文献速递-深度学习医疗AI最新文献
Title 题目 Hashimoto’s thyroiditis recognition from multi-modal data via globalcross-attention and distance-aware training 通过全局交叉注意力机制和距离感知训练从多模态数据中识别桥本氏甲状腺炎 01 文献速递介绍 桥本氏甲状腺炎(HT)&a…...
网络原理—应用层和数据链路层
IP协议 ⭐IP协议报头上面的知识 地址管理 使用一套地址体系(IP协议),来描述互联网上每个是被所在的位置。 IP数据报的长度(拆包和组包) 可以对CUP进行拆包,可以多个IP报头装一个CUP数据。 8位生存时间(TTL) 这里的时间不是传统意义上的,…...
Cell Res | Stereo-seq揭示人类肝癌浸润区促进肝细胞-肿瘤细胞串扰、局部免疫抑制和肿瘤进展
有同学给了一篇23年的空间文章,研究的一个核心概念是肿瘤边缘的"侵袭区",文章中定义的是以肿瘤边缘为中心的500微米宽的区域,这里是肿瘤细胞侵袭和转移的活跃前沿,包含复杂的细胞成分及独特的分子特征,存在免…...
Mybatis-plus代码生成器的创建使用与详细解释
Mybatis-plus代码生成器的创建使用与详细解释 一、代码生成器概述 1. 定义(什么是代码生成器) 在软件开发过程中,存在大量重复性的代码编写工作,例如实体类、Mapper 接口、Service 接口及实现类等。代码生成器就是为了解决这类问题而诞生的工具。MyBa…...
swagger2升级至openapi3的利器--swagger2openapi
背景: 因为项目需要升级JDK,涉及到swagger2升级至openapi3的情况。由于swagger 2和openapi 3的语法差距太大,需要对yaml进行升级。无奈单个yaml文件的内容太大,高至4万多行,手动进行语法的转换肯定是不可能了ÿ…...
私有云与虚拟化攻防2(OpenStack渗透场景,大部分云平台都是基于此进行二次开发)
虚拟化和私有云的一些区别 虚拟化只是简单资源虚拟化,一虚多私有云除了能够实现虚拟化以外更重要的是服务自助化、自动化什么是Openstack OpenStack是一个开源的云计算管理平台项目,是属于基础设施即服务(IaaS),是一个云操作系统。 Nova(控制 ) 提供计算资源,虚拟机、容…...
前缀和 后缀和 --- 寻找数组的中心下标
题目链接 寻找数组的中心下标 给你一个整数数组 nums ,请计算数组的 中心下标 。 数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。 如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为…...
关于插值和拟合(数学建模实验课)
文章目录 1.总体评价2.具体的课堂题目 1.总体评价 学校可以开设这个数学建模实验课程,我本来是非常的激动地,但是这个最后的上课方式却让我高兴不起哦来,因为老师讲的这个内容非常的简单,而且一个上午的数学实验,基本…...
深入学习解读:《数据安全技术 数据分类分级规则》【附全文阅读】
该文详细阐述了数据安全技术的数据分类分级规则,内容分为基本原则、数据分类规则、数据分级规则及数据分类分级流程四大部分。 基本原则强调科学实用、动态更新、就高从严及53原则(虽表述不清,但可理解为多重原则的结合),同时要求边界清晰、点面结合。 数据分类规…...
Windows环境下用pyinstaller将python脚本编译为exe文件
下载 https://pypi.org/project/pyinstaller/#filespyinstaller-6.13.0-py3-none-win_arm64.whl 安装 cmd命令行中执行:pip install pyinstaller-6.13.0-py3-none-win_amd64.whl得先安装pythonpip若找不到命令,需要加到环境变量 测试 pyinstaller --ve…...