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


文章目录
- 🏳️🌈一、UdpServer.hpp 更新
- 1.1 基本结构
- 1.2 构造函数
- 1.3 开始方法 - start
- 1.4 注册服务聊天室
- 🏳️🌈二、用户类 User
- 🏳️🌈三、路由类 Route
- 3.1 新增用户 Adduser
- 3.2 删除用户 DelAddr
- 3.3 路由发送功能 Router
- 3.4 整体代码
- 🏳️🌈四、UdpServer.cpp 更新
- 🏳️🌈五、UdpClient.cpp 更新
- 5.1 发送消息
- 5.2 退出方法
- 5.3 接收消息
- 🏳️🌈六、测试代码
- 🏳️🌈七、整体代码
- 7.1 UserServer.hpp
- 7.2 UserServer.cpp
- 7.3 UserClient.hpp
- 7.4 UserClient.cpp
- 7.5 User.hpp
- 7.6 ThreadPool.hpp
- 7.7 Thread.hpp
- 7.9 Mutex.hpp
- 7.10 Log.hpp
- 7.11 Cond.hpp
- 7.12 Common.hpp
- 7.13 InetAddr.hpp
- 7.14 Makefile
- 👥总结
🏳️🌈一、UdpServer.hpp 更新
我们这次的整体思路是,利用回调函数,实现聊天室的用户增加、用户删除和消息路由
1.1 基本结构
- 就上上次字典类一样, func_t 我们需要三个回调函数,分别用来用户增加、用户删除和消息路由,不仅如此,我们可以引入线程池的概念,将消息路由作为一个个线程任务,放入线程池中
using adduser_t = std::function<void(InetAddr& id)>;
using deluser_t = std::function<void(InetAddr& id)>;using task_t = std::function<void()>;
using route_t = std::function<void(int sockfd, const std::string& msg)>;class UdpServer : public nocopy{public:UdpServer(uint16_t localport = gdefaultport): _sockfd(gsockfd),_localport(localport),_isrunning(false){}void InitServer(){}// 注册聊天室服务void RegisterService(adduser_t adduser, deluser_t deluser, route_t route){}void Start(){}~UdpServer(){}private:int _sockfd; // 文件描述符uint16_t _localport; // 端口号std::string _localip; // 本地IP地址bool _isrunning; // 运行状态adduser_t _adduser; // 添加用户回调函数deluser_t _deluser; // 删除用户回调函数route_t _route; // 路由回调函数task_t _task; // 任务回调函数
};
1.2 构造函数
- 因为我们均用回调函数,来实现方法,所以可以在函数类外面,利用lambda函数放到RegisterService中初始化各个方法,降低耦合
UdpServer(uint16_t localport = gdefaultport): _sockfd(gsockfd), _localport(localport), _isrunning(false) {}
1.3 开始方法 - start
- 接收客户端消息等方法不变,我们根据客户端的消息判断当前用户是要增还是删,分别调用不同的方法,然后将对应的消息路由给当前聊天室中的各个用户
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) {InetAddr cli(peer);inbuffer[n] = 0;std::string message;if (strcmp(inbuffer, "QUIT") == 0) {// 删除用户_deluser(cli);message = cli.AddrStr() + "# " + "退出聊天室";} else {// 新增用户_adduser(cli);message = cli.AddrStr() + "# " + inbuffer;}// 转发消息task_t task = std::bind(UdpServer::_route, _sockfd, message);ThreadPool<task_t>::getInstance()->Equeue(task);}}
}
1.4 注册服务聊天室
- 这个部分只需要获取对应的方法,然后给类对象的回调函数就行了
// 注册聊天室服务
void RegisterService(adduser_t adduser, deluser_t deluser, route_t route) {_adduser = adduser;_deluser = deluser;_route = route;
}
🏳️🌈二、用户类 User
- 既然是聊天室,那除了聊天功能,最重要的就是用户了,所以我们需要为用户创建一个类对象,所需功能不需要很多,除了标准的构造、析构函数,只需要添加发送、判断、及获取地址的功能就行了。
所以,我们可以先定义一个父类,构建虚函数,然后再子类中实现。
class UserInterface{public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string& message) = 0;virtual bool operator==(const InetAddr& user) = 0;virtual std::string Id() = 0;
};class User :public UserInterface{public:User(const InetAddr& id) : _id(id) {};void SendTo(int sockfd, const std::string& message) override{LOG(LogLevel::DEBUG) << "send message to " << _id.AddrStr() << " info: " << message;int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());(void)n;}bool operator==(const InetAddr& user) override{return _id == user;}std::string Id() override{return _id.AddrStr();}~User(){}private:InetAddr _id;
};
为什么选择使用这种父类纯虚函数,子类实现的方法?
这是因为
多态性允许不同类的对象通过同一接口表现出不同行为
- 如果未来需要支持其他类型的用户(如 AdminUser、GuestUser),只需继承 UserInterface 并实现接口,无需修改 User 的代码。
🏳️🌈三、路由类 Route
- 这个类负责 对用户的管理,增删用户,同时执行将每个人发出的消息转发给在线的所有人的功能
class Route{public:Route(){}void AddUser(InetAddr& id){}void DelUser(InetAddr& id){}void Router(int sockfd, const std::string& message){}~Route(){}private:std::list<std::shared_ptr<UserInterface>> _online_user;Mutex _mutex;
};
为什么这里我们选择使用 链表 对所有用户进行管理,而不选择其他容器?
链表适合频繁增删, 因为聊天室人员变动是很随机的,很有可能会在中间随机删掉一个用户,用 vector 等顺序链表,虽然占空间会小一些,但是删除的时间复杂度是 O(n),而链表只是 O(1)。
3.1 新增用户 Adduser
主要逻辑就是,先判断该用户是否存在,如果存在就提示“已经存在”,不然地话,就增加到管理上线用户的链表中
void AddUser(InetAddr& id) {LockGuard lockguard(_mutex);for (auto& user : _online_user) {if (*user == id) {LOG(LogLevel::INFO) << id.AddrStr() << " 已经在线";return;}}// 到这里说明 在线用户中 不存在 新增用户LOG(LogLevel::INFO) << " 新增该用户: " << id.AddrStr();_online_user.push_back(std::make_shared<User>(id));
}
为何在尾插时不能直接使用 UserInterface?
UserInterface 定义了纯虚函数(如 SendTo、operator==),因此 无法直接实例化。必须通过其子类(如 User)实现这些接口。
3.2 删除用户 DelAddr
这里我们采用
remove_if + erase的方法,将 不满足删除条件 的元素移动到容器的前端,覆盖掉需要删除的元素,返回一个迭代器 pos,指向 保留元素的新逻辑结尾(即第一个需要删除的元素的位置),删除从 pos 到容器末尾的所有元素,调整容器大小。
void DelUser(InetAddr& id) {LockGuard lockguard(_mutex);// 遍历容器,将 不满足删除条件// 的元素移动到容器的前端,覆盖掉需要删除的元素 返回一个迭代器 pos,指向// 保留元素的新逻辑结尾(即第一个需要删除的元素的位置)auto pos =std::remove_if(_online_user.begin(), _online_user.end(),[&id](const std::shared_ptr<UserInterface>& user) {return *user == id;});// 删除从 pos 到容器末尾的所有元素,调整容器大小。_online_user.erase(pos, _online_user.end());
}
3.3 路由发送功能 Router
也就是整体遍历一边在线用户,然后逐个发送就行了
void Router(int sockfd, const std::string& message) {LockGuard lockguard(_mutex);for (auto& user : _online_user) {user->SendTo(sockfd, message);}
}
3.4 整体代码
为了一会便于观察用户是否上线等,我们可以添加一个
PrintUser的方法。
class Route{public:Route(){}void AddUser(InetAddr& id){LockGuard lockguard(_mutex);for(auto& user : _online_user){if(*user == id){LOG(LogLevel::INFO) << id.AddrStr() << " 已经在线";return;}}// 到这里说明 在线用户中 不存在 新增用户LOG(LogLevel::INFO) << " 新增该用户: " << id.AddrStr();_online_user.push_back(std::make_shared<User>(id));PrintUsers();}void DelUser(InetAddr& id){LockGuard lockguard(_mutex);// 遍历容器,将 不满足删除条件 的元素移动到容器的前端,覆盖掉需要删除的元素// 返回一个迭代器 pos,指向 保留元素的新逻辑结尾(即第一个需要删除的元素的位置)auto pos = std::remove_if(_online_user.begin(), _online_user.end(),[&id](const std::shared_ptr<UserInterface>& user){return *user == id;});// 删除从 pos 到容器末尾的所有元素,调整容器大小。_online_user.erase(pos, _online_user.end());PrintUsers();}void Router(int sockfd, const std::string& message){LockGuard lockguard(_mutex);for(auto& user : _online_user){user->SendTo(sockfd, message);}}void PrintUsers(){for(auto& user : _online_user){LOG(LogLevel::DEBUG) << "online user: " << user->Id();}}~Route(){}private:std::list<std::shared_ptr<UserInterface>> _online_user;Mutex _mutex;
};
🏳️🌈四、UdpServer.cpp 更新
因为我们在服务端头文件中设置了线程池,需要将相应的处理函数,绑定到 RegisterService 方法中,所以我们可以先用智能指针创建路由方法对象,然后将对应的方法绑定到 RegisterService 中
#include "UdpServer.hpp"
#include "User.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::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);std::shared_ptr<Route> route = std::make_shared<Route>();usvr->RegisterService([&route](InetAddr& id){route->AddUser(id);},[&route](InetAddr& id){route->DelUser(id);},[&route](int sockfd, const std::string& msg){route->Router(sockfd, msg);});usvr->InitServer(); // 初始化服务端usvr->Start(); // 启动服务端return 0;
}
🏳️🌈五、UdpClient.cpp 更新
5.1 发送消息
这部分完全不变,就是先创建套接字,然后利用sendto方法将消息发给服务端,至于后面是增、删还是消息路由就不用管了
5.2 退出方法
- 我们前面说过我们在遇到 QUIT 的时候会退出,我们可以将 crtl + c 即 2信号与退出方法连接起来,当输入 crtl + c 命令后,会退出。
int main(int argc, char* argv[]){// ...// 注册信号退出函数// 将信号 2 注册到 ClientQuit 函数// 信号2 是 SIGINT,表示 Ctrl-Csignal(2, ClientQuit);// ...return 0;
}
5.3 接收消息
- 因为我们这里是聊天室,所以客户端除了需要实现能够不断地发送消息,还需要做到接收消息,但不阻塞发送消息地方法。用户希望随时输入消息并立即看到其他人的回复,若接收操作阻塞主线程,需等待接收完成才能输入。
因此,我们可以利用接收线程持续监听服务端消息,主线程处理用户输入,两者并行不相互阻塞。
int sockfd = -1;
struct sockaddr_in server;void ClientQuit(int signo){(void)signo;const std::string msg = "QUIT";int n = ::sendto(sockfd, msg.c_str(), msg.size(), 0, CONV(&server), sizeof(server));
}int main(int argc, char* argv[]){if(argc != 3){std::cerr << argv[0] << " serverip server" << std::endl;Die(USAGE_ERR);}// 注册信号退出函数// 将信号 2 注册到 ClientQuit 函数// 信号2 是 SIGINT,表示 Ctrl-Csignal(2, ClientQuit);std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){std::cerr << "create socket error" << std::endl;Die(SOCKET_ERR);}// 1.1 填充 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());// 1.2 创建线程pthread_t tid;pthread_create(&tid, nullptr, Recver, nullptr);// 1.3 启动的时候,给服务器推送消息const std::string msg = "... 来了";int n = ::sendto(sockfd, msg.c_str(), msg.size(), 0, CONV(&server), sizeof(server));(void)n;// 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;}return 0;
}
🏳️🌈六、测试代码
- 我们运行端口号为 8080 的服务端
- 在root账号下运行第一个客户,显示 48627来了
- 再在我个人的账户上运行第二个客户,显示 42772来了

- 我们在root下输入
111,成功路由到我的个人账户上

- 当我们将个人账户给退出后,服务端会显示 退出聊天室,然后回显在线用户
- 再尝试在 root 账户下发送 111,只有root自己会收到消息

🏳️🌈七、整体代码
7.1 UserServer.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"
#include "ThreadPool.hpp"using namespace LogModule;
using namespace ThreadPoolModule;const static int gsockfd = -1;
const static std::string gdefaultip = "127.0.0.1"; // 表示本地主机
const static uint16_t gdefaultport = 8080;using adduser_t = std::function<void(InetAddr& id)>;
using deluser_t = std::function<void(InetAddr& id)>;using task_t = std::function<void()>;
using route_t = std::function<void(int sockfd, const std::string& msg)>;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): _sockfd(gsockfd),_localport(localport),_isrunning(false){}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 RegisterService(adduser_t adduser, deluser_t deluser, route_t route){_adduser = adduser;_deluser = deluser;_route = route;}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){InetAddr cli(peer);inbuffer[n] = 0;std::string message;if(strcmp(inbuffer, "QUIT") == 0){// 删除用户_deluser(cli);message = cli.AddrStr() + "# " + "退出聊天室";}else{// 新增用户_adduser(cli);message = cli.AddrStr() + "# " + inbuffer;}// 转发消息task_t task = std::bind(UdpServer::_route, _sockfd, message);ThreadPool<task_t>::getInstance()->Equeue(task);}}}~UdpServer(){// 判断 _sockfd 是否是一个有效的套接字文件描述符// 有效的文件描述符(如套接字、打开的文件等)是非负整数(>= 0)if(_sockfd > -1) ::close(_sockfd);}private:int _sockfd; // 文件描述符uint16_t _localport; // 端口号std::string _localip; // 本地IP地址bool _isrunning; // 运行状态adduser_t _adduser; // 添加用户回调函数deluser_t _deluser; // 删除用户回调函数route_t _route; // 路由回调函数task_t _task; // 任务回调函数
};
7.2 UserServer.cpp
#include "UdpServer.hpp"
#include "User.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::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port);std::shared_ptr<Route> route = std::make_shared<Route>();usvr->RegisterService([&route](InetAddr& id){route->AddUser(id);},[&route](InetAddr& id){route->DelUser(id);},[&route](int sockfd, const std::string& msg){route->Router(sockfd, msg);});usvr->InitServer(); // 初始化服务端usvr->Start(); // 启动服务端return 0;
}
7.3 UserClient.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>
#include <signal.h>
7.4 UserClient.cpp
#include "UdpClient.hpp"int sockfd = -1;
struct sockaddr_in server;void ClientQuit(int signo){(void)signo;const std::string msg = "QUIT";int n = ::sendto(sockfd, msg.c_str(), msg.size(), 0, CONV(&server), sizeof(server));exit(0);
}// int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
// POSIX线程库的设计规范要求线程函数必须遵循特定的函数签名
// void *(*start_routine)(void *)
// 线程入口函数必须满足 void *(*)(void *) 的签名,即:
// 接受一个 void* 参数。
// 返回一个 void* 值。
void* Recver(void* args){while(true){(void)args; // 如果没有使用这个参数,会报错struct sockaddr_in server;socklen_t len = sizeof(server);char buffer[1024];int n = ::recvfrom(sockfd, buffer,sizeof(buffer) - 1, 0, CONV(&server), &len);if(n > 0){buffer[n] = 0;std::cerr << buffer << std::endl;}}
}int main(int argc, char* argv[]){if(argc != 3){std::cerr << argv[0] << " serverip server" << std::endl;Die(USAGE_ERR);}// 注册信号退出函数// 将信号 2 注册到 ClientQuit 函数// 信号2 是 SIGINT,表示 Ctrl-Csignal(2, ClientQuit);std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);// 1. 创建套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if(sockfd < 0){std::cerr << "create socket error" << std::endl;Die(SOCKET_ERR);}// 1.1 填充 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());// 1.2 创建线程pthread_t tid;pthread_create(&tid, nullptr, Recver, nullptr);pthread_detach(tid);// 1.3 启动的时候,给服务器推送消息const std::string msg = "... 来了";int n = ::sendto(sockfd, msg.c_str(), msg.size(), 0, CONV(&server), sizeof(server));(void)n;// 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;}return 0;
}
7.5 User.hpp
#pragma once#include <iostream>
#include <string>
#include <list>
#include <memory>
#include <algorithm>
#include <sys/types.h>
#include <sys/socket.h>#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"// using namespace LockModule;
using namespace LogModule;using task_t = std::function<void()>;class UserInterface{public:virtual ~UserInterface() = default;virtual void SendTo(int sockfd, const std::string& message) = 0;virtual bool operator==(const InetAddr& user) = 0;virtual std::string Id() = 0;
};class User :public UserInterface{public:User(const InetAddr& id) : _id(id) {};void SendTo(int sockfd, const std::string& message) override{LOG(LogLevel::DEBUG) << "send message to " << _id.AddrStr() << " info: " << message;int n = ::sendto(sockfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());(void)n;}bool operator==(const InetAddr& user) override{return _id == user;}std::string Id() override{return _id.AddrStr();}~User(){}private:InetAddr _id;
};class Route{public:Route(){}void AddUser(InetAddr& id){LockModule::LockGuard lockguard(_mutex);for(auto& user : _online_user){if(*user == id){LOG(LogLevel::INFO) << id.AddrStr() << " 已经在线";return;}}// 到这里说明 在线用户中 不存在 新增用户LOG(LogLevel::INFO) << " 新增该用户: " << id.AddrStr();_online_user.push_back(std::make_shared<User>(id));PrintUsers();}void DelUser(InetAddr& id){LockModule::LockGuard lockguard(_mutex);// 遍历容器,将 不满足删除条件 的元素移动到容器的前端,覆盖掉需要删除的元素// 返回一个迭代器 pos,指向 保留元素的新逻辑结尾(即第一个需要删除的元素的位置)auto pos = std::remove_if(_online_user.begin(), _online_user.end(),[&id](const std::shared_ptr<UserInterface>& user){return *user == id;});// 删除从 pos 到容器末尾的所有元素,调整容器大小。_online_user.erase(pos, _online_user.end());PrintUsers();}void Router(int sockfd, const std::string& message){LockModule::LockGuard lockguard(_mutex);for(auto& user : _online_user){user->SendTo(sockfd, message);}}void PrintUsers(){for(auto& user : _online_user){LOG(LogLevel::DEBUG) << "在线用户: " << user->Id();}}~Route(){}private:std::list<std::shared_ptr<UserInterface>> _online_user;LockModule::Mutex _mutex;
};
7.6 ThreadPool.hpp
#pragma once#include <iostream>
#include <string>
#include <queue>
#include <vector>
#include <memory>
#include "Log.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"
#include "Thread.hpp"namespace ThreadPoolModule
{using namespace LogModule;using namespace ThreadModule;using namespace LockModule;using namespace CondModule;// 用来做测试的线程方法void DefaultTest(){while (true){LOG(LogLevel::DEBUG) << "我是一个测试方法";sleep(1);}}using thread_t = std::shared_ptr<Thread>;const static int defaultnum = 5;template <typename T>class ThreadPool{private:bool IsEmpty() { return _taskq.empty(); }void HandlerTask(std::string name){LOG(LogLevel::INFO) << "线程: " << name << ", 进入HandlerTask的逻辑";while (true){// 1. 拿任务T t;{LockGuard lockguard(_lock);while (IsEmpty() && _isrunning){_wait_num++;_cond.Wait(_lock);_wait_num--;}// 2. 任务队列为空 && 线程池退出了if (IsEmpty() && !_isrunning)break;t = _taskq.front();_taskq.pop();}// 2. 处理任务t(); // 规定,未来所有的任务处理,全部都是必须提供()方法!}LOG(LogLevel::INFO) << "线程: " << name << " 退出";}ThreadPool(const ThreadPool<T> &) = delete;ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;ThreadPool(int num = defaultnum) : _num(num), _wait_num(0), _isrunning(false){for (int i = 0; i < _num; i++){_threads.push_back(std::make_shared<Thread>(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1)));LOG(LogLevel::INFO) << "构建线程" << _threads.back()->Name() << "对象 ... 成功";}}public:static ThreadPool<T> *getInstance(){if (instance == NULL){LockGuard lockguard(mutex);if (instance == NULL){LOG(LogLevel::INFO) << "单例首次被执行,需要加载对象...";instance = new ThreadPool<T>();instance->Start();}}return instance;}void Equeue(T &in){LockGuard lockguard(_lock);if (!_isrunning)return;// _taskq.push(std::move(in));_taskq.push(in);if (_wait_num > 0)_cond.Notify();}void Start(){if (_isrunning)return;_isrunning = true; // bug fix??for (auto &thread_ptr : _threads){LOG(LogLevel::INFO) << "启动线程" << thread_ptr->Name() << " ... 成功";thread_ptr->Start();}}void Wait(){for (auto &thread_ptr : _threads){thread_ptr->Join();LOG(LogLevel::INFO) << "回收线程" << thread_ptr->Name() << " ... 成功";}}void Stop(){LockGuard lockguard(_lock);if (_isrunning){// 3. 不能在入任务了_isrunning = false; // 不工作// 1. 让线程自己退出(要唤醒) && // 2. 历史的任务被处理完了if (_wait_num > 0)_cond.NotifyAll();}}~ThreadPool(){}private:std::vector<thread_t> _threads;int _num;int _wait_num;std::queue<T> _taskq; // 临界资源Mutex _lock;Cond _cond;bool _isrunning;static ThreadPool<T> *instance;static Mutex mutex; // 只用来保护单例};template <typename T>ThreadPool<T> *ThreadPool<T>::instance = NULL;template <typename T>Mutex ThreadPool<T>::mutex; // 只用来保护单例
}
7.7 Thread.hpp
#ifndef _THREAD_HPP__
#define _THREAD_HPP__#include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
#include <sys/types.h>
#include <unistd.h>// v1
namespace ThreadModule
{using func_t = std::function<void(std::string name)>;static int number = 1;enum class TSTATUS{NEW,RUNNING,STOP};class Thread{private:// 成员方法!static void *Routine(void *args){Thread *t = static_cast<Thread *>(args);t->_status = TSTATUS::RUNNING;t->_func(t->Name());return nullptr;}void EnableDetach() { _joinable = false; }public:Thread(func_t func) : _func(func), _status(TSTATUS::NEW), _joinable(true){_name = "Thread-" + std::to_string(number++);_pid = getpid();}bool Start(){if (_status != TSTATUS::RUNNING){int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODOif (n != 0)return false;return true;}return false;}bool Stop(){if (_status == TSTATUS::RUNNING){int n = ::pthread_cancel(_tid);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}bool Join(){if (_joinable){int n = ::pthread_join(_tid, nullptr);if (n != 0)return false;_status = TSTATUS::STOP;return true;}return false;}void Detach(){EnableDetach();pthread_detach(_tid);}bool IsJoinable() { return _joinable; }std::string Name() {return _name;}~Thread(){}private:std::string _name;pthread_t _tid;pid_t _pid;bool _joinable; // 是否是分离的,默认不是func_t _func;TSTATUS _status;};
}// v2
// namespace ThreadModule
// {
// static int number = 1;
// enum class TSTATUS
// {
// NEW,
// RUNNING,
// STOP
// };// template <typename T>
// class Thread
// {
// using func_t = std::function<void(T)>;
// private:
// // 成员方法!
// static void *Routine(void *args)
// {
// Thread<T> *t = static_cast<Thread<T> *>(args);
// t->_status = TSTATUS::RUNNING;
// t->_func(t->_data);
// return nullptr;
// }
// void EnableDetach() { _joinable = false; }// public:
// Thread(func_t func, T data) : _func(func), _data(data), _status(TSTATUS::NEW), _joinable(true)
// {
// _name = "Thread-" + std::to_string(number++);
// _pid = getpid();
// }
// bool Start()
// {
// if (_status != TSTATUS::RUNNING)
// {
// int n = ::pthread_create(&_tid, nullptr, Routine, this); // TODO
// if (n != 0)
// return false;
// return true;
// }
// return false;
// }
// bool Stop()
// {
// if (_status == TSTATUS::RUNNING)
// {
// int n = ::pthread_cancel(_tid);
// if (n != 0)
// return false;
// _status = TSTATUS::STOP;
// return true;
// }
// return false;
// }
// bool Join()
// {
// if (_joinable)
// {
// int n = ::pthread_join(_tid, nullptr);
// if (n != 0)
// return false;
// _status = TSTATUS::STOP;
// return true;
// }
// return false;
// }
// void Detach()
// {
// EnableDetach();
// pthread_detach(_tid);
// }
// bool IsJoinable() { return _joinable; }
// std::string Name() { return _name; }
// ~Thread()
// {
// }// private:
// std::string _name;
// pthread_t _tid;
// pid_t _pid;
// bool _joinable; // 是否是分离的,默认不是
// func_t _func;
// TSTATUS _status;
// T _data;
// };
// }#endif
7.9 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;};
}
7.10 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()
}
7.11 Cond.hpp
#pragma once#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"namespace CondModule
{using namespace LockModule;class Cond{public:Cond(){int n = ::pthread_cond_init(&_cond, nullptr);(void)n;}void Wait(Mutex &lock) // 让我们的线程释放曾经持有的锁!{int n = ::pthread_cond_wait(&_cond, lock.LockPtr());}void Notify(){int n = ::pthread_cond_signal(&_cond);(void)n;}void NotifyAll(){int n = ::pthread_cond_broadcast(&_cond);(void)n;}~Cond(){int n = ::pthread_cond_destroy(&_cond);}private:pthread_cond_t _cond;};
}
7.12 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
};
7.13 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;_ip = ipbuffer;}public:InetAddr() {}// 通过网络字节序地址构造主机字节序地址InetAddr(const struct sockaddr_in &addr) : _net_addr(addr){PortNet2Host();IpNet2Host();}bool operator==(const InetAddr& addr){return _ip == addr._ip && _port == addr._port;}// 创建一个绑定到指定端口(主机字节序)的 IPv4 地址对象,默认监听所有本地网络接口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); }// 主机字节序 ip 地址std::string Ip() { return _ip; }// 主机字节序端口号uint16_t Port() { return _port; }// 字符串形式的主机字节序地址 IP + 端口号std::string AddrStr() { return _ip + ":" + std::to_string(_port); }// 析构~InetAddr(){}private:struct sockaddr_in _net_addr;std::string _ip;uint16_t _port;
};
7.14 Makefile
.PHONY: all
all:server_udp client_udpserver_udp:UdpServer.cppg++ -o $@ $^ -std=c++17 -lpthreadclient_udp:UdpClient.cpp g++ -o $@ $^ -std=c++17 -lpthread.PHONY: clean
clean:rm -f server_udp client_udp
👥总结
本篇博文对 【Linux网络】构建基于UDP的简单聊天室系统 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~
相关文章:
【Linux网络】构建基于UDP的简单聊天室系统
📢博客主页:https://blog.csdn.net/2301_779549673 📢博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson 📢欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正! &…...
【每天一个知识点】大模型的幻觉问题
“大模型的幻觉问题”是指大语言模型(如GPT系列、BERT衍生模型等)在生成内容时,产生不符合事实或逻辑的虚假信息,即所谓的“幻觉”(hallucination)。这在诸如问答、摘要、翻译、代码生成等任务中尤其常见。…...
机器学习06-RNN
RNN(循环神经网络)学习笔记 一、RNN 概述 循环神经网络(Recurrent Neural Network,RNN)是一类以序列数据为输入,在序列的演进方向进行递归且所有节点(循环单元)按链式连接的递归神…...
[大模型]什么是function calling?
什么是function calling? 大模型的 Function Calling(函数调用)是一种让大语言模型(如 GPT、Claude 等)与外部工具、API 或自定义函数交互的机制。 它的核心目的是让模型能够根据用户的需求,…...
C语言高频面试题——嵌入式系统中中断服务程序
在嵌入式系统中,中断服务程序(ISR)的设计需遵循严格的规则以确保系统稳定性和实时性。以下是对这段代码的分析及改进建议: 代码分析 __interrupt double compute_area (double radius) { double area PI * radius * radius; pri…...
Java高频面试之并发编程-05
hello啊,各位观众姥爷们!!!本baby今天来报道了!哈哈哈哈哈嗝🐶 面试官:线程有哪些调度方法? 在Java中,线程的调用方法主要包括以下几种方式,每种方式适用于…...
野外价值观:在真实世界的语言模型互动中发现并分析价值观
每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗?订阅我们的简报,深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同,从行业内部的深度分析和实用指南中受益。不要错过这个机会,成为AI领…...
【Linux】47.高级IO(1)
文章目录 1. 高级IO1.1 五种IO模型1.2 高级IO重要概念1.2.1 同步通信 vs 异步通信1.2.2 阻塞 vs 非阻塞 1.3非阻塞IO1.3.1 fcntl1.3.2 实现函数SetNoBlock1.3.3 轮询方式读取标准输入1.3.4 I/O多路转接之select1.3.4.1 初识select:1.3.4.2 select函数原型1.3.4.3 理…...
notepad++技巧:查找和替换:扩展 or 正则表达式
notepad 有很多优点:多标签,代码高亮,我最喜欢的是查找和替换。 除了可以一次性查找所有打开文件,还可以使用 扩展 or 正则表达式。 例如: 去掉空行:正则表达式: ^\s*$\r\n ^ 表示行首。\s*…...
【图像标注技巧】目标检测图像标注技巧
介绍一些图像标注技巧。之前引用过别人的文章 yolo目标检测 技巧 trick 提升模型性能,deep research检测调研报告也可以进行参考。 拉框类的标注,如果你不确定哪种方法好,你可以把所标注区域的都剪切出来,然后站在屏幕一米之外眯…...
MuJoCo中的机器人状态获取
UR5e机器人xml文件模型 <mujoco model"ur5e"><compiler angle"radian" meshdir"assets" autolimits"true"/><option integrator"implicitfast"/><default><default class"ur5e">&…...
pnpm解决幽灵依赖问题
文章目录 前言1. npm/yarn 现在还有幽灵依赖问题吗?2. pnpm 解决了幽灵依赖问题吗?3. pnpm 是如何解决的?举例说明 1. pnpm 的 node_modules 结构原理结构示意 2. 实际演示幽灵依赖的杜绝步骤1:初始化项目并安装依赖步骤2…...
测试第四课---------性能测试工具
作者前言 🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂 🎂 作者介绍: 🎂🎂 🎂 🎉🎉🎉…...
frp远程穿透配置
文章目录 准备工作服务端配置(toml)客户端配置(toml)访问内网服务使用ini文件配置 frp是一个高性能的反向代理应用,用于将位于内网的服务通过代理暴露到公网。以下是其基本使用步骤: 准备工作 拥有一台具有公网IP的服务器,作为frp的服务端。…...
【C++】新手入门指南(下)
文章目录 前言 一、引用 1.引用的概念和定义 2.引用的特性 3.引用的使用 4.const引用 5.指针和引用的关系 二、内联函数 三、nullptr 总结 前言 这篇续上篇的内容新手入门指南(上),继续带大家学习新知识。如果你感兴趣欢迎订购本专栏。 一、…...
Linux系统编程 day9 SIGCHLD and 线程
SIGCHLD信号 只要子进程信号发生改变,就会产生SIGCHLD信号。 借助SIGCHLD信号回收子进程 回收子进程只跟父进程有关。如果不使用循环回收多个子进程,会产生多个僵尸进程,原因是因为这个信号不会循环等待。 #include<stdio.h> #incl…...
前后端分离项目在未部署条件下如何跨设备通信
其实我此前也不知道这个问题怎么解决,也没有想过—因为做的项目大部分都是前后端分离的,前端直接用后端的部署好的环境就行了。最近也是有点心高气傲开始独立开发,一个人又写前端又写后端也是蛮累的,即使有强有力的cursor也很累很…...
基于Python的多光谱遥感数据处理与分类技术实践—以农作物分类与NDVI评估为例
多光谱遥感数据包含可见光至红外波段的光谱信息,Python凭借其丰富的科学计算库(如rasterio、scikit-learn、GDAL),已成为处理此类数据的核心工具。本文以Landsat-8数据为例,演示辐射校正→特征提取→监督分类→精度评…...
vscode python 代码无法函数跳转的问题
TL; DR; python.languageServer 配置成了 None 导致 vscode python 代码无法函数跳转 详细信息 mac 环境下 vscode 正常 command 鼠标左键 可以跳转到定义或者使用位置,但是我的为何不知道失效了 我一开始以为是热键冲突,结果发现 mac 好像没办法定…...
SAS宏核心知识与实战应用
1. SAS宏基础 1.1 核心概念 1.1.1 宏处理器 宏处理器在SAS程序运行前执行,用于生成动态代码,可实现代码的灵活定制。 通过宏处理器,可基于输入参数动态生成不同的SAS代码,提高代码复用性。 1.1.2 宏变量 宏变量是存储文本值的容器,用&符号引用,如&var,用于存储…...
Unity 脚本使用(二)——UnityEngine.AI——NavMesh
描述 Singleton class 用于访问被烘培好的 NavMesh. 使用NavMesh类可以执行空间查询(spatial queries),例如路径查找和可步行性测试。此类还允许您设置特定区域类型的寻路成本,并调整寻路和避免的全局行为。 静态属性࿰…...
从项目真实场景中理解二分算法的细节(附图解和模板)
遇到一个真实场景里使用二分算法的问题,本以为可以放心交给小师弟去做,结果出现了各种问题,在此梳理下二分算法的核心思想和使用细节。 文章目录 1.场景描述2.场景分析3.二分算法的精髓3.1 核心模板3.2 二分过程图解3.3 各种区间写法3.3.1 闭…...
金融图QCPFinancial
QCPFinancial 是 QCustomPlot 中用于绘制金融图表(如蜡烛图/K线图)的核心类。以下是其关键特性的详细说明: 一、主要属性 属性类型说明dataQSharedPointer<QCPFinancialDataContainer>存储金融数据的数据容器chartStyleQCPFinancial:…...
Jetson Orin NX 16G 配置GO1强化学习运行环境
这一次收到了Jrtson Orin NX, 可以进行部署了。上一次在nano上的失败经验 Jetson nano配置Docker和torch运行环境_jetson docker-CSDN博客 本次的目的是配置cuda-torch-python38环境离机运行策略。 Jetson Orin NX SUPER 1. 烧录镜像 参考链接在ubuntu系统中安装sdk manag…...
文档管理 Document Management
以下是关于项目管理中 文档管理 的深度解析,结合高项(如软考高级信息系统项目管理师)教材内容,系统阐述文档管理的理论框架、核心流程及实战应用: 一、文档管理的基本概念 1. 定义 文档管理是对项目全生命周期中产生的各类文档进行规范化管理的过程,包括创建、存储、版…...
【Pandas】pandas DataFrame truediv
Pandas2.2 DataFrame Binary operator functions 方法描述DataFrame.add(other)用于执行 DataFrame 与另一个对象(如 DataFrame、Series 或标量)的逐元素加法操作DataFrame.add(other[, axis, level, fill_value])用于执行 DataFrame 与另一个对象&…...
Linux 内核中 cgroup 子系统 cpuset 是什么?
cpuset 是 Linux 内核中 cgroup(控制组) 的一个子系统,用于将一组进程(或任务)绑定到特定的 CPU 核心和 内存节点(NUMA 节点)上运行。它通过限制进程的 CPU 和内存资源的使用范围,优…...
Windows 同步-互锁变量访问
互锁变量访问 应用程序必须同步对多个线程共享的变量的访问。 应用程序还必须确保对这些变量的作以原子方式执行(完全或根本不执行)。 对正确对齐的 32 位变量的简单读取和写入是原子作。 换句话说,你最终不会只更新变量的一部分;所有位都以…...
深度学习3.5 图像分类数据集
%matplotlib inline import torch import torchvision from torch.utils import data from torchvision import transforms from d2l import torch as d2l代码执行流程图 #mermaid-svg-WWhBmQvijswiICpI {font-family:"trebuchet ms",verdana,arial,sans-serif;font-…...
js原型链prototype解释
function Person(){} var personnew Person() console.log(啊啊,Person instanceof Function);//true console.log(,Person.__proto__Function.prototype);//true console.log(,Person.prototype.__proto__ Object.prototype);//true console.log(,Function.prototype.__prot…...
