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

一 程序介绍和核心功能

  这是基于 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.rhkb.cn/news/317911.html

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

相关文章

vue 实现项目进度甘特图

项目需求&#xff1a; 实现以1天、7天、30天为周期&#xff08;周期根据筛选条件选择&#xff09;&#xff0c;展示每个项目不同里程碑任务进度。 项目在Vue-Gantt-chart: 使用Vue做数据控制的Gantt图表基础上进行了改造。 有需要的小伙伴也可以直接引入插件&#xff0c;自己…

装饰器模式、代理模式、适配器模式对比

装饰器模式、代理模式和适配器模式都是结构型设计模式&#xff0c;它们的主要目标都是将将类或对象按某种布局组成更大的结构&#xff0c;使得程序结构更加清晰。这里将装饰器模式、代理模式和适配器模式进行比较&#xff0c;主要是因为三个设计模式的类图结构相似度较高、且功…

4-1 STM32C8T6控制OLED显示

实物接线&#xff1a; #include "stm32f10x.h" // Device header #include "delay.h" #include "LED.h" #include "Key.h" #include "Buzzer.h" #include "Oled.h"int main(void) {OLED_Init()…

基于SpringBoot实现各省距离Excel导出实战

目录 前言 一、列表及图表信息展示 1、数据过滤调整 2、信息列表及图表展示 3、Excel写入 二、界面可视化 1、Echarts图表和列表展示 2、城市详情和下载功能设计 三、成果展示 1、图表展示 2、部分城市数据分析 总结 前言 今天是五一黄金周假期第二天&#xff0c;不知…

搜索引擎的设计与实现参考论文(论文 + 源码)

【免费】搜索引擎的设计与实现.zip资源-CSDN文库https://download.csdn.net/download/JW_559/89249705?spm1001.2014.3001.5501 搜索引擎的设计与实现 摘要&#xff1a; 我们处在一个大数据的时代&#xff0c;伴随着网络信息资源的庞大&#xff0c;人们越来越多地注重怎样才能…

光模块基础概念

一:什么是光模块&#xff1f; 光模块作为光通信中的重要组成部分&#xff0c;是实现光信号传输过程中光电互相转换的光电子器件。 光模块通常由光发射组件、光接收组件、激光器芯片、探测器芯片等部件组成。光模块结构示意图&#xff08;SFP封装&#xff09;此图来源于光模块…

Tensorflow2.0笔记 - ResNet实践

本笔记记录使用ResNet18网络结构&#xff0c;进行CIFAR100数据集的训练和验证。由于参数较多&#xff0c;训练时间会比较长&#xff0c;因此只跑了10个epoch&#xff0c;准确率还没有提升上去。 import os import time import tensorflow as tf from tensorflow import keras …

自适应医疗决策框架 MDAgents:问题复杂度评估 + 医疗决策 + 多智能体协作

自适应医疗决策框架 MDAgents&#xff1a;问题复杂度评估 医疗决策 多智能体协作 提出背景MDAgents 拆解解法&#xff1a;MDAgents框架处理医疗问题3.1 查询复杂性评估例子&#xff1a;糖尿病患者的医疗查询 3.2 专家招募3.3 医疗协作与改良3.4 决策制定 分阶段决策1. 问题复…

【实时数仓架构】方法论

笔者不是专业的实时数仓架构&#xff0c;这是笔者从其他人经验和网上资料整理而来&#xff0c;仅供参考。写此文章意义&#xff0c;加深对实时数仓理解。 一、实时数仓架构技术演进 1.1 四种架构演进 1&#xff09;离线大数据架构 一种批处理离线数据分析架构&#xff0c;…

目标检测算法YOLOv3简介

YOLOv3由Joseph Redmon等人于2018年提出&#xff0c;论文名为&#xff1a;《YOLOv3: An Incremental Improvement》&#xff0c;论文见&#xff1a;https://arxiv.org/pdf/1804.02767.pdf &#xff0c;项目网页&#xff1a;https://pjreddie.com/darknet/yolo/ 。YOLOv3是对YOL…

leetcode870.优势洗牌

题目描述&#xff1a; 给定两个长度相等的数组 nums1 和 nums2&#xff0c;nums1 相对于 nums2 的优势可以用满足 nums1[i] > nums2[i] 的索引 i 的数目来描述。 返回 nums1 的任意排列&#xff0c;使其相对于 nums2 的优势最大化。 示例一&#xff1a; 输入&#xff…

BIO、NIO与AIO

文章目录 一 BIO同步阻塞案例BIO模式消息多发多收实现 二 NIONIO核心组件Buffer(缓冲区)Buffer常见方法缓冲区的数据操作直接内存与非直接内存 Channel(通道)channel常用操作 Selector(选择器)selector选择器处理流程NIO非阻塞式网络通信原理分析 NIO网络编程实现群聊系统服务端…

Acrobat Pro DC 2023:专业PDF编辑软件,引领高效办公新时代

Acrobat Pro DC 2023是一款专为Mac和Windows用户设计的专业PDF编辑软件&#xff0c;凭借其强大的功能和卓越的性能&#xff0c;成为现代职场人士不可或缺的得力助手。 这款软件拥有出色的PDF编辑能力。用户不仅可以轻松地对PDF文档中的文字、图片和布局进行编辑和调整&#xf…

【C++】哈希的应用---位图

目录 1、引入 2、位图的概念 3、位图的实现 ①框架的搭建 ②设置存在 ③设置不存在 ④检查存在 ​4、位图计算出现的次数 5、完整代码 1、引入 我们可以看一道面试题 给40亿个不重复的无符号整数&#xff0c;没排过序。给一个无符号整数&#xff0c;如何快速判断一个数…

菜鸡学习netty源码(一)——ServerBootStrap启动

1.概述 对于初学者而然,写一个netty本地进行测试的Server端和Client端,我们最先接触到的类就是ServerBootstrap和Bootstrap。这两个类都有一个公共的父类就是AbstractBootstrap. 那既然 ServerBootstrap和Bootstrap都有一个公共的分类,那就证明它们两个肯定有很多公共的职…

树莓派4B安装安卓系统LineageOS 21(Android14)

1&#xff1a;系统下载 2&#xff1a;下载好镜像后&#xff0c;准备写入SD卡&#xff0c;我这边使用的是 balenaetcher 3&#xff1a;插入树莓派&#xff0c;按照指示一步一步进行配置&#xff0c;可以配置时区&#xff0c;语言。 注意点 1》:想返回的时候按F2 2》:进入系统…

基于springboot实现中药实验管理系统设计项目【项目源码+论文说明】计算机毕业设计

基于springboot实现中药实验管理系统设计演示 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了中药实验管理系统的开发全过程。通过分析中药实验管理系统管理的不足&#xff0c;创建了一个计算机管理中药实验管…

AI视频教程下载:用ChatGPT提示词开发AI应用和GPTs

在这个课程中&#xff0c;你将深入ChatGPT的迷人世界&#xff0c;学习如何利用其能力构建创新和有影响力的工具。你将发现如何创建不仅吸引而且保持用户参与度的应用程序&#xff0c;将流量驱动到你的网站&#xff0c;并开辟新的货币化途径。 **课程的主要特点&#xff1a;** …

Python异步Redis客户端与通用缓存装饰器

前言 这里我将通过 redis-py 简易封装一个异步的Redis客户端&#xff0c;然后主要讲解设计一个支持各种缓存代理&#xff08;本地内存、Redis等&#xff09;的缓存装饰器&#xff0c;用于在减少一些不必要的计算、存储层的查询、网络IO等。 具体代码都封装在 HuiDBK/py-tools: …

蓝桥杯练习系统(算法训练)ALGO-953 混合积

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 众所周知&#xff0c;人人都在学习线性代数&#xff0c;既然都学过&#xff0c;那么解决本题应该很方便。   宇宙大战中&…