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

[net 6] udp_chat_server基于udp的简单聊天室(多线程的服务器与业务相分离)

目录

1. 网络聊天室的意义

2. 网络聊天室了解

2.1. 网络聊天室模块分析

2.2. 目标

3. 基本框架

3.1. 文件基本框架

3.2. 设计回调函数解耦

4. Route.hpp 模块(消息转发)

4.1. 头文件包含

4.2. 基本类框架

4.3. Route::Forward() 转发

4.3.1. 函数头设计

4.3.2. 维护用户列表

4.3.3. 客户退出聊天

4.3.4. 消息转发 -> 线程池

4.3.4.1. 引入线程池代码

4.3.4.2. 转发任务

4.4. 转发模块与服务器模块关联

4.5. 测试: 多个客户端与一个服务端

5. 客户端读写并行

5.1. 客户端初始化

5.2. 发消息客户端与收消息客户端解耦合

5.2.1. 收消息

5.2.2. 发消息

6. 参考代码

6.1. 核心代码

6.2. 其他代码


1. 网络聊天室的意义

为什么要写一个网络聊天室呢? 前面都写过字典那个例子了.

意义在于: 网络聊天室的意义在于多进程, 引入使用线程池.

2. 网络聊天室了解

2.1. 网络聊天室模块分析

  • udpserver
    • 功能: 读取数据
    • 读到两个东西: 数据 + clientinfo(ip/port...)
    • 在线用户列表, 维护"谁在线"的含义(ip + port).
  • 路由与转发模块
    • 功能: 转发消息(根据用户列表), 转发给对应的 client.
    • 实现方式: 线程池的方式进行转发.

2.2. 目标

要求与字典服务一样, 要求各个模块解耦合.

业务逻辑 与 IO 逻辑解耦.

3. 基本框架

3.1. 文件基本框架

同样我们把之前写的 echo_server 直接 CV 到我们的 chat_server 即可.

3.2. 设计回调函数解耦

解释一下各个参数的含义:

  • 返回值是 void, 因为是发送消息嘛, 可以不用返回值.
  • 参数 1: int, 这个是套接字
  • 参数 2: 发送的消息
  • 参数 3: 谁发的消息

解耦合: 把服务发消息给到转发模块, 所以下面这部分是不要了.

变成:

4. Route.hpp 模块(消息转发)

4.1. 头文件包含

4.2. 基本类框架

4.3. Route::Forward() 转发

4.3.1. 函数头设计

  • int sockfd: 从哪个套接字发?
  • message: 发送的消息是什么?
  • who: 谁来发?
4.3.2. 维护用户列表

同时, 还需要注意在 InetAddr 当中去重载比较运算符.

如果只保证 ip 的唯一性, 所以一个主机下的一个 ip 只能启动一个客户端, 所以不太好(正常来讲是 ip 唯一的), 我们这里保证端口唯一即可.

4.3.3. 客户退出聊天

我们约定: 假设 message == "QUIT", 我们移除 online_user 中的客户. 并且把这个消息同样做转发处理(告知所有人有个人走了~).

注意: 找到了一定要立刻 break, 这个地方存在迭代器失效问题.

4.3.4. 消息转发 -> 线程池

字节序列问题可以用 sockaddr 直接解决:

下面实际上还有俩活:

  1. 引入线程池
  2. 业务 与 服务器之间关联起来.
4.3.4.1. 引入线程池代码

4.3.4.2. 转发任务

啥意思呢? 你这样直接调用, 是主线程做的, 效率不高, 因此我们考虑把这个 ForwardHelper 任务外包出去, 让线程池的线程来进行处理, 而我们的主线程则有空闲去处理自己的事情.

包装任务:

构建任务:

获取线程池对象并入线程池任务队列:

4.4. 转发模块与服务器模块关联

因为线程池中的 func_t 类型已经占用了, 因此我们给 UdpServer.hpp 中的包装器改一下名字.

服务端对象:

绑定转发模块, 并且关联到服务器:

带上线程库进行编译:

4.5. 测试: 多个客户端与一个服务端

ps -aL可以查询当前的一个线程启动情况.

下面是一些方便验证的调试消息:

直接看源码吧, 改的比较多~

但是发现了一个大问题: 就是多个客户端在跟服务器去交互的时候, 只要你客户端不说话, 你就收不到消息, 虽然说服务器把消息转发了, 这就相当逆天!

因为客户端阻塞在发消息当中. 你想接受消息就必须发消息, 因为是阻塞状态嘛, 这就很坑爹. 所以说, 我们可以给每个客户端搞两个线程, 一个读一个发消息, 两个并行跑这样的话可能会比较好.

5. 客户端读写并行

5.1. 客户端初始化

5.2. 发消息客户端与收消息客户端解耦合

客户端加入线程模块:

创建读写线程并且启动:

主线程负责等待两个线程即可:

我们收消息的线程需要用到套接字, 因此给他传过去即可, 因为我们的 thread 自己封装的线程库只要求一个参数, 所以我们 bind 给他过去即可.

到了发消息呢? 发消息的线程需要套接字, 还需要知道服务器的 ip + port. 这个地方老师又把命令行参数放到 main 里了.

5.2.1. 收消息

上面 code 用的是 cerr, 是方便用来重定向打印到两个不同终端看效果的.

5.2.2. 发消息

定位一下不同的终端号:

将这个程序的标准错误全部重定向到 0 号终端.

6. 参考代码

6.1. 核心代码

ip + port -> 标识客户端的唯一性.

#include "UdpServer.hpp"
#include "Route.hpp"#include <memory>// ./udp_server local-port
// ./udp_server 8888
int main(int argc, char *argv[])
{if(argc != 2){std::cerr << "Usage: " << argv[0] << " local-port" << std::endl;exit(0);}uint16_t port = std::stoi(argv[1]);EnableScreen();Route messageRoute; // 实例化转发模块service_t message_route = std::bind(&Route::Forward,\&messageRoute, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); // 把转发模块中的转发函数给到服务器用来绑定到一起. std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(message_route, port); //C++14的标准usvr->InitServer();usvr->Start();return 0;
}
#pragma once#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>#include "nocopy.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
/* 
* 服务器用来读取数据
* 读取的数据有两个东西
* - 用户发送的数据/信息 -> 交给 [路由转发模块] 进行处理. 
* - 用户本身的信息 -> ip + port 构成一个 在线用户列表. 
* [路由转发模块] 根据 在线用户列表 来转发给 所有人 消息. 
* 线程池帮助路由转发模块进行转发. 目标: 服务器, 转发模块 以及 线程池 全部解耦合! 
*/ 
using namespace log_ns;static const int gsockfd = -1;
static const uint16_t glocalport = 8888;enum
{SOCKET_ERROR = 1,BIND_ERROR
};using service_t = std::function<void(int, const std::string &message, InetAddr &who)>; 
// 转发模块的函数指针. -> 帮助我们服务器来转发消息的. 
// 参数1: 从哪个套接字发消息? 
// 参数2: message -> 转发的消息内容 
// 参数3: 谁发的这个消息 -> 因为需要用到用户的ip + port来标识用户的唯一性. // UdpServer user("192.1.1.1", 8899);
// 一般服务器主要是用来进行网络数据读取和写入的。IO的
// 服务器IO逻辑 和 业务逻辑 解耦
class UdpServer : public nocopy
{
public:UdpServer(service_t func, uint16_t localport = glocalport): _func(func),_sockfd(gsockfd),_localport(localport),_isrunning(false){}void InitServer(){// 1. 创建socket文件_sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(FATAL, "socket error\n");exit(SOCKET_ERROR);}LOG(DEBUG, "socket create success, _sockfd: %d\n", _sockfd); // 3// 2. bindstruct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(_localport);// local.sin_addr.s_addr = inet_addr(_localip.c_str()); // 1. 需要4字节IP 2. 需要网络序列的IP -- 暂时local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意IP地址绑定int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(FATAL, "bind error\n");exit(BIND_ERROR);}LOG(DEBUG, "socket bind success\n");}void Start(){_isrunning = true;char message[1024];while (_isrunning){struct sockaddr_in peer;socklen_t len = sizeof(peer);ssize_t n = recvfrom(_sockfd, message, sizeof(message) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){InetAddr addr(peer);message[n] = 0; // 收到消息的缓冲区. LOG(DEBUG, "[%s]# %s\n", addr.AddrStr().c_str(), message); // 提示一下是否接受消息成功. _func(_sockfd, message, addr); // 把转发任务给到转发模块进行处理. LOG(DEBUG, "return udpserver\n"); // 提示回调完成! }else{std::cout << "recvfrom ,  error" << std::endl;}}_isrunning = false;}~UdpServer(){if (_sockfd > gsockfd)::close(_sockfd);}private:int _sockfd; // 读写都用同一个sockfd, 反应说明:UDP是 全双工 通信的!uint16_t _localport;// std::string _localip; // TODO:后面专门要处理一下这个IPbool _isrunning;service_t _func; // 该函数用来服务器回调转发模块, 是一个回调函数指针. 
};
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"/*
- 客户端要一边读一边写 -> 客户端要多线程化! 
- 线程1: 读数据 从服务端那边随时接受消息, 并立刻显示到终端上. 
- 线程2: 发消息, 从键盘上获取用户的消息, 显示到终端上, 并立刻发送给服务器. 
*/
// ./udpclient 127.0.0.1 8888 2>/dev/pts/0 -> 把./udpclient进程的标准错误信息都重定向到0号虚拟终端下, 方便观察现象. 
using namespace ThreadMoudle;int InitClient()
{int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){std::cerr << "create socket error" << std::endl;exit(1);}return sockfd;
}// 读线程: 需要知道从哪个socket读, 
void RecvMessage(int sockfd, const std::string &name)
{// 接受消息是不断去循环接受的. while (true){struct sockaddr_in peer;socklen_t len = sizeof(peer);char buffer[1024];int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (n > 0){buffer[n] = 0;std::cerr << buffer << std::endl; // 读取成功, 咱们打印出来. }else{std::cerr << "recvfrom error" << std::endl; // 读取失败, 咱们提示一下. break;}}
}void SendMessage(int sockfd, std::string serverip, uint16_t serverport, const std::string &name)
{// 填写目标结构体 struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(serverport);server.sin_addr.s_addr = inet_addr(serverip.c_str());// 准备要发送的内容的前缀. std::string cli_profix = name + "# "; // sender-thread# 你好while (true){std::string line; // 缓冲区 std::cout << cli_profix; // 打印发送前缀 std::getline(std::cin, line); // 获取用户输入的消息 int n = sendto(sockfd, line.c_str(), line.size(), 0, (struct sockaddr *)&server, sizeof(server)); // 把消息发给server. if (n <= 0)break;}
}int main(int argc, char *argv[])
{// 检查参数 if (argc != 3){std::cerr << "Usage: " << argv[0] << " server-ip server-port" << std::endl;exit(0);}std::string serverip = argv[1];uint16_t serverport = std::stoi(argv[2]);int sockfd = InitClient();// 收消息的线程 -> 并把收消息的函数给到该线程Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1));// string 用来让Thread标识线程名, func(string), 是为了方便在func里打印线程名. // 发消息的线程 -> 并把发消息的函数给到该线程Thread sender("sender-thread", std::bind(&SendMessage, sockfd, serverip, serverport, std::placeholders::_1));// 两个线程启动起来 recver.Start();sender.Start();// 主线程等待两个线程 recver.Join();sender.Join();::close(sockfd); // 关闭文件描述符return 0;
}
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <functional>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include "InetAddr.hpp"
#include "ThreadPool.hpp"
#include "LockGuard.hpp"// class user
// {};using task_t = std::function<void()>;class Route
{
public:Route(){pthread_mutex_init(&_mutex, nullptr);}// 检查用户是否在在线用户列表当中 + 对应的行为. // 不在 -> 添加用户. // 在 -> 什么都不做. void CheckOnlineUser(InetAddr &who){LockGuard lockguard(&_mutex);for (auto &user : _online_user){if (user == who) // 如果存在, 就什么都不做. -> 重载InetAddr的比较bool operator==(). {LOG(DEBUG, "%s is exists\n", who.AddrStr().c_str()); // 存在的用户消息提示. return; // 直接退出}}LOG(DEBUG, "%s is not exists, add it\n", who.AddrStr().c_str()); // 不存在的用户也提示一下. _online_user.push_back(who); // 不在, 我们就添加进去. }// for test// 在用户列表当中移除用户. void Offline(InetAddr &who){LockGuard lockguard(&_mutex); // 加锁. auto iter = _online_user.begin();for (; iter != _online_user.end(); iter++){if (*iter == who){LOG(DEBUG, "%s is offline\n", who.AddrStr().c_str()); // 日志: 提示某用户离开. _online_user.erase(iter);break; // 迭代器失效, 直接break; }}}void ForwardHelper(int sockfd, const std::string message, InetAddr who){// 从哪转发, 哪个套接字? -> sockfd// 转发啥消息? -> message// 谁发的? -> whoLockGuard lockguard(&_mutex); // 加锁. std::string send_message = "[" + who.AddrStr() + "]# " + message; // 准备要转发的消息for (auto &user : _online_user){struct sockaddr_in peer = user.Addr(); // Addr(): 返回的是网络序列的套接字信息. LOG(DEBUG, "Forward message to %s, message is %s\n", user.AddrStr().c_str(), send_message.c_str()); // debug: 即将转发的消息也提示一下. ::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer));}}// 消息转发void Forward(int sockfd, const std::string &message, InetAddr &who){// 1. 该用户是否在 在线用户列表中呢?如果在,什么都不做,如果不在,自动添加到_online_userCheckOnlineUser(who);// 1.1 用户可选择退出: message == "QUIT" "Q"if (message == "QUIT" || message == "Q"){Offline(who);}// 2. who 一定在_online_user列表里面// ForwardHelper(sockfd, message); // 这样是路由模块/服务器进行消息转发, 我们下面这种写法就算把这个转发任务给到线程. task_t t = std::bind(&Route::ForwardHelper, this, sockfd, message, who); // 包装一下转发函数. 因为线程池中的线程只接受void()类型的行为. ThreadPool<task_t>::GetInstance()->Equeue(t); // 启动线程池, 获取一个线程池单例对象 -> 派发任务给他的子线程.  }~Route(){pthread_mutex_destroy(&_mutex);}private:std::vector<InetAddr> _online_user; // 维护在线用户列表. pthread_mutex_t _mutex; // 用来保护公共资源. 
};
#pragma once#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>class InetAddr
{
private:void ToHost(const struct sockaddr_in &addr){_port = ntohs(addr.sin_port);// _ip = inet_ntoa(addr.sin_addr);char ip_buf[32];// inet_p to n// p: process// n: net// inet_pton(int af, const char *src, void *dst);// inet_pton(AF_INET, ip.c_str(), &addr.sin_addr.s_addr);::inet_ntop(AF_INET, &addr.sin_addr, ip_buf, sizeof(ip_buf));_ip = ip_buf;}public:InetAddr(const struct sockaddr_in &addr):_addr(addr){ToHost(addr);}// ip + port 唯一性: // 一般来说ip唯一就算是唯一的. // 这里为了方便测试, 把port也搞成区分唯一性的. bool operator == (const InetAddr &addr){return (this->_ip == addr._ip && this->_port == addr._port);}std::string Ip(){return _ip;}uint16_t Port(){return _port;}struct sockaddr_in Addr(){return _addr;}// 用来方便调试, 返回的是字符串, 方便打印. std::string AddrStr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:std::string _ip;uint16_t _port;struct sockaddr_in _addr;
};

6.2. 其他代码

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>namespace ThreadMoudle
{// 线程要执行的方法,后面我们随时调整// typedef void (*func_t)(ThreadData *td); // 函数指针类型// typedef std::function<void()> func_t;using func_t = std::function<void(const std::string&)>;class Thread{public:void Excute(){_isrunning = true;_func(_name);_isrunning = false;}public:Thread(const std::string &name, func_t func):_name(name), _func(func){}static void *ThreadRoutine(void *args) // 新线程都会执行该方法!{Thread *self = static_cast<Thread*>(args); // 获得了当前对象self->Excute();return nullptr;}bool Start(){int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);if(n != 0) return false;return true;}std::string Status(){if(_isrunning) return "running";else return "sleep";}void Stop(){if(_isrunning){::pthread_cancel(_tid);_isrunning = false;}}void Join(){::pthread_join(_tid, nullptr);}std::string Name(){return _name;}~Thread(){}private:std::string _name;pthread_t _tid;bool _isrunning;func_t _func; // 线程要执行的回调函数};
} // namespace ThreadModle
#pragma once#include <iostream>
#include <unistd.h>
#include <string>
#include <vector>
#include <queue>
#include <functional>
#include "Thread.hpp"
#include "Log.hpp"
#include "LockGuard.hpp"using namespace ThreadMoudle;
using namespace log_ns;static const int gdefaultnum = 5;void test()
{while (true){std::cout << "hello world" << std::endl;sleep(1);}
}template <typename T>
class ThreadPool
{
private:void LockQueue(){pthread_mutex_lock(&_mutex);}void UnlockQueue(){pthread_mutex_unlock(&_mutex);}void Wakeup(){pthread_cond_signal(&_cond);}void WakeupAll(){pthread_cond_broadcast(&_cond);}void Sleep(){pthread_cond_wait(&_cond, &_mutex);}bool IsEmpty(){return _task_queue.empty();}void HandlerTask(const std::string &name) // this{while (true){// 取任务LockQueue();while (IsEmpty() && _isrunning){_sleep_thread_num++;LOG(INFO, "%s thread sleep begin!\n", name.c_str());Sleep();LOG(INFO, "%s thread wakeup!\n", name.c_str());_sleep_thread_num--;}// 判定一种情况if (IsEmpty() && !_isrunning){UnlockQueue();LOG(INFO, "%s thread quit\n", name.c_str());break;}// 有任务T t = _task_queue.front();_task_queue.pop();UnlockQueue();// 处理任务t(); // 处理任务,此处不用/不能在临界区中处理// std::cout << name << ": " << t.result() << std::endl;// LOG(DEBUG, "hander task done, task is : %s\n", t.result().c_str());}}void Init(){func_t func = std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1);for (int i = 0; i < _thread_num; i++){std::string threadname = "thread-" + std::to_string(i + 1);_threads.emplace_back(threadname, func);LOG(DEBUG, "construct thread %s done, init success\n", threadname.c_str());}}void Start(){_isrunning = true;for (auto &thread : _threads){LOG(DEBUG, "start thread %s done.\n", thread.Name().c_str());thread.Start();}}ThreadPool(int thread_num = gdefaultnum): _thread_num(thread_num), _isrunning(false), _sleep_thread_num(0){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);}ThreadPool(const ThreadPool<T> &) = delete;void operator=(const ThreadPool<T> &) = delete;public:void Stop(){LockQueue();_isrunning = false;WakeupAll();UnlockQueue();LOG(INFO, "Thread Pool Stop Success!\n");}// 如果是多线程获取单例呢?static ThreadPool<T> *GetInstance(){if (_tp == nullptr){LockGuard lockguard(&_sig_mutex);if (_tp == nullptr){LOG(INFO, "create threadpool\n");// thread-1 thread-2 thread-3...._tp = new ThreadPool<T>();_tp->Init();_tp->Start();}else{LOG(INFO, "get threadpool\n");}}return _tp;}void Equeue(const T &in){LockQueue();if (_isrunning){_task_queue.push(in);if (_sleep_thread_num > 0)Wakeup();}UnlockQueue();}~ThreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:int _thread_num;std::vector<Thread> _threads;std::queue<T> _task_queue;bool _isrunning;int _sleep_thread_num;pthread_mutex_t _mutex;pthread_cond_t _cond;// 单例模式// volatile static ThreadPool<T> *_tp;static ThreadPool<T> *_tp;static pthread_mutex_t _sig_mutex;
};template <typename T>
ThreadPool<T> *ThreadPool<T>::_tp = nullptr;
template <typename T>
pthread_mutex_t ThreadPool<T>::_sig_mutex = PTHREAD_MUTEX_INITIALIZER;
#pragma once#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t *mutex):_mutex(mutex){pthread_mutex_lock(_mutex);}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t *_mutex;
};
#pragma once#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
#include <cstdarg>
#include <fstream>
#include <cstring>
#include <pthread.h>
#include "LockGuard.hpp"namespace log_ns
{enum{DEBUG = 1,INFO,WARNING,ERROR,FATAL};std::string LevelToString(int level){switch (level){case DEBUG:return "DEBUG";case INFO:return "INFO";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "UNKNOWN";}}std::string GetCurrTime(){time_t now = time(nullptr);struct tm *curr_time = localtime(&now);char buffer[128];snprintf(buffer, sizeof(buffer), "%d-%02d-%02d %02d:%02d:%02d",curr_time->tm_year + 1900,curr_time->tm_mon + 1,curr_time->tm_mday,curr_time->tm_hour,curr_time->tm_min,curr_time->tm_sec);return buffer;}class logmessage{public:std::string _level;pid_t _id;std::string _filename;int _filenumber;std::string _curr_time;std::string _message_info;};#define SCREEN_TYPE 1
#define FILE_TYPE 2const std::string glogfile = "./log.txt";pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER;// log.logMessage("", 12, INFO, "this is a %d message ,%f, %s hellwrodl", x, , , );class Log{public:Log(const std::string &logfile = glogfile) : _logfile(logfile), _type(SCREEN_TYPE){}void Enable(int type){_type = type;}void FlushLogToScreen(const logmessage &lg){printf("[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());}void FlushLogToFile(const logmessage &lg){std::ofstream out(_logfile, std::ios::app);if (!out.is_open())return;char logtxt[2048];snprintf(logtxt, sizeof(logtxt), "[%s][%d][%s][%d][%s] %s",lg._level.c_str(),lg._id,lg._filename.c_str(),lg._filenumber,lg._curr_time.c_str(),lg._message_info.c_str());out.write(logtxt, strlen(logtxt));out.close();}void FlushLog(const logmessage &lg){// 加过滤逻辑 --- TODOLockGuard lockguard(&glock);switch (_type){case SCREEN_TYPE:FlushLogToScreen(lg);break;case FILE_TYPE:FlushLogToFile(lg);break;}}void logMessage(std::string filename, int filenumber, int level, const char *format, ...){logmessage lg;lg._level = LevelToString(level);lg._id = getpid();lg._filename = filename;lg._filenumber = filenumber;lg._curr_time = GetCurrTime();va_list ap;va_start(ap, format);char log_info[1024];vsnprintf(log_info, sizeof(log_info), format, ap);va_end(ap);lg._message_info = log_info;// 打印出来日志FlushLog(lg);}~Log(){}private:int _type;std::string _logfile;};Log lg;#define LOG(Level, Format, ...)                                        \do                                                                 \{                                                                  \lg.logMessage(__FILE__, __LINE__, Level, Format, ##__VA_ARGS__); \} while (0)
#define EnableScreen()          \do                          \{                           \lg.Enable(SCREEN_TYPE); \} while (0)
#define EnableFILE()          \do                        \{                         \lg.Enable(FILE_TYPE); \} while (0)
};
.PHONY:all
all:udpserver udpclientudpserver:UdpServerMain.ccg++ -o $@ $^ -std=c++14 -lpthread // 线程库
udpclient:UdpClientMain.ccg++ -o $@ $^ -std=c++14 -lpthread // 线程库.PHONY:clean
clean:rm -rf udpserver udpclient
#pragma onceclass nocopy
{
public:nocopy(){}~nocopy(){}nocopy(const nocopy&) = delete;const nocopy& operator=(const nocopy&) = delete;
};

相关文章:

[net 6] udp_chat_server基于udp的简单聊天室(多线程的服务器与业务相分离)

目录 1. 网络聊天室的意义 2. 网络聊天室了解 2.1. 网络聊天室模块分析 2.2. 目标 3. 基本框架 3.1. 文件基本框架 3.2. 设计回调函数解耦 4. Route.hpp 模块(消息转发) 4.1. 头文件包含 4.2. 基本类框架 4.3. Route::Forward() 转发 4.3.1. 函数头设计 4.3.2. 维护…...

驱动-自旋锁

前面原子操作进行了讲解&#xff0c; 并使用原子整形操作对并发与竞争实验进行了改进&#xff0c;但是原子操作只能对整形变量或者位进行保护&#xff0c; 而对于结构体或者其他类型的共享资源&#xff0c; 原子操作就力不从心了&#xff0c; 这时候就轮到自旋锁的出场了。 两个…...

TDengine 存储引擎剖析:数据文件与索引设计(二)

TDengine 索引设计 索引设计关键特性 TDengine 的索引设计采用了多种技术和策略&#xff0c;以满足时序数据高效存储和快速查询的需求&#xff0c;具有以下关键特性&#xff1a; 多级时间戳压缩索引&#xff1a;TDengine 使用了时间戳压缩索引技术&#xff0c;能够有效减少索…...

基于Python的医疗质量管理指标智能提取系统【2025代码版】

系统概述 本系统旨在帮助医疗质量管理部从医院信息系统(HIS)中智能提取《2025年国家医疗质量安全改进目标》中的关键指标数据。系统采用Python编程语言,结合现代数据处理库,实现高效、准确的数据提取与分析功能。 import json import logging import logging.handlers impo…...

中介者模式(Mediator Pattern)

中介者模式(Mediator Pattern)是一种行为型设计模式。它通过引入一个中介者对象,来封装一系列对象之间的交互,使这些对象之间不再直接相互引用和通信,而是通过中介者进行间接通信,从而降低对象之间的耦合度,提高系统的可维护性和可扩展性。 一、基础 1. 意图 核心目的…...

Hbuilder 上的水印相机实现方案 (vue3 + vite + hbuilder)

效果 思路 通过 live-pusher 这个视频推流的组件来获取摄像头拿到视频的一帧图片之后&#xff0c;跳转到正常的 vue 页面&#xff0c;通过 canvas 来处理图片水印 源码 live-pusher 这个组件必须是 nvue 的 至于什么是 nvue&#xff0c;看这个官方文档吧 https://uniapp.dcl…...

聊聊Spring AI Alibaba的PdfTablesParser

序 本文主要研究一下Spring AI Alibaba的PdfTablesParser PdfTablesParser community/document-parsers/spring-ai-alibaba-starter-document-parser-pdf-tables/src/main/java/com/alibaba/cloud/ai/parser/pdf/tables/PdfTablesParser.java public class PdfTablesParser…...

二分查找-LeetCode

题目 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target&#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 示例 1: 输入: nums [-1,0,3,5,9,12], target 9 输出: 4 解释: …...

StarRocks Community Monthly Newsletter (Mar)

版本动态 3.4.1 版本更新 核心功能升级 数据安全与权限管控 支持「安全视图」功能&#xff0c;严格管控视图查询权限 MySQL协议连接支持SSL认证&#xff0c;保障数据传输安全 存算分离架构增强 支持自动创建Snapshot&#xff08;集群恢复更便捷&#xff09; Storage Volu…...

STM32+dht11+rc522+jq8400的简单使用

1.dht11的使用 硬件&#xff1a;3v3&#xff0c;gnd&#xff0c;data数据线接一个gpio&#xff0c;三根线即可 软件&#xff1a; ①dht11.c #include "dht11.h" #include "delay.h" #include "stdbool.h"static STRUCT_DHT11_TYPEDEF dht11;…...

mpstat指令介绍

文章目录 1. 功能介绍2. 语法介绍3. 应用场景4. 实际举例 1. 功能介绍 mpstat 英文全称( Multi-Processor Statistics)&#xff0c;多处理器统计信息的含义。 下面大致说一下功能作用&#xff1a; 多核性能监控 可实时监控每个 CPU 核心的利用率、中断频率、上下文切换等指标&…...

网络层IP协议知识大梳理

全是通俗易懂的讲解&#xff0c;如果你本节之前的知识都掌握清楚&#xff0c;那就速速来看我的IP协议笔记吧~ 自己写自己的八股&#xff01;让未来的自己看懂&#xff01; &#xff08;全文手敲&#xff0c;受益良多&#xff09; 网路基础3 网路层 TCP并没有把数据发到网路…...

Linux-codec

codec原理图 codec接口 ①音频输入接口&#xff0c;连接mic ②音频输出接口&#xff0c;连接speaker ③sai/i2s接口&#xff0c;连接soc&#xff0c;soc和codec互发音频数据 ④i2c接口&#xff0c;连接soc&#xff0c;soc配置codecsai音频接口 MCLK&#xff1a;主时钟&#x…...

HTTP协议与web服务器

HTTP协议与web服务器 目录 一、浏览器与服务器通信过程 1.1 域名解析与连接建立 1.2 数据交互 1.3 连接管理 二、HTTP请求报头 2.1 请求行 2.2 请求报头 2.3 空行 2.4 请求体 三、HTTP应答报头 3.1 http应答报文头部信息 1. 状态行 2. 服务器名称 3. 数据长度 4…...

ECharts散点图-散点图7,附视频讲解与代码下载

引言&#xff1a; ECharts散点图是一种常见的数据可视化图表类型&#xff0c;它通过在二维坐标系或其它坐标系中绘制散乱的点来展示数据之间的关系。本文将详细介绍如何使用ECharts库实现一个散点图&#xff0c;包括图表效果预览、视频讲解及代码下载&#xff0c;让你轻松掌握…...

蓝桥杯之二分法(二)

存在某条件使得一边均满足&#xff0c;一边均不满足&#xff1a; 如果问题满足某种条件&#xff0c;使得在某个点之前的所有值都满足条件&#xff0c;而之后的所有值都不满足条件&#xff08;或反之&#xff09;&#xff0c;那么可以使用二分法来找到这个边界。 1.问题的解具有…...

当 AI 有了 “万能插头” 和 “通用语言”:MCP 与 A2A 如何重构智能体生态

目录 一、MCP&#xff1a;让 AI 拥有 “万能工具插头” 1.1 从 “手工对接” 到 “即插即用” 1.2 架构解密&#xff1a;AI 如何 “指挥” 工具干活 1.3 安全优势&#xff1a;数据不出门&#xff0c;操作可追溯 二、A2A&#xff1a;让智能体学会 “跨语言协作” 2.1 从 “…...

从零开始 保姆级教程 Ubuntu20.04系统安装MySQL8、服务器配置MySQL主从复制、本地navicat远程连接服务器数据库

从零开始&#xff1a;Ubuntu 20.04 系统安装 MySQL 8、服务器配置 MySQL 主从复制、本地 Navicat 远程连接服务器数据库 初始化服务器1. 更新本地软件包列表2. 安装 MySQL 服务器3. 查看 MySQL 安装版本4. 登录 MySQL 管理终端5. 设置 root 用户密码&#xff08;推荐使用 nativ…...

PHP序列化/反序列化漏洞原理

PHP反序列化原理详解 引言 PHP反序列化是PHP中一个重要的概念&#xff0c;它允许将序列化后的数据重新转换为原始的数据结构。在PHP中&#xff0c;可以使用serialize()函数将数据序列化为字符串&#xff0c;然后使用unserialize()函数将序列化后的字符串反序列化为原来的数据结…...

并查集(力扣2316)

这种涉及不同连通分量的&#xff0c;看上去就可以用并查集。并查集的模板请参见上一篇内容。并查集&#xff08;力扣1971&#xff09;-CSDN博客 现在我们要求的是无法互相到达的点对。根据观察易得&#xff0c;我们只需要求出每个并查集的元素数量&#xff0c;然后遍历每个点&…...

【web服务_负载均衡Nginx】一、Nginx 基础与核心概念解析

一、Nginx 概述&#xff1a;从起源到行业地位​ Nginx&#xff08;发音为 “engine x”&#xff09;是一款高性能的开源 Web 服务器、反向代理服务器&#xff0c;同时具备负载均衡、内容缓存、TCP/UDP 代理及邮件代理等功能。它由俄罗斯工程师伊戈尔・赛索耶夫&#xff08;Igo…...

【Python入门】文件读取全攻略:5种常用格式(csv/excel/word/ppt/pdf)一键搞定 | 附完整代码示例

大家好&#xff0c;我是唐叔&#xff01;今天给大家带来一篇Python文件读取的终极指南。无论是数据分析、办公自动化还是爬虫开发&#xff0c;文件读取都是Python程序员必须掌握的核心技能。本文将详细介绍Python处理5大常用文件格式的方法&#xff0c;包含完整可运行的代码示例…...

考研系列-计算机网络冲刺考点汇总(下)

写在前面 本文将总结王道408考研课程的计算机网络冲刺考点的第四章到第六章内容&#xff08;网络层、传输层、应用层&#xff09;。 第四章、网络层 1.SDN SDN的基本概念 注意对应关系&#xff1a;数据平面-转发&#xff1b;控制平面-路由选择 2.路由选择算法 (1)RIP协议-基于…...

GitLab-CI集成FTP自动发布

简介 在某些场景下&#xff0c;代码是以 FTP 的方式部署到服务器上&#xff0c;那么我们可以使用 GitLab-CI 来实现自动发布。 配置参考 .sftp-deploy: &sftp-deploy |-files$(git log -10 --prettyformat: --name-only | grep -v ^$ | sort -u)include_patterns$(echo …...

Ubuntu 安装cuda踩坑记录

Ubuntu 安装cuda踩坑记录&#xff1a; 运行run文件时出错&#xff1a; sh cuda_12.4.0_550.54.14_linux.run 报错&#xff1a; ./cuda-installer: error while loading shared libraries: libxml2.so.2: cannot open shared object file: No such file or directory 解决&am…...

用GitHub Actions实现CI/CD

目录 简介GitHub Actions基础工作流配置文件实战案例 Node.js应用Python应用Docker容器构建与部署 最佳实践常见问题与解决方案总结 简介 持续集成/持续部署(CI/CD)已成为现代软件开发不可或缺的一部分。它通过自动化构建、测试和部署过程&#xff0c;帮助开发团队更快、更可…...

使用AI工具打造专业级PPT的完整方案,结合 DeepSeek构思、Kimi生成内容、Napkin优化设计 等工具,分阶段详细说明流程及工具使用

以下是使用AI工具打造专业级PPT的完整方案&#xff0c;结合 DeepSeek构思、Kimi生成内容、Napkin优化设计 等工具&#xff0c;分阶段详细说明流程及工具使用&#xff1a; 一、全流程阶段划分 阶段目标核心工具1. 构思阶段明确主题、结构、核心信息&#xff0c;生成大纲与逻辑…...

【数据结构】线性表( List)和 顺序表(ArrayList)

【数据结构】线性表&#xff08; List&#xff09;和 顺序表&#xff08;ArrayList&#xff09; 一、线性表 List二、List 接口的常用方法三、ArrayList与顺序表3.1 引入顺序表的原因&#xff1f;3.2 ArrayList 的使用3.2.1 ArrayList 的创建3.2.2 添加元素&#xff1a;list.ad…...

嵌入式开发--STM32软件和硬件CRC的使用--续篇

本文是《嵌入式开发–STM32软件和硬件CRC的使用》的续篇&#xff0c;又踩到一个坑&#xff0c;发出来让大家避一下坑。 按照G0系列的设置&#xff0c;得出错误的结果 前文对应的是STM32G0系列&#xff0c;今天在用STM32G4系列时&#xff0c;按照前文的设置&#xff0c;用硬件…...

探索鸡养殖虚拟仿真实验:科技赋能养殖新体验

在科技飞速发展的今天&#xff0c;虚拟仿真技术逐渐渗透到各个领域&#xff0c;就连传统的养殖业也迎来了数字化的变革。最近&#xff0c;我参与了一场别开生面的鸡养殖虚拟仿真实验&#xff0c;不仅学到了专业的养殖知识&#xff0c;还收获了前所未有的沉浸式体验。现在&#…...

知识图谱中医知识问答系统|养生医案综合可视化系|推荐算法|vue+flask+neo4j+mysql

文章结尾部分有CSDN官方提供的学长 联系方式名片 文章结尾部分有CSDN官方提供的学长 联系方式名片 关注B站&#xff0c;有好处&#xff01; ✅编号 :F040 pro ✅技术架构: vueflaskmysqlneo4jltpac ✅实现功能&#xff1a;实现基于中医药材和药方的知识图谱可视化&#xff0c;在…...

【AI】——结合Ollama、Open WebUI和Docker本地部署可视化AI大语言模型

&#x1f3bc;个人主页&#xff1a;【Y小夜】 &#x1f60e;作者简介&#xff1a;一位双非学校的大三学生&#xff0c;编程爱好者&#xff0c; 专注于基础和实战分享&#xff0c;欢迎私信咨询&#xff01; &#x1f386;入门专栏&#xff1a;&#x1f387;【MySQL&#xff0…...

AI 模型高效化:推理加速与训练优化的技术原理与理论解析

AI 模型高效化&#xff1a;推理加速与训练优化的技术原理与理论解析 文章目录 AI 模型高效化&#xff1a;推理加速与训练优化的技术原理与理论解析一、推理加速&#xff1a;让模型跑得更快的“程序员魔法”&#xff08;一&#xff09;动态结构自适应推理&#xff1a;像人类一样…...

python学习—详解word邮件合并

系列文章目录 python学习—合并TXT文本文件 python学习—统计嵌套文件夹内的文件数量并建立索引表格 python学习—查找指定目录下的指定类型文件 python学习—年会不能停&#xff0c;游戏抽签抽奖 python学习—循环语句-控制流 python学习—合并多个Excel工作簿表格文件 pytho…...

vscode与vim+cscope+tags热键冲突

[ctrl w] s 对于vim时水平分割窗口热键 对vscode, [ctrl w]时关闭当前窗口热键 在vscode中如下配置可以发送热键到shell, 跳过vscode:...

直播系统源码开发:解锁幸运礼物功能的商业魔力与运营策略

在当今如火如荼的直播经济中&#xff0c;幸运礼物功能已成为平台提升用户黏性、刺激消费的"黄金按钮"。山东布谷科技将深入剖析幸运礼物功能的技术逻辑与商业价值&#xff0c;并为运营者提供一套完整的策略框架&#xff0c;帮助您在激烈的直播赛道中脱颖而出。 一、…...

毕业设计效率提升工具与避坑指南

本文为毕业设计后的经验记录&#xff0c;包含写作过程中的一些实用工具和注意事项。 一、&#x1f4cc;实验及写作实用技巧二、&#x1f680; 效率提升工具三、&#x1f4ca;论文完成后的格式检查 本文为毕业设计后的经验记录&#xff0c;包含写作过程中的一些实用工具和注意事…...

Python网络爬虫设计(二)

目录 六、BeautifulSoup库 1、常见的提取分析网页内容的三种方式 &#xff08;1&#xff09;正则表达式 &#xff08;2&#xff09;BeautifulSoup库 &#xff08;3&#xff09;pyppeteer库中的元素查找函数 2、HTML中的tag 3、BeautifulSoup库的安装和导入 4、Beautiful…...

滑动窗口209. 长度最小的子数组

1.题目 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度。如果不存在符合条件的子数组&#xff0c;返回 0 。 示例 1&#xff1a; 输入&…...

如何避免被目标网站识别为爬虫?

文章目录 前言1. 合理设置请求头2. 控制请求频率3. 模拟真实用户行为4. 使用代理 IP5. 处理验证码6. 会话管理 前言 为避免被目标网站识别为爬虫&#xff0c;可从请求头设置、请求频率控制、模拟用户行为、使用代理、处理验证码和会话管理等多个方面采取措施&#xff0c;以下是…...

Dell戴尔服务器 PowerEdge R750xs + window server2012r2 || 2016

因要求需要给新服务器装个 win server2012或者2016系统 XXX使用U盘制作PE系统U盘安装系统不行&#xff0c;适合普通win8&#xff0c;win10&#xff0c;win11U盘制作PE系统U盘安装win10系统教程U盘制作PE系统U盘安装win10系统教程https://mp.weixin.qq.com/s/t0W8aNJaHPAU8T78nh…...

如何通过数据分析提升软件开发项目的成功率?

引言 在软件开发中&#xff0c;项目延期、超预算、需求反复变更等问题屡见不鲜。数据分析作为项目管理的重要工具&#xff0c;正在被越来越多的企业用于提升项目成功率。通过科学利用项目数据&#xff0c;团队可以做出更准确的决策&#xff0c;避免重复踩坑&#xff0c;从而大幅…...

模型的RAG

RAG 什么是RAG 当岳不群相当武林的盟主时候&#xff0c;你的给他一个葵花宝典(秘籍RAG) RAG的原理 建立索引&#xff1a; 首先要清洗和提取原始数据&#xff0c;将 PDF、Docx等不同格式的文件解析为纯文本数据 然后将文本数据分割成更小的片段&#xff08;chunk&#xff09…...

基于多模态双路TCN-SE-YOLO的小目标检测

首先声明:该思路在小目标检测领域尚未有成果发表,感兴趣的小伙伴可以借鉴! 一、引言 1.1 研究背景 小目标检测在交通监控(车牌识别)、工业检测(PCB缺陷)及农业(病虫害斑点)等领域具有重要应用价值传统单模态检测方法在复杂场景下的漏检率高达40%以上(VisDrone 2021…...

idea maven 命令后控制台乱码

首先在idea中查看maven的编码方式 执行mvn -v命令 查看编码语言是GBK C:\Users\13488>mvn -v Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: D:\maven\apache-maven-3.6.3\bin\.. Java version: 1.8.0_202, vendor: Oracle Corporation, runt…...

在Vmware15(虚拟机免费) 中安装纯净win10详细过程

一、软件备选 1. VMware15.5.1 网盘下载地址 链接: https://pan.baidu.com/s/1y6GLJ2MG-1tomWblt3otsg?pwdim8e 提取码: im8e 2. windows镜像下载 去官网下载ios包 链接&#xff1a;https://www.microsoft.com/zh-cn/software-download/windows10 二、在VMware15.5.1下安装w…...

RISC-V 与 OpenHarmony 的结合意义与应用建议

RISC-V 与 OpenHarmony 的结合意义与应用建议 一、结合的意义 &#xff08;一&#xff09;硬件与软件的协同创新 RISC-V 作为硬件层的开源指令集架构&#xff0c;为 OpenHarmony 提供了强大的硬件支持。这种支持不仅体现在硬件性能的提升上&#xff0c;还为 OpenHarmony 的分…...

让SQL飞起来:搭建企业AI应用的SQL性能优化实战

我上一篇文章已经讲解过了如何使用公开的AI模型来优化SQL.但这个优化方法存在一定的局限性.因为公开的AI模型并不了解你的数据表结构是什么从而导致提供的优化建议不太准确.而sql表结构又是至关重要的安全问题,是不能泄露出去的.所以在此背景下我决定搭建一个自己的AI应用在内网…...

驱动开发硬核特训 · Day 14:深入理解 Power 管理驱动架构与实战应用

在嵌入式系统中&#xff0c;Power&#xff08;电源&#xff09;管理驱动既关乎系统稳定性&#xff0c;又直接影响功耗与续航&#xff0c;是系统设计中绕不开的核心模块。今天我们通过理论实战的形式&#xff0c;一次性讲清楚&#xff1a; Linux 中电源管理驱动的核心框架Regul…...

备份思科路由器设备文件实例

实例需求: (1)备份路由器的配置文件startup-config和映像文件 (2)备份交换机的配置文件startup-config和映像文件 注:PC3为TFTP服务器 结构示意图: 实例配置一: 备份路由器的配置文件startup-config和映像文件 步骤: 在PC3上打开tftp服务。确保PC3可以ping通11.1.1.…...