计算机网络—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! 

                                       

                                                                        给个三连再走嘛~  

  • 98
    点赞
  • 102
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 115
    评论
### 回答1: 使用Python设计UDP协议需要以下几个步骤: 1. 导入socket模块,创建socket对象 ``` import socket udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ``` 其中,`AF_INET`表示使用IPv4协议,`SOCK_DGRAM`表示使用UDP协议。 2. 绑定IP地址和端口号 ``` udp_socket.bind(('127.0.0.1', 8888)) ``` 这里将IP地址设置为本地回环地址`127.0.0.1`,端口号设置为`8888`。 3. 发送数据 ``` udp_socket.sendto(data, (dest_ip, dest_port)) ``` 其中,`data`表示要发送的数据,`(dest_ip, dest_port)`表示目标IP地址和端口号。 4. 接收数据 ``` recv_data, (src_ip, src_port) = udp_socket.recvfrom(1024) ``` 其中,`recv_data`表示接收到的数据,`(src_ip, src_port)`表示发送方的IP地址和端口号。 完整的UDP通信示例: ``` import socket udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_socket.bind(('127.0.0.1', 8888)) data = 'hello, world!'.encode('utf-8') dest_ip = '127.0.0.1' dest_port = 8889 udp_socket.sendto(data, (dest_ip, dest_port)) recv_data, (src_ip, src_port) = udp_socket.recvfrom(1024) print('received from {}:{}'.format(src_ip, src_port)) print(recv_data.decode('utf-8')) udp_socket.close() ``` ### 回答2: 使用Python设计UDP协议可以通过socket模块实现。首先,我们需要创建一个UDP套接字,并指定IP地址和端口号。 ```python import socket UDP_IP = "127.0.0.1" # 设置本地IP地址 UDP_PORT = 5005 # 设置本地端口号 # 创建UDP套接字 sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # 绑定IP地址和端口号 sock.bind((UDP_IP, UDP_PORT)) ``` 然后,我们可以使用`sendto`函数发送数据报文,使用`recvfrom`函数接收数据报文。 ```python # 发送数据报文 message = "Hello, UDP!" sock.sendto(message.encode(), (UDP_IP, UDP_PORT)) # 接收数据报文 data, addr = sock.recvfrom(1024) print("Received message:", data.decode()) ``` 在发送数据报文时,需要将字符串编码为字节流;而在接收数据报文时,需要将字节流解码为字符串。 最后,我们可以关闭UDP套接字。 ```python # 关闭UDP套接字 sock.close() ``` 这样,我们就使用Python设计了一个基本的UDP协议。使用UDP协议进行通信时,需要注意数据报文的大小限制和丢包的可能性。 ### 回答3: Python提供了socket模块来实现基于UDP协议网络通信。下面是使用Python设计UDP协议的基本过程: 1. 导入socket模块: ``` python import socket ``` 2. 创建UDP套接字: ``` python udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) ``` 其中,`AF_INET`表示使用IPv4地址族,`SOCK_DGRAM`表示使用数据报套接字,即UDP。 3. 绑定套接字到指定的IP地址和端口: ``` python udp_socket.bind(('127.0.0.1', 8888)) ``` 这里将UDP套接字绑定到本地IP地址`127.0.0.1`和端口`8888`。 4. 接收数据: ``` python data, addr = udp_socket.recvfrom(1024) ``` `recvfrom`方法用于接收数据,返回接收到的数据以及发送方的地址。 5. 发送数据: ``` python udp_socket.sendto(data, ('127.0.0.1', 9999)) ``` `sendto`方法用于发送数据,第一个参数为要发送的数据,第二个参数为接收方的地址。 6. 关闭套接字: ``` python udp_socket.close() ``` 使用完套接字后需要关闭释放。 以上就是使用Python设计UDP协议的基本过程。通过创建UDP套接字、绑定地址和端口、接收和发送数据,可以实现基于UDP协议网络通信。
评论 115
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

慕斯( ˘▽˘)っ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值