UDP实现Mini聊天室

实现原理

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

上线步骤:

通信步骤:

下线步骤

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

服务器要做的事

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

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

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

客户端要做的事

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

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

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

server服务端代码实现

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

server.cc代码:

cpp 复制代码
#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函数进行回调处理。

cpp 复制代码
#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是对在线用户的管理,提供了增删查操作,以及消息广播。

cpp 复制代码
#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代码:

cpp 复制代码
#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重定向到一个命名管道中。

cpp 复制代码
#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+退格键才能退格,而退格之后会产生乱码.....这些都是小事情,和程序本身无关。

相关推荐
爱勇宝15 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒19 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码20 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo20 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志21 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..21 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects21 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰21 小时前
一些基础和问题解决
后端
陈随易1 天前
前端项目部署只要30秒
前端·后端·程序员
YIAN1 天前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp