目录
简单介绍
在讲解完上一篇文章之后,看过的人我相信对本篇文章要讲的内容已经有基本轮廓了,因为我这篇要讲的内容就是把业务功能切换成聊天室,把聊天室一讲之后我相信大家对udp网络通信的基本逻辑就有了更深刻的认知,那么废话不多说,我们直接开始。
代码部分
我先将原先没变过的3个老朋友代码展示一下
网络地址封装类InetAddr.hpp
cpp#pragma once #include <iostream> #include <string> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.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_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); // 将addr进行转换 } std::string AddrStr() { return _ip + ":" + std::to_string(_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; } ~InetAddr() { } private: std::string _ip; uint16_t _port; struct sockaddr_in _addr; };
日志类Log.hpp
cpp#pragma once #include <iostream> #include <string> #include <unistd.h> #include <sys/types.h> #include <ctime> #include <stdarg.h> #include <fstream> #include <string.h> #include <pthread.h> 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 "UNKNOW"; } } 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 2 const std::string glogfile = "./log.txt"; pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER; 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) { pthread_mutex_lock(&glock); switch (_type) { case SCREEN_TYPE: FlushLogToScreen(lg); break; case FILE_TYPE: FlushLogToFile(lg); break; } pthread_mutex_unlock(&glock); } 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) }
防拷贝类nocopy.hpp
cpp#pragma once // 防拷贝 class nocopy { public: nocopy() {} ~nocopy() {} nocopy(const nocopy &) = delete; const nocopy &operator=(const nocopy &) = delete; };
这三个老朋友展示完成之后我们就来认识认识新朋友。
线程封装类Thread.hpp
cpp#pragma once #include <iostream> #include <unistd.h> #include <string> #include <functional> #include <pthread.h> namespace ThreadMudle { 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(); // 调用回调函数func的方法 return nullptr; } bool Start() // 启动线程 { //::强调此调用为系统调用 int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this); if (n != 0) return false; // 创建失败返回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; // 线程id bool _isrunning; // 线程是否在运行 func_t _func; // 线程要执行的回调函数 }; }
大家一看到这个头文件是不是有种似曾相识的感觉?没错,我们在讲线程池的时候我们就讲过它了,我们这里要将他们再拿回来用用,因为我们聊天室肯定不是一个人在那里聊天,肯定是多个人一起聊才叫聊天室嘛,所以我们这里要用到多线程,恰好我们以前写过多线程的代码,我们这里直接就将它拿来用就再合适不过了。为此,我会重新将这部分再给大家简单梳理梳理,为了使我们本次项目的运行逻辑能更加通顺这部分大家得有个清晰的认知。
这一块是我们封装的线程,我们来看看它能提供什么功能
我们先来看看成员变量:这里既然是我们对线程的封装那么我们的主角pthread_t类型的变量是一定要有的。
pthread_t
是 Linux 系统中用于表示线程的数据类型 。它是在 POSIX 线程(通常称为 pthreads)标准中定义的,是 Linux 和其他类 Unix 系统(如 macOS、BSD 等)上进行多线程编程的核心组成部分。有了这个变量只能满足我们对线程进行操作,但是还不满足我们的需求,我们为了更好的使用这个线程类型的对象,我们还得需要知道它的线程名(我们自己定义),还有这个线程对象是否正在运行,以及我们的这个线程要执行的方法是什么。
我们要能够使我们的聊天室各个成员之间能够互相通信我们就得达成一个回调函数的"协议",这层协议能使我们的代码编写更具有逻辑性和安全性。要想知道怎么设计这个函数包装器我们就得知道上层是怎么运行它的,这里我就干脆跟大家说好了,我们聊天室的每一个成员就是一个线程,每个线程之间互相发送数据,作为我们用户我们关心的是谁把数据发给我了,以及我要把数据发给谁,所以我们线程这里也是一样,我们得告诉对方我们这个线程叫什么,因此我们的参数设计的就是一个字符串类型。
我们的构造函数要做的事情很简单,就是将我们的线程对象赋予名字和要执行的回调函数。
我们创建好了Thread类型的对象接下来就要启动他们,大家注意我们并没有实际的创建线程,只是对我们封装的Thread进行了命名和传递回调函数。
所以,到启动部分才是我们真正的创建线程,ThreadRoutine就是我们这个线程要执行的线程函数。
我们所有创建的新线程都会执行这个函数,而我们的参数就是我们封装的Thread类型的对象本身。我们的线程函数要执行的行为是先将我们获取的当前对象进行安全的类型转换,然后调用我们的回调函数(业务),但是我们的这个回调函数还需要再包装一层,因为我们要真正的做到对整个程序的解耦。
而这个函数的功能其实就是调用我们的func,只不过我们顺带设置了这个线程的运行状态。
这个函数的作用就是获取我们当前的状态,究竟是运行还是休眠。
这段代码实现了一个名为
Stop()
的成员函数,其设计目的是:异步地请求中止一个正在运行的线程,并更新该线程的运行状态标志。具体执行步骤:
条件检查 :
首先检查
_isrunning
标志是否为true
,确保目标线程确实处于运行状态。发送取消请求 :
如果线程正在运行,调用
::pthread_cancel(_tid)
。这个函数向_tid
所标识的线程发送一个取消请求。更新状态标志 :
立即将
_isrunning
标志设置为false
,表明该线程现在被视为"非运行"状态。关键行为特征:
异步操作 :函数发送取消请求后立即返回,不会等待目标线程实际终止。
状态更新 :无论取消请求是否会被目标线程处理或何时处理,它都会立即将
_isrunning
设置为false
。非阻塞 :调用
Stop()
的线程不会被阻塞,它会继续执行后续代码。简单来说:这段代码的功能是"通知目标线程它应该退出了,同时将本地的运行状态标记为已停止",但它不保证线程会立即停止,也不等待线程停止的确认。
这段代码明显是有改进空间的,只不过在我们的这个项目中不会出现这些问题,所以我们用这种最简单的代码方式来实现中止的功能。
中止完后我们就要进行等待回收的操做。
我们再提供一个返回我们的这个线程对象名字的接口。
线程池类ThreadPool.hpp
cpp#pragma once #include <iostream> #include <unistd.h> #include <string> #include <unistd.h> #include <vector> #include <functional> #include <queue> #include <pthread.h> #include "Thread.hpp" #include "Log.hpp" using namespace log_ns; using namespace ThreadMudle; // 开放封装好的线程的命名空间 static const int gdefaultnum = 5; // 线程池的个数 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 obj %s done, init sucess\n", threadname.c_str()); } } void Start() // 复用封装好的线程类里面的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> &t) = delete; void operator=(const ThreadPool<T> &t) = delete; public: void Stop() // 停止执行任务 { LockQueue(); _isrunning = false; WakeupAll(); UnlockQueue(); LOG(INFO, "thread Pool Stop Success!\n"); } static ThreadPool<T> *GetInstance() { if (_tp == nullptr) { pthread_mutex_lock(&_sig_mutex); if (_tp == nullptr) { LOG(INFO, "creat threadpool\n"); _tp = new ThreadPool<T>(); _tp->Init(); _tp->Start(); } else { LOG(INFO, "get threadpool\n"); } pthread_mutex_unlock(&_sig_mutex); } return _tp; } void Equeue(const T &in) // 生产任务 { LockQueue(); if (_isrunning) { _task_queue.push(in); if (_sleep_thread_num > 0) { Wakeup(); // 唤醒之前Sleep的线程 } } 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; // 条件变量 // 单例模式 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;
这个线程池我们前面有一篇文章专门讲过,我这里就不细致说了,把逻辑说一下就可以了,毕竟我们的本篇文章重点不在这。
我们这个线程池的目的简单来讲就是为我们封装好的线程做更上一层的统筹管理。比如基础的用pthread_mutex_t进行保证原子操作的加锁去锁阶段,以及用pthread_cond_t来进行对线程的唤醒和休眠操作,我们使用两种数据结构来分别管理我们的每个线程(顺序表)和每个任务(队列),我们的线程池的主要函数有创建线程池,启动线程池函数和一个处理任务函数,那么这个线程池究竟是怎么运作的,我在这一层是没法说清楚的,我们来看它的更上一层大家就会清晰里面的逻辑,在讲这更上一层业务类的时候我就会帮大家理清楚其中的门道。
业务类Route.hpp
cpp#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" using task_t = std::function<void()>; class Route { public: Route() { pthread_mutex_init(&_mutex, nullptr); } void CheckOnlineUser(InetAddr &who) { pthread_mutex_lock(&_mutex); for (auto &user : _online_user) { if (user == who) { LOG(DEBUG, "%s is exists\n", who.AddrStr().c_str()); pthread_mutex_unlock(&_mutex); return; } } LOG(DEBUG, "%s is not exists, add it\n", who.AddrStr().c_str()); _online_user.push_back(who); pthread_mutex_unlock(&_mutex); } void Offline(InetAddr &who) { pthread_mutex_lock(&_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); pthread_mutex_unlock(&_mutex); break; } } pthread_mutex_unlock(&_mutex); } void ForwardHelper(int sockfd, const std::string &message, InetAddr who) { pthread_mutex_lock(&_mutex); std::string send_message = "[" + who.AddrStr() + "]#" + message; for (auto &user : _online_user) { struct sockaddr_in peer = user.Addr(); LOG(DEBUG, "Forward message to %s, message is %s\n", user.AddrStr().c_str(), send_message.c_str()); ::sendto(sockfd, send_message.c_str(), send_message.size(), 0, (struct sockaddr *)&peer, sizeof(peer)); } pthread_mutex_unlock(&_mutex); } void Forward(int sockfd, const std::string &message, InetAddr &who) { // 1. 该用户是否在 在线用户列表中呢?如果在,什么都不缺,如果不在,自动添加到_online_user CheckOnlineUser(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); ThreadPool<task_t>::GetInstance()->Equeue(t); } ~Route() { pthread_mutex_destroy(&_mutex); } private: std::vector<InetAddr> _online_user; // 聊天室在线人数 pthread_mutex_t _mutex; };
这个类就是我们聊天室的核心实现类了。我们一个一个来看:
先来看我们的成员变量,既然是聊天室那么聊天室的各个成员的相关信息我们得是要知道的,在这里我们的成员信息比较简单,就是网络地址信息,我们只需要知道双方的地址我们就可以向对方发送消息,所以我们这里的类型就是InetAddr这个我们封装好的网络地址类就够了,我们还需要一个锁来保证我们的操作都是原子的,保证我们多线程的安全稳定运行。
我们还是需要我们的函数包装器来使我们的任何对象都能直接调用我们的公共方法。
初始化我们这里就只需要给我们的锁进行初始化创建就好了。
我们先来把简单的函数功能给讲了,这是我们成员上线的函数,外部传一个网络地址对象过来,我们循环遍历我们的顺序表来看看成员是否存在,存在就打印它的日志信息不做处理,不存在就讲他的信息添加到我们的顺序表里。中间return的时候不要忘记了解锁。
有上线自然也有下线,这个就是我们的成员下线的函数,上线也好下线也罢我们的操作都得要是原子的,所以我们的锁都是要加上的。下线跟上线的操作差不多我们都是要对顺序表进行遍历操作,找到了就对其进行删除,没找到也就不做处理。
接下来就是我们的转发函数,欸?转发是什么意思呢?转发转发顾名思义就是将对象转发给别人,欸?转发给谁呢?转发给的就是我们的线程池,我们的线程池再将这个任务分配给空闲的线程,到这一步大家是不是突然有种逻辑清晰起来了的感觉?还没完,等我讲到后面大家就会对这个逻辑有更清晰的认知。
我们的转发逻辑是这样的:用户上线了,我们的服务器要创建sockfd用来接收数据和发送数据,我们还要知道客户的网络地址,我们的这三个参数就是以此来设计的,进入转发阶段,我们得先将用户添加到我们的在线用户列表中,然后我们规定了一个规则就是当用户输入"QUIT"或"Q"时,代表用户想要退出聊天室,我们就将他下线,否则这条信息就是用户想说的话,我们就将这句话交给下一个功能实现函数,让它代替去完成,这是为了更好的让每个模块之间进行解耦,我们也可以看到基础的写法确实也是我们注释的那行那样直接调用函数,但是我们有一个进阶的写法,就是绑定成一个包装器,这样的好处是什么呢?
将转发 ForwardHelper 函数绑定为任务包装器(通过 std::bind 生成可调用对象)并提交到线程池执行,主要有以下几个重要好处,尤其在网络编程场景中价值显著:
异步处理提升并发性能
直接调用
ForwardHelper
会在当前 IO 线程中同步执行消息转发逻辑(如遍历在线用户、调用 send 发送数据)。如果在线用户多或网络 IO 阻塞,会阻塞当前线程处理新的客户端连接 / 消息。通过线程池异步执行,当前 IO 线程可以立刻返回继续处理其他客户端请求,大幅提升服务器的并发处理能力。我们本项目引入线程池的最核心的提升方式也是在这里。
解耦 IO 处理与业务逻辑
网络编程中通常建议 IO 线程(处理 accept/recv)尽量轻量,避免在 IO 线程中执行耗时业务逻辑。
用线程池处理转发任务后,IO 线程只负责 "接收消息",业务逻辑(消息转发)交给线程池,符合 "职责单一" 原则,模块间耦合度更低。
控制资源占用
线程池可以限制最大线程数量,避免因并发请求过多导致线程创建泛滥(线程创建 / 销毁成本高)。
若直接调用
ForwardHelper
并在其中创建线程,可能导致系统线程数量失控,引发性能问题。便于扩展任务调度能力
基于线程池可以方便地添加任务优先级、延迟执行、任务队列监控等功能。例如:可以给重要用户的消息设置更高优先级,确保优先转发。
简化异常处理
线程池中可以统一捕获任务执行过程中的异常,避免单个任务的崩溃影响整个 IO 线程。若直接在 IO 线程中执行,一个未捕获的异常可能导致整个服务器进程退出。
也就是说,这种做法本质上是将 "IO 密集型操作" 与 "CPU / 业务密集型操作" 分离,通过线程池实现资源的合理分配和任务的异步调度,这在高并发网络服务器中是一种非常经典的优化模式。
我们再将这个给每个在线用户发送数据的方法转发给线程池让它去派各个空闲线程去进行处理就好了。
这个就是负责我们真正消息转发的接口,就是将我们的消息遍历式的发给每个用户。
客户端源文件
cpp#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" // 客户端未来一定要知道服务器的IP地址和端口号 // ./udpclient 127.0.0.1 8888 using namespace ThreadMudle; int InitClient() { int sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); if (sockfd < 0) { std::cerr << "create socket error" << std::endl; exit(1); } return sockfd; } void RecvMessage(int sockfd, const std::string &name) { while (true) { struct sockaddr_in peer; socklen_t len = sizeof(peer); char buffer[1024]; int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len); if (m > 0) { buffer[m] = 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 + "# "; 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)); 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(); // client 的端口号,一般不让用户自己设定,而是让client OS随机选择?怎么选择,什么时候选择呢? // client 需要bind它自己的IP和端口,但是client不需要"显示"bind它自己的IP和端口 // client 在首次向服务器发送数据的时候,OS会自动给client bind它自己的IP和端口 Thread recver("recver-thread", std::bind(&RecvMessage, sockfd, std::placeholders::_1)); 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; }
我们客户端的作用就是稳定维持用户的发送数据和接收数据,这两个业务逻辑我们就使用两个线程来完成。
这开头对命令行参数的处理是基本的操作,更上一篇文章我们讲的业务是一样的。
为了更好的对我们的程序进行解耦,我们单独将创建sockfd的代码拿出来实现。
然后就是将我们的发送和接收两个业务逻辑的线程创建好,将各自的任务通过bind交付进去并启动,当结束的时候对两个线程进行回收最后再关闭文件描述符就可以了。
这个就是我们的接收数据的接口,我们的接收逻辑就是跟正常的一样的,等待别人的数据到来接收它,当接收错误的时候就结束。
这个就是我们的发送数据业务接口,发送数据除了我们之前讲过的sento之外我们还多了一点东西,一个就是名字,因为这回我们的业务功能是聊天,所以我们还需要传递我们的线程名,但是我们之前讲Thread类的时候讲过,我们的函数包装器只有一个参数,所以我们才需要在初始化时传递这个参数的时候bind绑定一下前面几个参数的对象。第二个就是多了个接收一行数据的getline,获取用户想输入的对话。
服务器端源文件
cpp#include "UdpServer.hpp" #include "Route.hpp" #include <memory> // ./udpserver 8888 int main(int argc, char *argv[]) { // std::string ip = "127.0.0.1"; // 本主机 localhost 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); usvr->InitServer(); usvr->Start(); return 0; }
我们先来讲源文件再来了解头文件的具体实现会比较清晰。
我们服务器开启的时候我们还是先要获取用户输入的命令行参数来确定端口号,然后将日志模式设置成打印到屏幕上。
所以我们的这段代码还是不变的。
我们的修改也就只涉及到我们的调用业务代码部分,我们需要将业务的对象创建出来,然后将要调用的业务函数跟它的具体实例以及三个要填入的参数进行绑定,形成一个可调用对象,然后我们再创建服务器对象,用智能指针来封装它对它进行操作。我们调用服务器头文件的创建服务器接口以及启动服务器接口我们的源文件的任务就完成了。
服务器端头文件UdpServer.hpp
cpp#pragma once #include <iostream> #include <unistd.h> #include <string> #include <cstring> #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" #include <functional> using namespace log_ns; // 打开日志的命名空间 static const int gsocketfd = -1; // default socket套接字文件描述符 static const uint16_t glocalport = 8888; // default 端口号 enum { SOCKET_ERROR = 1, BIND_ERROR }; using service_t = std::function<void(int, const std::string &message, InetAddr &who)>; // UdpServer user("192.1.1.1",8888); // 一般服务器主要是用来进行网络数据读取和写入的。IO的。 // 服务器 IO逻辑 和 业务逻辑 解耦 class UdpServer : public nocopy { public: // UdpServer(const std::string &localip, uint16_t localport = glocalport) UdpServer(service_t func, uint16_t localport = glocalport) : _func(func), _socketfd(gsocketfd), _localport(localport), _isrunning(false) { } void InitServer() { // 1.创建socket文件 _socketfd = ::socket(AF_INET, SOCK_DGRAM, 0); // AF_INET网络通信方式创建,SOCK_DGRAM udp协议 if (_socketfd < 0) { LOG(FATAL, "socket error\n"); exit(SOCKET_ERROR); } LOG(DEBUG, "socket create success,_sockfd: %d\n", _socketfd); // 2.bind struct 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());//需要4字节、需要网络序列的ip local.sin_addr.s_addr = INADDR_ANY; // 服务器端,进行任意ip地址绑定0 int n = ::bind(_socketfd, (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(_socketfd, 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(_socketfd, message, addr); LOG(DEBUG, "return udpserver\n"); } else { std::cout << "recvfrom , error" << std::endl; } } _isrunning = false; } ~UdpServer() { if (_socketfd > gsocketfd) ::close(_socketfd); } private: int _socketfd; // 套接字fd//读写都用同一个sockfd,反应说明:UDP是全双工通信的! uint16_t _localport; // 端口号 // std::string _localip; // ip bool _isrunning; // 服务端业务是否执行 service_t _func; };
这一部分内容我们是没有变动过的,这里我就不细说了。
这个可调用对象类型就是根据我们的业务接口去设计的,如下:
我们的成员变量也是不需要改变的,其实基本上我们的服务器也不需要做什么太大的改动,这就是我们层层解耦的好处,写的时候会累一点,但是后续的维护与更新就要轻松得多,不信大家往后面看。
我们的构造函数也不需要变。
甚至我们的创建服务器接口也是不需要改变的。因为我们的创建服务器接口的功能是创建绑定sockfd,这一块代码是通用的。
最后看看我们的启动服务器,我们的服务器启动也是基本上不需要什么改动,因为我们服务器启动的目的就是接收客户端传递来的数据做响应处理,我们又对代码进行过层层解耦,所以实际上我们只需要替换我们的_func业务函数就可以了(实际就是对参数的修改)。
到这一步,相信大家已经清楚了解耦的好处,我们替换业务的时候确实是很方便。
运行截图
如图所示,左边的窗口是我们的服务器,中间和右边的是我们的两个客户端,双方发的消息双方都能看到,且网络地址确实是不同的,我们的简易聊天室确实是成功了。
当其中一个退出时,服务器的相关线程也会退出。
UDP-Server(3)chat聊天室
板鸭〈小号〉2025-09-15 17:42
相关推荐
Nuyoah11klay5 小时前
华清远见25072班网络编程学习day5weixin_456904276 小时前
使用HTTPS 服务在浏览器端使用摄像头的方式解析CyHacker_10106 小时前
网络编程-day4疯狂的维修9 小时前
关于Gateway configration studio软件配置网关wow_DG9 小时前
【WebSocket✨】入门之旅(五):WebSocket 的安全性会开花的二叉树10 小时前
UDP Socket 进阶:从 Echo 到字典服务器,学会 “解耦” 网络与业务-SGlow-10 小时前
Linux相关概念和易错知识点(45)(网络层、网段划分)阿部多瑞 ABU10 小时前
《学校机房终端安全全链条攻防分析与防御体系建设报告》