【网络】网络聊天室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;
}
相关推荐
傻啦嘿哟27 分钟前
代理IP在后端开发中的应用与后端工程师的角色
网络·网络协议·tcp/ip
Red Red31 分钟前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
亚远景aspice2 小时前
ISO 21434标准:汽车网络安全管理的利与弊
网络·web安全·汽车
Estar.Lee3 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
友友马3 小时前
『 Linux 』网络层 - IP协议(一)
linux·网络·tcp/ip
码老白4 小时前
【老白学 Java】Warshipv2.0(二)
java·网络
HackKong4 小时前
小白怎样入门网络安全?
网络·学习·安全·web安全·网络安全·黑客
vmlogin虚拟多登浏览器5 小时前
虚拟浏览器可以应对哪些浏览器安全威胁?
服务器·网络·安全·跨境电商·防关联
澜世5 小时前
2024小迪安全基础入门第三课
网络·笔记·安全·网络安全