UDP实现Mini版在线聊天室

news2025/4/6 22:36:50

实现原理

只有当客户端先对服务器发送online消息的时候,服务器才会把客户端加入到在线列表。当在线列表的用户发消息的时候,服务器会把消息广播给在线列表中的所有用户。而当用户输入offline时,表明自己要下线了,此时服务器把该用户踢出在线列表。此时的用户看不到公屏的信息也无法在发送信息。

上线步骤:

在这里插入图片描述

通信步骤:

在这里插入图片描述

下线步骤

只要把用户踢出在线列表,那么它就是离线了,因为服务器只关心在线列表中的客户。

在这里插入图片描述

服务器要做的事

1. 判断收到的消息是否是online或者offline

2. 收到online则把用户添加进在线列表,offline则移除在线列表。

3. 如果发送的消息不是offline,切用户在线,则对发送的消息进行广播,广播给在线列表的所有用户

客户端要做的事

1. 向服务器发送online申请上线

2. 主线程负责发送消息,不发也可以

3. 创建一个线程时时刻刻接收消息,收到消息即显示到自己的公屏上

server服务端代码实现

服务端需要有一个UserManage类,来管理在线用户,这也是我们的在线列表。这个类只有一个哈希表成员,用来管理在线的用户。还要提供四个成员函数,分别有上线,下线,判断是否在线,以及广播功能。

server.cc代码:

#include "server.hpp"
#include <memory>
#include <unistd.h>
#include <fcntl.h>
#include <vector>
#include <sys/wait.h>
#include <cstring>
#include "User.hpp"

UserManage um;

void ChatRoomMessage(int _sock,std::string ip,uint16_t port,std::string message)
{
    //如果用户输入online,那么就把用户添加到在线列表
    if(message == "online") um.online(port,ip); 
    //如果用户输入offline,那么把用户移除在线列表
    if(message == "offline") um.offline(port,ip);
    //用户在线才能广播消息
    if(um.isonline(port,ip))    
        um.broadcastMessage(message,_sock,ip,port); //广播消息
}

int main(int argc , char* argv[])
{
    if(argc != 2) //命令行参数不为2就退出
    {
        std::cout << "Usage : " << argv[0] << "   bindport" << std::endl;  //打印使用手册
        exit(1);
    }
    uint16_t port = atoi(argv[1]); //命令行传的端口转换成16位整形
    std::unique_ptr<UdpServer> s(new UdpServer(port,ChatRoomMessage)); //创建UDP服务器
    s->init(); //初始化服务器,创建 + 绑定
    s->start(); //运行服务器
}

server.hpp代码:

这个类主要是对服务器的封装,在收到消息后通过用户传入的callback函数进行回调处理。


#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <functional>

typedef std::function<void(int,std::string,uint16_t,std::string)> func_t;

class UdpServer
{
private:
    int _sock; 
    uint16_t _port;
    func_t _callback;
public:
    UdpServer(uint16_t port,func_t callback): _port(port) ,_callback(callback){ }
    ~UdpServer() { close(_sock); }
    void init()
    {
        _sock = socket(AF_INET,SOCK_DGRAM,0);  //创建套接字
        if(_sock < 0)
        {
            //创建失败
            std::cout << "create socket failed...." << std::endl;
            abort();
        }
        //绑定 
        struct sockaddr_in ser; 
        ser.sin_port = htons(_port);  //填入端口
        ser.sin_family = AF_INET; // 填入域
        ser.sin_addr.s_addr = INADDR_ANY; //填入IP地址
        if(bind(_sock,(sockaddr*)&ser,sizeof ser) != 0) //绑定
        {
            //绑定失败
            std::cout << "bind socket failed...." << std::endl;
            abort();
        }
    }

    void start()
    {
        struct sockaddr_in peer; //对端
        socklen_t peer_len = sizeof peer;
        char buff[1024] = {0};   
        while(1)
        {
            int n = recvfrom(_sock,buff,1023,0,(struct sockaddr*)&peer,&peer_len); 
            buff[n] = 0;
            if(read == 0)
            {
                std::cout << "one client quit..." << std::endl;
                continue;
            }else if(read < 0)
            {
                 std::cout << "read error..." << std::endl;
                 break;
            }
            //获取客户端的端口和IP
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << buff << std::endl; //回显客户端信息
            //调用回调函数处理数据
            _callback(_sock,clientip,clientport,buff);
        }
    } 
};

User.hpp代码:

这个头文件有2个类,User类是对用户的一层抽象,如果你用户还有其他的信息也可以加入到User类中。UserManage是对在线用户的管理,提供了增删查操作,以及消息广播。

#pragma once 
#include <iostream>
#include <string>
#include <unordered_map>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>


class User
{
public:
    User(uint16_t port , const std::string& ip) :_port(port),_ip(ip){}

    std::string ip(){return _ip;}
    uint16_t port(){return _port;}
private:
    uint16_t _port;
    std::string _ip; 
};


class UserManage
{
public:
    //上线
    bool online(uint16_t port,const std::string& ip)
    {
        std::string id = ip + "-" + std::to_string(port); 
        auto it = _users.find(id); 
        User u(port,ip);
        //如果不在上线列表中,加入到上线列表
        if(it == _users.end()) 
        {
            _users.insert(std::make_pair(id,u));
            std::cout <<"[" <<  id << "  is online]" << std::endl;
        }
        else return false;

        return true;
    }
    //下线
    bool offline(uint16_t port,const std::string& ip)
    {
         std::string id = ip + "-" + std::to_string(port);
         auto it = _users.find(id);
         if(it != _users.end())
         {
            _users.erase(id); //移除在线列表
            std::cout <<"[" <<  id << "  is offline]" << std::endl;
            return true;
         }
         //没找到该用户,下线错误
         return false;
    }

    //用户是否在线
    bool isonline(uint16_t port,const std::string& ip)
    {
         std::string id = ip + "-" + std::to_string(port);
        auto it = _users.find(id);
        return it != _users.end();
    }

    //消息转发,把消息转发给用户列表的所有人
    void broadcastMessage(const std::string& message, int _sock,std::string ip,uint16_t port)
    {
        for(auto& u : _users)
        {
            //构建客户端sockaddr_in
            uint16_t u_port = u.second.port(); //要广播的客户端端口
            std::string u_ip = u.second.ip();  //要广播的客户端ip
            struct sockaddr_in client; 
            client.sin_family = AF_INET; 
            client.sin_port = htons(u_port); 
            client.sin_addr.s_addr = inet_addr(u_ip.c_str()); 
            //这里的ip和port是发送消息人的端口和port
            std::string response = ip + "-" + std::to_string(port) + " :" + message;

            //发送消息
            sendto(_sock,response.c_str(),response.size(),0,(struct sockaddr*)&client,sizeof client);
        }
    }

private:
    std::unordered_map<std::string,User> _users;  //记录在线用户
};

client客户端实现

客户端必须要保证至少2个线程,因为读消息和发送消息在一个线程里进行的话,会发送IO阻塞。除非你用多路转接=,=这里暂时不使用这种方法。

client.cc代码:

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

int main(int argc , char* argv[])
{
    if(argc != 3)
    {
        std::cout << "Usage : " << argv[0] << "   serverip  serverport" << std::endl; 
        exit(1);
    }
    uint16_t port = atoi(argv[2]); 
    std::string ip = argv[1];
    std::unique_ptr<UdpClient> cli(new UdpClient(port,ip)); 
    cli->init();
    cli->start();
}

client.hpp代码:

start函数负责处理用户发送的消息,RecvMessageThread函数是线程的执行函数,负责收服务器广播回来的消息,并把消息打印在公屏上,注意回显消息要用cerr打印!因为我们测试的时候会把cout重定向到一个命名管道中。

#pragma once
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <iostream>
#include <arpa/inet.h>
#include <cstdio>
#include <cstring>
#include <pthread.h>

class UdpClient
{
public:
    UdpClient(uint16_t port, const std::string &ip) : _port(port), _svr_ip(ip) {}
    ~UdpClient() { close(_sock); }

    void init()
    {
        // 套接字创捷
        _sock = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sock < 0)
        {
            std::cout << "create socket failed...." << std::endl;
            abort();
        }
    }

    // 线程执行函数,负责接收消息
    static void *RecvMessageThread(void *args)
    {
        while (1)
        {
            int *sock = (int *)args; //提取sock套接字

            // 收服务器广播来的消息
            char recvbuff[1024 * 4] = {0};
            recvfrom(*sock, recvbuff, sizeof recvbuff - 1, 0, nullptr, nullptr);
            // 打印回收到的消息
            std::cout << recvbuff << std::endl;
        }
        return nullptr;
    }
    void start()
    {
        // 创建服务器的 sockaddr结构
        struct sockaddr_in svr;
        svr.sin_port = htons(_port);
        svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());
        svr.sin_family = AF_INET;

        // 发送消息的缓冲区
        char sendbuff[1024] = {0};

        // 创建一个线程来接收别人发送的信息
        pthread_t tid;
        pthread_create(&tid, nullptr, RecvMessageThread, (void *)&_sock);

        // 该线程负责发送消息
        while (1)
        {
            // 输入消息
            std::cerr << "Enrty # ";
            fgets(sendbuff, sizeof sendbuff - 1, stdin);
            sendbuff[strlen(sendbuff) - 1] = 0;
            std::string message = sendbuff;
            // 发送消息
            sendto(_sock, message.c_str(), message.size(), 0, (struct sockaddr *)&svr, sizeof svr);
        }
    }

private:
    int _sock;
    uint16_t _port;
    std::string _svr_ip;
};

测试代码:

首先我们启动服务器,绑定端口8080(这个绑定其他的也可以)。

在这里插入图片描述

随后启动一个客户端,创建一个管道,这里的管道就相当于聊天室中的公屏,而自己在命令行里输入的是自己的输入窗口。而不是输入栏和接收栏都用一个窗口,这样显得十分怪异,因为自己的消息会回显2次。

在这里插入图片描述

随后一个窗口启动客户端并把内容重定向到管道,一个窗口监视管道。

在这里插入图片描述

此时的客户端是没有在线的,我们输入online即可上线。

在这里插入图片描述

此时我们再创建一个客户端,进行同样的操作,但是暂时不要上线,看看没上线的客户端是否可以和上线的客户端通信。我们会发现没上线的客户端发消息,上线的客户端是看不见的。

在这里插入图片描述

我们让上线的客户端也发送消息,我们发现客户端2是无法收到的。

在这里插入图片描述

我们让客户端2输入online登录,随即两个客户端进行通信,而一旦客户端2下线后,客户端2的消息将无法被送达客户端1。

在这里插入图片描述

因为命令行,只有使用ctrl+退格键才能退格,而退格之后会产生乱码…这些都是小事情,和程序本身无关。

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

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

相关文章

keil参数删除后补写没有代码提示

代码提示快捷键 ctrlalt空格 如果按了之后没有提示&#xff0c;那说明跟输入法的快捷键冲突了。

C++引用和右值引用

我最近开了几个专栏&#xff0c;诚信互三&#xff01; > |||《算法专栏》&#xff1a;&#xff1a;刷题教程来自网站《代码随想录》。||| > |||《C专栏》&#xff1a;&#xff1a;记录我学习C的经历&#xff0c;看完你一定会有收获。||| > |||《Linux专栏》&#xff1…

【vue】toRef,toRefs

toRef&#xff1a;把一个 响应式对象 转换为对应的ref变量toRefs&#xff1a;把一个 响应式对象 转换为对应的ref对象 代码 <template><P>mname: {{ mname }} </P><P>mage: {{ mage }} </P><P>msex: {{ msex }} </P><P>mhobb…

c语言中<string.h>的strstr与strtok函数

c语言中string.h的strstr与strtok函数 代码运行结果 代码 #include <stdio.h> #include <string.h>///1.在字符串str1里面,查找第一次出现str2的位置 //char * strstr(const char * str1,const char * str2)///2.sep为分割符,根据分割符来对str进行分割 //char * …

【计算机毕业设计】基于Java+SSM的实战开发项目150套(附源码+演示视频+LW)

大家好&#xff01;我是程序猿老A&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f9e1;今天给大家分享150的Java毕业设计&#xff0c;基于ssm框架&#xff0c;这些项目都经过精心挑选&#xff0c;涵盖了不同的实战主题和用例&#xff0c;可做毕业设计和课程…

轻量级 S3 协议存储客户端

目前大家一般不会把二进制文件直接放在应用服务器上&#xff0c;而是存在“对象存储”的方案中&#xff0c;例如亚马逊的 AWS&#xff0c;阿里云的 OSS、Cloudflare R2 等。AWS 为最早的始作俑者&#xff0c;因此其 S3 协议也近乎标准化&#xff0c;各大厂商的对象存储方案都实…

苹果全力升级:用专注AI的M4芯片彻底改造Mac系列

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

vue3+elment复杂详情页面打开后,再打开其他页面都显示空白,控制台也没什么特殊报错

页面使用了el-tabs 、 el-tab-pane、el-table 等标签 但是经测试不是这些问题导致的 js也使用了onMounted &#xff0c;但是除掉也时空白页面 反正之前人写的页面可乱&#xff0c;尤其是js这块&#xff0c;穿插引用import一大堆 主题页面样式布局如下 最后看到页面代码太乱…

funasr 麦克风实时流语音识别;模拟vad检测单独输出完整每句话

参考: https://github.com/alibaba-damo-academy/FunASR chunk_size 是用于流式传输延迟的配置。[0,10,5] 表示实时显示的粒度为 1060=600 毫秒,并且预测的向前信息为 560=300 毫秒。每个推理输入为 600 毫秒(采样点为 16000*0.6=960),输出为相应的文本。对于最后一个语音…

【树莓派初始化】教你从0开始搭建树莓派的使用环境

文章目录 前言1.什么是树莓派&#xff1f;1.1什么用户适合购买树莓派学习编程&#xff1f; 2.如何初始化一个树莓派2.1 烧录系统2.2 测试开机2.3 设置树莓派显示输出的分辨率2.4 网络链接2.5 Putty链接树莓派2.6 VNC链接树莓派2.7 使用filezilla软件传输文件到树莓派 3.使用Xsh…

从 0 搭建公司Jenkins服务 Centos7

从 0 搭建公司Jenkins服务 Centos7 安装 (运维人员) 安装环境 配置DNS安装JDK17安装Jenkins安装Docker安装GIT安装Ansible启动Jenkins安装插件配置凭据配置共享库配置 (开发经理)使用 (开发、测试人员) 安装 (运维人员) 安装环境 配置DNS 新安装系统的服务器无法解析域名&a…

【数据结构】二叉爆炸

【数据结构】二叉爆炸 按照惯例整点抽象的&#xff0c;贴上这篇博客的名字由来&#xff1a; 言归正传&#xff0c;本篇博客介绍二叉树的构造方式、前中后序遍历、层序遍历以及代码随想录中二叉树章节的相关题目&#xff1a; 代码随想录 (programmercarl.com) 一、啥是二叉树 …

【150套】基于SSM框架的Java毕业设计开发实战项目(附源码+演示视频+LW)

大家好&#xff01;我是程序员一帆&#xff0c;感谢您阅读本文&#xff0c;欢迎一键三连哦。 &#x1f9e1;今天给大家分享150的Java毕业设计&#xff0c;基于ssm框架&#xff0c;这些项目都经过精心挑选&#xff0c;涵盖了不同的实战主题和用例&#xff0c;可做毕业设计和课程…

服务器docker应用一览

文章目录 一、需求概况二、业务流程三、运行效果四、实现过程1. 基础前提2. 源码放送3.核心代码4. 项目打包5.部署步骤 一、需求概况 现有某云主机服务器&#xff0c;用来做项目演示用&#xff0c;上面运行了docker应用&#xff0c;现希望有一总览页面&#xff0c;用来展示部署…

fastjson 序列化问题

问题: 使用fastjson 的 对同一个JSONObject对象 多次引用后, 通过 JSON.toJSONString() 方法进行json序列化时出现只有第一次的可以成功序列化未json string 字符串, 后面的对象都为引用地址; 示例: public static void main(String[] args) {JSONObject jsonObject new JSON…

【C语言__结构体__复习篇3】

目录 前言 一、结构体基础知识 1.1 结构体的语法形式 1.2 创建结构体变量 1.3 结构体变量的初始化 1.4 点(.)操作符和箭头(->)操作符 二、匿名结构体 三、结构体自引用 四、结构体内存对齐 4.1 内存对齐的规则 4.2 出现结构体内存对齐的原因 4.3 修改默认对齐数 五、结…

【YOLOv9】完胜V8的SOTA模型Yolov9(论文阅读笔记)

官方论文地址&#xff1a; 论文地址点击即可跳转 官方代码地址&#xff1a; GitCode - 开发者的代码家园 官方代码地址点击即可跳转 1 总述 当输入数据经过各层的特征提取和变换的时候&#xff0c;都会丢失一定的信息。针对这一问题&#xff1a; 论文中提出的可编程梯度信息…

高颜值的B端界面,如果漂亮也有错,就让它错下去吧。

分享一波漂亮的B端界面&#xff0c;内行看门道&#xff0c;外行看热闹&#xff0c;不喜勿喷昂。

防止狗上沙发,写一个浏览器实时识别目标检测功能

家里有一条狗&#x1f436;&#xff0c;很喜欢乘人不备睡沙发&#x1f6cb;️&#xff0c;恰好最近刚搬家 狗迎来了掉毛期 不想让沙发上很多毛。所以希望能识别到狗&#xff0c;然后播放“gun 下去”的音频&#x1f4e3;。 需求分析 需要一个摄像头&#x1f4f7; 利用 chrome…

反激电源RC吸收电路设计

一、什么是RC吸收&#xff1f; RC吸收是指在电路设计中&#xff0c;尤其是在开关电源、功率电子设备以及电力电子系统中&#xff0c;使用电阻与电容串联组成的电路结构&#xff0c;用于吸收和衰减电路中由于开关元件&#xff08;如MOSFET、IGBT等&#xff09;的快速切换所产生的…