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

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

🏳️‍🌈六、测试代码

  1. 我们运行端口号为 8080 的服务端
  2. 在root账号下运行第一个客户,显示 48627来了
  3. 再在我个人的账户上运行第二个客户,显示 42772来了

在这里插入图片描述

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

在这里插入图片描述

  1. 当我们将个人账户给退出后,服务端会显示 退出聊天室,然后回显在线用户
  2. 再尝试在 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的简单聊天室系统

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客仓库&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &…...

【微知】git reset --soft --hard以及不加的区别?

背景 在 Git 里&#xff0c;git reset 是用来将当前的 HEAD 复位到指定状态的命令。--soft、--hard 是它的两个常用选项&#xff0c;本文简单介绍他们的区别&#xff0c;以及不添加选项时的默认情况。 在 Git 里&#xff0c;HEAD 是一个重要的引用&#xff0c;它指向当前所在的…...

制作一个简单的操作系统7

实模式下到保护模式下并打印 运行效果: 完整代码: 【免费】制作一个简单的操作系统7的源代码资源-CSDN文库https://download.csdn.net/download/farsight_2098/90670296 从零开始写操作系统引导程序:实模式到保护模式的跨越 引言 操作系统的启动过程是计算机系统中最神…...

2025企微CRM系统功能对比:会话存档、客户画像与数据分析如何重构客户运营?

一、企微CRM管理系统&#xff1a;从“连接工具”到“智能中枢” 随着企业微信生态的成熟&#xff0c;企微CRM管理软件已从简单的客户沟通渠道&#xff0c;升级为融合数据、策略与服务的核心平台。2025年&#xff0c;企业对企微CRM系统的需求聚焦于三大能力&#xff1a;会话存档…...

【JAVA】十三、基础知识“接口”精细讲解!(二)(新手友好版~)

哈喽大家好呀qvq&#xff0c;这里是乎里陈&#xff0c;接口这一知识点博主分为三篇博客为大家进行讲解&#xff0c;今天为大家讲解第二篇java中实现多个接口&#xff0c;接口间的继承&#xff0c;抽象类和接口的区别知识点&#xff0c;更适合新手宝宝们阅读~更多内容持续更新中…...

小白工具视频转MPG, 功能丰富齐全,无需下载软件,在线使用,超实用

在视频格式转换需求日益多样的今天&#xff0c;小白工具网的在线视频转 MPG 功能https://www.xiaobaitool.net/videos/convert-to-mpg/ &#xff09;脱颖而出&#xff0c;凭借其出色特性&#xff0c;成为众多用户处理视频格式转换的优质选择。 从格式兼容性来看&#xff0c;它支…...

#define RFOREACH(var, arr) for (ARR2IDX(arr) var=(arr).size(); var-->0; )

这个宏的定义&#xff1a; #define RFOREACH(var, arr) for (ARR2IDX(arr) var (arr).size(); var-- > 0; )是用来 反向遍历一个容器&#xff08;比如 vector&#xff09; 的&#xff0c;非常紧凑而且聪明的写法。 逐步解释一下&#xff1a; 假设你有一个容器&#xff0c…...

MYSQL—两阶段提交

binlog 和 redo log&#xff1a; 有binlog了为什么还要有redo log&#xff1a; 历史原因&#xff0c;MyISAM不支持崩溃恢复&#xff0c;而InnoDB在加入MySQL前就已经支持崩溃恢复了InnoDB使用的是WAL技术&#xff0c;事务提交后&#xff0c;写完内存和日志&#xff0c;就算事…...

Qt之moveToThread

文章目录 前言一、基本概念1.1 什么是线程亲和性&#xff1f;1.2 moveToThread 的作用 二、使用场景三、使用方法四、使用示例五、注意事项六、常见问题总结 前言 moveToThread 是 Qt 中用于管理对象线程亲和性&#xff08;Thread Affinity&#xff09;的核心方法。它的作用是…...

Nacos 2.0.2 在 CentOS 7 上开启权限认证(含 Docker Compose 配置与接口示例)

介绍如何在 Nacos 2.0.2 CentOS 7 环境中开启权限认证&#xff0c;包括 解压部署 和 Docker Compose 部署 两种方式&#xff0c;提供客户端 Spring Boot 项目的接入配置和nacos接口验证示例。 环境说明 操作系统&#xff1a;CentOS 7Nacos 版本&#xff1a;2.0.2部署方式&…...

Oracle--SQL事务操作与管理流程

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 数据库系统的并发控制以事务为单位进行&#xff0c;通过内部锁定机制限制事务对共享资源的访问&#xff0c;确保数据并行性和一致性。事务是由一系列语…...

Qt -对象树

博客主页&#xff1a;【夜泉_ly】 本文专栏&#xff1a;【暂无】 欢迎点赞&#x1f44d;收藏⭐关注❤️ 目录 前言构造QObject::QObjectQObjectPrivate::setParent_helper 析构提醒 #mermaid-svg-FTUpJmKG24FY3dZY {font-family:"trebuchet ms",verdana,arial,sans-s…...

Unity 带碰撞的粒子效果

碰撞效果&#xff1a;粒子接触角色碰撞体弹起&#xff0c;粒子接触地面弹起。 粒子效果&#xff1a;粒子自行加速度下落&#xff0c;并且在接触碰撞体弹起时产生一个小的旋转。 *注意使用此效果时&#xff0c;自行判断是否需要调整碰撞层级。 以下为角色身高为1.7m时&#x…...

扩散模型(Diffusion Models)

扩散模型&#xff08;Diffusion Models&#xff09;是近年来在生成式人工智能领域崛起的一种重要方法&#xff0c;尤其在图像、音频和视频生成任务中表现突出。其核心思想是通过逐步添加和去除噪声的过程来学习数据分布&#xff0c;从而生成高质量样本。 ​​核心原理​​ 扩散…...

JSP服务器端表单验证

JSP服务器端表单验证 一、引言 在Web开发中&#xff0c;表单验证是保障数据合法性的重要环节。《Web编程技术》第五次实验要求&#xff0c;详细讲解如何基于JSP内置对象实现服务器端表单验证&#xff0c;包括表单设计、验证逻辑、交互反馈等核心功能。最终实现&#xff1a;输…...

Anaconda、conda和PyCharm在Python开发中各自扮演的角色

Anaconda、conda和PyCharm在Python开发中各自扮演不同角色&#xff0c;它们的核心用处、区别及相互关系如下&#xff1a; 一、Anaconda与conda的用处及区别 1. Anaconda - 定义&#xff1a;Anaconda是一个开源的Python和R语言发行版&#xff0c;专为数据科学、机器学习等场景…...

【数据结构 · 初阶】- 堆的实现

目录 一.初始化 二.插入 三.删除&#xff08;堆顶、根&#xff09; 四.整体代码 Heap.h Test.c Heap.c 我们使用顺序结构实现完全二叉树&#xff0c;也就是堆的实现 以前学的数据结构只是单纯的存储数据。堆除了存储数据&#xff0c;还有其他的价值——排序。是一个功能…...

Ubuntu与OpenHarmony OS 5.0显示系统架构比较

1. 总体架构对比 1.1 Ubuntu显示架构 Ubuntu采用传统Linux显示栈架构&#xff0c;自顶向下可分为&#xff1a; 应用层&#xff1a;GNOME桌面环境和应用程序显示服务器层&#xff1a;X11或Wayland图形栈中间层&#xff1a;Mesa, DRM/KMS硬件层&#xff1a;GPU驱动和硬件 1.2 …...

一键配置多用户VNC远程桌面:自动化脚本详解

在当今远程工作盛行的时代,高效且安全地管理多用户远程桌面访问变得至关重要。本文将介绍一个强大的自动化脚本,该脚本能够快速创建用户并配置VNC远程桌面环境,大大简化了系统管理员的工作。 一、背景介绍 在Linux系统中,手动配置VNC服务器通常需要执行多个步骤,包括创建…...

Qt进阶开发:鼠标及键盘事件

文章目录 一、Qt中事件的概念二、Qt中事件处理方式三、重新实现部件的事件处理函数3.1 常用事件处理函数3.2 自定义控件处理鼠标和绘图事件3.3 常用事件处理函数说明四、重写notify()函数五、QApplication对象上安装事件过滤器六、重写event()事件七、在对象上安装事件过滤器八…...

鸿蒙生态新利器:华为ArkUI-X混合开发框架深度解析

鸿蒙生态新利器&#xff1a;华为ArkUI-X混合开发框架深度解析 作者&#xff1a;王老汉 | 鸿蒙生态开发者 | 2025年4月 &#x1f4e2; 前言&#xff1a;开发者们的新机遇 各位鸿蒙开发者朋友们&#xff0c;是否还在为多平台开发重复造轮子而苦恼&#xff1f;今天给大家介绍一位…...

VSCode 用于JAVA开发的环境配置,JDK为1.8版本时的配置

插件安装 JAVA开发在VSCode中&#xff0c;需要安装JAVA的必要开发。当前安装只需要安装 “Language Support for Java(TM) by Red Hat”插件即可 安装此插件后&#xff0c;会自动安装包含如下插件&#xff0c;不再需要单独安装 Project Manager for Java Test Runner for J…...

Git Flow分支模型

经典分支模型(Git Flow) 由 Vincent Driessen 提出的 Git Flow 模型,是管理 main(或 master)和 dev 分支的经典方案: main 用于生产发布,保持稳定; dev 用于日常开发,合并功能分支(feature/*); 功能开发在 feature 分支进行,完成后合并回 dev; 预发布分支(rele…...

机器人进阶---视觉算法(六)傅里叶变换在图像处理中怎么用

傅里叶变换在图像处理中怎么用 傅里叶变换的基本原理应用场景Python代码示例逐行解释总结傅里叶变换在图像处理中是一种重要的工具,它将图像从空间域转换到频域,从而可以对图像的频率特性进行分析和处理。傅里叶变换在图像滤波、图像增强、图像压缩和图像分析等方面都有广泛应…...

Linux-skywalking部署步骤并且添加探针

skywalking部署步骤 上传skywalking安装包并解压将skywalking安装包apache-skywalking-apm-10.1.0.tar.gz上传到服务器/data目录下 用解压命令解压 cd /data tar -xvf apache-skywalking-apm-10.1.0.tar.gz 解压后重名目录 mv apache-skywalking-apm-bin skywalking 上传…...

开启报名!火山引擎 x PICO-全国大学生物联网设计竞赛赛题发布

全国大学生物联网设计竞赛&#xff08;以下简称“竞赛”&#xff09;是教育部高等学校计算机类专业教学指导委员会创办的物联网领域唯一的学科竞赛&#xff0c;是以学科竞赛推动专业建设、培养大学生创新能力为目标&#xff0c;面向高校大学生举办的全国性赛事。自 2014 年开始…...

【遥感科普】光谱分辨率是什么?

光谱分辨率是指传感器或光谱仪器在电磁波谱中区分相邻波长或频率的能力。它反映了设备对光谱细节的捕捉能力&#xff0c;通常用波长间隔&#xff08;如纳米&#xff0c;nm&#xff09;或波数&#xff08;cm⁻&#xff09;表示。例如&#xff0c;若光谱分辨率为10 nm&#xff0c…...

Trae国内版怎么用?Trae IDE 内置 MCP 市场配置使用指南

近日&#xff0c;字节跳动旗下Trae IDE发布了全新版本&#xff0c;新版本中&#xff0c;Trae IDE 的自定义智能体能力让 AI 能够基于开发者需求灵活调度多维度的工具和资源&#xff0c;从而为任务提供全方位的支持&#xff0c;只需一下即可召唤智能体&#xff0c;这个过程中&am…...

Javase 基础入门 —— 02 基本数据类型

本系列为笔者学习Javase的课堂笔记&#xff0c;视频资源为B站黑马程序员出品的《黑马程序员JavaAI智能辅助编程全套视频教程&#xff0c;java零基础入门到大牛一套通关》&#xff0c;章节分布参考视频教程&#xff0c;为同样学习Javase系列课程的同学们提供参考。 01 注释 单…...

模型 螃蟹效应

系列文章分享模型&#xff0c;了解更多&#x1f449; 模型_思维模型目录。个体互钳&#xff0c;团队难行。 1 螃蟹效应的应用 1.1 教育行业—优秀教师遭集体举报 行业背景&#xff1a;某市重点中学推行绩效改革&#xff0c;将班级升学率与教师奖金直接挂钩&#xff0c;打破原…...

597页PPT丨流程合集:流程梳理方法、流程现状分析,流程管理规范及应用,流程绩效的管理,流程实施与优化,流程责任人的角色认知等

流程梳理是通过系统化分析优化业务流程的管理方法&#xff0c;其核心包含四大步骤&#xff1a;①目标确认&#xff0c;明确业务痛点和改进方向&#xff1b;②现状分析&#xff0c;通过流程图、价值流图还原现有流程全貌&#xff0c;识别冗余环节和瓶颈节点&#xff1b;③优化设…...

Kotlin集合全解析:List和Map高频操作手册

Kotlin 中 Map 和 List 常用功能总结 List 常用功能 创建 List val immutableList listOf(1, 2, 3) // 不可变列表 val mutableList mutableListOf("a", "b", "c") // 可变列表 val emptyList emptyList<String>() // 空列表基本…...

【springsecurity oauth2授权中心】自定义登录页和授权确认页 P2

上一篇跑通了springsecurity oauth2的授权中心授权流程&#xff0c;这篇来将内置的登录页和授权确认页自定义一下 引入Thymeleaf 在模块authorization-server下的pom.xml里引入模板引擎 <dependency><groupId>org.springframework.boot</groupId><arti…...

Springboot整合MyBatisplus和快速入门

MyBatisPlus MyBatis-Plus &#xff08;简称 MP&#xff09;是一个 MyBatis的增强工具&#xff0c;在 MyBatis 的基础上只做增强不做改变&#xff0c;为简化开发、提高效率而生。 MyBatisPlus的官方网址: MyBatis-Plus &#x1f680; 为简化开发而生 快速入门 导入起步依赖…...

Vue2-基础使用模板

data和el的第一种写法 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>VUE</title><script type"text/javascript" src"../js/vue.js"></script> </head&g…...

Vue2-指令语法

v-bind和v-model <a v-bind:href"url">笔记1</a> <a :href"url">笔记2</a><input type"text" v-model:value"name"/> <input type"text" v-model"name"/>data(){return {ur…...

Cesium学习笔记——坐标系统及坐标转换

前言 在Cesium的学习中&#xff0c;学会读文档十分重要&#xff01;&#xff01;&#xff01;在这里附上Cesium中英文文档1.117。 在Cesium中&#xff0c;一共有四种比较重要的坐标系&#xff0c;分别是地理坐标系&#xff0c;地心地固坐标系&#xff0c;东-北-上局部坐标系和屏…...

【AI微信小程序开发】大转盘小程序项目代码:自设转盘选项和概率(含完整前端+后端代码)

系列文章目录 【AI微信小程序开发】AI减脂菜谱小程序项目代码:根据用户身高/体重等信息定制菜谱(含完整前端+后端代码)【AI微信小程序开发】AI菜谱推荐小程序项目代码:根据剩余食材智能生成菜谱(含完整前端+后端代码)【AI微信小程序开发】图片工具小程序项目代码:图片压…...

C语言文件操作完全手册:读写·定位·实战

1.什么是文件 1.1文件的概念 文件&#xff08;File&#xff09;是计算机中用于持久化存储数据的基本单位。它可以存储文本、图片、音频、程序代码等各种信息&#xff0c;并在程序运行结束后仍然保留数据。 1.2文件名 一个文件要有一个唯一的文件标识&#xff0c;以便用户识别…...

网络协议之详解(Detailed Explanation of Network Protocol)

NFS、FTP、SMB、WebDav、DLNA协议 大家好&#xff01;今天来和大家聊聊让很多人都感到困惑的 NFS、FTP、SMB、WebDav、DLNA 这几种协议。相信不少人在面对它们的时候&#xff0c;常常是一头雾水&#xff0c;傻傻分不清。别担心&#xff0c;看完这篇文章&#xff0c;你就会对它…...

VICP(Velocity-based ICP):通过运动校准实现精准姿态估计

在三维点云配准的领域&#xff0c;经典的ICP&#xff08;Iterative Closest Point&#xff09;算法已经成为广泛使用的方法&#xff0c;尤其是在处理静态环境中的点云配准时&#xff0c;效果非常好。然而&#xff0c;随着动态场景的出现&#xff0c;物体运动和传感器的变动引发…...

典籍知识问答典籍查询界面前端界面设计效果实现

根据组内负责前端界面设计的同学的界面设计&#xff0c;进行典籍查询前端界面的实现 1.实现效果 2.前端代码 ClassicView.vue <template> <div class"classics"> <img className"back" alt"Back" src"https://c.animaapp…...

Megatron - LM 重要文件解析 - /tools/preprocess_data.py

preprocess_data.py 的主要功能。这是 Megatron-LM 的数据预处理脚本&#xff0c;主要用于将原始文本数据转换为模型训练所需的格式。 核心功能&#xff1a; 1. 数据预处理流程&#xff1a; 输入&#xff1a;原始文本文件&#xff08;JSON格式&#xff09; 处理&#xff1a…...

探索RTOS在电力控制系统中的应用价值

电力控制系统对实时性、可靠性和高效性有着严苛要求&#xff0c;实时操作系统&#xff08;RTOS&#xff09;凭借其独特性能优势&#xff0c;在其中扮演关键角色。本文深入剖析RTOS在电力控制系统中的重要作用&#xff0c;探讨其应用前景&#xff0c;助力推动电力行业智能化、现…...

第5章-1 优化服务器设置

上一篇&#xff1a;《第4章-5 linux 网络管理》&#xff0c;接着服务器设置 本章我们将解释如何为MySQL服务器创建合适的配置文件。这是一个迂回的旅程&#xff0c;有许多兴趣点和可以俯瞰风景的短途旅程。这些短途旅程是必要的。确定合适配置的最短路径并不是从研究配置选项并…...

进阶篇 第 4 篇:驾驭季节性波动 - SARIMA 模型实战

进阶篇 第 4 篇&#xff1a;驾驭季节性波动 - SARIMA 模型实战 (图片来源: Pixabay on Pexels) 在上一篇中&#xff0c;我们深入探索了经典的 ARIMA(p,d,q) 模型。它通过整合自回归 (AR)、差分 (I) 和移动平均 (MA) 提供了一个强大的框架来对&#xff08;处理后&#xff09;平…...

Android调用springboot接口上传大字段,偶现接口超时的优化

介绍 最近有个功能&#xff0c;Android通过okhttp上传实体类&#xff0c;实体类包含一个大字段&#xff0c;上传的字符串长度达到300k&#xff0c;偶现接口超时的情况&#xff0c;大概100次有5次&#xff0c;看日志发现数据并没有到达接口&#xff0c;可能在网络传输中就超时了…...

[特殊字符]【Qt自定义控件】创意开关按钮 - 丝滑动画+自定义样式+信号交互 | 附完整源码

话不多说直接上代码 1、.mybutton.h #ifndef MYBUTTON_H #define MYBUTTON_H#include <QWidget> #include <QPropertyAnimation>class MyButton : public QWidget {Q_OBJECT public:explicit MyButton(QWidget *parent nullptr);protected:void paintEvent(QPain…...

大数据开发的基本流程

大数据开发通常围绕数据的“采集 → 存储 → 处理 → 分析 → 展示”几个阶段来展开。下面是一个典型的大数据开发基本流程&#xff08;适用于离线或实时场景&#xff09;&#xff1a; 一、数据采集&#xff08;Data Ingestion&#xff09; 目标&#xff1a;从各种来源采集原始…...

关于创建UNIX/Linux daemon进程的笔记

Linux daemon程序简单说就是Linux后台服务进程。 传统的、标准的创建方法&#xff1a;2次fork setsid 详细步骤 进程1&#xff08;父进程&#xff09;调用fork&#xff0c;创建子进程2&#xff0c;进程1退出。 1&#xff09;这个步骤是为第二部做铺垫。 2&#xff09;此时&a…...