Linux网络编程——UDP网络通信的简单实现

目录

UDP网络通信

简单的udpServer实现

头文件

封装udp服务

1、私有成员

 2、构造函数和析构函数

3、 udp服务器初始化

4、udp服务器运行

5、完整代码

简单的udpClient实现

UDP实现最简单的公共聊天

 完整代码 

inet_ntoa()相关问题 


UDP网络通信

在正式实现UDP网络通信前,我们先实现一个打印日志内容的文件。

#pragma once

#include<cstdio>
#include<ctime>
#include<cassert>
#include<cerrno>
#include<cstdlib>
#include<cstdarg>

#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3

const char* log_level[]={"DEBUG","NOTICE","WARINING","FATAL"};
void logMessage(int level,const char* format,...)
{
    assert(level>=DEBUG);
    assert(level<=FATAL);

    char* name=getenv("USER");
    char logBuffer[1024];
    va_list ap;
    va_start(ap,format);
    vsnprintf(logBuffer,sizeof(logBuffer)-1,format,ap);
    va_end(ap);

    FILE* out=(level==FATAL)? stderr:stdout;
    time_t tm = time(nullptr);
    struct tm* localTm=localtime(&tm);

    char timeStr[26]; // 考虑到asctime函数返回的字符串自带换行符,为了打印出来好看,下面手动去除这个换行符
    snprintf(timeStr, sizeof(timeStr), "%s", asctime(localTm));
    timeStr[strcspn(timeStr, "\n")] = 0; // Remove trailing newline
   
    fprintf(out,"%s | %s | %s | %s\n",
            log_level[level],
            timeStr,
            name==nullptr?"unknow":name,
            logBuffer);
}

1、上面运用了可变参数的知识点,简单介绍一下可变参数的使用

  • 首先,需要包含<stdarg.h>(C语言)或<cstdarg>(C++)头文件。
  • 使用 va_list 类型声明一个变量来存储参数信息。
  • 调用 va_start 宏初始化上述变量,使其指向可变参数列表中的第一个参数。
  • 使用 va_arg 宏访问参数。
  • 最后调用 va_end 宏完成清理工作。

2、vsnprintf()

   它用于格式化字符串输出,并允许传递一个 va_list 类型的参数来支持可变数量的参数。

int vsnprintf(char *str, size_t size, const char *format, va_list ap);
  • str: 指向用于存储生成字符串的缓冲区。
  • size: 缓冲区str的大小(以字符为单位)。如果生成的字符串长度(包括终止的空字符)超过这个值,则会被截断。
  • format: 格式化字符串,指定如何转换参数并格式化到输出字符串中。
  • ap: 一个va_list类型的对象,包含了要使用的所有参数的信息

3、获取当前时间

time_t tm = time(nullptr);
struct tm* localTm=localtime(&tm);

第一行代码调用了 time 函数,该函数返回自纪元(1970年1月1日00:00:00 UTC)以来的秒数,并存储在 time_t 类型的变量 tm 中。

第二行使用 localtime函数将 time_t 类型的值转换为本地时间表示结果是一个指向 struct tm类型的指针 localTm。struct tm 结构体包含了年、月、日、时、分、秒等详细的时间信息。


下面就开始实现一个简单的UDP网络通信

简单的udpServer实现

实现简单的udpServer的功能

  1. 创建套接字:首先,服务端需要创建一个UDP套接字。
  2. 绑定地址和端口:然后,将该套接字绑定到特定的IP地址和端口号上,以便客户端能够发送数据到这个地址和端口。
  3. 接收数据:使用 recvfrom 函数来接收来自客户端的数据。此函数不仅可以接收数据,还可以获取发送方的地址信息。
  4. 发送响应:如有必要,可以使用 sendto 函数向客户端发送响应(这里暂时不响应)。
头文件
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
封装udp服务
1、私有成员

在进行网络通信时我们需要的是 端口号、IP还有 套接字 信息 。

private:
    uint16_t _port;
    string _ip;
    int _sockFd;

 2、构造函数和析构函数
udpServer(uint16_t port,string ip="")
        :_port(port),
        _ip(ip)
        {}
~udpServer()
 {}

 这里需要传入的参数是端口号和IP地址。

传入的IP地址可以默认为空,即当创建这个UDP服务器对象时,如果没有提供第二个参数(IP地址),那么_ip 成员变量将被初始化为空字符串。这是因为如果用户没有特别指出要监听哪个IP地址,那就默认监听所有的网络接口.换句话说,它会监听该主机上所有的IP地址,无论数据包到达哪个网络接口,只要目标端口匹配,你的服务就能接收到这些数据包。

同一台主机可以拥有多个IP地址。

  • 多个网络接口:如果一台计算机配置了多个网络接口(例如,一个以太网卡和一个无线网卡),每个接口都可以被分配一个独立的IP地址。
  • 子接口(VLANs):在一些情况下,单个物理网络接口可以通过虚拟局域网(VLAN)技术划分成多个逻辑子接口,每个子接口可以配置不同的IP地址。
  • IPv6与IPv4共存:现代操作系统支持同时配置IPv4和IPv6地址。这意味着即使只有一个网络接口,它也可能拥有至少两个IP地址——一个IPv4地址和一个IPv6地址。
  • 别名接口:某些操作系统允许给单一的网络接口配置多个IP地址,通过创建所谓的“别名”接口来实现。这对于需要托管多个网站或服务的服务器特别有用,因为每个服务可以绑定到不同的IP地址。
  • 动态分配的临时地址:在某些情况下,比如使用DHCP时,可能会为同一个接口动态分配额外的临时IP地址。

3、 udp服务器初始化
 void init()
    {
        _sockFd=socket(AF_INET,SOCK_DGRAM,0);//创建套接字
        if(_sockFd<0)
        {
            logMessage(FATAL,"socket() fail::%s :%d",
                    strerror(errno),_sockFd);
            exit(1);
        }
        logMessage(DEBUG,"socket creat success: %d", _sockFd);


        struct sockaddr_in local;//创建结构体对象
        bzero(&local,sizeof(local));//该函数的作用是将一段内存区域的内容设置为0,这里是将结构体內容全置空。


        local.sin_family=AF_INET;//地址类型,选择协议
        local.sin_port=htons(_port);//选择端口号,端口号是要发送到网络的,所以需要从本地字节序转成网络字节序

        local.sin_addr.s_addr=_ip.empty()?htonl(INADDR_ANY):inet_addr(_ip.c_str());

        if(bind(_sockFd,(const struct sockaddr*)&local,sizeof(local))==-1){
            //绑定失败
            logMessage(FATAL,"bind() failed::%s : %d",
                strerror(errno),_sockFd);
            exit(2);
        }
            //绑定成功
        logMessage(DEBUG,"socket bind success: %d",_sockFd);
    }
  •   local.sin_addr.s_addr=_ip.empty()?htonl(INADDR_ANY):inet_addr(_ip.c_str());

    下面是对于这一行代码的解释

  • IP 不能直接填充到结构体中, 因为类中的 _ip 是字符串,  而网络中的IP 通常用4字节的二进制表示, 结构体中同样是此类型 所以 还需要将 点分十进制字符串型的_ip 转换为 uint32_t 才能填充到 结构体中。
  • 判断ip地址是否为空,如果为空则传入INADDR_ANY结构体,否则就将 _ip字符串转换为 in_addr_t 类型, 然后填充到结构体中
  • INADDR_ANY 其实是 强转为 in_addr_t 类型的 0.
  •  网络服务器 使用 INADDR_ANY 作为IP, 绑定到主机中. 表示监听本机上所有的IP 网络接口.一台服务器主机可能有许多的IP, 使用 INADDR_ANY 意思就是说, 其他主机可以通过服务器主机上的 任意IP:指定port 找到服务器进程实现通信
  • 当绑定指定的IP时, 就表示 其他主机只能通过服务器主机上的 指定IP:指定port 找到服务器进程实现通信.
  • 如果使用其他本机上的IP:指定port, 服务器是不会响应的. 因为服务器进程 只接收通过指定 IP 发送给服务器进程的信息
  • IP也是要向网络中发送的, 所以要将 IP转换成网络字节序. inet_addr() 则会自动将ip转换为网络字节序 
  • 需要了解的是, inet_addr() 接口 可以将点分十进制字符串类型的IP地址, 转换为 in_addr_t 类型的IP

    同时, 也会 将转换后的IP自动转换为网络字节序存储形式


4、udp服务器运行

其具体功能就是循环地监听、接收发送到服务器上的信息

     void start()
        {
            char inBuffer[1024];//用于储存接收到的信息
            while(1)
            {
                struct sockaddr_in peer;//输出型参数,用于接收对方的主机网络信息
                socklen_t peerlen=sizeof(peer);//输入输出型参数

                ssize_t s=recvfrom(_sockFd,inBuffer,sizeof(inBuffer)-1,0,(struct sockaddr*)&peer,&peerlen);//接收发送到服务器上的信息及发送端的网络信息
                if(s>0)//当字符串结尾
                {
                    inBuffer[s]=0;
                }
                else if(s==-1)//读取失败
                {
                    logMessage(WARINING,"recvfrom() error::%s: %d",strerror(errno),_sockFd);
                    continue;
                }
                //读取成功,除了读取到发过来的信息,还要读到对方的网络信息
                string peerIP=inet_ntoa(peer.sin_addr);//拿到对方的ip
                uint32_t peerPort=ntohs(peer.sin_port);//拿到对方的端口号

                logMessage(NOTICE,"[%s:%d]# %s",peerIP.c_str(),peerPort,inBuffer);//将对方发送过来的信息打印出来
            }
        }    

 介绍一下接收接口 recvfrom

是用于接收通过套接字发送的数据的系统调用,特别适用于无连接的协议如UDP(用户数据报协议),或者在需要知道数据来源的情况下使用。它不仅可以接收数据,还能获取发送方的信息。

ssize_t recvfrom(int socket, void* restrict buffer, size_t length, 
int flags, struct sockaddr* restrict address, 
socklen_t* restrict address_len);
  • socket: 套接字描述符,标识一个已绑定并监听连接的套接字。\
  • buffer: 指向用于存储接收到的数据的缓冲区的指针。
  • length:缓冲区的最大长度,即可以接收的最大数据量。
  • flags: 调用操作的选项,通常设置为0。其他可能的值包括 MSG_PEEK(查看数据而不实际读取)、MSG_WAITALL(等待直到请求的数据量全部到达)等。
  • address: (可选)指向一个结构体的指针,该结构体用来保存源地址信息(即发送数据的主机的地址)。如果不需要这个信息,可以传递NULL。
  • address_len: (与address一起使用)在调用时,应设置为address所指向结构体的大小;返回时,包含实际写入的地址结构体的大小。

5、主函数

//main函数需要通过获取命令行参数实现获取端口号和ip
int main(int argc,char*argv[])
{
    if(argc != 2&& argc!=3)
    {
        Usage(argv[0]);
        exit(3);
    }
//获取端口号和ip
    uint16_t port=atoi(argv[1]);
    string ip;
    if(argc==3)
    {
        ip=argv[2];
    }
//使用端口号和ip实例化udpServer对象
    udpServer uSvr(port,ip);
//初始化并启动服务器
    uSvr.init();
    uSvr.start();
    return 0;
}

5、完整代码
#include<iostream>
#include<string>
#include<cstring>
#include<cstdlib>
#include<cerrno>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>

#include"logMessage.hpp"

using std::cout;
using std::endl;
using std::string;

class udpServer{
public:
    udpServer(uint16_t port,string ip="")
        :_port(port),
        _ip(ip)
        {}
    ~udpServer()
    {}

    //server init,creat socket,bind host
    void init()
    {
        _sockFd=socket(AF_INET,SOCK_DGRAM,0);
        if(_sockFd<0)
        {
            logMessage(FATAL,"socket() fail::%s :%d",
                    strerror(errno),_sockFd);
            exit(1);
        }
        logMessage(DEBUG,"socket creat success: %d", _sockFd);


        struct sockaddr_in local;
        bzero(&local,sizeof(local));


        local.sin_family=AF_INET;
        local.sin_port=htons(_port);
        local.sin_addr.s_addr=_ip.empty()?htonl(INADDR_ANY):inet_addr(_ip.c_str());


        if(bind(_sockFd,(const struct sockaddr*)&local,sizeof(local))==-1){
            logMessage(FATAL,"bind() failed::%s : %d",
                strerror(errno),_sockFd);
            exit(2);
        }
        logMessage(DEBUG,"socket bind success: %d",_sockFd);
    }

        void start()
        {
            char inBuffer[1024];
            while(1)
            {
                struct sockaddr_in peer;
                socklen_t peerlen=sizeof(peer);

                ssize_t s=recvfrom(_sockFd,inBuffer,sizeof(inBuffer)-1,0,(struct sockaddr*)&peer,&peerlen);
                if(s>0)
                {
                    inBuffer[s]=0;
                }
                else if(s==-1)
                {
                    logMessage(WARINING,"recvfrom() error::%s: %d",strerror(errno),_sockFd);
                    continue;
                }
                string peerIP=inet_ntoa(peer.sin_addr);
                uint32_t peerPort=ntohs(peer.sin_port);

                logMessage(NOTICE,"[%s:%d]# %s",peerIP.c_str(),peerPort,inBuffer);
            }
        }    
private:
    uint16_t _port;
    string _ip;
    int _sockFd;
};
static void Usage(const string porc)//用于帮助用户正确了解特定的命令行程序,即在用户没有正确
//输入命令的情况下该函数作为提示
{
    cout<<"Usage:\n\t"<<porc<<"port [ip]"<<endl;
}

int main(int argc,char*argv[])
{
    if(argc != 2&& argc!=3)
    {
        Usage(argv[0]);
        exit(3);
    }
    //****main函数的参数在c和c++中主要用于接收命令行参数,这些参数默认是以字符串形式传递的。
    //****argv是一个指向字符串数组的指针,所以需要atoi函数来转换为整数
    uint16_t port=atoi(argv[1]);
    string ip;
    if(argc==3)
    {
        ip=argv[2];
    }

    udpServer uSvr(port,ip);
    uSvr.init();
    uSvr.start();
    return 0;
}

运行结果

 查看网络信息

 这个简单的udpserver就已经完成了,只会接收信息,没有回复功能。

简单的udpClient实现

udp客户端需要什么功能呢?

  • 创建套接字:类似于服务端,客户端也需要创建一个UDP套接字。
  • 指定服务端地址:定义一个 sockaddr_in 结构体,设置服务端的IP地址和端口号。
  • 发送数据:使用 sendto 函数向服务端发送数据。
  • 接收响应:如果服务端会返回响应,则客户端可以用 recvfrom 接收响应。

实现了服务器端的代码,客户端这里也是大同小异,如下 

#include <cstdint>
#include <iostream>
#include <ostream>
#include <string>
#include <cstdlib>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "logMessage.hpp"

using std::cout;
using std::cin;
using std::endl;
using std::getline;
using std::string;

static void Usage(const string porc)
{
    cout<<"Usage::\n\t"<<porc<<"server_IP server_Port"<<endl;
}

int main(int argc,char* argv[])//服务器的ip和端口号是从这里输入的
{
    if(argc!=3)
    {
        Usage(argv[0]);
        exit(1);
    }
    获取服务器的ip和端口号
    string server_IP=argv[1];
    uint16_t server_Port=atoi(argv[2]);;
    //创建客户端套接字
    int sockFd=socket(AF_INET,SOCK_DGRAM,0);
    if(sockFd<0)
    {
        logMessage(FATAL,"socket() failed:: %s :%d",strerror(errno),sockFd);
        exit(2);
    }
    logMessage(DEBUG,"socket() success:: %d",sockFd);
    //填充服务器的网络信息
    struct sockaddr_in server;
    bzero(&server,sizeof(server));
    server.sin_family=AF_INET;
    server.sin_port=htons(server_Port);
    server.sin_addr.s_addr=inet_addr(server_IP.c_str());
    //通信
    string inBuffer;
    while(true)
    {
        cout<<"Please Enter >>";
        getline(cin,inBuffer);
        //向server发送消息
         从命令行接收到的服务器IP和端口号, 是需要填充在 sockaddr_in 结构体中的, 因为 向服务器网络进程发送信息需要使用
        sendto(sockFd,inBuffer.c_str(),inBuffer.size(),0,(const struct sockaddr*)&server,sizeof(server));
        // 在首次向 server 发送消息的时候, 操作系统会自动将Client网络进程信息 绑定到操作系统内核
    }

    close(sockFd);
    return 0;
}

需要注意的是:

      服务端的网络信息和绑定都需要手动去输入,但是客户端的最好不要我们自己去做, 让操作系统自动帮我们完成,因为我们不需要手动指定IP以及端口号, 尤其是端口号. 如果手动指定了端口号 还有可能会造成其他问题,
    服务器需要手动指定端口号, 是因为服务器是需要让其他主机去连接的, 所以知道且固定. 如果是随机的, 那服务器绝对没人用.
    而客户端一般没人会主动来连接、访问, 一般都是每次打开客户端绑定网络时, 就让操作系统代操作, 不然手动指定端口号 可能会影响其他的网络进程
    所以 我们不需要手动去填充 udpClient 的网络信息, 也不需要手动绑定。

运行结果


下面让客户端和服务端交互一下看看效果

 如图所示,可以进行正常的简单通信。


UDP实现最简单的公共聊天

在上面的部分中,我们只是简单实现了客户端发消息,服务端接收消息而已,服务端并没有消息回复功能。下面我们就在上面所完成的基础上做一些修改和添加,以实现相互通信的功能。

  • 服务器内,使用哈希表实现一个存储不同主机进程信息的 用户表
  • 让服务器收到消息之后可以转发给 用户表内的所有用户进程
  • 让客户端也可以 接收来自服务器的信息, 并输出

在udpServer.cc中添加了如下代码: 

void CheckOnlineUsers(string &ip,uint16_t port,struct sockaddr_in &peer)
        {
            string key=ip;
            key += ":";
            key += std::to_string(port);
            auto itUser=_users.find(key);
            if(itUser==_users.end())
            {
                _users.insert({key,peer});
            }
        }
        void messageRount(string &ip ,uint16_t port,string info)
        {
            string message="[";
            message += ip;
            message +=":";
            message +=std::to_string(port);
            message +="]";
            message +=info;
            for(auto &user : _users)
            {
                sendto(_sockFd,message.c_str(),message.size(),0,(struct sockaddr*)&(user.second),sizeof(user.second));
            }
        }

在类中添加了一个成员变量 _users ,是一个储存用户网络信息的哈希表。

然后增加了上面的两个成员函数 

  • CheckOnlineUsers:用来检测 向服务器发送消息的客户端是否已经在服务器的用户表中. 如果不在, 则添加
  • messageRount:则是实现消息路由转发的功能. 服务器 接收到 某个客户端发来的消息之后, 会将客户端的信息(IP:Port) 以及发过来的消息, 传入此函数内. 然后 此函数整合信息和消息, 再将整合后的信息 转发给所有在服务器用户表中的客户端用户

在udpClient中添加了如下代码:

void* recverAndPrint(void* args)//函数接收一个 void* 类型的参数 args,这是一个指向整数类型的指针,实际上存储的是一个套接字描述符 (sockFd)。
{
    while(true)
    {
        int sockFd=*(int*)args;//在每次循环迭代中,都将 args 转换为 int* 并解引用以获取套接字描述符。
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t templen=sizeof(temp);
        ssize_t s=recvfrom(sockFd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&templen);
        if(s>0)
        {
            buffer[s]=0;
            cout<<buffer<<endl;
        }
    }
}
pthread_t t;
    pthread_create(&t,nullptr,recverAndPrint,&sockFd);
    while(true)
    {
        std::cerr<<"please enter>>";
        string inBuffer;
        inBuffer +="[";
        inBuffer +="nick_Name";
        inBuffer +="]# ";
        string tempS;
        getline(cin,tempS);
        inBuffer +=tempS;
        sendto(sockFd, inBuffer.c_str(), inBuffer.size(), 0,
               (const struct sockaddr*)&server, sizeof(server));
        close(sockFd);

 这部分多了一个线程执行函数recverAndPrint,是为了接收来自服务器的消息. 其实就是 接收所有人发送的消息.此函数需要多线程执行,

我们在 udpClient 代码中, 获取用户在命令行输入的内容的实现是用 getline(); 实现的.

是一个阻塞式的等待输入操作.

如果 recverAndPrint() 也在主线程内执行. 那么就会出现 只有用户输入完毕之后, 来自服务器的消息才能输出在客户端中,这样显然是不正确的. 所以使用多线程执行 recverAndPrint(). 主线程不干扰此线程.

为了方便展示、查看我们要知道

  • 标准输出 (stdout)这是程序默认的输出流,通常用于输出程序的结果或正常信息。可以通过命令行中的 > 操作符进行重定向。

  • 标准错误 (stderr)这是一个独立于标准输出的流,通常用于输出错误消息或其他诊断信息。即使标准输出被重定向,标准错误仍然会显示在终端上。

客户端接收到的服务器发来的信息. 演示时, 将 udpClient 的标准输出内容 重定向到了一个 命名管道文件中.

       并且, 为了将来自服务器的信息重定向到其中 并且不出现扰乱信息, 我们将 udpClient 中其他 部分输出 由 std::cout 标准输出 换成了 std::cerr 标准错误. 比如, 输入提示的部分:这样 可以避免将 输入提示符 也重定向到管道文件中. 因为 命令行中 > 是标准输出重定向。


 完整代码 

udpServer.cc:

#include<iostream>
#include<string>
#include<unordered_map>
#include<cassert>
#include<cstdlib>
#include<cerrno>
#include<unistd.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<cstdarg>
#include"logMessage.hpp"
using std::cout;
using std::endl;
using std::string;
using std::unordered_map;

class udpServer
{
    public:
        udpServer(uint16_t port,string ip="")
            :_port(port)
            ,_ip(ip)
            {} 
        ~udpServer()
        {}
        void Init()
        {
            _sockFd=socket(AF_INET,SOCK_DGRAM,0);
            if(_sockFd<0)
            {
        logMessage(FATAL,"socket() failed:: %s : %d",strerror(errno),_sockFd);
                exit(1);
            }
            logMessage(DEBUG,"socket() success:: %d",_sockFd);
            
            struct sockaddr_in local;
            bzero(&local,sizeof(local));
            local.sin_family=AF_INET;
            local.sin_port=htons(_port);
            local.sin_addr.s_addr=_ip.empty()?htonl(INADDR_ANY):inet_addr(_ip.c_str());

            if(bind(_sockFd,(const struct sockaddr*)&local,sizeof(local))==-1)
            {
                logMessage(FATAL,"bind() failed::%s : %d",strerror(errno),_sockFd);
                exit(2);
            }
            logMessage(DEBUG,"bind() success::%d",_sockFd);
        }
        void start()
        {
            char serBuffer[1024];
            while(1)
            {
                struct sockaddr_in peer;
                socklen_t peerlen=sizeof(peer);

                ssize_t s=recvfrom(_sockFd,serBuffer,sizeof(serBuffer)-1,0,(struct sockaddr*)&peer,&peerlen);
                if(s>0)
                {
                    serBuffer[s]=0;
                }
                else if(s==-1)
                {
                    logMessage(WARINING,"recvfrom()::%s : %d",strerror(errno),_sockFd);
                    continue;
                }
                string peerIp=inet_ntoa(peer.sin_addr);
                uint16_t peerPort=ntohs(peer.sin_port);
                CheckOnlineUsers(peerIp,peerPort,peer);
                logMessage(NOTICE,"[%s:%d]# %s",peerIp.c_str(),peerPort,serBuffer);
                string infoUser(serBuffer);
                messageRount(peerIp,peerPort,infoUser);
            }   
        }
        void CheckOnlineUsers(string &ip,uint16_t port,struct sockaddr_in &peer)
        {
            string key=ip;
            key += ":";
            key += std::to_string(port);
            auto itUser=_users.find(key);
            if(itUser==_users.end())
            {
                _users.insert({key,peer});
            }
        }
        void messageRount(string &ip ,uint16_t port,string info)
        {
            string message="[";
            message += ip;
            message +=":";
            message +=std::to_string(port);
            message +="]";
            message +=info;
            for(auto &user : _users)
            {
                sendto(_sockFd,message.c_str(),message.size(),0,(struct sockaddr*)&(user.second),sizeof(user.second));
            }
        }
    private:
        uint16_t _port;
        string _ip;
        int _sockFd;
        unordered_map<string, struct sockaddr_in> _users;
};

static void Usage(const string proc)
{
    cout<<"Usage:\n\t"<<proc<<"port [ip]"<<endl;
}

int main(int argc,char* argv[])
{
    if(argc!=2&&argc!=3)
    {
        Usage(argv[0]);
        exit(3);
    }
    uint16_t port=atoi(argv[1]);
    string ip;
    if(argc==3)
    {
        ip=argv[2];
    }
    udpServer uSvr(port,ip);
    uSvr.Init();
    uSvr.start();
    return 0;

}


udpClient.cc: 

#include <iostream>
#include <string>
#include <cstdlib>
#include <cassert>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include<cstdarg>
#include "logMessage.hpp"

using std::cin;
using std::cout;
using std::endl;
using std::getline;
using std::string;

void* recverAndPrint(void* args) 接收并打印服务器消息
{
    while(true)
    {
        int sockFd=*(int*)args;
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t templen=sizeof(temp);
        ssize_t s=recvfrom(sockFd,buffer,sizeof(buffer),0,(struct sockaddr*)&temp,&templen);
        if(s>0)
        {
            buffer[s]=0;
            cout<<buffer<<endl;//为了方便演示这里保持使用 std::cout,以便重定向到命名管道文件
        }
    }
}

static void Usage(const string proc){
    cout<<"Usage::\n\t"<<proc<<"server_IP server_Port nick_Name";
}

int main(int argc,char* argv[])
{
    if(argc!=4)
    {
        Usage(argv[0]);
        exit(1);
    }
    string server_IP = argv[1];
    uint16_t server_Port = atoi(argv[2]);
	string nick_Name = argv[3];
    int sockFd = socket(AF_INET, SOCK_DGRAM, 0);//只有创建了本地的套接字,才能使用recvfrom循环监听别人发向本地接口的信息
    if (sockFd < 0) {
        logMessage(FATAL, "socket() faild:: %s : %d", strerror(errno), sockFd);
        exit(2);
    }
    logMessage(DEBUG, "socket create success: %d", sockFd);
    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(server_Port);
    server.sin_addr.s_addr = inet_addr(server_IP.c_str());

    pthread_t t;
    pthread_create(&t,nullptr,recverAndPrint,&sockFd);
    while(true)
    {
        std::cerr<<"please enter>>";//使用 std::cerr 输出提示信息
        string inBuffer;
        inBuffer +="[";
        inBuffer +=nick_Name;
        inBuffer +="]# ";
        string tempS;
        getline(cin,tempS);//用于从标准输入读取用户的消息输入到tempS,能读取包含空格的字符串直到遇到换行符
        inBuffer +=tempS;//最终组合发送给服务端
        sendto(sockFd, inBuffer.c_str(), inBuffer.size(), 0,
               (const struct sockaddr*)&server, sizeof(server));
    }
    close(sockFd);
    return 0;
}

makefile: 

.PHONY:all
all:udpServer udpClient

udpServer: udpServer.cc
	g++ -std=c++11 -o $@ $^
udpClient: udpClient.cc
	g++ -std=c++11 -o $@ $^ -lpthread

.PHONY:clean
clean:
	rm -rf udpServer udpClient

inet_ntoa()相关问题 

在前面向 struct sockaddr_in 结构体中填充 IP 地址时使用了一个接口 :inet_addr(),并做了简单介绍,它是将 点分十进制(我们能看懂的)的 ip 转换成了 uint32_t(int _addt_t)类型的4字节的表示的ip,并且会自动把4字节的ip存储数据设置为网络字节序。使用起来是非常方便的,但是还存在着另一个接口: inet_nota()

该接口的作用是将 struct sockaddr_in 结构体 里面存储的4字节IP,转换成我们看的懂得点分十进制得IP字符串,然后以 char*类型返回。所以就需要一块空间来存储这个转换后的字符串,事实上这个空间是处于静态区的空间,返回的字符串都会放在这里,且值得注意的是,整个程序中 inet_ntoa() 也就只会返回处于这块的静态区空间。这也就是说,在一个 inet_ntoa()被多次执行的程序中,后面被转换出来的字符串会覆盖掉之前的IP字符串。所以在实际使用中要避免多次使用或者避免直接使用函数返回的指针。

但是我们可以使用另一个接口来代替: inet_ntop()

该接口可以将网络字节序IPv4或者IPv6地址转换成 点分十进制字符串表示

  1. af: 地址族,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)
  2. src: 指向网络地址(以网络字节序存储)的指针。对于IPv4来说,这个参数是一个 struct in_addr 类型的指针;对于IPv6,则是 struct in6_addr 类型的指针。
  3. dst: 目标缓冲区,用于存放转换后的ASCII表示的IP地址。
  4. size: 目标缓冲区的大小

总结

通过上述步骤,UDP服务端和客户端就可以进行基本的数据交换了。由于UDP是无连接的,每次数据传输都是独立的,因此每次调用 sendto recvfrom 都不需要事先建立连接。这种方式非常适合那些对延迟敏感的应用程序,但也意味着数据包可能丢失或乱序到达。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Prometheus】层层解析prometheus如何监控k8s核心组件

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全…

【JavaEE】多线程进阶(2)

【JavaEE】多线程进阶&#xff08;2&#xff09; 一、JUC(java.util.concurrent) 的常⻅类1.1 Callable 接⼝1.2 ReentrantLock1.3 原子类原子类的特性&#xff1a;常见原子类&#xff1a;原子类的实例&#xff1a; 1.4 线程池1.5 信号量 Semaphore代码实例 1.6 CountDownLatch…

SpringBoot 如何调用 WebService 接口

前言 调用WebService接口的方式有很多&#xff0c;今天记录一下&#xff0c;使用 Spring Web Services 调用 SOAP WebService接口 一.导入依赖 <!-- Spring Boot Web依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId…

从Manus到OpenManus:多智能体协作框架如何重构AI生产力?

文章目录 Manus&#xff1a;封闭生态下的通用AI智能体OpenManus&#xff1a;开源社区的闪速复刻挑战与未来&#xff1a;框架落地的现实边界当前局限性未来演进方向 OpenManus使用指南1. 环境配置2. 参数配置3. 替换搜索引擎4. 运行效果 协作框架开启AI生产力革命 Manus&#xf…

1.5 双指针专题:有效三⻆形的个数(medium)

1.题目链接 611. 有效三角形的个数 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/valid-triangle-number/submissions/609232447/ 2.题目描述 给定⼀个包含⾮负整数的数组 nums &#xff0c;返回其中可以组成三⻆形三条边的三元组个数。 ⽰例 1: 输…

大数据学习(59)-DataX执行机制

&&大数据学习&& &#x1f525;系列专栏&#xff1a; &#x1f451;哲学语录: 承认自己的无知&#xff0c;乃是开启智慧的大门 &#x1f496;如果觉得博主的文章还不错的话&#xff0c;请点赞&#x1f44d;收藏⭐️留言&#x1f4dd;支持一下博主哦&#x1f91…

《面向对象程序设计-C++》实验一 熟悉Visual C++开发环境及上机过程

一、实验目的 了解和使用VC集成开发环境&#xff1b;熟悉VC环境的基本命令和功能键&#xff1b;熟悉常用的功能菜单命令&#xff1b;学习使用VC环境的帮助&#xff1b;学习完整的C程序开发过程&#xff1b;理解简单的C程序结构。 二、实验内容 使用Visual C 6.0集成环境来编…

Chebykan wx 文章阅读

文献筛选 [1] 神经网络&#xff1a;全面基础 [2] 通过sigmoid函数的超层叠近似 [3] 多层前馈网络是通用近似器 [5] 注意力是你所需要的 [6] 深度残差学习用于图像识别 [7] 视觉化神经网络的损失景观 [8] 牙齿模具点云补全通过数据增强和混合RL-GAN [9] 强化学习&#xff1a;一…

2025解决软件供应链安全,开源安全的版本答案:SCA+SBOM

GitHub&#xff1a; https://github.com/XmirrorSecurity/OpenSCA-cli/ Gitee&#xff1a; https://gitee.com/XmirrorSecurity/OpenSCA-cli/ OpenSCA官网&#xff1a; https://opensca.xmirror.cn/ 根据Sonatype 发布的《软件供应链现状》报告&#xff0c;其中强调软件供…

Linux 系统负载过高的排查思路

技术探讨&#xff1a;Linux系统负载过高的排查思路 在Linux服务器运行过程中&#xff0c;如果系统负载过高&#xff0c;可能会导致性能下降和服务不稳定。以下是针对Linux系统负载过高问题的排查思路和解决方法&#xff1a; 1. 查看系统负载&#xff1a; 使用uptime或top命令查…

typora高亮方案+鼠标侧键一键改色

引言 在typora里面有一个自定义的高亮, <mark></mark>>但是单一颜色就太难看了, 我使用人工智能, 搜索全网艺术家, 汇集了几种好看的格式,并且方便大家侧键一键 调用, 是不是太方便啦 ! 示例 午夜模式 春意盎然 深海蓝调 石墨文档 秋日暖阳 蜜桃宣言 使用方法 …

自然语言处理文本分析:从词袋模型到认知智能的进化之旅

清晨&#xff0c;当智能音箱准确识别出"播放周杰伦最新专辑"的模糊语音指令时&#xff1b;午间&#xff0c;企业舆情系统自动标记出十万条评论中的负面情绪&#xff1b;深夜&#xff0c;科研人员用GPT-4解析百万篇论文发现新材料线索——这些场景背后&#xff0c;是自…

基于SSM+Vue+uniapp的考研交流(带商城)小程序+LW示例参考

系列文章目录 1.基于SSM的洗衣房管理系统原生微信小程序LW参考示例 2.基于SpringBoot的宠物摄影网站管理系统LW参考示例 3.基于SpringBootVue的企业人事管理系统LW参考示例 4.基于SSM的高校实验室管理系统LW参考示例 5.基于SpringBoot的二手数码回收系统原生微信小程序LW参考示…

浙江大学:DeepSeek行业应用案例集(153页)(文末可下载PDF)

浙江大学&#xff1a;DeepSeek行业应用案例集&#xff08;153页&#xff09;&#xff08;文末可下载PDF&#xff09; 全文链接&#xff1a;浙江大学&#xff1a;DeepSeek行业应用案例集&#xff08;153页&#xff09;&#xff08;文末可下载PDF&#xff09; | AI探金 全文链接&…

深度学习分类回归(衣帽数据集)

一、步骤 1 加载数据集fashion_minst 2 搭建class NeuralNetwork模型 3 设置损失函数&#xff0c;优化器 4 编写评估函数 5 编写训练函数 6 开始训练 7 绘制损失&#xff0c;准确率曲线 二、代码 导包&#xff0c;打印版本号&#xff1a; import matplotlib as mpl im…

共享经济时代下,鲲鹏共享科技如何逆袭改命?

2016年&#xff0c;当共享充电宝顶着“资本泡沫”的质疑横空出世时&#xff0c;没人能想到&#xff0c;这个曾被王思聪嘲讽“能成我吃翔”的行业&#xff0c;竟在短短几年内成为共享经济领域最顽强的幸存者。数据显示&#xff0c;2019年共享充电宝用户规模突破3亿&#xff0c;单…

说一下spring的事务隔离级别?

大家好&#xff0c;我是锋哥。今天分享关于【说一下spring的事务隔离级别&#xff1f;】面试题。希望对大家有帮助&#xff1b; 说一下spring的事务隔离级别&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring的事务隔离级别是指在数据库事务管理中…

Web开发第五节

一.结构伪类选择器 &#xff08;一&#xff09;选择单个 &#xff08;二&#xff09;选择多个 注&#xff1a;1.n5指的是5以后的数字&#xff0c;包含5&#xff0c;n从0开始 2.-n5指的是5以前的数字&#xff0c;同样包含5&#xff0c;并且n从0开始 二.伪元素选择器 注&…

计算机毕业设计:驾校综合信息系统

驾校综合信息系统mysql数据库创建语句驾校综合信息系统oracle数据库创建语句驾校综合信息系统sqlserver数据库创建语句驾校综合信息系统springspringMVChibernate框架对象(javaBean,pojo)设计驾校综合信息系统springspringMVCmybatis框架对象(javaBean,pojo)设计 驾校综合信息系…

无标签数据增强+高效注意力GAN:基于CARLA的夜间车辆检测精度跃升

目录 一、摘要 二、引言 三、框架 四、方法 生成合成夜间数据 昼夜图像风格转换 针对夜间图像的无标签数据增强技术 五、Coovally AI模型训练与应用平台 六、实验 数据 图像风格转换 夜间车辆检测和分类 结论 论文题目&#xff1a;ENHANCING NIGHTTIME VEHICLE D…