计算机网络 -- 多人聊天室

一 程序介绍和核心功能

  这是基于 UDP 协议实现的一个网络程序,主要功能是 构建一个多人聊天室,当某个用户发送消息时,其他用户可以立即收到,形成一个群聊。

   这个程序由一台服务器和n个客户端组成,服务器扮演了一个接受信息和分发信息的角色,将信息发送给所有已知的用户主机。

  

二 程序结构 

  将服务器接收消息看作生产商品、分发消息看作消费商品,这不就是一个生动形象的 「生产者消费者模型」 吗?

「生产者消费者模型」 必备 321

  • 3三组关系
  • 2两个角色
  • 1一个交易场所

  其中两个角色可以分别创建两个线程,一个负责接收消息,放入 「生产者消费者模型」,另一个则是负责从 「生产者消费者模型」 中拿去消息,分发给用户主机。

  这对我们客户端也有相似的地方,但是与服务器不同,我们每个 客户端都认为自己只需要与服务器 1对1 连接就可以了,因此我们 每个客户端都只需要即使接收和发送 资源就可以了,只需要创建两个线程即可。

这里的交易场所可以选则 阻塞队列,也可以选择 环形队列。

 三 服务器

在引入 「生产者消费者模型」 后,服务器头文件结构将会变成下面这个样子

  • 启动服务器,原初始化服务器、启动线程
  • 接收消息,将收到的消息存入环形队列
  • 发送消息,从环形队列中获取消息,并派发给线程

3.1 引入生产者消费者模型 

这里我们直接使用一个vector数组模拟实现环形队列,同时借用信号量实现生产者消费者模型。

RingQueue.hpp 头文件

#pragma once

#include <vector>
#include <semaphore.h>

namespace My_RingQueue
{
const int DEF_CAP=10;

    template<class T>
    class RingQueue
    {
    public:
        RingQueue(size_t cap = DEF_CAP)
            :_cap(cap)
            ,_pro_step(0)
            ,_con_step(0)
        {
            _queue.resize(_cap);
            // 初始化信号量
            sem_init(&_pro_sem, 0, _cap);
            sem_init(&_con_sem, 0, 0);
        }

        ~RingQueue(){
            // 销毁信号量
            sem_destroy(&_pro_sem);
            sem_destroy(&_con_sem);
        }

        // 生产商品
        void Push(const T &inData){
            // 申请信号量
            P(&_pro_sem);
            // 生产
            _queue[_pro_step++] = inData;
            _pro_step %= _cap;
            // 释放信号量
            V(&_con_sem);
        }

        // 消费商品
        void Pop(T *outData){
            // 申请信号量
            P(&_con_sem);
            // 消费
            *outData = _queue[_con_step++];
            _con_step %= _cap;
            // 释放信号量
            V(&_pro_sem);
        }

    private:
        void P(sem_t *sem){
            sem_wait(sem);
        }

        void V(sem_t *sem){
            sem_post(sem);
        }

    private:
        std::vector<T> _queue; //这个环形队列我们直接使用数组实现
        size_t _cap;
        sem_t _pro_sem; //生产者信号量
        sem_t _con_sem;  //消费者信号量
        size_t _pro_step; // 生产者下标
        size_t _con_step; // 消费者下标
    };
}

3.2 客户端代码

3.2.1 引入用户信息

在首次接收到某个用户的信息时,需要将其进行标识,以便后续在进行消息广播时分发给他

有点类似于用户首次发送消息,就被拉入了 “群聊”。

目前可以使用 IP + Port 的方式标识用户,确保用户的唯一性,这里选取 unordered_map 这种哈希表结构,方便快速判断用户是否已存在

  • key用户标识符
  • value用户客户端的 sockaddr_in 结构体

注意: 这里的哈希表后面会涉及多线程的访问,需要加锁保护。

3.2.2 LockGuard小组件

利用RAII思想实现锁的自动化

#pragma once

#include<pthread.h>

class LockGuard{
    public:

     LockGuard(pthread_mutex_t *pmtx)
      :_mtx(pmtx)
     {
        pthread_mutex_lock(_mtx);
     }

     ~LockGuard(){
        pthread_mutex_unlock(_mtx);
     }
    private:
    pthread_mutex_t *_mtx;
};

3.2.3 Thread.hpp头文件

用自己的线程库

#pragma once

#include<iostream>
#include<string>
#include<pthread.h>
#include<functional>

enum class Status{
    NEW=0,//代表新建线程
    RUNNING,//代表运行
    EXIT //已退出线程
};
// 参数、返回值为 void 的函数类型
//typedef void (*func_t)(void*);
using func_t = std::function<void(void*)>;  // 使用包装器设定函数类型

class Thread{
public:
   Thread(int num=0,func_t func=nullptr,void *args=nullptr)
     :_tid(0)
     ,_status(Status::NEW)
     ,_func(func)
     ,_args(args)
   {
    //写入线程名字
    char name[128];
    snprintf(name,sizeof name,"thraed-%d",num);
    _name=name;
   }

   ~Thread(){}

   //获取线程id
   pthread_t getTID() const{
    return _tid;
   }
    
   //获取线程名字
   std::string getName() const{
     return _name;
   }
   

   //获取线程状态
   Status getStatus() const{
    return _status;
   }

     // 回调方法
    static void* runHelper(void* args){
        Thread* myThis = static_cast<Thread*>(args);
        // 很简单,回调用户传进来的 func 函数即可
        myThis->_func(myThis->_args);

        return nullptr;
    }

     // 启动线程
    void run(){
        int ret = pthread_create(&_tid, nullptr, runHelper, this);
        if(ret != 0){
            std::cerr << "create thread fail!" << std::endl;
            exit(1); // 创建线程失败,直接退出
        }
        _status =  Status::RUNNING; // 更改状态为 运行中
    }

    // 线程等待
    void join(){
        int ret = pthread_join(_tid, nullptr);
        if(ret != 0){
            std::cerr << "thread join fail!" << std::endl;
            exit(1); // 等待失败,直接退出
        }
        _status = Status::EXIT; // 更改状态为 退出
    }
private:
    pthread_t _tid; // 线程 ID
    std::string _name; // 线程名
    Status _status; // 线程状态
    func_t _func; // 线程回调函数
    void* _args; // 传递给回调函数的参数
};

3.2.4 server.hpp 代码

#include<iostream>
#include<string>
#include<functional>
#include<cerrno>
#include<cstring>
#include<cstdlib>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"err.hpp"
#include"RingQueue.hpp"
#include<unordered_map>
#include"Thread.hpp"
#include"LockGuard.hpp"
#include<cstdio>


namespace My_server{

   //端口号默认值
   const uint16_t default_port=8888;

   class server
   {
   private:
    /* data */
    int _sock;// 服务端套接字
    uint16_t _port;//端口号
    My_RingQueue::RingQueue<std::string> _rq; //阻塞队列
    std::unordered_map<std::string, struct sockaddr_in> _userTable; // <用户标识符, sockaddr_in 结构体>

    pthread_mutex_t _mtx; // 互斥锁,保护哈希表

    Thread* _producer;//生产者线程
    Thread* _consumer;//消费者线程

   public:
    server(uint16_t port=default_port)
     :_port(port)
    {
      pthread_mutex_init(&_mtx,nullptr);

       //创建线程,因为类内成员有隐含的this指针,需要bind固定该参数
      _producer = new Thread(1,std::bind(&server::RecvMessage,this));
      _consumer = new Thread(2,std::bind(&server::BroadcastMessage,this));
    }
    ~server(){

      //等待线程结束
      _producer->join();
      _consumer->join();

      //销毁互斥锁
      pthread_mutex_destroy(&_mtx);
     
     //释放对象
      delete _producer;
      delete _consumer;
    }

    //初始化服务器
    void StartServer(){

        //1 创建套接字
        _sock = socket(AF_INET,SOCK_DGRAM,0);
        if(_sock==-1){
            std::cout<<"Create Socket Fail:: "<<strerror(errno)<<std::endl;
            exit(SOCKET_ERR);
        }

        //创建成功
        std::cout<<"Create Success Socket: "<<_sock<<std::endl;

        //2. 绑定IP地址和端口号
        struct sockaddr_in local;
        bzero(&local,sizeof(local));// 将结构体内容置0

        //填充字段
        local.sin_family= AF_INET; //设置为网络通信
        local.sin_port=htons(_port);//主机序列转换为网络序列
        local.sin_addr.s_addr=INADDR_ANY; //服务器端要绑定任何可用IP

        //绑定 IP 地址和端口号
        if(bind(_sock,(const sockaddr*)&local,sizeof(local))){
            std::cout<<"Bind IP&&Port Fail: "<<strerror(errno)<<std::endl;
            exit(BIND_ERR);
        }
        
        //绑定成功
        std::cout<<" Bind IP&&Port Success"<< std::endl;

        _producer->run();
        _consumer->run();
    }
    
   //接收信息
    void RecvMessage(){

        //服务器不断运行,使用需要使用 一个whilc(true) 死循环
        char buff[1024];
        while(true){
            //1 作为客户端 要接收信息 
            struct sockaddr_in peer;// 客户端结构体
            socklen_t len = sizeof(peer); //客户端结构体大小

            ssize_t n=recvfrom(_sock,buff,sizeof(buff)-1,0,(struct sockaddr*)&peer,&len);

            if(n>0){
                buff[n]='\0';
            }
            else{
                continue;
            }

            //2. 处理数据
            std::string clientIp=inet_ntoa(peer.sin_addr);// 获取服务端IP地址
            uint16_t clientPort = ntohs(peer.sin_port);// 获取端口号
            printf("Server get message from [%s:%d]$ %s\n",clientIp.c_str(),clientPort,buff);

            //3 判断是否在聊天室加入该用户
            std::string user = clientIp + "-" + std::to_string(clientPort);
            
            //花括号作用域内使用锁 限定RAII锁的作用域
            {
              LockGuard lockguard(&_mtx);
              if(_userTable.count(user)==0){ //首次出现,加入用户表
                 _userTable[user]=peer;
              }
            }

            //4 将信息添加至环形队列
            std::string msg="["+ clientIp +":"+std::to_string(clientPort)+"] say#" + buff;
            _rq.Push(msg);
    }
    }
      // 广播消息
        void BroadcastMessage(){

            while(true) {
                // 1.从环形队列中获取消息
                std::string msg;
                _rq.Pop(&msg);

                // 2.将消息发给用户
                // TODO
                std::vector<sockaddr_in> arr;

                {
                 LockGuard lockguard(&_mtx);
                 for(auto &user:_userTable){
                    arr.push_back(user.second);
                 }
                }

                for(auto &add:arr){
                    //向客户端发送信息
                    sendto(_sock,msg.c_str(),msg.size(),0,(const sockaddr*)&add,sizeof(add));
                }
            } 
        }
   };
   
}

3.2.5 server.cc源文件

几乎不需要更改

#include<memory>
#include"server.hpp"

using namespace My_server;

int main()
{
    std::unique_ptr<server> msvr(new server());

    //初始化服务器
    msvr->StartServer();


    return 0;
}

四 客户端

  有了之前 server.hpp 服务器头文件多线程化的经验后,改造 client.hpp 客户端头文件就很简单了,同样是创建两个线程,一个负责发送消息,一个负责接收消息

4.1 client.hpp头文件

#pragma once

#include<iostream>
#include <string>
#include "err.hpp"
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstdlib>
#include <cstring>

namespace My_client{
    
    class client{
      private:
      /* data */
      std::string server_ip;//服务端 IP 地址
      uint16_t server_port;//服务器端口号
      int _sock;
      struct sockaddr_in _svr;
      public:
      //构造函数
      client(const std::string& ip,uint16_t port)
      :server_ip(ip)
      ,server_port(port)
      {}
      //析构函数
      ~client(){
      }
      // 初始化客户端
      void InitClient() {
        
         //1. 创建套接字
         _sock=socket(AF_INET,SOCK_DGRAM,0);
         if(_sock==-1){
           std::cout << "Create Socket Fail: " << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
         }

         std::cout<<"Create Success Socket:"<<_sock<<std::endl;

         //2. 构建服务器的sockaddr_in 结构体信息
         bzero(&_svr,sizeof(_svr));
         _svr.sin_family=AF_INET;
          // 绑定服务器IP地址
         _svr.sin_addr.s_addr=inet_addr(server_ip.c_str());
         //绑定服务器端口号
         _svr.sin_port=htons(server_port);
      }
      // 启动客户端
      void StartClient() {
       
       char buff[1024];
        // 1. 启动客户端
        while(true){
        std::string msg;
        std::cout<<"Input Message# ";
        std::getline(std::cin,msg);

        ssize_t n=sendto(_sock,msg.c_str(),msg.size(),0,(const struct sockaddr*)&_svr, sizeof(_svr));

        if(n==-1){
          std::cout<<"Send Message Fail: "<<strerror(errno)<<std::endl;
           continue;
        }

        //2 因为是回响 使用也要接收信息
        socklen_t len = sizeof(_svr);
         n = recvfrom(_sock,buff,sizeof(buff)-1,0,(struct sockaddr *)&_svr,&len);

         if(n>0){
            buff[n]='\0';
         }
         else{
            continue;
         }
         
         //可以再次获取 IP地址和 端口号
         std::string ip=inet_ntoa(_svr.sin_addr);
         uint16_t port=ntohs(_svr.sin_port);

          printf("Client get message from [%s:%d]# %s\n",ip.c_str(), port, buff);
      }
      }
    };
}

4.2 client.cc 客户端源文件

#include<memory>
#include"client.hpp"
#include"err.hpp"


using namespace My_client;

void Usage(const char* program){
    std::cout<<"Usage:"<<std::endl;
    std::cout<<"\t"<<program<<"ServerIP ServerPort" << std::endl;
}

int main(int argc,char *argv[]){

    if(argc!=3){
        //启动方式是错误的,提升错误信息
        Usage(argv[0]);
        return USAGE_ERR; 
    }

    std::string ip = argv[1];
    uint16_t port = std::stoi(argv[2]);

    std::unique_ptr<client> mcit(new client(ip,port));

    //启动客户端
    mcit->StartClient();

    return 0;
}

示例:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/561064.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【汇编语言】汇编语言程序

【汇编语言】汇编语言程序 文章目录 【汇编语言】汇编语言程序前言一、用汇编语言写的源程序汇编语言编写程序的工作过程程序中的三种伪指令源程序编译链接后变为机器码汇编程序的结构如何写出一个程序来程序中可能的错误 二、由源程序到程序运行由源程序到执行可执行文件的过程…

区间图着色问题:贪心算法设计及实现

区间图着色问题&#xff1a;贪心算法设计及实现 1. 问题定义2. 贪心算法设计2.1 活动排序2.2 分配教室2.3 算法终止 3. 伪代码4. C语言实现5. 算法分析6. 结论7. 参考文献 在本文中&#xff0c;我们将探讨如何使用贪心算法解决一个特定的资源分配问题&#xff0c;即区间图着色问…

【深度学习-番外1】Win10系统搭建VSCode+Anaconda+Pytorch+CUDA深度学习环境和框架全过程

专栏的老读者们都知道&#xff0c;以前的文章以使用MATLAB的为多。 不过后续陆续开始展开深度学习算法的应用&#xff0c;就会逐渐引入Python语言了&#xff08;当然MATLAB的代码也会同步更新&#xff09;&#xff0c;这是由于在深度学习领域&#xff0c;Python应用更为广泛。…

Matlab|【复现】主动配电网故障定位方法研究

目录 1 主要内容 算例模型 期望故障电流状态函数 评价函数&#xff08;膨胀率函数&#xff09; 算例验证方法 详实的文档说明 2 部分程序 3 程序结果 4 下载链接 1 主要内容 该程序方法复现了《基于改进多元宇宙算法的主动配电网故障定位方法研究》_郑聪&#xff0c;建…

在ELF 1开发环境中使用Qt Creator进行远程调试

Qt Creator是一款跨平台集成开发环境&#xff08;IDE&#xff09;&#xff0c;主要适用于支持Qt框架的各类应用程序开发。其内置的远程调试机制使得开发者能够在本地开发环境中对部署在远程设备上的代码进行调试&#xff0c;无需直接对远程设备进行操作。Qt Creator会通过网络连…

2.Vue简介

Vue简介 Vue (发音为 /vjuː/&#xff0c;类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的界面&#xff0c;V…

在 Linux 中删除文件和文件夹

目录 ⛳️推荐 前言 删除文件 &#x1f3cb;️练习文件删除 小心删除 删除目录 &#x1f3cb;️练习文件夹删除 测试你的知识 ⛳️推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到…

VSCode搭建内核源码阅读开发环境

0. 参考链接 使用VSCode进行linux内核代码阅读和开发_vscode阅读linux内核-CSDN博客 1. 搭建Linux内核源码阅读环境 现状&#xff0c;Linux内核源码比较庞大文件非常多&#xff0c;其中又包含的众多的宏定义开关配置选项&#xff0c;这使得阅读内核源代码称为一件头疼的事。 …

电脑工作者缓解眼部疲劳问题的工具分享

背景 作为以电脑为主要工作工具的人群&#xff0c;特别是开发人员&#xff0c;我们每天都需要长时间紧盯着屏幕&#xff0c;进行代码编写、程序调试、资料查询等工作。这种持续的工作模式无疑给我们的眼睛带来了不小的负担。一天下来&#xff0c;我们常常会感到眼睛干涩、疲劳…

[笔试强训day02]

文章目录 BC64 牛牛的快递DP4 最小花费爬楼梯[编程题]数组中两个字符串的最小距离 BC64 牛牛的快递 BC64 牛牛的快递 #include<iostream> #include<cmath> using namespace std;double a; char b;int main() {cin>>a>>b;int ans0;if(a<1.0){ans20;…

Go程序设计语言 学习笔记 第十三章 低级编程

Go的设计保证了一系列安全性&#xff0c;限制了Go程序可能出现问题的方式。在编译期间&#xff0c;类型检查会检测到大多数试图将操作应用于不适合其类型的值的尝试&#xff0c;例如&#xff0c;从一个字符串中减去另一个字符串。严格的类型转换规则阻止了直接访问内置类型&…

数字接龙(蓝桥杯)

文章目录 数字接龙【问题描述】解题思路DFS 数字接龙 【问题描述】 小蓝最近迷上了一款名为《数字接龙》的迷宫游戏&#xff0c;游戏在一个大小为N N 的格子棋盘上展开&#xff0c;其中每一个格子处都有着一个 0 . . . K − 1 之间的整数。游戏规则如下&#xff1a; 从左上…

【图解计算机网络】从浏览器地址输入到网页显示的整个过程

从浏览器地址输入到网页显示的整个过程 整体流程DHCPhttp协议报文组装DNSTCP协议封装与TCP三次握手IP协议封装与路由表MAC地址与ARP协议交换机路由器 整体流程 从往浏览器输入一个地址到网页的显示&#xff0c;要经过很长的一个流程&#xff0c;中间涉及到计算机网络的许多知识…

力扣-LCP 02.分式化简

题解&#xff1a; class Solution:def fraction(self, cont: List[int]) -> List[int]:# 初始化分子和分母为 0 和 1n, m 0, 1# 从最后一个元素开始遍历 cont 列表for a in cont[::-1]:# 更新分子和分母&#xff0c;分别为 m 和 (m * a n)n, m m, (m * a n)# 返回最终的…

VOJ 等边三角形 题解 DFS

等边三角形 代码 #include <bits/stdc.h> using namespace std; typedef long long ll; int n, flag 0, sum 0, tag; int length[20]; // 木棍长度 int group[3] {0}; // 三条边的当前边长 void dfs(int i, int index) {group[index] length[i];if (group[1] &g…

2024蓝桥杯嵌入式模板代码详解

文章目录 一、STM32CubeMx配置二、LED模板代码三、LCD模板代码 一、STM32CubeMx配置 打开STM32CubeMx&#xff0c;选择【File】->【New Project】&#xff0c;进入芯片选择界面&#xff0c;搜索到蓝桥杯官方的芯片型号&#xff0c;并点击收藏&#xff0c;下次直接点击收藏就…

React【Day4】

路由快速上手 1. 什么是前端路由 一个路径 path 对应一个组件 component 当我们在浏览器中访问一个 path 的时候&#xff0c;path 对应的组件会在页面中进行渲染 2. 创建路由开发环境 # 使用CRA创建项目 npm create-react-app react-router-pro# 安装最新的ReactRouter包 …

第64天:服务攻防-框架安全CVE复现Apache ShiroApache Solr

目录 思维导图 案例一&#xff1a;Apache Shiro-组件框架安全 shiro反序列化 cve_2016_4437 CVE-2020-17523 CVE-2020-1957 案例二&#xff1a;Apache Solr-组件框架安全 远程命令执行 RCE&#xff08;CVE-2017-12629&#xff09; 任意文件读取 AND 命令执行&#xff08…

建筑楼宇VR火灾扑灭救援虚拟仿真软件厂家

在传统消防安全教育方式中&#xff0c;往往存在内容枯燥、参与度低和风险大等问题&#xff0c;使得消防安全知识难以深入人心。然而&#xff0c;借助VR消防安全逃生教育系统&#xff0c;我们可以打破这一困境&#xff0c;为公众带来前所未有的学习体验。 VR消防安全逃生教育系统…

Java多线程-API

常见API一览 Thread t1 new Thread(() -> {System.out.println("我是线程t1");System.out.println("Hello, World!"); }); t1.start(); // 获取线程名称 getName() // 线程名称默认是Thread-0, Thread-1, ... System.out.println(t1.getName());// 通过…