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

一 程序介绍和核心功能

  这是基于 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(){}//获取线程idpthread_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; // 线程 IDstd::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.将消息发给用户// TODOstd::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;
}

示例:

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[Kubernetes] etcd的集群基石作用

文章目录 1. 配置存储2. 数据一致性3. 服务发现与协调4. 集群状态中枢5. 集群稳定性 1. 配置存储 etcd作为一个高度可靠的分布式键值存储系统&#xff0c;存储了Kubernetes集群的完整配置和状态数据。集群的元数据&#xff0c;包括节点信息、命名空间、部署、副本集、服务、持…

5.11 mybatis之returnInstanceForEmptyRow作用

文章目录 1. 当returnInstanceForEmptyRowtrue时2 当returnInstanceForEmptyRowfalse时 mybatis的settings配置中有个属性returnInstanceForEmptyRow&#xff0c;该属性新增于mybatis的3.4.2版本&#xff0c;低于此版本不可用。该属性的作用官方解释为&#xff1a;当返回行的所…

使用Python+opencv实现自动扫雷

大家好&#xff0c;相信许多人很早就知道有扫雷这么一款经典的游戏&#xff0c;更是有不少人曾听说过中国雷圣&#xff0c;也是中国扫雷第一、世界综合排名第二的郭蔚嘉的顶顶大名。扫雷作为一款在Windows9x时代就已经诞生的经典游戏&#xff0c;从过去到现在依然都有着它独特的…

汽车4S集团数据分析

派可数据分析--汽车4S集团。 派可数据汽车4S集团数据分析概述。派可数据汽车4S集团分析主题全面涵盖行业内各板块业务分析&#xff0c;具体包括&#xff1a;保险业务分析、客户关系分析、汽车保养情况分析、售后维修主题分析、整车销售分析、整车库存分析、装具销售分析、配件…

dbearver达梦连接

1、新建达梦驱动 新建驱动管理器 点击“数据库”&#xff0c;选择“驱动管理器” 配置 点击“新建”&#xff0c;出现配置界面 类名&#xff1a;dm.jdbc.driver.DmDriver #固定值&#xff0c;不能修改URL模板&#xff1a;jdbc:dm://{host}/DMHR #配置要连接的数据库信息默认…

【VIC水文模型】准备工作:平台软件安装

VIC水文模型所需平台软件安装 1 Arcgis安装2 Cygwin安装&#xff08;Linux系统&#xff09;3 Matlab/R/Fortran的安装Notepad 4 VIC模型程序代码获取参考 由于VIC模型的编程语言为C语言&#xff0c;交互方式为控制台输指令&#xff0c;需要在Linux系统上运行。Windows 上使用 …

Https网站接口被黑被恶意调取

背景&#xff1a; 维护的一个网站最近短信接口被黑&#xff0c;发送大量短信。起初以为是在网站内部操作&#xff0c;优化了发送短信前的操作&#xff0c;如添加图形验证码&#xff0c;屏蔽国外IP等。但后续还存在被调取情况&#xff0c;定位排查到是该接口在外部被恶意调取。 …

免费使用ChatGPT 4.0 和 文心一言 4.0

前言 今天给大家分享如何免费使用ChatGPT4.0 和 文心一言 4.0&#xff0c;废话就不多说了&#xff0c;我们直接入正题。 ChatGPT 4.0 先来看看如何免费使用ChatGPT 4.0 进入Coze登录 https://www.coze.com 选择大圣-GPT-4 文心一言 4.0 通过文心智能体平台&#xff0c;就…

Java 笔记 03:Java 基础知识,使用 IDEA 创建 Java 项目、设置注释颜色,以及自动生成 JavaDoc

一、前言 记录时间 [2024-04-21] 系列文章简摘&#xff1a; Java 笔记 01&#xff1a;Java 概述&#xff0c;MarkDown 常用语法整理 Java 笔记 02&#xff1a;Java 开发环境的搭建&#xff0c;IDEA / Notepad / JDK 安装及环境配置&#xff0c;编写第一个 Java 程序 本文讲述了…

OJ:数字三角形(搜索)

&#x1f381;个人主页&#xff1a;我们的五年 &#x1f50d;系列专栏&#xff1a;每日一练 &#x1f337;追光的人&#xff0c;终会万丈光芒 &#x1f337;1.问题描述&#xff1a; ⛳️题目描述&#xff1a; 示出了一个数字三角形。 请编一个程序计算从顶至底的某处的一条路…

对接浦发银行支付(六)-- 请求退款接口与查询退款结果接口

一、概述 本文介绍浦发银行支付的请求退款和查询退款结果两个接口&#xff0c;浦发银行的退款流水号是以5901开头。发起退款的时候&#xff0c;浦发银行返回浦发银行退款流水号给我们&#xff08;这里的我们是指对接浦发银行支付的一方&#xff0c;于浦发银行而言&#xff0c;…

面向对象设计模式之概念

设计模式系列的观点结合了《HeadFirst设计模式》(中文版)以及《设计模式&#xff1a;可复用面向对象软件的基础》两本书的知识&#xff0c;以及Sunny(刘伟)的博客 《HeadFirst设计模式》(中文版)&#xff1a; 百度网盘链接&#xff1a;https://pan.baidu.com/s/1osvnUGZZREm8Jb…

「GO基础」变量

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

python-自动化篇-终极工具-用GUI自动控制键盘和鼠标-pyautogui-键盘

文章目录 键盘键盘——记忆宫殿入门——通过键盘发送一个字符串——typewrite()常规——键名——typewrite()常规——按下键盘——keyDown()常规——释放键盘——keyUp()升级——热键组合——hotkey() 键盘 pyautogui也有一些函数向计算机发送虚拟按键&#xff0c;让你能够填充…

【介绍下WebStorm开发插件】

&#x1f3a5;博主&#xff1a;程序员不想YY啊 &#x1f4ab;CSDN优质创作者&#xff0c;CSDN实力新星&#xff0c;CSDN博客专家 &#x1f917;点赞&#x1f388;收藏⭐再看&#x1f4ab;养成习惯 ✨希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出…

深入了解PBKDF2:密码学中的关键推导函数

title: 深入了解PBKDF2&#xff1a;密码学中的关键推导函数 date: 2024/4/20 20:37:35 updated: 2024/4/20 20:37:35 tags: 密码学对称加密哈希函数KDFPBKDF2安全密钥派生 第一章&#xff1a;密码学基础 对称加密和哈希函数 对称加密&#xff1a;对称加密是一种加密技术&…

[阅读笔记15][Orca]Progressive Learning from Complex Explanation Traces of GPT-4

接下来是微软的Orca这篇论文&#xff0c;23年6月挂到了arxiv上。 目前利用大模型输出来训练小模型的研究都是在模仿&#xff0c;它们倾向于学习大模型的风格而不是它们的推理过程&#xff0c;这导致这些小模型的质量不高。Orca是一个有13B参数的小模型&#xff0c;它可以学习到…

Java中的四种引用类型

6.Java中的引用类型 1.强引用 一个对象A被局部变量、静态变量引用了就产生了强引用。因为局部变量、静态变量都是被GC Root对象关联上的&#xff0c;所以被引用的对象A&#xff0c;就在GC Root的引用链上了。只要这一层关系存在&#xff0c;对象A就不会被垃圾回收器回收。所以只…

计算机视觉——OpenCV Python位运算与图像掩码

概述 位运算与图像掩码的结合允许对图像的特定区域进行精确的操作。通过使用位运算&#xff08;如AND、OR、XOR和NOT&#xff09;&#xff0c;可以基于掩码的选择性地修改图像数据。位运算与图像掩码结合使用的一些关键点和应用场景&#xff1a; 选择性修改&#xff1a; 通过位…

内网云盘如何内网穿透实现公网访问

云盘是一种专业的互联网存储工具&#xff0c;是互联网云技术的产物&#xff0c;它通过互联网为企业和个人提供信息的存储、读取、下载等服务&#xff0c;具有安全稳定、海量存储的特点。随着企业信息化发展&#xff0c;云盘系统需求不断扩大&#xff0c;相关系统软件被广泛应用…