计算机网络—UDP协议详解:特性、应用

                                        🎬慕斯主页修仙—别有洞天

                                       ♈️今日夜电波:マリンブルーの庭園—ずっと真夜中でいいのに。

                                                           0:34━━━━━━️💟──────── 3:34
                                                                🔄   ◀️   ⏸   ▶️    ☰  

                                 💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍


目录

网络字节序的概念

对应的网络字节序转换接口

地址转换函数

字符串转in_addr的函数:

in_addr转字符串的函数:

注意事项

socket编程

socket中sockaddr结构

socket在UDP中常用接口的介绍

socket

bind

recvfrom

sendto

什么是UDP协议?

通过程序来理解UDP

服务端

客户端


网络字节序的概念

        内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

        发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

        接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

        因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

        TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

        不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

        如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

对应的网络字节序转换接口

        为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换:

#include <arpa/inet.h>  
uint32_t htonl(uint32_t hostlong);

        这里的hostlong是一个32位无符号长整型数,表示主机字节序的数值。函数返回转换后的网络字节序的32位无符号长整型数。

#include <arpa/inet.h>  
uint16_t htons(uint16_t hostshort);

        它接受一个16位无符号整数作为输入参数,并返回转换后的网络字节顺序的数值。

#include <arpa/inet.h>  
uint32_t ntohl(uint32_t netlong);

        在网络编程中,当从网络接收数据时,可能需要将数据从网络字节顺序转换为主机字节顺序,以便正确解读数据。ntohl通常用于解析IPv4地址、端口号或其他在网络传输中使用的32位数值;

#include <arpa/inet.h>  
uint16_t ntohs(uint16_t netshort);

        在网络编程中,当从网络接收数据时,可能需要将数据从网络字节顺序转换为主机字节顺序,以便正确解读数据。ntohs通常用于解析端口号或其他在网络传输中使用的16位数值;

地址转换函数

        我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换:

字符串转in_addr的函数:


    inet_aton是一个用于将点分十进制表示的IPv4地址字符串转换为网络字节序的32位整数的函数。它的原型如下:

#include <arpa/inet.h>
int inet_aton(const char *cp, struct in_addr *inp);

参数:

  • cp:指向点分十进制表示的IPv4地址字符串的指针。
  • inp:指向一个struct in_addr结构体的指针,用于存储转换后的32位整数。

返回值:

  • 成功时返回1,失败时返回0。

示例代码:

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    const char *ip_str = "192.168.1.1";
    in_addr_t ip_addr;

    ip_addr = inet_addr(ip_str);
    if (ip_addr == INADDR_NONE) {
        printf("无效的IP地址");
        return 1;
    }


    printf("转换后的IP地址:%d", ip_addr);
    return 0;
}

    inet_addr 是一个在早期的 Unix-like 系统(如 Linux)中用于将点分十进制的 IP 地址字符串转换为网络字节序的 32 位整数的函数。然而,需要注意的是,这个函数在现代的编程实践中已经较少使用,因为其功能可以被更安全的函数(如 inet_pton)所替代,并且 inet_addr 在处理非法的 IP 地址时可能不会返回错误。

函数原型

#include <arpa/inet.h>
in_addr_t inet_addr(const char *cp);

参数

  • cp:这是一个指向以 null 结尾的字符串的指针,该字符串表示一个点分十进制的 IPv4 地址(例如 "192.168.1.1")。

返回值:

如果输入的字符串是一个有效的 IPv4 地址,inet_addr 将返回一个表示该地址的 32 位无符号整数(网络字节序)。如果输入的字符串不是一个有效的 IPv4 地址,函数将返回 INADDR_NONE(通常是 -1)。

使用示例:

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    const char *ip_str = "192.168.1.1";
    in_addr_t ip_addr = inet_addr(ip_str);

    if (ip_addr == INADDR_NONE) {
        printf("Invalid IP address\n");
    } else {
        printf("IP address in network byte order: %u\n", ip_addr);
    }

    return 0;
}

    inet_pton 函数用于将点分十进制表示的IP地址转换为网络字节序的二进制形式。该函数原型如下:

#include <arpa/inet.h>
int inet_pton(int af, const char *src, void *dst);

其中:

  • af 参数指定地址族类型,可以是 AF_INET(IPv4)或 AF_INET6(IPv6)。
  • src 参数指向包含点分十进制表示的 IP 地址字符串。
  • dst 参数指向一个缓冲区,用于存储转换后的二进制结果。

返回值:

  • 成功时返回1,失败时返回0。如果出现其他错误,则返回-1。

使用示例:

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct sockaddr_in sa;
    char *ip_str = "192.168.1.1";

    if (inet_pton(AF_INET, ip_str, &(sa.sin_addr))) {
        printf("转换成功,二进制表示为:
");
        printf("%02x:%02x:%02x:%02x:%02x:%02x:%02x:%02x
",
               (unsigned char)sa.sin_addr.s_addr & 0xff,
               (unsigned char)(sa.sin_addr.s_addr >> 8) & 0xff,
               (unsigned char)(sa.sin_addr.s_addr >> 16) & 0xff,
               (unsigned char)(sa.sin_addr.s_addr >> 24) & 0xff,
               (unsigned char)(sa.sin_addr.s_addr >> 32) & 0xff,
               (unsigned char)(sa.sin_addr.s_addr >> 40) & 0xff,
               (unsigned char)(sa.sin_addr.s_addr >> 48) & 0xff,
               (unsigned char)(sa.sin_addr.s_addr >> 56) & 0xff);
    } else {
        printf("无效的IP地址
");
    }

    return 0;
}

in_addr转字符串的函数:

    inet_ntoa 是一个常用于将网络字节序的 IPv4 地址(通常是一个 in_addr 结构的实例或该结构的 in_addr_t 类型的值)转换为其点分十进制字符串表示形式的函数。这个函数在早期的网络编程中非常常见,但同样地,现代编程实践中更推荐使用 inet_ntop 函数,因为它提供了更强大的功能,包括对 IPv6 的支持以及更详细的错误处理。

函数原型:

#include <arpa/inet.h>
char *inet_ntoa(struct in_addr in);

参数:

  • in:一个 in_addr 结构体的实例,其中包含要转换的网络字节序的 IPv4 地址。

返回值:

inet_ntoa 返回一个指向静态内存区域中存储的点分十进制字符串的指针。这意味着连续调用 inet_ntoa 可能会覆盖之前的结果,因为它总是使用相同的静态缓冲区。因此,如果你需要保留转换后的字符串,应该立即将其复制到另一个缓冲区中。

使用示例:

#include <stdio.h>
#include <arpa/inet.h>

int main() {
    struct in_addr addr;
    if (inet_aton("192.168.1.1", &addr) == 1) {
        char *ip_str = inet_ntoa(addr);
        if (ip_str != NULL) {
            printf("IP address in dot-decimal notation: %s\n", ip_str);
        } else {
            printf("Error converting IP address\n");
        }
    } else {
        printf("Invalid IP address\n");
    }
    return 0;
}

    inet_ntop函数是一个用于将网络字节序的IPv4和IPv6地址从二进制格式转换为人类可读的字符串格式的函数。它在网络编程中广泛使用,尤其是当需要将IP地址从二进制形式转换为点分十进制或其他格式时。

函数原型:

#include <arpa/inet.h>
const char *inet_ntop(int af, const void *src, char *dst, socklen_t size);

参数:

  • af:地址族(Address Family),指定了IP地址的类型。对于IPv4,它应该是AF_INET;对于IPv6,它应该是AF_INET6
  • src:指向存储要转换的二进制IP地址的缓冲区的指针。
  • dst:指向存储转换后的字符串形式IP地址的缓冲区的指针。
  • size:目标缓冲区dst的大小,以防止缓冲区溢出。

返回值:

如果转换成功,inet_ntop函数返回一个指向转换后的字符串形式IP地址的指针(即dst的值)。如果发生错误,则返回NULL,并设置相应的错误码(通过errno变量获取)。

使用示例:

#include <stdio.h>
#include <arpa/inet.h>
#include <string.h>

int main() {
    struct in_addr addr;
    addr.s_addr = inet_addr("192.168.0.1");

    char str[INET_ADDRSTRLEN];
    const char *result = inet_ntop(AF_INET, &addr, str, INET_ADDRSTRLEN);

    if (result == NULL) {
        perror("inet_ntop");
        return 1;
    }

    printf("IP address in string format: %s\n", str);
    return 0;
}

        在这个示例中,我们首先将字符串形式的IP地址"192.168.0.1"转换为in_addr结构体的实例。然后,我们使用inet_ntop函数将该地址转换为点分十进制的字符串形式,并将结果存储在str数组中。最后,我们打印出转换后的字符串。

注意事项

    inet_addr 仅支持 IPv4 地址。对于 IPv6 地址,因此推荐使用 inet_pton 函数inet_ntoa 仅支持 IPv4 地址。对于 IPv6 地址,你应该使用 inet_ntop 函数

        man手册上说, inet_ntoa函数, 是把这个返回结果放到了静态存储区. 这个时候不需要我们手动进行释放. 因为inet_ntoa把结果放到自己内部的一个静态存储区, 第二次调用时的结果会覆盖掉上一次的结果. 如果有多个线程调用 inet_ntoa, 是否会出现异常情况呢? 在APUE中, 明确提出inet_ntoa不是线程安全的函数
        因此,在多线程环境下, 推荐使用inet_ntop, 这个函数由调用者提供一个缓冲区保存结果, 可以规避线程安全问题;

        在inet_ntop中,为确保目标缓冲区dst足够大,以容纳转换后的字符串形式IP地址。对于IPv4地址,通常使用INET_ADDRSTRLEN作为缓冲区大小是安全的

socket编程

socket中sockaddr结构

    sockaddr是一个通用的套接字地址结构体,用于在网络编程中表示不同类型的套接字地址。这个结构体最初在网络编程函数诞生时就被使用,当时主要是为了IPv4协议。为了向前兼容,现在的sockaddr在很多时候退化为了传递地址给函数的作用,具体的地址类型(如sockaddr_insockaddr_in6)由地址族(address family)确定,然后在函数内部再强制类型转化为所需的地址类型。

sockaddr结构体定义如下:

struct sockaddr {
    unsigned short sa_family;   // 地址族,用于指定地址类型,例如AF_INET(IPv4)
    char sa_data[14];           // 地址数据,具体格式和长度取决于地址族的不同
};

        其中,sa_family字段用来指定地址族,即地址类型。常见的取值有AF_INET(IPv4)、AF_INET6(IPv6)和AF_UNIX(UNIX域套接字)等。sa_data字段用来存储实际的地址数据,其格式和长度会根据地址族的不同而变化。

        socket API可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

        为了更方便地使用,通常会使用sockaddr的变体结构,如sockaddr_in(用于IPv4地址)和sockaddr_in6(用于IPv6地址)。这些变体结构体在sockaddr的基础上进行了扩展,增加了一些额外的字段来保存特定类型的地址信息。例如,sockaddr_in结构体包含了sin_family(地址族)、sin_port(端口号)、sin_addr(IP地址信息)以及sin_zero(为了保持与sockaddr结构大小相同而保留的空字节)等字段。如下为三个sockaddr的结构图示:分别为:struct sockaddr(常用于强转另外两个)、struct sockaddr_in(用于网络的套接字)、struct sockaddr_un(用于本地的套接字

        如果我们要使用socket进行网络编程,那么首先做好对于sockaddr的初始化是很重要的,它在网络编程中扮演着至关重要的角色。它的主要作用是用于表示套接字的地址信息,这包括地址族、端口号以及具体的IP地址或路径等信息。例如下面对于sockaddr_in的示例(通过结构体来设置好地址族(确定使用的协议)、确定好自身的ip、确定好对应的端口。以便后续bind将用户态的sockaddr_in设置进内核态):

sockaddr_in addr;
addr.sin_family = AF_INET; //确定协议
addr.sin_addr.s_addr = inet_addr(ip);//将点分十进制的IPv4地址转换成一个长整数型数
addr.sin_port = htons(port);//注意转换位网络字节序。需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的

socket在UDP中常用接口的介绍

socket

        socket函数是一种在网络编程中广泛使用的函数,它用于根据指定的地址族、数据类型和协议来分配一个套接口(socket)的描述字及其所用的资源。以下是关于socket函数的详细解释:

函数声明

#include <sys/socket.h>
int socket(int domain, int type, int protocol);
  • domain(协议域/协议族):决定了socket的地址类型。常用的协议族有AF_INET(IPv4)、AF_INET6(IPv6)、AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等。在通信中,必须采用与协议族对应的地址。例如,AF_INET决定了要使用IPv4地址(32位)与端口号(16位)的组合。
  • type(socket类型):指定了socket的类型。常用的socket类型有SOCK_STREAM(流式套接字,用于TCP)、SOCK_DGRAM(数据报套接字,用于UDP)、SOCK_RAW(原始套接字,允许对底层协议如IP或ICMP进行直接访问)等。
  • protocol(协议):通常情况下,可以将其设置为0,让系统自动选择type类型对应的默认协议。

返回值

  • 当socket函数成功创建了一个套接字时,它返回一个有效的套接字描述符(socket descriptor)。这个描述符是一个非负整数,用于后续的网络操作,如绑定、监听、连接、发送和接收数据等。
  • 如果在创建套接字时发生错误,socket函数返回-1,并设置全局变量errno以指示错误原因。此时,可以调用errno变量或perror()函数来获取具体的错误信息。常见的错误码包括EACCES(权限不足)、EADDRINUSE(地址已经被占用)、EAFNOSUPPORT(地址族不支持)、EINVAL(参数无效)、EMFILE(达到进程允许打开的最大文件数目)、ENFILE(系统打开文件数目过多)、ENOBUFS/ENOMEM(内存不足)、EPROTONOSUPPORT(协议不支持)等。

        需要注意的是,socket函数返回的套接字描述符本质上是一个文件描述符,因此在系统中,它被当作文件来对待。这意味着可以使用与文件操作类似的系统调用来对其进行读写操作。

bind

        bind函数在网络编程中扮演着至关重要的角色,它主要用于将一个本地协议地址(包括IP地址和端口号)赋予一个套接字。以下是关于bind函数的详细解释:

函数声明

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd:这是由socket()函数返回的文件描述符,代表已经创建的套接字。
  • addr:这是一个指向特定协议地址结构的指针,如struct sockaddr_instruct sockaddr_un,它包含了地址、端口和可能的IP地址信息。
  • addrlen:这是地址结构的长度,通常以字节为单位。对于IPv4,通常使用sizeof(struct sockaddr_in);对于IPv6,使用sizeof(struct sockaddr_in6);对于Unix域套接字,使用sizeof(struct sockaddr_un)

返回值

  • 如果bind函数成功执行,它返回0。
  • 如果出现错误,返回-1,并设置全局变量errno以指示错误原因。常见的错误包括EACCES(权限不足)、EADDRINUSE(地址已经被使用)、EADDRNOTAVAIL(地址不可用)、EAFNOSUPPORT(地址族不支持该套接字类型)、EINVAL(套接字未打开)、ENOTSOCK(文件描述符不是套接字)等。

使用场景

  • 在TCP服务器程序中,bind函数通常用于指定服务器应监听的端口号。服务器在启动时捆绑其众所周知的端口,以便客户端可以连接到它。
  • 对于UDP套接字,bind函数同样用于指定接收数据的端口号。
  • 在Unix域套接字中,bind函数可以用来指定套接字在文件系统中的路径名。

注意事项

  • 在调用bind函数之前,套接字必须处于未连接状态(对于面向连接的套接字如TCP)。
  • 如果addr参数中的地址或端口号为0,系统将为套接字自动选择一个可用的地址或端口号。
  • 在多线程环境中,应确保对bind函数的调用是线程安全的,避免竞态条件。
  • 绑定的本质:将用户态的sockaddr_in设置进内核变为系统态。

recvfrom

        recvfrom函数是一个在POSIX兼容操作系统(如Linux)中用于接收数据的系统调用。它主要用于从指定的套接字接收数据,并适用于面向无连接的协议,如UDP(用户数据报协议)。

recvfrom函数的原型如下:

#include <sys/socket.h>
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                   struct sockaddr *src_addr, socklen_t *addrlen);

参数解释:

  • sockfd:已经创建并绑定的套接字的文件描述符。
  • buf:指向用于接收数据的缓冲区的指针。
  • len:指定接收数据的最大长度。
  • flags:用于控制接收数据的方式。常用的选项有:
    • MSG_WAITALL:阻塞等待直到len字节的数据接收完毕。
    • MSG_DONTWAIT:非阻塞模式,如果没有数据可读,立即返回-1,同时errno设置为EAGAIN或EWOULDBLOCK。
  • src_addr:指向一个sockaddr结构体,用于存储发送方的地址信息。如果不需要这个信息,可以设置为NULL。
  • addrlen:表示src_addr结构体的长度。

返回值:

  • 成功时,返回接收到的字符数(字节数)。
  • 如果没有可用数据或者连接已经关闭,返回0。
  • 如果出现错误,返回-1,并设置errno错误号。此时可以通过perror()函数来打印出错误信息。

注意事项:

  • 在调用recvfrom函数之前,需要先使用bind函数将socket绑定到一个地址上。
  • 如果套接字是非阻塞的,recvfrom函数可能会在没有接收到任何数据时返回-1,并设置errno为EAGAIN或EWOULDBLOCK。
  • 如果接收到的数据比缓冲区还大,那么只会取缓冲区大小的数据,并将剩余的数据丢弃。

sendto

sendto函数是一个系统调用,用于将数据从指定的套接字发送到目标地址。它通常用于UDP(用户数据报协议)通信,因为UDP是无连接的,所以sendto函数允许你向一个特定的地址发送数据报,而不需要事先建立连接。

sendto函数的原型如下:

#include <sys/socket.h>
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

  • sockfd:已经创建好的socket文件描述符。
  • buf:指向要发送的数据的缓冲区。
  • len:要发送的数据长度。
  • flags:发送选项标志,可以是0或者像MSG_DONTWAIT这样的选项。MSG_DONTWAIT表示非阻塞发送,如果发送缓冲区满,则不等待直接返回。
  • dest_addr:目标地址的sockaddr结构体指针。对于IPv4,这通常是一个指向struct sockaddr_in的指针;对于IPv6,则是一个指向struct sockaddr_in6的指针。
  • addrlen:目标地址结构体的长度,例如sizeof(struct sockaddr_in)sizeof(struct sockaddr_in6)

返回值:

sendto函数的返回值是一个long类型的整数,表示发送的字节数。具体返回值有以下几种可能:

  • 如果返回值大于0,则表示数据已经成功发送到了目标地址。返回值代表实际发送的字节数。
  • 如果返回值等于0,表示发送的数据长度为0。这可能是因为buf指向的空间长度为0,或者在使用UDP协议时,sendto函数成功地发送了0字节的数据。
  • 如果返回值等于-1,表示发送过程中出现了错误。此时,可以通过检查errno的值来确定具体的错误原因。例如,如果errno为EINTR,表示sendto函数被一个信号中断了;如果errno为EAGAIN或EWOULDBLOCK,表示发送缓冲区已满,无法立即发送数据(这通常发生在使用了MSG_DONTWAIT标志的情况下)。

        需要注意的是,sendto函数不保证数据的可靠传输。也就是说,发送的数据可能会丢失,或者接收方可能无法按照发送的顺序接收数据。如果需要可靠的数据传输,应该使用TCP协议而不是UDP。

        此外,在使用sendto函数之前,需要确保已经通过socket函数创建了一个套接字,并且(对于面向连接的套接字类型)已经通过connect函数与目标地址建立了连接(尽管对于UDP,连接通常不是必需的,但也可以通过connect建立默认的目标地址)。同时,也需要确保目标地址是有效的,并且发送的数据缓冲区是正确设置的。

什么是UDP协议?

        UDP协议,全称为用户数据报协议(User Datagram Protocol),是一种无连接的传输层协议,它为网络应用程序提供了一种在IP网络上发送封装好的IP数据包的方法。以下是对UDP协议的一些详细理解:

  1. 无连接性:与TCP协议不同,UDP不建立持久的连接。这意味着每个数据报都是独立发送的,无需事先建立或维护任何连接状态。
  2. 不可靠性:UDP不保证数据报的传递,没有流控制和应答确认机制。因此,它可能会发生数据丢失、重复或者乱序到达的情况。
  3. 头部简单:相比于TCP,UDP的报文头更简单,只包含端口号、校验和以及数据长度等信息,这使得它的开销较小。
  4. 快速传输:由于UDP没有复杂的传输机制,它在传输速度上通常比TCP快,适用于对传输速度和延迟要求较高的应用。
  5. 校验和:虽然UDP是不可靠的,但它通过校验和来提供一定程度的数据完整性检查,可以检测出数据在传输过程中是否发生变化。
  6. 应用场景:UDP适用于那些即使偶尔出现数据丢失也不影响整体功能的实时应用,如在线游戏、实时视频或音频通信、DNS查询等。
  7. 基于IP数据包服务:UDP构建在IP协议之上,增加了一些简单的功能,如端口号和校验和,以支持不同的应用程序在同一台设备上的数据传输。
  8. 与TCP的关系:尽管UDP和TCP都属于传输层协议,但它们有着截然不同的特性。TCP是面向连接的、可靠的协议,提供了数据排序、错误检测和流量控制等功能,而UDP则更加轻量级和简单。
  9. 抓包分析:在实际的网络调试中,可以使用Wireshark等工具来捕获和分析UDP数据包,帮助诊断网络问题和优化性能。

        重点总结如下:传输层协议、无连接、不可靠传输、面向数据报

通过程序来理解UDP

服务端

        实际上对于服务端最开始的代码都是差不多的!步骤:1、创建一个struct sockaddr_in结构体,将本服务器对应的族、端口、ip地址填好。2、使用socket函数按照对应的族、选取的协议(UDP、TCP)来获取sockfd。3、使用bind将上面获取的sockfd、sockaddr_in进行绑定,实际上就是:将用户态的sockaddr_in设置进内核变为系统态!4、根据需求用recvfrom、sendto来收发消息。

UdpServer.hpp

#pragma once

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

const uint16_t defaultport = 8888;
std::string defaultip = "0.0.0.0";
const int size = 1024;

using func_t = function<string(const string &)>;

enum
{
    SOCKET_ERR = 1,
    BIND_ERR
};

class UdpServer
{
public:
    UdpServer(uint16_t port = defaultport)
        : _sockfd(0), _ip(defaultip), _port(port), _isrunning(false)
    {
    }

    void UdpInit()
    {
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            lg.LogMessage(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
            exit(SOCKET_ERR);
        }
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = inet_addr(_ip.c_str());
        // local.sin_addr.s_addr = htonl(INADDR_ANY); //设置为ip为0,表示自动选择地址
        local.sin_port = htons(_port);

        if (bind(_sockfd, (const struct sockaddr *)&local, sizeof(local)) < 0)
        {
            lg.LogMessage(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        lg.LogMessage(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }

    void run(func_t func) // 使用回调函数是为了将应用层与传输层分离
    {
        _isrunning = true;
        char inbuffer[size];
        while (_isrunning)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            bzero(&client, len);
            bzero(&inbuffer, sizeof(inbuffer));
            ssize_t n = recvfrom(_sockfd, inbuffer, sizeof(inbuffer) - 1, 0, (struct sockaddr *)&client, &len);
            if (n < 0)
            {
                lg.LogMessage(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }
            inbuffer[n] = 0;
            std::string info = inbuffer;
            std::string echo_string = func(info);//回调传入的函数
            sendto(_sockfd,echo_string.c_str(),echo_string.size(),0,(const struct sockaddr *)&client,len);
        }
    }

    ~UdpServer()
    {
        if (_sockfd > 0)
            close(_sockfd);
    }

private:
    int _sockfd;
    string _ip;
    uint16_t _port;
    bool _isrunning;
};

main.cc

#include "UdpServer.hpp"
#include <memory>

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

std::string Handler(const std::string &str)//简单处理字符串后返回
{
    std::string res = "Server get a message: ";
    res += str;
    std::cout << res << std::endl;


    return res;
}

std::string ExcuteCommand(const std::string &cmd)//远程运行指令
{
    // SafeCheck(cmd);

    FILE *fp = popen(cmd.c_str(), "r");
    if(nullptr == fp)
    {
        perror("popen");
        return "error";
    }
    std::string result;
    char buffer[4096];
    while(true)
    {
        char *ok = fgets(buffer, sizeof(buffer), fp);
        if(ok == nullptr) break;
        result += buffer;
    }
    pclose(fp);

    return result;
}

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));
    svr->UdpInit();
    cout<<endl;
    svr->run(Handler);
    return 0;
}

客户端

        对于客户端而言,大致的初始化过程同服务端是差不多的,但是对于客户端而已,客户端可以虽然也需要绑定,但是不需要用户显示的bind!,一般由OS自由的选择!一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!其实client的port是多少,其实不重要,只要能保证主机上的唯一性就可以!系统什么时候给我bind呢?首次发送数据的时候!

#include <iostream>
#include <cstdlib>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

using namespace std;

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

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }

    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct sockaddr_in server;
    bzero(&server, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    socklen_t len = sizeof(server);

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        cout << "socker error" << endl;
        return 1;
    }

    string message;
    char buffer[1024];
    while (true)
    {
        cout << "Please Enter@ ";
        getline(cin, message);

        // std::cout << message << std::endl;
        // 1. 数据 2. 给谁发
        sendto(sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&server, len);

        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        ssize_t s = recvfrom(sockfd, buffer, 1023, 0, (struct sockaddr *)&temp, &len);
        if (s > 0)
        {
            buffer[s] = 0;
            cout << buffer << endl;
        }
    }

    close(sockfd);

    return 0;
}

                       感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o! 

                                       

                                                                        给个三连再走嘛~  

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

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

相关文章

Rust 02.控制、引用、切片Slice、智能指针

1.控制流 //rust通过所有权机制来管理内存&#xff0c;编译器在编译就会根据所有权规则对内存的使用进行 //堆和栈 //编译的时候数据的类型大小是固定的&#xff0c;就是分配在栈上的 //编译的时候数据类型大小不固定&#xff0c;就是分配堆上的 fn main() {let x: i32 1;{le…

美术馆设计方案优化布局与设施提升观众体验!

如今&#xff0c;美术馆不仅仅是作为展示艺术作品的平台&#xff0c;也是吸引公众参与和创造独特体验的数字艺术体验空间&#xff0c;因此许多传统美术馆在进行翻修改造时&#xff0c;都会更加注重用户体验&#xff0c;并在其中使用大量的多媒体互动&#xff0c;让参观者能够在…

基于 YOLO V8 Fine-Tuning 训练自定义的目标检测模型

一、YOLO V8 YOLO V8 是由 2023 年 ultralytics 公司开源的发布&#xff0c;是结合了前几代 YOLO 的融合改进版。YOLO V8 支持全方位的视觉 AI 任务&#xff0c;包括检测、分割、姿态估计、跟踪和分类。并且在速度和准确性方面具有无与伦比的性能。能够应用在各种对速度和精度…

【重制版】在Android手机上安装kali Linux

前言 由于kali官方的Nethunter2的安装代码因为…无法访问&#xff0c;手头又没有一些受支持的机器3&#xff0c;所以做了这个脚本&#xff0c;供大家使用。 工具 搭载基于Android的手机TermuxVNC Viewer 安装必备软件(如已安装请忽略) 请到 https://www.hestudio.net/post…

制造出海,灵途科技助力割草机器人、泳池清洁机器人全方位感知

近年来&#xff0c;越来越多的中国企业开始对外开拓&#xff0c;走向海外市场、挖掘和满足全球消费者的需求。在消费机器人领域&#xff0c;中国企业出海成绩亮眼&#xff01;在2024 ces 和上海AWE展会上&#xff0c;多家机器人公司展示了家用智能割草机器人、泳池清洁机器人的…

C#基础知识总结

C语言、C和C#的区别 ✔ 面向对象编程&#xff08;OOP&#xff09;&#xff1a; C 是一种过程化的编程语言&#xff0c;它不直接支持面向对象编程。然而&#xff0c;C 是一种支持 OOP 的 C 的超集&#xff0c;它引入了类、对象、继承、多态等概念。C# 是完全面向对象的&#xff…

【C++】string类(常用接口)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;http://t.csdnimg.cn/eCa5z 目录 修改操作 push_back append operator assign insert erase replace c_str find string类非成…

淘宝商品详情数据(商品分析,竞品分析,代购商城建站与跨境电商,ERP系统商品数据选品)

淘宝商品详情数据在多个业务场景中发挥着关键作用&#xff0c;以下是一些主要的应用场景&#xff1a; 请求示例&#xff0c;API接口接入Anzexi58 商品分析&#xff1a;通过对淘宝商品详情的全面分析&#xff0c;商家可以深入了解商品的属性、价格、销售量、评价等信息。这些数…

手写简易操作系统(十八)--实现用户进程

一、TSS TSS是Task State Segment的缩写&#xff0c;即任务状态段&#xff0c;早在简述特权级的时候我们就讲过了一点 手写简易操作系统(八)&#xff0c;现在我们讲一下这些保存的寄存器是干嘛的。 这一部分需要讲点历史&#xff0c;硬件与软件的关系是相互促进的&#xff0c…

基于SpringBoot + Vue实现的中国陕西民俗网设计与实现+毕业论文

介绍 本系统包含管理员、用户两个角色。 管理员角色&#xff1a;登录、用户管理功能、民俗介绍管理功能(发布和管理民俗文化的介绍文章)、公告信息管理功能(发布网站的重要通知和活动信息)、商品管理功能(对商家发布的商品进行监管)、商品评价管理功能(监管商品评价内容&#…

乐理通识

2023 年搞了台雅马哈 61 键的电子琴&#xff0c;顺手看了下啊 B 的上的课程 《零基础自学音乐学乐理合集-第一季》&#xff0c;这里是部分笔记&#xff08;给博客加点不一样的东西&#x1f440;&#xff09;。 简谱各部分一览 C 表示音名竖线为小节线 音名 完整钢琴键盘 88 键…

leetcode:392. 判断子序列

题目&#xff1a; class Solution { public:bool isSubsequence(string s, string t) {} }; 题解&#xff1a; 很巧妙的题解&#xff1a;循环遍历两个字符串&#xff0c;两个字符串都没遍完就继续遍历&#xff0c;字符串s先遍历完结果为true&#xff0c;字符串t先遍历完结果为…

项目管理【环境】概述

系列文章目录 【引论一】项目管理的意义 【引论二】项目管理的逻辑 【环境】概述 一、组织运行环境 1.1 事业环境因素EEFs 1.2 组织过程资产OPA 1.3 二者差异 二、组织结构类型 2.1 组织架构 2.2 职能型组织 2.3 项目型组织 2.4 矩阵型组织 2.5 项目管理者在不同组织中的特…

NSSCTF Round#20 Basic 真亦假,假亦真 CSDN_To_PDF V1.2 出题笔记 (附wp+源码)

真亦假&#xff0c;假亦真 简介&#xff1a;java伪造php一句话马。实则信息泄露一扫就出&#xff0c;flag在/flag里面。 题目描述&#xff1a;开开心心签个到吧&#xff0c;祝各位师傅们好运~ 静态flag&#xff1a;NSS{Checkin_h4v3_4_g00D_tINNe!} /路由显示 <?php e…

数据库安全(redis、couchdb、h2database)CVE复现

redis服务默认端口&#xff1a;6379&#xff1b;我们可以通过端口扫描来判断是否存在该服务。 Redis 是一套开源的使用ANSI C 编写、支持网络、可基于内存亦可持久化的日志型、键值存储数据库&#xff0c;并提供多种语言的API。 Redis 如果在没有开启认证的情况下&#xff0c;…

论文笔记:TALK LIKE A GRAPH: ENCODING GRAPHS FORLARGE LANGUAGE MODELS

ICLR 2024&#xff0c;reviewer评分 6666 1 intro 1.1 背景 当下LLM的限制 限制1&#xff1a;对非结构化文本的依赖 ——>模型有时会错过明显的逻辑推理或产生错误的结论限制2&#xff1a;LLMs本质上受到它们训练时间的限制&#xff0c;将“最新”信息纳入到不断变化的世…

MySQL优化之外连接消除----空值拒绝

什么叫空值拒绝呢&#xff1f;请看下面用例 可以看到&#xff0c;外连接中&#xff0c;从表的表连接列&#xff0c;tb2.id出现了null&#xff0c;因为tb2中并没有id5的情况 点击(此处)折叠或打开 select * from tb1 left join tb2 on tb1.idtb2.id where tb2.id>1; sel…

商品说明书的制作工具来啦,用这几个就够了!

商品说明书是用户了解产品特性、性能、使用方法的重要途径。一个明确、易懂的商品说明书&#xff0c;可以显著提升用户体验&#xff0c;进而提升产品的销量。但我们都知道&#xff0c;制作一份高质量的说明书并不容易&#xff0c;需要仔细设计、计划和撰写。幸运的是&#xff0…

解决 linux 服务器 java 命令不生效问题

在Linux系统中&#xff0c;当你安装Java并设置了JAVA_HOME环境变量后&#xff0c;你可能需要使用source /etc/profile命令来使Java命令生效。这是因为/etc/profile是一个系统级的配置文件&#xff0c;它包含了系统的全局环境变量设置。 但是需要注意的是&#xff0c;source /e…

数据运营分析-详解

一、指标与指标体系 指标体系就是业务逻辑的框架,也是思考业务逻辑的第一步 案例: 老板,我负责的用户活跃,主要考察每天启动产品的注册用户数量,整体来看,每月活跃保持7.3%的增长,是因为渠道团队的拉新活动带来很多新增注册用户,占每月活跃用户的40%,新一年会继续沿…