> 技术文档 > Linux网络编程:基于UDP 的聊天室雏形

Linux网络编程:基于UDP 的聊天室雏形


前言:

大家好,我们之前的两天已经实现了基于UDP的网络通信。

我们已经能够将我们的信息基于客户端通过网络发送给我们的服务端。这是用户与服务器之间的互动。

那我们今天就来继续拓展这个代码,使其可以实现一个聊天室的简单效果。

如果我们的聊天室能够实现,那么用户对用户的单人聊天,自然也能就实现出来了。

一、如何拓展

首先我们之前的代码只能让用户将消息发送到服务端,也就是只实现了用户内容,信息的接受工作。

如果想让其他用户能够看见发送的消息,是不是还需要有一个把消息转发的过程啊!!

所以我们就需要实现一个模块,负责我们的消息转发。

但是转发给谁?这是个问题,有多少人连接到了这个服务器?我们需要知道吗?

答案是需要的,我们不仅要知道有多少人,我们还要对这些连接进来的用户进行管理。

把这些实现后,我们如何让服务端从消息接收再转到消息转发呢?

请大家思考一下。

如果多个人都在发送消息,你一个线程一个线程的去搬运,是不是就来不及啊?

如果有多个人发送消息,我们也有多个线程去轮循获取,然后再执行转发这条消息给每一个用户,我们是不是就能完美的执行这个同步消息的功能了?

这个像不像我们之前所讲的生产者消费者模型啊?

这就是一个生产者消费者模型。还记得我们之前说生产者消费者模型时的疑惑,这个生产者生产的数据从哪里来啊?

答案就是从网络里来。而我们刚刚所说的多线程的实现形式是什么啊?

就是我们的线程池!

那么我们就开始对我们之前的代码进行改造吧!!


一、用户类的实现

我们先创建一个用户类头文件,实现一个用户类:

为了我们的代码的拓展性,我们这里选择使用继承的方式:就是实现一个基类代表这个一个用户,随后我们可以新建多个类来继承他,这就象征着用户类型的不同。体现出面向对象的思想。

#pragma once#ifndef USER_HPP#define USER_HPP#include #include \"InetAddr.hpp\"#include \"log.hpp\"using namespace LogModule;class UserInterface{public: virtual ~UserInterface() = default;//为了防止调用接口时,调用到UserInterface的析构函数,导致析构函数调用不明确 virtual void Sendto(int sockfd,const std::string &message) = 0;//纯虚函数必须要求子类对纯虚函数进行重定义实现};class User : public UserInterface//public继承{public: void Sendto(int sockfd,const std::string &message) override//sendto要使用到文件描述符和信息都是从外界传入的 { LOG(LogLevel::DEBUG) << \"sendto message to: \"; } ~User() { }private: InetAddr _adr; // 用户的地址};#endif

 我们之前用户的地址等信息封装到了我们的InetAddr类中,我们这里同样也是用该类来记录每个用户的IP地址端口等信息。

希望同学没有忘记我们C++的语法:继承与多态的相关知识


二、用户管理

然而这只是一个用户。我们将来会有许多用户进入,所以还要定义一个专门对用户进行管理的类型,我们称为UserManager。为了耦合性,我们选择把这个管理类新开一个头文件:
 

class UserManager{ public: UserManager() {} ~UserManager() {} void AddUser(InetAddr & id)//增加用户到我们的链表中 {} void DelUser(InetAddr& id)//删除用户 {} private: std::list<std::shared_ptr> _users; // 存储用户的列表,我们这里用基类指针(或引用)指向子类对象,实现多态};

在这里我们使用了一个链表来对用户进行管理。

我们的添加用户和删除用户接口的参数为什么是一个InetAddr类型呢?

这是因为我们用InetAddr封装了这些信息,可以唯一标识一个用户连接。在User中分辨User的也是通过这个InetAddr。

我们先来完善一下我们的添加用户的接口:
 

 void AddUser(InetAddr & id)//增加用户到我们的链表中 { _users.emplace_back(std::make_shared(id)); }

但是这样就对了吗(我们这里使用User类,符合多态的思想并且基类是一个纯虚类不能生成对象),答案是还没有,我们并没有判断这个用户之前是否已经加入过了。

我们之前的User类的构造函数没有生成这种构造,都需要我们完善一下:

 User(InetAddr &id):_adr(id) { }

随后完善我们的添加用户代码:

当我们使用*it与传进来InetAddr类型变量id进行比较时,他们两个是一个不同类型的参数,所以我们需要在User及其父类中重载一下相应的==符号:

 我们这里分别给基类与父类都重载了一个与InetAddr类型做比较的函数。但是我们的InetAddr内部之前并没有重载他的比较好函数,所以我们也需要给InetAddr重载一个:
随后我们的添加用户的接口就能够实现了:
 

 void AddUser(InetAddr & id)//增加用户到我们的链表中 { for(auto &it:_users) { if(*it==id)//代表之前存在该用户 { LOG(LogLevel::DEBUG)<<id.Addr()<<\"该用户已经存在,无法添加\"; return; } } LOG(LogLevel::DEBUG) << \"Add user: \" << id.Addr(); _users.emplace_back(std::make_shared(id));//如果不存在,就添加该用户 }

我们这里还剩下一个删除用户没有实现,但是我们先不急,我们还要给这个用户管理创建一个接口Router,这个Router就负责实现我们之前所说的,将消息转发给每一个用户:

 void Router(int sockfd,const std::string message) { for(auto &it:_users) { it->Sendto(sockfd,message); // 调用每个用户的Sendto方法发送消息 } }

这里就体现出我们多态的好处了,虽然_user是一个类型为 指针的链表,但是我们通过UserInterface指针里面的对象在创建进去是都是用的子类对象。

这就满足了我们多态的条件,从而实际调用的是User的Sendto函数。

以上我们通过一个管理用户类,实现对所有用户进行管理,当有条件出发时,调用Router进行状态更新的思想叫做观察者模式

三、服务端代码修改

我们上面还剩下一个删除用户没有实现。但是我们先不要着急,我们先来对服务端的代码进行一定程度的修改,以便进行测试。

与前面写过的字典的代码思路一样,我们首先得在main函数里定义一个用户管理者类,并通过传递回调函数的方式,让我们的服务端类接收并调用。

我们先创建一个管理者对象并调用的智能指针进行管理:

 std::shared_ptrum=std::make_shared(); //我们先创建一个用户管理器对象,用来管理我们的用户

随后,删除我们之前为了改造成字典而传入的Find_t的参数,并重新修改服务端的构造,删除其成员变量函数(就是之前创建的接口回调函数)

我们今天不学昨天,在构造函数中把我们的回调函数传进去。而是选择在服务端类中创建一个新的接口,通过这个接口的调用,来实现对其类成员回调方法的初始化。

我们目前需要传进去的回调窗口有两个,这里我们需要在UdpServer.hpp中定义两个新的类型:

using Add_t = std::function;using Route_t = std::function;

 并在服务端类中新增对应成员变量,并新增一个注册接口,用来对其进行初始化:

 void Register(Add_t add, Route_t route) { _add = add; _route = route; // 我们注册了一个添加用户的回调函数和一个路由的回调 }......... private: Add_t _add; // 添加用户的回调函数 Route_t _route; // 路由的回调函数

由于不是在构造函数中初始化了,所以我们需要手动调用注册函数。

 svr_ptr->Register( [&um](InetAddr id){um->AddUser(id);}, // 绑定用户添加函数 [&um](int sockfd,std::string &message){um->Router(sockfd,message);} // 绑定路由函数 );

调用注册函数,并给他传递对应的回调接口,此时我们的在服务端类中的回调函数就被初始化了,随后我们就可以进行使用。

根据聊天通话的逻辑,我们的服务端首先得接收到消息,随后执行添加用户接口,再执行转发路由信息的接口。

我们之前说过要用到线程池,根据之前所学线程池的内容,我们这个时候传进去的回调方法就应该是路由Router函数,这样才能让消费者进行消费。

注意,由于我们这个是单例化线程池,所以线程池的创建必定要通过调用getInstance接口,这个是我们单例化线程池新增的内容。

但是我们先来对线程池接口进行一点小修改,首先是这里的回调函数的使用:

去掉name,因为我们这里传进来的方法不会有name。(原因后面解释)

接下来我们打算让传进来的接口进行参数的绑定,定义一个task_t的类型,这个就是我们即将传给线程池的新任务的类型:

由于我们定义为void(),所以我们的线程池在回调任务时自然不可能带name参数。

我们的Router函数有两个参数每一个文件描述符,一个是要发送的字符串,所以,我们就现在服务端把这两个参数绑定到他上面,形成一个满足task_t类型的调用:

所以:

 //接收到消息后,我们就需要尝试把发送该消息的用户添加到用户管理中去(实际上就是IP加端口,因为我们没学协议,所以这里就简单用IP加端口来判断一个用户  AddUser(temp); // 调用注册的用户添加函数,将用户添加到用户管理中  //之后,我们需要调用路由函数进行路由转发消息。  //我们就把之前使用的单例化线程池拿过来  std::string message = temp.Addr() + \"# \" + buffer;  task_t t = std::bind(_route, _sockfd, message );  ThreadPool::getInstance()->Equeue(std::move(t));  //创建完后进行生产任务,这样里面的代码就会被执行起来

 主要的变化有这四步。

最后,让我们填上之前没有弄好的坑:完成Sendto函数:

 void Sendto(int sockfd, const std::string &message) override { LOG(LogLevel::DEBUG) << \"sendto message to: \" << _adr.Addr(); ::sendto(sockfd, message.c_str(), message.size(), 0, _adr.Getsockaddr(), _adr.GetSockaddrLen()); }

 但是只发送了消息还不够,我们的用户端还没改变。

目前为止我们的用户端的代码就暂时修改完了。

四、服务端多线程的修改

在之前的代码中,我们的客户端是只有一个线程。

且接收与发送都是这个线程进行的,这就难免会造成阻塞,所以我们使用多线程分开执行。

我们注意到我们的客户端的调用是这样的:

int main(int argc, char *argv[]){ if (argc != 3) // 客户端必须传入我们要发送的目的地的IP和端口号 { std::cout << \"Usage: ./client ip port\" << std::endl; return 1; } std::string ip = argv[1]; uint16_t port = std::stoi(argv[2]); client = std::make_unique(ip, port); client->InitClient(); client->Start(); return 0;}

会调用一个start函数,并且我们客户端的死循环逻辑就是在这里运行的。

所以我们可以在这里死循环开始前,创建一个新线程,并在我们的客户端类中创建一个类成员函数让我们的新线程执行这个函数。

这里我们打算让主线程一直发送消息。新线程一直打印消息:
 

 private: void Recev() { while (true) { struct sockaddr_in temp; socklen_t len = sizeof(temp); char buffer[1024]; ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)(&temp), &len); if (n > 0) {  buffer[n] = 0;  std::cerr << buffer << std::endl;  // std::cerr.flush(); // 强制刷新缓冲区 } } } public: void Start() { std::thread t(&UdpClient::Recev, this); // 创建一个线程来接收消息 // 启动的时候,给服务器推送消息即可 const std::string online = \" ... 来了哈!\"; int n = ::sendto(_sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)(&_server), sizeof(_server)); while (true) { std::cout << \"Please input your message: \"; std::string message; getline(std::cin, message); // 从标准输入读取一行消息 // 发送消息 ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server)); } t.join(); // 等待接收线程结束 }

这样我们的多线程修改就大功告成了。

值得注意的是,我们这里为了区分,所以我们使用错误流打印收到的消息,后面可以把错误流重定向方便我们分开查看。

接下来就是我们上面代码的缺漏,我们的添加用户与路由函数都是对临界区资源的访问,所以我们需要加锁,而这里我们就使用我们之前的LockGuard,这个已经使用过很多次了所以不再赘述:

class UserManager{public: UserManager() { } ~UserManager() { } void AddUser(InetAddr &id) // 增加用户到我们的链表中 { LockGuard lockguard(_mutex); for (auto &it : _users) { if (*it == id) // 代表之前存在该用户 { LOG(LogLevel::DEBUG) << id.Addr() << \"该用户已经存在,无法添加\"; return; } } LOG(LogLevel::DEBUG) << \"Add user: \" << id.Addr(); _users.emplace_back(std::make_shared(id)); // 如果不存在,就添加该用户 } void Router(int sockfd, std::string message) { LockGuard lockguard(_mutex); for (auto &it : _users) { it->Sendto(sockfd, message); // 调用每个用户的Sendto方法发送消息 } }private: Mutex _mutex; std::list<std::shared_ptr> _users; // 存储用户的列表,我们这里用基类指针(或引用)指向子类对象,实现多态};

五、删除用户的实现:

我们这里如何删除一个用户了,在大部分情况,我们退出客户端所用的方式都是直接ctrl c退出,所以我们可以在客户端捕捉这个信号,然后对其的处理方式进行自定义,在这个自定义中,我们就可以发送对应信息给服务端,让服务端检测到这个特殊信息后执行退出删除用户的代码。

我们现在manager中完善我们的删除代码:
 

void DelUser(InetAddr &id) // 删除用户 {  auto pos = std::remove_if(_users.begin(), _users.end(), [&id](std::shared_ptr &user){ return *user == id; }); _users.erase(pos, _users.end()); PrintUser(); } void PrintUser() { for(auto user : _users) { LOG(LogLevel::DEBUG) < \"<GetId(); } }

这里为了方便我加了一个打印剩余用户的函数。

所以我们就希望服务端接收到特殊消息并执行这个函数,跟之前的方法一样,我们这里同样是选择在服务端使用回调函数的方法去执行这个代码。

所以:
 

using Del_t = std::function;using Add_t = std::function;using Route_t = std::function;using task_t = std::function;#define Die(code) \\ do  \\ {  \\ exit(code); \\ } while (0)......namespace UdpServerModule{ class UdpServer { public: ...... void Start() { is_running = true; while (is_running) { char buffer[1024]; struct sockaddr_in peer; // 输出型参数 socklen_t len = sizeof(peer); // 也是一个输出型参数 ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len); if (n > 0) {  InetAddr temp(peer); // 通过上面的peer来进行初始化,这样以来我们就能获取到相关ip地址与端口并打印  buffer[n] = \'\\0\'; // 确保字符串以null结尾  LOG(LogLevel::INFO) << \"client ip: \" << temp.GetIp() << \", port: \" << temp.GetPort()  << \" client say: \" << buffer;  std::string echo_str; // 我们要给客户端回显一条消息  if (strcmp(buffer, \"QUIT\") == 0)  { _del(temp); // 删除用户 echo_str = temp.Addr() + \"# \" + \"我走了,你们聊!\";  }  else  { _add(temp); // 添加用户 echo_str = temp.Addr() + \"# \" + buffer;  }  task_t t = std::bind(_route, _sockfd, echo_str);  ThreadPool::getInstance()->Equeue(std::move(t));  // 我们将echo_str作为参数传递给路由函数,这样路由 } } } void Register(Add_t add, Route_t route,Del_t del) { _add = add; _route = route; _del = del; // 我们注册了一个添加用户的回调函数和一个路由的回调 } private: ...... Add_t _add; // 添加用户的回调函数 Del_t _del; // 删除用户的回调函数 Route_t _route; // 路由的回调函数 };}

我们在收到客户端的消息后,会对这个消息进行判断,如果是特殊的QUIT,就执行删除用户的回调,否则就执行添加客户任务。

所以,我们应该如何从客户端发送这个消息给服务端呢?

这个看起来简单,但实际还是要费一点脑筋:

因为我们要对信号进行捕捉,且发送QUIT给服务端。这里就涉及到我们的客户端类的变量fd与addr。但是这两个都是类成员变量,如果我们不把函数定义在类里,就拿不到。

想要定义在外面,就得通过一些巧劲,比如,我们制作两个接口分别返回这两个变量的值:

 int fd(){ return _sockfd;} struct sockaddr *Inetaddress () {return (struct sockaddr *)&_server;}

随后,类似的,由于我们的函数在外部,但是我们的逻辑已经在类里了。所以我们需要把外部的函数拿到客户端类里使用,这就必须把函数传进来,也就是之前的回调函数。

之前我们的都是用的function,这次我们可以使用一个函数指针,我们定义这个函数的类型为:
 

 using SigCb = void(*)(int);

随后,我们在类成员变量中增加这个函数类型成员,并学会跟服务端一样写一个注册接口初始化这个回调函数:

 void SetQuitCb(const SigCb& cb) { _quit_cb = cb; } void Start() { if(_quit_cb) {  signal(SIGINT,_quit_cb); } std::thread t(&UdpClient::Recev, this); // 创建一个线程来接收消息 // 启动的时候,给服务器推送消息即可 const std::string online = \" ... 来了哈!\"; int n = ::sendto(_sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)(&_server), sizeof(_server)); while (true) { std::cout << \"Please input your message: \"; std::string message; getline(std::cin, message); // 从标准输入读取一行消息 // 发送消息 ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server)); } t.join(); // 等待接收线程结束 } int fd(){ return _sockfd;} struct sockaddr *Inetaddress () {return (struct sockaddr *)&_server;} private: int _sockfd; // socket文件描述符 std::string _ip; // IP地址 uint16_t _port; // 端口号 struct sockaddr_in _server; // 我们的类初始化时必须传入目的地的IP与端口 SigCb _quit_cb;

在我们的start函数中,进行对信号的捕捉,当我们检测到ctrl c信号,就会去执行外界的quit函数:
 

std::unique_ptr client;void quit(int sig){ std::cout<<\"ctrc + c \"<fd(), quit.c_str(), quit.size(), 0,client->Inetaddress(), sizeof(client->Inetaddress())); exit(0);}int main(int argc, char *argv[]){ if (argc != 3) // 客户端必须传入我们要发送的目的地的IP和端口号 { std::cout << \"Usage: ./client ip port\" << std::endl; return 1; } std::string ip = argv[1]; uint16_t port = std::stoi(argv[2]); client = std::make_unique(ip, port); client->InitClient(); client->SetQuitCb(quit); client->Start(); return 0;}

这里我们还取了个巧,为了防止传过多参数而选择把我们的client对象变成全局的了。

至此,我们的代码就完成了。 

六、总代码如下:

User.hpp:

#pragma once#include #include \"InetAddr.hpp\"#include \"log.hpp\"#include #include \"ThreadPool.hpp\"#include \"mutex.hpp\"#include using namespace LogModule;class UserInterface{public: virtual ~UserInterface() = default; // 为了防止调用接口时,调用到UserInterface的析构函数,导致析构函数调用不明确 virtual void Sendto(int sockfd, const std::string &message) = 0; // 纯虚函数必须要求子类对纯虚函数进行重定义实现 virtual std::string GetId() = 0;  // 获取用户的ID virtual bool operator==(const InetAddr &u) = 0;};class User : public UserInterface // public继承{public: User(InetAddr &id) : _adr(id) { } bool operator==(const InetAddr &id) override { return _adr == id; // 使用InetAddr的重载==运算符 } void Sendto(int sockfd, const std::string &message) override { LOG(LogLevel::DEBUG) << \"sendto message to: \" << _adr.Addr(); ::sendto(sockfd, message.c_str(), message.size(), 0, _adr.Getsockaddr(), _adr.GetSockaddrLen()); } std::string GetId() override { return _adr.GetIp() + \":\" + std::to_string(_adr.GetPort()); } ~User() { }private: InetAddr _adr; // 用户的地址};

 UserManager.hpp:
 

#pragma once#include\"mutex.hpp\"#include\"User.hpp\"#include\"InetAddr.hpp\"#include\"log.hpp\"using namespace MutexModule;class UserManager{public: UserManager() { } ~UserManager() { } void AddUser(InetAddr &id) // 增加用户到我们的链表中 { LockGuard lockguard(_mutex); for (auto &it : _users) { if (*it == id) // 代表之前存在该用户 { LOG(LogLevel::DEBUG) << id.Addr() << \"该用户已经存在,无法添加\"; return; } } LOG(LogLevel::DEBUG) << \"Add user: \" << id.Addr(); _users.emplace_back(std::make_shared(id)); // 如果不存在,就添加该用户 } void DelUser(InetAddr &id) // 删除用户 {  auto pos = std::remove_if(_users.begin(), _users.end(), [&id](std::shared_ptr &user){ return *user == id; }); _users.erase(pos, _users.end()); PrintUser(); } void PrintUser() { for(auto user : _users) { LOG(LogLevel::DEBUG) < \"<GetId(); } } void Router(int sockfd, std::string message) { LockGuard lockguard(_mutex); for (auto &it : _users) { it->Sendto(sockfd, message); // 调用每个用户的Sendto方法发送消息 } }private: Mutex _mutex; std::list<std::shared_ptr> _users; // 存储用户的列表,我们这里用基类指针(或引用)指向子类对象,实现多态};

UdpServer.hpp:

#ifndef __UDP_SERVER_HPP__#define __UDP_SERVER_HPP__#pragma once#include \"log.hpp\"#include #include #include\"UserManager.hpp\"#include \"ThreadPool.hpp\"#include #include \"InetAddr.hpp\"#include #include #include #include #include using namespace LogModule;using namespace ThreadPoolModule;using Del_t = std::function;using Add_t = std::function;using Route_t = std::function;using task_t = std::function;#define Die(code) \\ do  \\ {  \\ exit(code); \\ } while (0)static int defaultfd = -1;static std::string defaultip = \"127.0.0.1\"; // 默认的IP地址,代表本地IP地址static uint16_t defaultport = 8080; // 默认的端口号,用来测试namespace UdpServerModule{ class UdpServer { public: UdpServer(const std::string &ip = defaultip, const uint16_t port = defaultport) : _sockfd(defaultfd),  local(port), // 初始化本地地址信息  is_running(false) { } ~UdpServer() { } void InitServer() { // 1.创建一个socket _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // 这里我们使用了C标准库的socket函数来创建一个UDP socket // 注意:在实际的代码中,我们需要检查socket函数的返回值,以确保 // socket创建成功。 if (_sockfd < 0) { LOG(LogLevel::FATAL) << \"Failed to create socket\" << strerror(errno); // 如果socket创建失败,我们记录一条FATAL级别的日志,并返回。 Die(1); } // 如果socket创建成功,我们记录一条INFO级别的日志,表示socket创建成功。 LOG(LogLevel::INFO) << \"Socket created successfully, sockfd: \" << _sockfd; // 2.绑定地址信息 // struct sockaddr_in local; // bzero(&local, sizeof(local)); // 清空结构体 // // 这里我们使用了bzero函数来清空local结构体,确保没有残留数据,垃圾值 // local.sin_family = AF_INET; // 设置地址族为IPv4 // local.sin_port = htons(_port); // 设置端口 // // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将IP地址转换为网络字节序 // local.sin_addr.s_addr = INADDR_ANY; // 绑定到任意IP地址,这样服务器可以接收来自任何IP的消息 int n = ::bind(_sockfd, (struct sockaddr *)&local, sizeof(local)); if (n < 0) { // 如果bind函数返回小于0,表示绑定失败,我们记录一条FATAL级别的日志,并返回。 // 这里我们使用了strerror函数来获取错误信息,并将其记录到日志中。 LOG(LogLevel::FATAL) << \"bind: \" << strerror(errno); Die(2); } // 如果绑定成功,我们记录一条INFO级别的日志,表示绑定成功。 LOG(LogLevel::INFO) < 0) {  InetAddr temp(peer); // 通过上面的peer来进行初始化,这样以来我们就能获取到相关ip地址与端口并打印  buffer[n] = \'\\0\'; // 确保字符串以null结尾  LOG(LogLevel::INFO) << \"client ip: \" << temp.GetIp() << \", port: \" << temp.GetPort()  << \" client say: \" << buffer;  std::string echo_str; // 我们要给客户端回显一条消息  if (strcmp(buffer, \"QUIT\") == 0)  { _del(temp); // 删除用户 echo_str = temp.Addr() + \"# \" + \"我走了,你们聊!\";  }  else  { _add(temp); // 添加用户 echo_str = temp.Addr() + \"# \" + buffer;  }  task_t t = std::bind(_route, _sockfd, echo_str);  ThreadPool::getInstance()->Equeue(std::move(t));  // 我们将echo_str作为参数传递给路由函数,这样路由 } } } void Register(Add_t add, Route_t route,Del_t del) { _add = add; _route = route; _del = del; // 我们注册了一个添加用户的回调函数和一个路由的回调 } private: int _sockfd; // socket文件描述符 InetAddr local; // 本地地址信息 // std::string _ip; // 默认IP地址 // uint16_t _port; // 默认端口号 bool is_running; // 服务器是否在运行 Add_t _add; // 添加用户的回调函数 Del_t _del; // 删除用户的回调函数 Route_t _route; // 路由的回调函数 };}#endif

UdpServerMain.cc:
 

#include\"UdpServer.hpp\"#include\"User.hpp\"using namespace UdpServerModule;int main(){ std::shared_ptrum=std::make_shared(); //我们先创建一个用户管理器对象,用来管理我们的用户 std::unique_ptr svr_ptr=std::make_unique();//我们先创建一个服务器对象,并用智能指针管理它 svr_ptr->Register( [&um](InetAddr& id){um->AddUser(id);}, // 绑定用户添加函数 [&um](int sockfd,std::string &message){um->Router(sockfd,message);} ,// 绑定路由函数 [&um](InetAddr& id){um->DelUser(id);} ); //那我们是不是要先初始化一下我们的服务器对象呢? svr_ptr->InitServer(); //假设UdpServer类有一个InitServer方法来初始化服务器 //初始化好了,我们是不是应该启动我们的服务端。由于服务端一般都是启动了不会停止的,所以我们可以使用while循环 svr_ptr->Start();}

UdpClient.hpp:
 

#pragma once#include \"log.hpp\"#include #include #include #include #include #include #include #include #include #include using namespace LogModule;#define Die(code) \\ do  \\ {  \\ exit(code); \\ } while (0)static int defaultfd = -1;namespace UdpClientModule{ using SigCb = void(*)(int); class UdpClient { private: void Recev() { while (true) { struct sockaddr_in temp; socklen_t len = sizeof(temp); char buffer[1024]; ssize_t n = ::recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)(&temp), &len); if (n > 0) {  buffer[n] = 0;  std::cerr << buffer << std::endl;  // std::cerr.flush(); // 强制刷新缓冲区 } } } // static void ClientQuit(int signo) // { // const std::string quit = \"QUIT\"; // int n = ::sendto(fd, quit.c_str(), quit.size(), 0, (struct sockaddr *)(&_server), sizeof(_server)); // exit(0); // } public: UdpClient(const std::string &ip, const uint16_t port) : _sockfd(defaultfd),  _ip(ip),  _port(port) { } ~UdpClient() { } void SetQuitCb(const SigCb& cb) { _quit_cb = cb; } void InitClient() { // 1.创建一个socket _sockfd = ::socket(AF_INET, SOCK_DGRAM, 0); // 这里我们使用了C标准库的socket函数来创建一个UDP socket // 注意:在实际的代码中,我们需要检查socket函数的返回值,以确保 // socket创建成功。 if (_sockfd < 0) { LOG(LogLevel::FATAL) << \"Failed to create socket\" << strerror(errno); // 如果socket创建失败,我们记录一条FATAL级别的日志,并返回。 Die(3); } // 如果socket创建成功,我们记录一条INFO级别的日志,表示socket创建成功。 LOG(LogLevel::INFO) << \"Socket created successfully, sockfd: \" << _sockfd; // 2.绑定地址信息 memset(&_server, 0, sizeof(_server)); // 清空结构体 // 这里我们使用了bzero函数来清空local结构体,确保没有残留数据,垃圾值 _server.sin_family = AF_INET;  // 设置地址族为IPv4 _server.sin_port = htons(_port);  // 设置端口号 _server.sin_addr.s_addr = inet_addr(_ip.c_str()); // 将IP地址转换为网络字节序 // client必须也要有自己的ip和端口!但是客户端,不需要自己显示的调用bind!! // 而是,客户端首次sendto消息的时候,由OS自动进行bind // 1. 如何理解client自动随机bind端口号? 一个端口号,只能被一个进程bind // 2. 如何理解server要显示的bind?服务器的端口号,必须稳定!!必须是众所周知且不能改变轻易改变的! // 如果服务端改变,那么他所服务对接的众多客户端都无法正常运行 } void Start() { if(_quit_cb) {  signal(SIGINT,_quit_cb); } std::thread t(&UdpClient::Recev, this); // 创建一个线程来接收消息 // 启动的时候,给服务器推送消息即可 const std::string online = \" ... 来了哈!\"; int n = ::sendto(_sockfd, online.c_str(), online.size(), 0, (struct sockaddr *)(&_server), sizeof(_server)); while (true) { std::cout << \"Please input your message: \"; std::string message; getline(std::cin, message); // 从标准输入读取一行消息 // 发送消息 ssize_t m = ::sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&_server, sizeof(_server)); } t.join(); // 等待接收线程结束 } int fd(){ return _sockfd;} struct sockaddr *Inetaddress () {return (struct sockaddr *)&_server;} private: int _sockfd; // socket文件描述符 std::string _ip; // IP地址 uint16_t _port; // 端口号 struct sockaddr_in _server; // 我们的类初始化时必须传入目的地的IP与端口 SigCb _quit_cb; };}

UdpClientMain.cc:
 

#include \"UdpClient.hpp\"#include using namespace UdpClientModule;std::unique_ptr client;void quit(int sig){ std::cout<<\"ctrc + c \"<fd(), quit.c_str(), quit.size(), 0,client->Inetaddress(), sizeof(client->Inetaddress())); exit(0);}int main(int argc, char *argv[]){ if (argc != 3) // 客户端必须传入我们要发送的目的地的IP和端口号 { std::cout << \"Usage: ./client ip port\" << std::endl; return 1; } std::string ip = argv[1]; uint16_t port = std::stoi(argv[2]); client = std::make_unique(ip, port); client->InitClient(); client->SetQuitCb(quit); client->Start(); return 0;}

ThreadPool.hpp:
 

#ifndef _THREAD_POOL_HPP_#define _THREAD_POOL_HPP_#pragma once#include \"log.hpp\"#include \"mutex.hpp\"#include \"Cond.hpp\"#include #include \"Mythread.hpp\"#include #include #include namespace ThreadPoolModule{ using thread_t = std::shared_ptr; using namespace LogModule; using namespace MutexModule; using namespace CondModule; using namespace ThreadModule; static int default_num = 5; template  class ThreadPool { private: bool IsEmpty() // 判断任务队列是否为空 { return _taskqueue.empty(); } void HandlerTask(std::string name) // 回调函数,负责在线程初始化时调用 { LOG(LogLevel::INFO) << \"线程\" << name << \"开始执行回调函数逻辑\"; while (true) { T t; {  LockGuard lock(_mutex);  while (IsEmpty() && is_running)  { wait_num++; _cond.Wait(_mutex); wait_num--;  }  if (IsEmpty() && !is_running)  { break; // 退出回调函数  }  // 此时只会出现有任务的情况,无任务且is_running的情况已经在while循环中处理了,无任务且!is_running的情况再上面if中处理了  t = _taskqueue.front(); // 拿出任务  _taskqueue.pop(); // 删除任务 } // 执行任务不用上锁 t(); // 执行任务,我们这里规定传进来的任务必须重载()运算符 } LOG(LogLevel::INFO) << \"线程\" << name << \"执行回调函数逻辑结束\"; } ThreadPool(const ThreadPool &) = delete;  // 禁止拷贝函数 ThreadPool &operator=(const ThreadPool &) = delete; // 禁止赋值函数 ThreadPool(int num = default_num)// 将构造函数放在私人权限中防止外部随意创建对象 : _num(num), is_running(false), wait_num(0) { for (int i = 0; i < _num; ++i) { _threadpool.push_back(std::make_shared(std::bind(&ThreadPool::HandlerTask, this, std::placeholders::_1))); // 我们这里使用bind的原因是因为HandlerTask是一个类成员函数,只传递函数名会导致函数签名不匹配, // 我们想要在不是这个类的线程变量中调用这个函数,需要需要 this 提供调用上下文 // 我们需要将this指针绑定到HandlerTask函数中,这样才能在HandlerTask函数中访问到线程池的成员变量 } } public: static ThreadPool *getInstance() { if (instance == NULL) { LockGuard lockguard(mutex); if (instance == NULL) {  LOG(LogLevel::INFO) << \"单例首次被执行,需要加载对象...\";  instance = new ThreadPool();  instance->Start(); // 启动线程池 } } return instance; } void Stop() { if (is_running) { is_running = false; // 此时不能再入任务队列了 if (wait_num > 0) {  _cond.SignalAll(); // 唤醒所有线程 } } } void Start() { is_running = true; for (auto &it : _threadpool) { LOG(LogLevel::INFO) << \"启动线程\" <Name() <start(); } } void Equeue(T &&task) // 我们这里的线程池是一个模板,这个模板的划分是根据我们传进来的任务类型来划分的。 // 所以我们这里要使用模板参数T // 这里的T && task是一个语法:引用折叠 // 如果我们传进来的是一个左值,那么T && task会被折叠成T & &&,根据引用折叠规则,会被折叠成T & // 如果我们传进来的是一个右值,那么T && task会被折叠成T && &&,根据引用折叠规则,会被折叠成T && // 如果我们传进来的是一个task_t,T&& 就是普通的右值引用(task_t&&) { LockGuard lock(_mutex); if (!is_running) { return; // 如果不为运行状态,就不能新入任务 } _taskqueue.push(std::forward(task)); // 我们这里使用std::forward来进行完美转发 LOG(LogLevel::INFO) < 0) { _cond.SignalAll(); // 唤醒所有线程 } } void Wait() { for (auto &it : _threadpool) { it->join(); LOG(LogLevel::INFO) << \"回收线程\" <Name() << \"...成功\"; } } private: std::vector _threadpool; // 线程管理数组 std::queue _taskqueue; // 任务队列 int _num; // 线程数量 Mutex _mutex;// 锁 Cond _cond; // 条件变量 int wait_num;// 进行等待的线程数量 bool is_running;  // 线程池是否在运行 static ThreadPool *instance; static Mutex mutex; // 只用来保护单例 }; template  // static的类成员变量的初始化需要放在类外 ThreadPool *ThreadPool::instance = NULL; template  Mutex ThreadPool::mutex; // 只用来保护单例}#endif

锁,信号量的头文件我们这里就不再赘述,没有进行修改,之前文章已经写出了大家可以去看。

总结:

这个聊天室的启动如下:

先启动服务器: 

同时执行一个客户端,并使其错误流重定向到一个fifo命名管道里,此时空间看好我们的那些初始化打印都执行了。

 依次类推开始我们的第二个客户端,但是记得重定向给另外一个管道:

 让客户端1输入nihao:

 

可以看见两个客户端的管道都出现了你好,前面的ip加端口代表我们的这个客户端的唯一身份。大家其实也可以自己拓展成花样的用户名。

 

再让用户2输入消息,同样的,其他人都会接收到,并且在聊天框里我们通过ip加端口可以分辨出来。