【Linux网络编程】基于udp套接字实现的网络通信

发布于:2025-06-15 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

一、实现目标:

二、实验步骤:

1、服务端代码解析:

Init():

Run():

2、客户端代码:

主函数逻辑:

send_message发送数据:

recv_message接收数据:

三、实验结果:

四、拓展:

五、全部代码:


一、实现目标:

实现基于udp套接字实现的网络通信,这里我们实现客户端和服务端

首先在服务端中维护一张哈希表,存储的kv值是客户端的ip地址和sockaddr_in,然后服务端用于接收客户端发送的信息,并进行处理,如果当前客户端在哈希表中就不做处理,如果不在就添加到哈希表中,并且广播给哈希表中的所有用户

对于客户端,为了完成类似于QQ这样的方式,能够一边发送信息给服务端,并且能够保证在不发送的时候也能从服务端中读取到数据,所以就需要用到多线程并发了,一个线程从服务端中读取数据,并且打印出来看看;另一个线程向服务端中发送数据

通信的原理就是向_sockfd这个网络文件描述符中同时进行读写

二、实现代码:

其中log.hpp是在系统部分学到的,当时封装好的一个日志文件

在本次实验中将服务端进行封装了,客户端未进行封装

1、服务端代码解析:

如下是服务端的main.cc,就是通过智能指针实现服务端的类,然后初始化,启动服务器即可

void Usege(char* proc)
{
    std::cout<<"\n\tUsage: "<<proc<<"port[1024+]\n"<<std::endl;
}

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usege(argv[0]);
        exit(1);
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    svr->Init();
    svr->Run(/*ExcuteCommand*/);

    return 0; 
}

接着是服务端的核心代码框架:

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

#include "log.hpp"
#define SIZE 1024
Log lg;
// enum 搞一个错误码合集
enum
{
    SOCKET_ERR = 1,
    BIND_ERR = 2
};
// const uint16_t defaultport = 3306;
const std::string defaultip = "0.0.0.0";
class UdpServer
{
public:
    UdpServer(const uint16_t& port/* = defaultport*/,const std::string& ip = defaultip)
        :_sockfd(0),_port(port),_ip(ip),_isrunning(false)
    {}
    void Init()
    {}
    void CheckUser(const struct sockaddr_in& client,const uint16_t& clientport,const std::string& clientip)
    {}
    void Broadcast(const std::string& info,const uint16_t& clientport,const std::string& clientip)
    {}
    void Run()
    {}
    ~UdpServer()
    {}
private:
    int _sockfd; // 网络文件描述符
    std::string _ip; // 
    uint16_t _port; // 服务器进程的端口号
    bool _isrunning; // 服务器在启动后要一直保证运行中
    std::unordered_map<std::string,struct sockaddr_in> online_user;// 将在聊天室中的人都存储在哈希表中
};

其中成员变量:

_sockfd就是网络文件描述符

_ip就是指定服务器绑定的IP地址,并且这里给了缺省值,也就是在外部如果没有传ip就采用默认值表示绑定所有可用网络接口

_port表示服务器进程的端口号

_isrunning表示服务器是否在运行中的状态

online_user是一个哈希表,表示当前聊天室中存在的人

接下来依次实现各个函数的功能即可

Init():

在初始化服务端这里:

首先就是床加你socket,这里是采用的IPV4,所以需要初始化struct sockaddr_in,初始化里面的IP地址和端口号等等的成员变量,然后进行bind绑定,这步就是将栈上的数据绑定到内核中

    void Init()
    {
        // socket创建,记得加上log日志
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            lg(FATAL,"socket create fail, sockfd : %d",_sockfd);
            exit(SOCKET_ERR);
        }
        lg(INFO,"socket create success, sockfd = %d",_sockfd);
        // 首先搞一个sockaddr_in,然后将里面都初始化为0,并且初始化内部成员,这里有三个
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET; // 设置为AF_INET表示IPv4
        local.sin_port = htons(_port); // 端口转换
        local.sin_addr.s_addr = inet_addr(_ip.c_str());
        // local.sin_addr.s_addr = INADDR_ANY; // 定义为0.0.0.0,这里是必须填在sin_addr里面的s.addr的,
                                            // 因为第一个sin_addr里面还是一个结构体,这个结构体里面才是s_addr
        
        // 在进行bind绑定,这步才是将栈上的数据都绑定到内核中,将数据转到网络文件描述符中
        int n = bind(_sockfd,(const struct sockaddr*)&local,sizeof(local));
        if(n < 0)
        {
            lg(FATAL, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(INFO, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }

Run():

这个函数就是将我们的服务端启动起来,所以首先要将_isrunning修改为true,接着通过recvfrom接收从客户端发来的消息,然后进行CheckUser检查,最后进行Broadcast广播

    void Run()
    {
        // 修改_isrunning
        _isrunning = true;
        // 进入while循环
        char inbuffer[SIZE];
        while(_isrunning)
        {
            memset(inbuffer, 0, sizeof(inbuffer));
            // recvfrom函数,这里把接收来的当做字符串,也就是在sizeof(inbuffer)的大小上-1,最后接收完成后,将第n的位置设置为\0也就是0
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
            if(n < 0)
            {
                lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            // inbuffer[n] = 0; // 这里为什么要去掉
            
            // 这里需要拿到client中的端口号和ip然后传给CheckUser,进行通信室中的人员是否上线管理
            uint16_t clientport = ntohl(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);
            CheckUser(client,clientport,clientip);

            // 当一个用户上线后,并且发送消息了,此时将消息和用户人员进行广播
            std::string info = inbuffer; // 此时这个info也就要传参到Broadcast进行统一处理
            Broadcast(info,clientport,clientip);
        }
    }

接下来就是实现CheckUser和Broadcast了

CheckUser

    void CheckUser(const struct sockaddr_in& client,const uint16_t& clientport,const std::string& clientip)
    {
        // 在哈希表中进行查找,如果没找到就添加,找到了什么都不用做
        auto pos = online_user.find(clientip);
        if(pos == online_user.end())
        {
            online_user.insert({clientip,client});
            std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;
        }
    }

Broadcast

void Broadcast(const std::string& info,const uint16_t& clientport,const std::string& clientip)
    {
        // 遍历整个哈希表,给这个哈希表中的所有人都发送message
        for(const auto& ch : online_user)
        {
            std::string message = "[";
            message += clientip;
            message += ":";
            message += to_string(clientport);
            message += "]#";
            message += info;
            socklen_t len = sizeof(ch.second);  
            // 然后处理完后的数据用sendto接口发送回给对方
            sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)(&ch.second),len);
        }
    }

2、客户端代码:

客户端代码采用两个线程进行并发执行,并且没有对客户端进行封装,直接就是主函数

如下是框架:

#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>

#define SIZE 1024

void Use(const std::string proc)
{
    std::cout << "\n\t" << proc << " serveip serveport\n" << std::endl;
}

struct ThreadDate
{
    struct sockaddr_in server;
    int sockfd;
};


void* recv_message(void* argv)
{}

void* send_message(void* argv)
{}

int main(int argc,char* argv[])
{
    // pthread_create(&recvr,nullptr,recv_message,&td);
    // pthread_create(&sender,nullptr,send_message,&td);

    // pthread_join(recvr,nullptr);
    // pthread_join(sender,nullptr);
}

主函数逻辑:

首先通过命令行拿到服务端的端口号和服务端的IP地址,接着创建ThreadDate结构体,方便后续的线程中进行传参,最后创建好线程然后实现好对应的方法即可

// 这个是多线程版本的,思路是创建两个线程,一个线程从服务端中读数据,一个线程向服务端中发送数据
int main(int argc,char* argv[])
{
    // 检查命令行
    if(argc != 3)
    {
        Use(argv[0]);
        exit(1);
    }
    // .udpclient serveip serveport
    std::string serveip = argv[1];
    uint16_t serveport = std::stoi(argv[2]);

    // 初始化server sockaddr_in,为了在后面sendto给server
    ThreadDate td;
    bzero(&td.server,sizeof(td.server)); 

    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serveport); // 需要从主机序列转换成网络序列
    td.server.sin_addr.s_addr = inet_addr(serveip.c_str()); // 把点分十进制格式的IPv4地址转换为网络字节序的 32 位无符号整数


    // sockfd,类似与创建文件描述符,在最后记得关闭,其实不关闭也行,毕竟最后程序都结束了,sockfd的生命周期是随进程的
    td.sockfd = socket(AF_INET,SOCK_DGRAM,0);

    if(td.sockfd < 0)
    {
        std::cout<<"socket error"<<std::endl;
        exit(2);
    }

    // 创建两个线程
    pthread_t recvr,sender;
    pthread_create(&recvr,nullptr,recv_message,&td);
    pthread_create(&sender,nullptr,send_message,&td);

    pthread_join(recvr,nullptr);
    pthread_join(sender,nullptr);

    close(td.sockfd);
    return 0;
}

注意:

client客户端要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择
一个端口号只能被一个进程bind,对server是如此,对于client,也是如此
其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以
系统什么时候给我bind呢?首次发送数据的时候

send_message发送数据:

我们通过geiline函数在标准输入流中进行读取,然后通过sendto接口发送给服务端

void* send_message(void* argv)
{
    ThreadDate* td = static_cast<ThreadDate*>(argv);
    std::string message;
    socklen_t len = sizeof(td->server);
    while(true)
    {
        // 从cin中获得数据
        std::cout<<"Please enter#";
        getline(std::cin,message);

        // std::cout<<message<<std::endl;
        // sendto发送数据
        int st = sendto(td->sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&td->server,len);
        if(st<0)
        {
            std::cout<<"sendto error"<<std::endl;
            continue;
        }
    }
}

recv_message接收数据:

通过recvfrom接口接收数据后,打印出来看看

void* recv_message(void* argv)
{
    struct ThreadDate* td = static_cast<ThreadDate*>(argv);
    char buffer[SIZE];
    while(true)
    {
        memset(buffer, 0, sizeof(buffer));
        // recvfrom接收数据
        
        // 在接收消息的时候,可能会从多台主机上收消息,所以recvfrom后面的参数就不能是上述确定的某一个服务器
        // 但是又必须要填参数,所以这里新创建一个sockaddr_in
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        
        ssize_t s = recvfrom(td->sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
        // std::cout << "recvfrom over"<<std::endl;
        if(s > 0)
        {
            buffer[s] = 0;
            std::cerr<<buffer<<std::endl;
        }
    }
}

三、实验结果:

如上,这样成功的写出了基于udp套接字实现的网络通信

首先将服务端进程进行打开

接着打开客户端

如下是华为云的客户端:

如下是腾讯云的客户端:

这里有个细节,为了将我发的消息和接收的消息分开看,我们在编码的时候是在标准错误中打印,所以在启动的时候可以直接将标准错误重定向到别的终端,这样就能够进行分离了

接着在腾讯云中发送你好

在服务端中就能够看到我们上线的消息了

但是在华为云的客户端中却不能够看到,这是因为我们的华为云还没有上线,接着在华为云中发送haha,就能够发现华为云这个客户端也上线了

此时在腾讯云中发送你吃了吗,在华为云的客户端中就能够看到了,注意,在华为云中,我们并没有将发送消息和接收消息分离

这样就能够实现网络通信了

四、拓展:

在本次实验中,我们并没有让服务端进行处理消息,只是处理了用户添加到哈希表时上线的消息,如果想让服务端进行消息的处理,可以使用function包装器实现服务端网络通信的功能和处理数据的功能的解耦

思路就是你在服务端代码中增加包装器

然后在run这个成员函数中,通过包装器实现一个回调

    // 如下是第一个版本,在这个版本中,是让网络接收数据和处理数据实现了解耦
    void Run(func_t func)
    {
        // 修改_isrunning
        _isrunning = true;
        // 进入while循环
        char inbuffer[SIZE];
        while(_isrunning)
        {
            // recvfrom函数,这里把接收来的当做字符串,也就是在sizeof(inbuffer)的大小上-1,最后接收完成后,将第n的位置设置为\0也就是0
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
            if(n < 0)
            {
                lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            inbuffer[n] = 0;
            
            // 拼接字符串,这里充当一次数据的处理
            std::string info = inbuffer;
            // std::string echo_string = "server echo@" + info;
            std::string echo_string = func(info);
            std::cout<<echo_string<<std::endl;

            // 然后处理完后的数据用sendto接口发送回给对方
            sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);
        }
    }

最后在main.cc中实现想要让服务端执行的代码即可

std::string Handler(std::string str)
{
    std::string res = "Server get a message ";
    res += str; 
    return res;
}

五、全部代码:

main.cc

#include <memory>
#include <cstdio>
#include <vector>

#include "UdpServer.hpp"

void Usege(char* proc)
{
    std::cout<<"\n\tUsage: "<<proc<<"port[1024+]\n"<<std::endl;
}

std::string Handler(std::string str)
{
    std::string res = "Server get a message ";
    res += str; 
    return res;
}

bool SafeCheck(const std::string& cmd)
{
    // 搞一个vector的数组,然后遍历它,进行find查找
    vector<string> check = {
        "rm",
        "mv",
        "cp",
        "kill",
        "sudo",
        "unlink",
        "uninstall",
        "yum",
        "top",
        "while"
    };
    for(const auto& ch : check)
    {
        auto pos = cmd.find(ch);
        if(pos != std::string::npos) return false;
    }
    return true;
}

// 理解远端指令是怎么一回事
std::string ExcuteCommand(const std::string& cmd)
{
    std::cout<<"cmd:" << cmd << std::endl;
    // 根据SafeCheck函数做安全检查
    if(!SafeCheck(cmd)) return "error";

    // 建立好管道
    // 创建子进程
    // 子进程执行的结果通过管道交给父进程
    // 父进程想读到执行结果可以在FILE*指针也就是fp中读到
    FILE* fp = popen(cmd.c_str(),"r");

    if(fp == nullptr)
    {
        perror("popen");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while(true)
    {
        char* res = fgets(buffer,sizeof(buffer),fp);
        if(res == nullptr) break;
        result += buffer;
    }

    pclose(fp);
    return result;
}

int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usege(argv[0]);
        exit(1);
    }

    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    svr->Init();
    svr->Run(/*ExcuteCommand*/);

    return 0; 
}

log.hpp

#pragma once

#include <iostream>
#include <ctime>
#include <cstdarg>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#define INFO 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

#define SCREEN 1
#define ONEFILE 2
#define MOREFILE 3

#define SIZE 1024
#define logname "log.txt"

using namespace std;

class Log
{
public:
    Log()
    :printstyle(SCREEN)
    ,path("./log/")// 默认路径是当前路径下的log文件夹
    {
        // mkdir(path.c_str(),0765);
    }

    void change(int style)
    {
        printstyle = style;
    }
    string leveltostring(int level)
    {
        switch (level)
        {
        case INFO:
            return "INFO";
        case DEBUG:
            return "DEBUG";
        case WARNING:
            return "WARNING";
        case ERROR:
            return "ERROR";
        case FATAL:
            return "FATAL";
        default:
            return "NON";
        }
    }

    void operator()(int level, const char *format, ...)
    {
        // 处理时间
        time_t now = time(nullptr);
        // 将时间戳转为本地时间
        struct tm *local_time = localtime(&now);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", leveltostring(level).c_str(),
                 local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday,
                 local_time->tm_hour, local_time->tm_min, local_time->tm_sec);

        // 处理可变参数
        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 将两个消息组合起来成为一个完整的日志消息
        // 默认部分+自定义部分
        char logbuffer[SIZE * 2];
        snprintf(logbuffer, sizeof(logbuffer), "%s %s", leftbuffer, rightbuffer);

        printlog(level, logbuffer);
    }

    void printlog(int level, const string &logbuffer) // 这里引用避免大型字符串的拷贝开销,优化性能
    {
        switch (printstyle)
        {
        case SCREEN:
            cout << logbuffer << endl;
            break;
        case ONEFILE:
            printonefile(logname, logbuffer);
            break;
        case MOREFILE:
            printmorefile(level, logbuffer);
            break;
        }
    }

    void printonefile(const string &_logname, const string &logbuffer)
    {
        string __logname = path + _logname;
        int fd = open(__logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
        if (fd < 0)
        return;
        write(fd, logbuffer.c_str(), logbuffer.size());
        close(fd);
    }

    void printmorefile(int level, const string &logbuffer)
    {
        // 思路:通过不同的文件名进行区分
        string _logname = logname;
        _logname += ".";
        _logname += leveltostring(level);
        printonefile(_logname, logbuffer);
    }

    ~Log()
    {
    }

private:
    int printstyle;
    string path;
};

Udpserver.hpp

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

#include "log.hpp"

// 网络接收数据和处理数据的耦合度太高了,所以就需要把网络通信的功能和处理数据的功能做一下适当的解耦
typedef std::function<std::string(const std::string&)> func_t; // function包装器
                       // 返回值          // 参数

#define SIZE 1024

Log lg;
// enum 搞一个错误码合集
enum
{
    SOCKET_ERR = 1,
    BIND_ERR = 2
};

// const uint16_t defaultport = 3077;
const std::string defaultip = "0.0.0.0";

class UdpServer
{
public:
    UdpServer(const uint16_t& port/* = defaultport*/,const std::string& ip = defaultip)
        :_sockfd(0),_port(port),_ip(ip),_isrunning(false)
    {}

    void Init()
    {
        // socket创建,记得加上log日志
        _sockfd = socket(AF_INET,SOCK_DGRAM,0);
        if(_sockfd < 0)
        {
            lg(FATAL,"socket create fail, sockfd : %d",_sockfd);
            exit(SOCKET_ERR);
        }
        lg(INFO,"socket create success, sockfd = %d",_sockfd);
        // 首先搞一个sockaddr_in,然后将里面都初始化为0,并且初始化内部成员,这里有三个
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET; // 设置为AF_INET表示IPv4
        local.sin_port = htons(_port); // 端口转换
        local.sin_addr.s_addr = inet_addr(_ip.c_str());
        // local.sin_addr.s_addr = INADDR_ANY; // 定义为0.0.0.0,这里是必须填在sin_addr里面的s.addr的,
                                            // 因为第一个sin_addr里面还是一个结构体,这个结构体里面才是s_addr
        
        // 在进行bind绑定,这步才是将栈上的数据都绑定到内核中
        int n = bind(_sockfd,(const struct sockaddr*)&local,sizeof(local));
        if(n < 0)
        {
            lg(FATAL, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg(INFO, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }

    // // 如下是第一个版本,在这个版本中,是让网络接收数据和处理数据实现了解耦
    // void Run(func_t func)
    // {
    //     // 修改_isrunning
    //     _isrunning = true;
    //     // 进入while循环
    //     char inbuffer[SIZE];
    //     while(_isrunning)
    //     {
    //         // recvfrom函数,这里把接收来的当做字符串,也就是在sizeof(inbuffer)的大小上-1,最后接收完成后,将第n的位置设置为\0也就是0
    //         struct sockaddr_in client;
    //         socklen_t len = sizeof(client);
    //         ssize_t n = recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
    //         if(n < 0)
    //         {
    //             lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
    //             continue;
    //         }
    //         inbuffer[n] = 0;
            
    //         // 拼接字符串,这里充当一次数据的处理
    //         std::string info = inbuffer;
    //         // std::string echo_string = "server echo@" + info;
    //         std::string echo_string = func(info);
    //         std::cout<<echo_string<<std::endl;

    //         // 然后处理完后的数据用sendto接口发送回给对方
    //         sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);
    //     }
    // }

    void CheckUser(const struct sockaddr_in& client,const uint16_t& clientport,const std::string& clientip)
    {
        // 在哈希表中进行查找,如果没找到就添加,找到了什么都不用做
        auto pos = online_user.find(clientip);
        if(pos == online_user.end())
        {
            online_user.insert({clientip,client});
            std::cout << "[" << clientip << ":" << clientport << "] add to online user." << std::endl;
        }
    }

    void Broadcast(const std::string& info,const uint16_t& clientport,const std::string& clientip)
    {
        // 遍历整个哈希表,给这个哈希表中的所有人都发送message
        for(const auto& ch : online_user)
        {
            std::string message = "[";
            message += clientip;
            message += ":";
            message += to_string(clientport);
            message += "]#";
            message += info;
            socklen_t len = sizeof(ch.second);  
            // 然后处理完后的数据用sendto接口发送回给对方
            sendto(_sockfd,message.c_str(),message.size(),0,(struct sockaddr*)(&ch.second),len);
        }
    }

    // 这是第二个版本,为了实现基于udp协议的聊天室,我要让每个人上线后,能够知道是谁进行发送的
    void Run()
    {
        // 修改_isrunning
        _isrunning = true;
        // 进入while循环
        char inbuffer[SIZE];
        while(_isrunning)
        {
            memset(inbuffer, 0, sizeof(inbuffer));
            // recvfrom函数,这里把接收来的当做字符串,也就是在sizeof(inbuffer)的大小上-1,最后接收完成后,将第n的位置设置为\0也就是0
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(_sockfd,inbuffer,sizeof(inbuffer)-1,0,(struct sockaddr*)&client,&len);
            if(n < 0)
            {
                lg(WARNING, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            // inbuffer[n] = 0; // 这里为什么要去掉
            
            // 这里需要拿到client中的端口号和ip然后传给CheckUser,进行通信室中的人员是否上线管理
            uint16_t clientport = ntohl(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);
            CheckUser(client,clientport,clientip);

            // 当一个用户上线后,并且发送消息了,此时将消息和用户人员进行广播
            std::string info = inbuffer; // 此时这个info也就要传参到Broadcast进行统一处理
            Broadcast(info,clientport,clientip);
        }
    }

    ~UdpServer()
    {
        if(_sockfd>0) close(_sockfd);
    }
private:
    int _sockfd; // 网络文件描述符
    std::string _ip; // 
    uint16_t _port; // 服务器进程的端口号
    bool _isrunning; // 服务器在启动后要一直保证运行中
    std::unordered_map<std::string,struct sockaddr_in> online_user;// 将在聊天室中的人都存储在哈希表中
};

UdpClient.cc

#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <pthread.h>

#define SIZE 1024

void Use(const std::string proc)
{
    std::cout << "\n\t" << proc << " serveip serveport\n" << std::endl;
}

struct ThreadDate
{
    struct sockaddr_in server;
    int sockfd;
};

void* recv_message(void* argv)
{
    struct ThreadDate* td = static_cast<ThreadDate*>(argv);
    char buffer[SIZE];
    while(true)
    {
        memset(buffer, 0, sizeof(buffer));
        // recvfrom接收数据
        
        // 在接收消息的时候,可能会从多台主机上收消息,所以recvfrom后面的参数就不能是上述确定的某一个服务器
        // 但是又必须要填参数,所以这里新创建一个sockaddr_in
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        
        ssize_t s = recvfrom(td->sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
        // std::cout << "recvfrom over"<<std::endl;
        if(s > 0)
        {
            buffer[s] = 0;
            std::cerr<<buffer<<std::endl;
        }
    }
}

void* send_message(void* argv)
{
    ThreadDate* td = static_cast<ThreadDate*>(argv);
    std::string message;
    socklen_t len = sizeof(td->server);
    while(true)
    {
        // 从cin中获得数据
        std::cout<<"Please enter#";
        getline(std::cin,message);

        // std::cout<<message<<std::endl;
        // sendto发送数据
        int st = sendto(td->sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&td->server,len);
        if(st<0)
        {
            std::cout<<"sendto error"<<std::endl;
            continue;
        }

    }
}
// 这个是多线程版本的,思路是创建两个线程,一个线程从服务端中读数据,一个线程向服务端中发送数据
int main(int argc,char* argv[])
{
    // 检查命令行
    if(argc != 3)
    {
        Use(argv[0]);
        exit(1);
    }
    // .udpclient serveip serveport
    std::string serveip = argv[1];
    uint16_t serveport = std::stoi(argv[2]);

    // 初始化server sockaddr_in,为了在后面sendto给server
    ThreadDate td;
    bzero(&td.server,sizeof(td.server)); 

    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serveport); // 需要从主机序列转换成网络序列
    td.server.sin_addr.s_addr = inet_addr(serveip.c_str()); // 把点分十进制格式的IPv4地址转换为网络字节序的 32 位无符号整数


    // sockfd,类似与创建文件描述符,在最后记得关闭,其实不关闭也行,毕竟最后程序都结束了,sockfd的生命周期是随进程的
    td.sockfd = socket(AF_INET,SOCK_DGRAM,0);

    if(td.sockfd < 0)
    {
        std::cout<<"socket error"<<std::endl;
        exit(2);
    }

    // 创建两个线程
    pthread_t recvr,sender;
    pthread_create(&recvr,nullptr,recv_message,&td);
    pthread_create(&sender,nullptr,send_message,&td);

    pthread_join(recvr,nullptr);
    pthread_join(sender,nullptr);

    close(td.sockfd);
    return 0;
}

// // 如下是单进程版本的
// int main(int argc,char* argv[])
// {
//     // 检查命令行
//     if(argc != 3)
//     {
//         Use(argv[0]);
//         exit(1);
//     }
//     // .udpclient serveip serveport
//     std::string serveip = argv[1];
//     uint16_t serveport = std::stoi(argv[2]);

//     // 初始化server sockaddr_in,为了在后面sendto给server
//     struct sockaddr_in server;
//     bzero(&server,sizeof(server)); 

//     server.sin_family = AF_INET;
//     server.sin_port = htons(serveport); // 需要从主机序列转换成网络序列
//     server.sin_addr.s_addr = inet_addr(serveip.c_str()); // 把点分十进制格式的IPv4地址转换为网络字节序的 32 位无符号整数
//     socklen_t len = sizeof(server);

//     // sockfd,类似与创建文件描述符,在最后记得关闭,其实不关闭也行,毕竟最后程序都结束了,sockfd的声明周期是随进程的
//     int sockfd = socket(AF_INET,SOCK_DGRAM,0);

//     if(sockfd < 0)
//     {
//         std::cout<<"socket error"<<std::endl;
//         exit(2);
//     }

//     // client客户端要bind吗?要!只不过不需要用户显示的bind!一般有OS自由随机选择
//     // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此
//     // 其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以
//     // 系统什么时候给我bind呢?首次发送数据的时候

//     std::string message;
//     char buffer[SIZE];
//     while(true)
//     {
//         // 从cin中获得数据
//         std::cout<<"Please enter#";
//         getline(std::cin,message);

//         // std::cout<<message<<std::endl;
//         // sendto发送数据
//         int st = sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);
//         if(st<0)
//         {
//             std::cout<<"sendto error"<<std::endl;
//             continue;
//         }
        
//         // recvfrom接收数据

//         // 在接收消息的时候,可能会从多台主机上收消息,所以recvfrom后面的参数就不能是上述确定的某一个服务器
//         // 但是又必须要填参数,所以这里新创建一个sockaddr_in
//         struct sockaddr_in temp;
//         socklen_t len = sizeof(temp);

//         ssize_t s = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
//         // std::cout << "recvfrom over"<<std::endl;
//         if(s > 0)
//         {
//             buffer[s] = 0;
//             std::cout<<buffer<<std::endl;
//         }
//     }
//     close(sockfd);
//     return 0;
// }