41 .UDP -3 群聊功能实现:线程池助力多客户端通信

🔥个人主页: Milestone-里程碑

❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>

🌟心向往之行必能至

目录

一.UdpServer.cc

[二. Route ::路由服务--聊天的前提](#二. Route ::路由服务--聊天的前提)

三.UdpClient.cc

[四. UdpServer.hpp](#四. UdpServer.hpp)

五.遇到的问题

六.总结


在前面两章中,我们一篇实现了基本的UDP的实现和增加了一个中文查英文的功能,这篇文章,我们将增加群聊功能(支持多个客户端在线聊天)

根据我们前面学习过的通信,传输数据知识,我们知道发消息不在同一局域网,需要通过路由发送,而取消息也不是从发送者那取,而是从中转站,发消息也是发给中转站,这不就是我们前面说过的生产者-消费者模型吗?因此我们可以引入线程池

一.UdpServer.cc

主要提供三个功能的入口

路由服务

线程池

网路服务对象的通信功能

bash 复制代码
#include <iostream>
#include <memory>
#include <functional>
#include "UdpServer.hpp"
#include "Route.hpp"
#include"ThreadPool.hpp"
using namespace ThreadPoolModule;
using task_t=std::function<void()>;
std::string defaulthandler(const std::string &message)
{
    std::string hello = "hello, ";
    hello += message;
    return hello;
}
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        std::cerr << "Usage: " << argv[0] << " port" << std::endl;
        return 1;
    }
    //  std ::string ip =argv[1];
    uint16_t port = std::stoi(argv[1]); // xiecuo
    Enable_Console_Log_Strategy();
    // 1. 路由服务
    Route r;
    //  std::unique_ptr<UdpServer> usvr =std::make_unique<UdpServer>(port,defaulthandler);
    //2.线程池
    auto tp=ThreadPool<task_t>::GetInstance();
    //3.网络服务对象,提供通信功能
    std::unique_ptr<UdpServer> usvr=std::make_unique<UdpServer>(port,[&r,&tp](int sockfd, const std::string &message, InetAddr&peer){
        task_t t=std::bind(&Route::MessageRoute,&r,sockfd,message,peer);
        tp->Enqueue(t);
    }
);
//    std::unique_ptr<UdpServer> usvr = std::make_unique<UdpServer>(port, [&r](int sockfd, const std::string &message, InetAddr&peer){
//         r.MessageRoute(sockfd, message, peer);
//     });
    usvr->Init();
    usvr->Start();
    return 0;
}

二. Route ::路由服务--聊天的前提

注意前提:消息路由转发时要进行加锁,防止一遍在遍历,一边又在删除,导致迭代器失效

bash 复制代码
#pragma once
#include <iostream>
#include <vector>
#include "InetAddr.hpp"
#include"Mutex.hpp"
using namespace MutexModule;
class Route
{
private:
    bool Isonline(InetAddr &user)
    {
        for (auto &e : _online_user)
        {
            // 重载运算符
            if (e == user)
                return true;
        }
        return false;
    }
    void Adduesr(InetAddr &user)
    {
        _online_user.emplace_back(user);
        LOG(LogLevel::INFO) << "新用户上线: " << user.StringAddr();
    }
    void DeleteUser(InetAddr &peer)
    {
        for (auto iter = _online_user.begin(); iter != _online_user.end(); iter++)
        {
            if (*iter == peer)
            {
                LOG(LogLevel::INFO) << "删除一个在线用户:" << peer.StringAddr() << "成功";
                _online_user.erase(iter);
                break;
            }
        }
    }

public:
    Route() {}
    void MessageRoute(int sockfd, std::string message, InetAddr &user)
    {
        LockGuard lg(mutex);//加锁是为了防止有人在遍历,又有人在删除
        if (!Isonline(user))
        {
            Adduesr(user);
        }
        std::string sendmessage = user.StringAddr() + "#" + message;
        for (auto &users : _online_user)
        {
            //存在问题,用户必须自己先输入,才能看到其他人发的消息,原因是在客户端设计的问题
            sendto(sockfd, sendmessage.c_str(), sendmessage.size(), 0, (const struct sockaddr *)&(users.in_addr()), sizeof(users.in_addr()));
        }

        if (message == "QUIT")
        {
            {
                LOG(LogLevel::INFO) << "删除一个在线用户: " << user.StringAddr();
                DeleteUser(user);
            }
        }
    }
    ~Route() {}

private:
    std::vector<InetAddr> _online_user;
    Mutex mutex;
};

三.UdpClient.cc

bash 复制代码
#include <iostream>
#include <string>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "Log.hpp"
#include "Thread.hpp"
using namespace LogModule;
using namespace ThreadModlue;
std::string server_ip;
std::uint16_t server_port;
struct sockaddr_in server;
int sockfd;
pthread_t id;
void read()
{
   while (true)
   {
      char buffer[1024];
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);

      int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
      if (m > 0)
      {
         buffer[m] = 0;
         // std::cout << buffer << std::endl;将读取与输入分开
         std::cerr << buffer << std::endl; // 开两个终端,一个为显示他人输入,一个为自己输入
      }
   }
}
void send()
{
   while (true)
   {
      // 接受信息
      std::string input;
      std::cout << "PLEASE Enter# ";
      std::getline(std::cin, input);

      int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
      (void)n;
      if (input == "QUIT")
      {
         pthread_cancel(id);
         
         break;
      }
   }
}
int main(int args, char *argv[])
{
   if (args != 3)
   {
      std::cerr << "Usage: " << argv[0] << " server_ip server_port" << std::endl;
      return 1;
   }
   // 1. 创建socket
   // int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
   sockfd = socket(AF_INET, SOCK_DGRAM, 0);
   if (sockfd < 0)
   {
      std::cerr << "socket fail" << std::endl;
      return 2;
   }
   // 2. 本地的ip和端口是什么?要不要和上面的"文件"关联呢?
   // 问题:client要不要bind?需要bind.
   //       client要不要显式的bind?不要!!首次发送消息,OS会自动给client进行bind,OS知道IP,端口号采用随机端口号的方式
   //   为什么?一个端口号,只能被一个进程bind,为了避免client端口冲突
   //   client端的端口号是几,不重要,只要是唯一的就行!
   // 填写服务器信息
   server_ip = argv[1];
   server_port = std::stoi(argv[2]); // 转数字
   memset(&server, 0, sizeof(server));
   server.sin_family = AF_INET;
   server.sin_port = htons(server_port);
   server.sin_addr.s_addr = inet_addr(server_ip.c_str());

   Thread R([]()
            { read(); });
   Thread S([]()
            { send(); });
   R.Start();
   S.Start();

   //  id = recver.Id();
   id=R.Id();

   R.Join();
   S.Join();
   while (true)
   {
      // 接受信息
      std::string input;
      std::cout << "PLEASE Enter# ";
      std::getline(std::cin, input);
      int n = sendto(sockfd, input.c_str(), input.size(), 0, (struct sockaddr *)&server, sizeof(server));
      (void)n;
      char buffer[1024];
      struct sockaddr_in peer;
      socklen_t len = sizeof(peer);

      int m = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)&peer, &len);
      if (m > 0)
      {
         buffer[m] = 0;
         std::cout << buffer << std::endl;
      }
   }
   return 0;
}

四. UdpServer.hpp

bash 复制代码
#pragma once
#include <iostream>
#include <string>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "InetAddr.hpp"
// #include "InetAddr.hpp"
using namespace LogModule;
using func_t = std::function<void(int sockfd,const std::string &, InetAddr&peer)>;

int defaultsockfd = -1;
class UdpServer
{
public:
    UdpServer(uint16_t port, func_t func)
        : _sockfd(defaultsockfd), _func(func), _port(port), _isrunning(false)
    {
    }
    void Init()
    {
        // 1. 创建套接字
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket0 error!";
            exit(1);
        }
        LOG(LogLevel::INFO) << "socket success,sockfd: " << _sockfd;
        // 2. 绑定socket信息,ip和端口, ip(比较特殊,后续解释)
        // 2.1 填充sockaddr_in结构体
        struct sockaddr_in local;
        bzero(&local, sizeof(local)); // 初始化
        // 我会不会把我的IP地址和端口号发送给对方?
        // IP信息和端口信息,一定要发送到网络!
        // 本地格式->网络序列
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // TODO
        local.sin_addr.s_addr = INADDR_ANY;
        // IP也是如此,1. IP转成4字节 2. 4字节转成网络序列 -> in_addr_t inet_addr(const char *cp);
        // 那么为什么服务器端要显式的bind呢?IP和端口必须是众所周知且不能轻易改变的!
        // 此时只开在了栈上,要通过系统调用写入内核
        int n = bind(_sockfd, (struct sockaddr *)&local, sizeof(local));
        if (n == -1)
        {
            perror("bind:");
            LOG(LogLevel::FATAL) << "bind fail";
            exit(2);
        }
        LOG(LogLevel::INFO) << "bind success!";
    }
    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            char buffer[1024];
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            // 1. 收消息, client为什么要个服务器发送消息啊?不就是让服务端处理数据。
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {

                buffer[n] = 0;
                InetAddr client(peer);
                _func(_sockfd,buffer,client);
                // LOG(LogLevel::DEBUG) << "[" << peer_ip << ":" << peer_port << "]# " << buffer; // 1. 消息内容 2. 谁发的??
                // // 2. 发消息
                // std::string echo_string = "server echo#";
                // echo_string += buffer;
                // 收到单词
                // std::string buffer_str(buffer);
                // std::string result = _func(buffer, client);
                // LOG(LogLevel::INFO)<<"翻译结果:"<<buffer <<"->"<<result.c_str();
                // std::string result =_func(buffer,client);
                // sendto(_sockfd, result.c_str(), result.size(), 0, (struct sockaddr *)&peer, sizeof(peer));
            }
            else
            {
                LOG(LogLevel::FATAL) << "recvfrom fail";
                // exit(1);
            }
        }
    }

    ~UdpServer() {}

private:
    int _sockfd;
    uint16_t _port;
    // std::string _ip;
    // 用的是字符串风格,点分十进制, "192.168.1.1"
    func_t _func;
    bool _isrunning;
};

五.遇到的问题

  1. 实验发现,上线用户必须先自己发消息,才能收到别人前面发的消息,客户端未读写分离(解决:客户端进行读写分离,再对读写创建线程,互不影响即可同时进行)
  2. 实验发现,出现了迭代器失效问题,原因是在进行路由转发遍历时,未进行加锁,导致有可能删除用户与遍历同时存在

六.总结

  1. 写代码先搭出大致框架,再进行完善
  2. 测试代码数据要广泛 ,才能尽可能测出潜在的问题
相关推荐
山栀shanzhi2 小时前
【FFmpeg】是什么是未压缩的裸流?
c++·ffmpeg
zly88653722 小时前
windsurf rules与skill的使用
linux·c语言·开发语言·驱动开发
笨笨马甲2 小时前
Qt network开发
开发语言·qt
ALex_zry2 小时前
通用RPC跨平台方案设计
网络·网络协议·rpc
bbbb3652 小时前
排序算法的演进史:从冒泡到快速再到TimSort的技术7
数据结构·算法·排序算法
不染尘.2 小时前
排序算法详解1
开发语言·数据结构·c++·算法·排序算法
Via_Neo2 小时前
JAVA中对数的表达,将浮点数转为保留指定位数的字符串
java·开发语言
啊我不会诶2 小时前
25CCPC东北邀请赛vp补题
c++·算法