【网络】网络聊天室udp

网络聊天室udp


一、低耦合度代码

1、代码

2、测试结果

二、高耦合度代码

1、服务端小改

(1)维护一个unordered_map用户列表

(2)服务端代码

cpp 复制代码
#pragma once 

#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include "Log.hpp"

using func_t = std::function<std::string(const std::string&, const std::string&, uint16_t)>; // 将返回值为string,参数为const string&的函数包装起来

extern Log log;

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";

enum 
{
    SOCKET_ERR=1,
    BIND_ERR
};

class UdpServer
{
public:
    // 构造函数
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : _socketfd(0)
        , _port(port)
        , _ip(ip)
        , _isrunning(false)
    {}
    void Init()
    {
        // 1.创建udp套接字socket
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        // 创建失败
        if (_socketfd < 0)
        {
            log(Fatal, "socket create error,socketfd:%d", _socketfd);
            exit(SOCKET_ERR);
        }
        // 创建成功
        log(Info, "socket create sucess,socketfd:%d", _socketfd);

        // 2.绑定端口号bind socket
        struct sockaddr_in local; // 网络套接字结构体
        bzero(&local, sizeof(local)); // 将该套接字结构体对象全部清零
        local.sin_family = AF_INET; // 类型:ipv4
        local.sin_port = htons(_port); // 端口号:是在网络中来回发送的,我发过去要让对面知道我发的端口号是什么,所以必须是网络字节序列
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string->unit_32 2.来回通信对方要知道发送的ip,所以ip的unit_32必须是网络序列的
        int n = bind(_socketfd, (const struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            log(Fatal, "bind error, erron:%d, errno string:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info, "bind sucess");
    }

    void CheckUser(const struct sockaddr_in &client, const std::string& clientip, uint16_t clientport)
    {
        auto it = _online_user.find(clientip);
        if (it == _online_user.end())
        {
            // 添加
            _online_user.insert({clientip, client});
            std::cout << "{ " << clientip << " }" << "[ " << clientport << "]" << "#addr onlineuser sucess" << std::endl;
        }
    }
    void Broadcast(const std::string& info, const std::string& clientip, uint16_t clientport)
    {
        for (const auto& user : _online_user)
        {
            std::string message = "{ ";
            message += clientip;
            message += " }";
            message += "[ ";
            message += std::to_string(clientport);
            message += " ]#";
            message += info;
            socklen_t len = sizeof(user.second);
            sendto(_socketfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), len);
        }
    }

    void Run(/*func_t func*/) // 对代码进行分层
    {
        _isrunning = true;
        char inbuffer[1024];
        while (_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_socketfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if (n < 0)
            {
                log(Warning, "recvfrom error");
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port); // 网络序列转主机序列,拿到port
            std::string clientip = inet_ntoa(client.sin_addr); // 将网络序列的ip地址转化成为主机的序列的ip地址,风格是字符串类型的
            
            // 判断是不是新用户,新用户则添加到onlineuser
            CheckUser(client, clientip, clientport);
            
            std::string info = inbuffer;
            // 转发给所有人
            Broadcast(info, clientip, clientport);
        }
    }
    // 析构函数
    ~UdpServer()
    {
        if (_socketfd > 0) 
        {
            close(_socketfd);
        }
    }
private:
    int _socketfd; // 网络文件描述符,表示socket返回的文件描述符
    uint16_t _port; // 表明服务器进程的端口号
    std::string _ip; // ip地址,任意地址绑定为0
    bool _isrunning; // 判断是否运行
    std::unordered_map<std::string, struct sockaddr_in> _online_user; // client列表 主机序列的ip地址,网络序列的套接字信息
};

(3)客户端不改的情况下结果展示

出现一个比较奇葩的问题,我上面linux客户端是先链接的,发出一个消息halo,发现能够显示出来,我下面的vs是后面链接的,发了个haha,发现是能够显示的,但是我上面linux客户端发现halo被haha覆盖住了,而我们知道udp是个全双工的,就是接收消息和发送消息是可以分开的,可以套在多线程中的,但是上面消息却会被覆盖,因为在客户端的getline那边会阻塞住,会让优先级发送消息,我客户端要一直按回车才能收到别人发的消息,而不能实现消息的自动弹出,客户端只能一条一条收到消息,所以我们在客户端需要使用多线程版本的。我想实现一个我不发消息也能看到别人群聊发的消息。

2、大改客户端(udp全双工用多线程)

我们因为上面客户端不改变的情况下发现的是我要一直摁回车才能收到别人发的消息,其本质原因就是因为getline会阻塞住消息,即一条消息一发一条消息一收,所以我们下面客户端改成全双工,即接消息和发消息分开,这样能够实现我不发消息依旧能够收到别人发的消息,不需要按回车,我们看一下现象,再分析代码:

i、结果
ii、代码分析
iii、源代码
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>

void Usage(std::string proc)
{
    std::cout << "\n\rUsages: " << proc << "serverip serverport\n" << std::endl;
}

struct ThreadData
{
    struct sockaddr_in server;
    int socketfd;
};

void* recv_message(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    char buffer[1024];
    while (true)
    {
        // 收数据 -- 从socket文件中的数据拿出来到buffer中,并将收到的对方的个人信息进行保存到temp中
        struct sockaddr_in temp;
        socklen_t len2 = sizeof(temp);
        ssize_t n = recvfrom(td->socketfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len2);
        if (n > 0)
        {
            buffer[n] = 0;
            // 打印数据
            std::cout << buffer << std::endl;
        }
    }
}

void* send_message(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    std::string message;
    socklen_t len = sizeof(td->server);
    while (true)
    {
        // 数据
        std::cout << "Please Enter# ";
        getline(std::cin, message);
        
        // 发送数据 -- 把数据发送到socketfd文件中,并将server信息提炼出来发送给server,可以理解成唤醒server
        sendto(td->socketfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);
    }
}

// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1]; // serverip
    uint16_t serverport = std::stoi(argv[2]); // serverport

    struct ThreadData td;

    // 给谁发
    bzero(&td.server, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());

    td.socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
    if (td.socketfd < 0)
    {
        std::cout << "socket create error" << std::endl;
        return 1;
    }

    pthread_t recver;
    pthread_t sender;
    pthread_create(&recver, nullptr, recv_message, &td);
    pthread_create(&sender, nullptr, send_message, &td);

    pthread_join(recver, nullptr);
    pthread_join(sender, nullptr);

    close(td.socketfd);
    return 0;
}

3、ls /dev/pts/

我们发现的是/dev/pts/的3号文件是我们的1号终端,我们发送消息给3号文件就是发送给了1号终端了。

4、重定向输出dup2后在终端打印效果

test.cc:

cpp 复制代码
g++ test.cc

5、利用终端创建一个简易的聊天室

(1)代码

(2)结果展示

(3)也可以在终端用一个命令

这里的3是你需要ls 命令去看哪个终端能用的文件信息的,因为linux下一切皆文件,那么终端也是文件。

6、代码汇总

udpserver.hpp:

cpp 复制代码
#pragma once 

#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unordered_map>
#include "Log.hpp"

using func_t = std::function<std::string(const std::string&, const std::string&, uint16_t)>; // 将返回值为string,参数为const string&的函数包装起来

extern Log log;

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";

enum 
{
    SOCKET_ERR=1,
    BIND_ERR
};

class UdpServer
{
public:
    // 构造函数
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip)
        : _socketfd(0)
        , _port(port)
        , _ip(ip)
        , _isrunning(false)
    {}
    void Init()
    {
        // 1.创建udp套接字socket
        _socketfd = socket(AF_INET, SOCK_DGRAM, 0);
        // 创建失败
        if (_socketfd < 0)
        {
            log(Fatal, "socket create error,socketfd:%d", _socketfd);
            exit(SOCKET_ERR);
        }
        // 创建成功
        log(Info, "socket create sucess,socketfd:%d", _socketfd);

        // 2.绑定端口号bind socket
        struct sockaddr_in local; // 网络套接字结构体
        bzero(&local, sizeof(local)); // 将该套接字结构体对象全部清零
        local.sin_family = AF_INET; // 类型:ipv4
        local.sin_port = htons(_port); // 端口号:是在网络中来回发送的,我发过去要让对面知道我发的端口号是什么,所以必须是网络字节序列
        local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 1.string->unit_32 2.来回通信对方要知道发送的ip,所以ip的unit_32必须是网络序列的
        int n = bind(_socketfd, (const struct sockaddr *)&local, sizeof(local));
        if (n < 0)
        {
            log(Fatal, "bind error, erron:%d, errno string:%s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info, "bind sucess");
    }

    void CheckUser(const struct sockaddr_in &client, const std::string& clientip, uint16_t clientport)
    {
        auto it = _online_user.find(clientip);
        if (it == _online_user.end())
        {
            // 添加
            _online_user.insert({clientip, client});
            std::cout << "{ " << clientip << " }" << "[ " << clientport << "]" << "#addr onlineuser sucess" << std::endl;
        }
    }
    void Broadcast(const std::string& info, const std::string& clientip, uint16_t clientport)
    {
        for (const auto& user : _online_user)
        {
            std::string message = "{ ";
            message += clientip;
            message += " }";
            message += "[ ";
            message += std::to_string(clientport);
            message += " ]#";
            message += info;
            socklen_t len = sizeof(user.second);
            sendto(_socketfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(user.second), len);
        }
    }

    void Run(/*func_t func*/) // 对代码进行分层
    {
        _isrunning = true;
        char inbuffer[1024];
        while (_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_socketfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr*)&client, &len);
            if (n < 0)
            {
                log(Warning, "recvfrom error");
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port); // 网络序列转主机序列,拿到port
            std::string clientip = inet_ntoa(client.sin_addr); // 将网络序列的ip地址转化成为主机的序列的ip地址,风格是字符串类型的
            
            // 判断是不是新用户,新用户则添加到onlineuser
            CheckUser(client, clientip, clientport);
            
            std::string info = inbuffer;
            // 转发给所有人
            Broadcast(info, clientip, clientport);
        }
    }
    // 析构函数
    ~UdpServer()
    {
        if (_socketfd > 0) 
        {
            close(_socketfd);
        }
    }
private:
    int _socketfd; // 网络文件描述符,表示socket返回的文件描述符
    uint16_t _port; // 表明服务器进程的端口号
    std::string _ip; // ip地址,任意地址绑定为0
    bool _isrunning; // 判断是否运行
    std::unordered_map<std::string, struct sockaddr_in> _online_user; // client列表 主机序列的ip地址,网络序列的套接字信息
};

udpclient.cc:

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <string>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "terminal.hpp"

void Usage(std::string proc)
{
    std::cout << "\n\rUsages: " << proc << "serverip serverport\n" << std::endl;
}

struct ThreadData
{
    struct sockaddr_in server;
    int socketfd;
    std::string serverip;
};

void* recv_message(void* args)
{
    openterminal();
    ThreadData* td = static_cast<ThreadData*>(args);
    char buffer[1024];
    while (true)
    {
        // 清空
        memset(buffer, 0, sizeof(buffer));
        // 收数据 -- 从socket文件中的数据拿出来到buffer中,并将收到的对方的个人信息进行保存到temp中
        struct sockaddr_in temp;
        socklen_t len2 = sizeof(temp);
        ssize_t n = recvfrom(td->socketfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len2);
        if (n > 0)
        {
            buffer[n] = 0;
            // 打印数据
            std::cerr << buffer << std::endl;
        }
    }
}

void* send_message(void* args)
{
    ThreadData* td = static_cast<ThreadData*>(args);
    std::string message;
    socklen_t len = sizeof(td->server);

    std::string welcome = td->serverip;
    welcome += "coming...";
    sendto(td->socketfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);

    while (true)
    {
        // 数据
        std::cout << "Please Enter# ";
        getline(std::cin, message);
        
        // 发送数据 -- 把数据发送到socketfd文件中,并将server信息提炼出来发送给server,可以理解成唤醒server
        sendto(td->socketfd, message.c_str(), message.size(), 0, (struct sockaddr *)&(td->server), len);
    }
}

// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1]; // serverip
    uint16_t serverport = std::stoi(argv[2]); // serverport

    struct ThreadData td;

    // 给谁发
    bzero(&td.server, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());

    td.socketfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
    if (td.socketfd < 0)
    {
        std::cout << "socket create error" << std::endl;
        return 1;
    }

    td.serverip = serverip;

    pthread_t recver;
    pthread_t sender;
    pthread_create(&recver, nullptr, recv_message, &td);
    pthread_create(&sender, nullptr, send_message, &td);

    pthread_join(recver, nullptr);
    pthread_join(sender, nullptr);

    close(td.socketfd);
    return 0;
}

main.cc:

cpp 复制代码
#include "udp.hpp"
#include "Log.hpp"
#include <memory>
#include <cstdio>
#include <vector>

Log log;

void Usage(std::string proc)
{
    std::cout << "\n\rUsages: " << proc << "port[1024+]\n" << std::endl;
}

std::string Handler(const std::string& info, const std::string& ip, uint16_t port)
{
    std::cout << "{ " << ip << " }" << "[ " << port << "]" << "# " << info << std::endl;
    std::string res = "recv a message# ";
    res += info;
    std::cout << res << std::endl;
    return res;
}

bool SafeCheck(const std::string& cmd)
{
    std::vector<std::string> word_key = {
        "rm",
        "top",
        "cp",
        "yum",
        "while",
        "kill",
        "unlink"
        "uninstall",
        "top"
    };
    for (auto &word : word_key)
    {
        auto pos = cmd.find(word);
        if (pos != std::string::npos)
        {
            return false;
        }
    }
    return true;
}

std::string ExcuteCommand(const std::string& cmd)
{
    std::cout << "get a massage:" << cmd << std::endl;
    // 做一个保护
    if (!SafeCheck(cmd)) return "bad man";
    
    FILE* fp = popen(cmd.c_str(), "r"); // 管道创建好,子进程创建好,子进程通过管道放到父进程
    if (nullptr == fp)
    {
        perror("popen failed");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while (true)
    {
        char* ok = fgets(buffer, sizeof(buffer), fp); // 写到buffer缓冲区中
        if (ok == nullptr)
        {
            break;
        }
        result += buffer;
    }
    pclose(fp);
    return result;
}

// 以后用的是./udpserver + port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port)); // new一个对象

    svr->Init(); // 初始化
    svr->Run(/*Handler*/);  // 跑起来
}

terminal.hpp:

cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string>

std::string terminal = "/dev/pts/3";

int openterminal()
{
    int fd = open(terminal.c_str(), O_WRONLY);
    if (fd < 0)
    {
        std::cerr << "open terminal err" << std::endl;
        return 1;
    }
    dup2(fd, 2); // 重定向到标准错误
    return 0;
}
相关推荐
JaguarJack6 小时前
PHP 的异步编程 该怎么选择
后端·php·服务端
BingoGo6 小时前
PHP 的异步编程 该怎么选择
后端·php
JaguarJack21 小时前
为什么 PHP 闭包要加 static?
后端·php·服务端
ServBay2 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
用户962377954482 天前
CTF 伪协议
php
BingoGo4 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack4 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo5 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack5 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack6 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端