专业制作彩铃网站app和手机网站的区别
1. 封装群发模块【route】
上一篇博客我们实现了基于UDP网络通信的翻译服务,我们这里来实现基于UDP网络通信的群聊服务。怎么设计呢??
在我们的目前服务器当中有收发功能,但是,现在我们是把收到的客户端的信息处理后发送给对应的客户端。这样的话,我们就无法把所有客户发送的信息send到全部用户了!!现在,我们先认为只要有一个客户端向服务器中发送信息,我们就认为该用户登入到我们的群聊当中了,并且这第一条消息也要发送给所有的已经登入的用户。
既如此,我们势必就要管理所有已经登入的用户,而登入的用户我们通过它们的IP地址和端口号不就可以找到吗!!这两个重要的信息我们之前就用InetAddr类描述起来了!!所以,现在我们就需要封装一个route模块来管理这些用户并实现群发功能!route就是路由的意思,其主要功能就是管理所有用户【新增用户,用户退出删除用户】,把每个用户发送的信息群发给所有的用户。
在服务器中我们就这样改,因为在群发消息的时候需要_sockfd,所以我们的包装器也需要添加一些参数类型。


在上层的lambda表达式稍作修改即可:

完善route类:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include "inet_addr.hpp"
#include <vector>class route
{
private:bool is_exist(InetAddr &user){for (auto &e : _online_users){if (user == e){return true;}}return false;}void add_user(InetAddr &user){_online_users.emplace_back(user);}public:route(){}// 群发void message_route(int fd, const std::string message, InetAddr &user){// 判断用户是否在群中if (!is_exist(user)){// 不在群中就添加进来add_user(user);LOG(log_level::Info) << "新增一名用户 " << "[" << user.IP() << " : " << user.port() << "]";}// 群发信息 遍历所有的用户std::string send_message = "[" + user.IP() + " : " + std::to_string(user.port()) + "]#" + message;for (auto &e : _online_users){ssize_t m = sendto(fd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&(e.AddrIn()), sizeof(e.AddrIn()));}}~route(){}private:std::vector<InetAddr> _online_users; // 管理在线的用户
};> 客户端阻塞等待问题
由于客户端是单进程的,所以我们的输入是一直等待阻塞的,也就是说如果有人在群中发送了消息,那我们的显示器一定要在外面输入数据后才会回显出来。但是,我们并不期望这样,我们希望别人发的消息可以立即显示。所以,我们可以引入线程来解决这个问题。
创建两个线程,一个线程负责获取输入信息并发送到服务器中,另一个线程负责接收来自网络的信息并回显到显示器中。
引入我们之前封装的线程,稍作修改即可:


2. 引入线程池执行任务
在我们的服务器当中,我们只有一个主进程来执行任务,但是,如果我们群聊的人数多起来了。群发消息的效率就不高了。我们之前就自定义了一个线程池,现在我们就可以引入线程池模块,将route模块中群发的函数封装成一个任务push到线程池的任务队列中,让多线程高效完成任务!!


因为push到任务队列中的任务类型是返回值void无参数,因此我们不能直接将message_route成员函数push到任务队列当中。所以我们用bind包装器绑死任务模块中负责群发的函数,绑死一些参数,我们之前在C++就说过bind可以修改可调用对象的参数个数和位置。我们当时还不能理解这有什么用,现在这种场景就非常好用。这里注意,我们包装类成员函数的时候,需要示例化的类对象地址和类成员函数的地址!
3. 完善InetAddr
在InetAddr中,我们的构造函数可以帮我们完成IP地址和端口号从网络到主机的转换,但是,并没有实现从主机到网络的转换。我们这里重载一下构造函数即可,如果外层出传递的是const std::string , uin16_t port参数,我们就认为用户需要将信息从主机转换到网络中!!
关于inet_ntoa
inet_ntoa这个函数返回了⼀个char*,很显然是这个函数自己在内部为我们申请了一块内存来保存ip的 结果.那么是否需要调用者手动释放呢?

man手册上说,inet_ntoa函数,是把这个返回结果放到了静态存储区.这个时候不需要我们手动进行释 放.
那么问题来了,如果我们调⽤多次这个函数,会有什么样的效果呢?参见如下代码:
运⾏结果如下:
因为inet_ntoa把结果放到自己内部的⼀个静态存储区,这样第⼆次调用时的结果会覆盖掉上一次的结 果.
• 思考:如果有多个线程调用inet_ntoa,是否会出现异常情况呢?
• 在APUE中,明确提出inet_ntoa不是线程安全的函数;
• 但是在centos7上测试,并没有出现问题,可能内部的实现加了互斥锁;
• 在多线程环境下,推荐使用inet_ntop/inet_pton,这两个函数由调用者提供⼀个缓冲区保存结果,可以规避线程安全问题;


// 网络转主机InetAddr(const sockaddr_in &addr) : _addr(addr){_port = ntohs(_port); // 端口号// _ip = inet_ntoa(_addr.sin_addr); // IP地址->点分十进制char ip_buffer[64];inet_ntop(AF_INET, &_addr.sin_addr, ip_buffer, sizeof(ip_buffer));_ip = ip_buffer;}// 主机转网络InetAddr(const std::string &ip, uint16_t port): _ip(ip),_port(port){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr); // IP地址_addr.sin_port = htons(_port);}4. 源码
server.cc
#include <iostream>
#include "server.hpp"
#include <memory>
#include "dict.hpp"
#include "route.hpp"
#include "pthread_pool.hpp"
#include <functional>using namespace pthread_pool_module;using task_t = std::function<void()>;int main(int argc, char *argv[])
{if (argc != 2){std::cout << "Usage: ./server port" << std::endl;return 1;}using_screen_strategy();uint16_t port = std::stoi(argv[1]);// 任务模块route r;// 线程池模块auto p = pthread_pool<task_t>::get_instance();// 网络通信模块std::unique_ptr<server> p1 = std::make_unique<server>(port, [&r, &p](int fd, const std::string &english, InetAddr &client){//r.message_route(fd, english, client);task_t t = std::bind(&route::message_route,&r,fd,english,client);p->equeue(t); });p1->init();p1->start();return 0;
}server.hpp
#pragma once#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>
#include <functional>
#include "inet_addr.hpp"using namespace log_module;const int default_sockfd = -1;using func_t = std::function<void(int fd, const std::string &, InetAddr &)>;class server
{
public:server(uint16_t port, func_t func): _sockfd(default_sockfd),_func(func),// _ip(ip),_port(port),_isrunning(false){}~server(){}// 服务端初始化和启动void init(){// 创建套接字->打开网络文件_sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (_sockfd < 0){LOG(log_level::Fatal) << "socket 创建失败!";exit(1);}LOG(log_level::Info) << "socket 创建成功!" << _sockfd;// 填充sockaddr_in结构体struct sockaddr_in local;// 数据清空bzero(&local, sizeof(local));// 数据将来一定会发送到网络// 端口号和IP地址 本地格式->网络序列local.sin_family = AF_INET; // 协议家族local.sin_port = htons(_port); // 端口号// local.sin_addr.s_addr = inet_addr(_ip.c_str()); // IP地址local.sin_addr.s_addr = INADDR_ANY; // IP地址任意绑定int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));if (n < 0){LOG(log_level::Fatal) << "bind 失败!";exit(2);}LOG(log_level::Info) << "bind 成功!" << _sockfd;}void start(){_isrunning = true;while (_isrunning){char buffer[1024];struct sockaddr_in local;socklen_t len = sizeof(local);// 收信息ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&local, &len);if (n > 0) // 成功是收到信息{// 处理从网络获取的信息后续交给群发InetAddr client(local);buffer[n] = 0;// 讲数据回调给外层的函数进行群发消息_func(_sockfd, buffer, client);}}}private:int _sockfd;// std::string _ip; // IP地址用的字符串风格uint16_t _port; // 端口号bool _isrunning;func_t _func;
};
client.cc
#include <iostream>
#include "server.hpp"
#include <memory>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>
#include "pthread.hpp"using namespace pthread_module;
std::string ip;
uint16_t port;int sockfd = -1;void recv()
{while (true){// 接收消息【有可能连接多个服务器,所以要接收服务器端口号】char buffer[1024];sockaddr_in peer;bzero(&peer, 0);socklen_t len = sizeof(peer);int m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);if (m > 0){buffer[m] = 0;std::cout << buffer << std::endl;}}
}void send()
{// 填写服务器信息sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = inet_addr(ip.c_str());while (true){std::string input;std::cout << "请输入#";std::getline(std::cin, input);// 发送消息int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&local, sizeof(local));}
}// 客户端知道服务器的IP地址和端口号
int main(int argc, char *argv[])
{if (argc != 3){std::cout << "Usage: ./server IP port" << std::endl;return 1;}// using_screen_strategy();ip = argv[1];port = std::stoi(argv[2]);// 创建套接字sockfd = socket(AF_INET, SOCK_DGRAM, 0);if (sockfd < 0){LOG(log_level::Fatal) << "socket 创建失败!";exit(1);}LOG(log_level::Info) << "socket 创建成功!" << sockfd;pthread recver([](){ recv(); });pthread sender([](){ send(); });recver.start();sender.start();recver.join();sender.join();return 0;
}InetAddr.hpp
#pragma once#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <string>
#include <string.h>
#include <netinet/in.h>
#include "log.hpp"
#include <arpa/inet.h>
#include <functional>class InetAddr
{
public:// 网络转主机InetAddr(const sockaddr_in &addr) : _addr(addr){_port = ntohs(_port); // 端口号// _ip = inet_ntoa(_addr.sin_addr); // IP地址->点分十进制char ip_buffer[64];inet_ntop(AF_INET, &_addr.sin_addr, ip_buffer, sizeof(ip_buffer));_ip = ip_buffer;}// 主机转网络InetAddr(const std::string &ip, uint16_t port): _ip(ip),_port(port){memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr); // IP地址_addr.sin_port = htons(_port);}uint16_t port(){return _port;}std::string IP(){return _ip;}const struct sockaddr_in &AddrIn(){return _addr;}bool operator==(const InetAddr &e){return _port == e._port && _ip == e._ip;}~InetAddr(){}private:struct sockaddr_in _addr;uint16_t _port;std::string _ip;
};route.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include "inet_addr.hpp"
#include <vector>
#include "mutex.hpp"using namespace mutex_module;class route
{
private:bool is_exist(InetAddr &user){for (auto &e : _online_users){if (user == e){return true;}}return false;}void add_user(InetAddr &user){_online_users.emplace_back(user);}public:route(){}// 群发void message_route(int fd, const std::string message, InetAddr &user){lock_guard lg(_mutex);{// 判断用户是否在群中if (!is_exist(user)){// 不在群中就添加进来add_user(user);LOG(log_level::Info) << "新增一名用户 " << "[" << user.IP() << " : " << user.port() << "]";}// 群发信息 遍历所有的用户std::string send_message = "[" + user.IP() + " : " + std::to_string(user.port()) + "]#" + message;for (auto &e : _online_users){ssize_t m = sendto(fd, send_message.c_str(), send_message.size(), 0, (const struct sockaddr *)&(e.AddrIn()), sizeof(e.AddrIn()));}}}~route(){}private:std::vector<InetAddr> _online_users; // 管理在线的用户mutex _mutex;
};