UDP

1. UDP 概述

UDP 网络通信协议特色

  • 面向无连接
  • 数据传递速度较快
  • 数据传递不安全,无法保证数据传递的完整性。

网络通信和本地主机在内存【字节序】不同,主机采用的都是【小端字节序】,网络数据传递过程中采用的数据字节序为【大端字节序】

利用相关函数将本地的小端字节序数据,转换网络通信使用的大端字节序数据内容。

#include <stdio.h>

// 当前 Data 是共用体,目前情况下,占用内存字节数为 4 个字节
union Data
{
    int val;
    char arr[4];
};

int main(int argc, char const *argv[])
{
    union Data data;

    // 当前给予共用体 Data 中 val 赋值数据为 0x12345678
    // 0x12345678 为十六进制数据,占用内存 4 个字节。
    data.val = 0x12345678;  
  
    // 小端字节序数据存储方式
    printf("data.arr[0] = 0x%02x\n", data.arr[0]); // 0x78
    printf("data.arr[1] = 0x%02x\n", data.arr[1]); // 0x56
    printf("data.arr[2] = 0x%02x\n", data.arr[2]); // 0x34
    printf("data.arr[3] = 0x%02x\n", data.arr[3]); // 0x12

    return 0;
}

2. 本机数据和网络数据转换相关函数

2.1.1 htonl 函数

函数文档

#include <arpa/inet.h>

uint32_t htonl(uint32_t host_int_32);
  • 函数功能
    • 将 32 位主机字节序转换为网络字节序 Host to Network Long
  • 形式参数列表
    • uint32_t host_int_32 : 无符号 int 类型数据 unsigned int
  • 返回值类型
    • 返回值类型 uint32_t ,得到当前网络字节序数据内容。
2.1.2 htons 函数

函数文档

#include <arpa/inet.h>

uint16_t htons(uint16_t host_int_16);
  • 函数功能
    • 将 16 位主机字节序转换为网络字节序 Host to Network short
  • 形式参数列表
    • uint16_t host_int_16 : 要求提供的数据是一个 16 位无符号整数类型,对应 unsigned short
  • 返回值类型
    • 返回值类型 uint16_t ,得到当前网络字节序数据内容。
2.1.3 ntohl 函数

函数文档

#include <arpa/inet.h>

uint32_t ntohl(uint32_t net_int_32);
  • 函数功能
    • 将 32 位网络字节序转换为本机字节序 Network to Host Long
  • 形式参数列表
    • uint32_t net_int_32: 无符号 int 类型数据 unsigned int 网络字节序数据
  • 返回值类型
    • 返回值类型 uint32_t ,得到当前本机字节序数据内容。
2.1.4 ntohs 函数

函数文档

#include <arpa/inet.h>

uint16_t ntohs(uint16_t net_int_16);
  • 函数功能
    • 将 16 位网络字节序转换为本机字节序 Network to Host short
  • 形式参数列表
    • uint16_t net_int_16: 要求提供的数据是一个 16 位无符号整数类型,对应 unsigned short
  • 返回值类型
    • 返回值类型 uint16_t ,得到当前本机字节序数据内容。
#include <stdio.h>
#include <arpa/inet.h>

int main(int argc, char const *argv[])
{
    // 32 位数据
    uint32_t data_32 = 0x12345678;
    uint32_t net_data_32 = 0;

    // 16 位数据
    uint16_t data_16 = 0xABCD;
    uint16_t net_data_16 = 0;

    /*
    uint32_t htonl(uint32_t host_int_32);
    uint32_t ntohl(uint32_t net_int_32);
    */

    net_data_32 = htonl(data_32);
    printf("data_32 : %#x\n", data_32);
    printf("net_data_32 : %#x\n", net_data_32);
    printf("ntohl(net_data_32) : %#x\n", ntohl(net_data_32));

    printf("-----------------------------\n");

    /*
    uint16_t htons(uint16_t host_int_16);
    uint16_t ntohs(uint16_t net_int_16);
    */

    net_data_16 = htons(data_16);
    printf("data_16 : %#x\n", data_16);
    printf("net_data_16 : %#x\n", net_data_16);
    printf("ntohs(net_data_16) : %#x\n", ntohs(net_data_16));

    return 0;
}

3. IP 地址转换函数

3.1 inet_pton

函数文档

#include <arpa/inet.h>

int inet_pton(int family, const char *src, void *addrptr);
  • 函数功能:
    • 将【点分十进制】字符串,例如 "192.168.13.20" 转换为 32位整数 IP 描述形式
  • 函数参数
    • int family : 协议族,当前函数支持 IPv4 和 IPv6,协议族有 AF_INET, AF_INET6
    • const char *src : 提供给当前函数【点分十进制】字符串,例如 `"192.168.13.20"
    • void *addrptr : 目前十进制存储当前 IP 地址数据的变量地址
  • 返回值
    • 若转换成功,返回 1。
    • 若输入的地址族不支持,返回 0。
    • 若输入的字符串不是有效的 IP 地址格式,返回 -1,并设置 errnoEINVAL
3.2 inet_ntop

函数文档

#include <arpa/inet.h>

const char *inet_ntop(int family, const void *addrstr, char *str_ip_addr, 
	socklen_t size);
  • 函数功能:
    • 将 32 位整数 IP 地址,按照协议族要求,转换为目标 IP 【点分十进制】地址,支持 IPv4 和 IPv6
  • 函数参数
    • int family : 协议族,当前函数支持 IPv4 和 IPv6,协议族有 AF_INET, AF_INET6
    • const void *addrstr : 32位整数 IP 变量首地址
    • char *str_ip_addr : 存储 IP 地址的 char 类型缓冲空间, 最小要求 16 个字节
    • socklen_t size : unsigned int 对应 str_ip_addr 缓冲区字节长度
  • 返回值
    • 若转换成功,返回对应IP 【点分十进制】地址
    • 失败返回 NULL

tips:

  • "192.168.13.20" ==> int 32 位数据 ==> "192.168.13.20"
  • 字符数组要求最小内存空间需求多少? 16 = 3(每一 IP 位 3 个字符) * 4(位) + 3(点分隔符) + 1(\0)
#include <stdio.h>
#include <arpa/inet.h>

#define ADDR_BUFFER_SIZE 16

int main(int argc, char const *argv[])
{
    // 点分十进制 IP 字符串地址
    char *ip_addr = "192.168.13.20";
    // 用于存储网络端所需的 32位 整数 IP 地址数据
    int net_addr = 0;

    inet_pton(
        AF_INET,  // IPv4 协议
        ip_addr,  // 点分十进制 IP 字符串地址
        &net_addr // 存储网络所需 32位 整数 IP 地址变量地址
    );

    printf("ip_addr : %s\n", ip_addr);
    printf("net_addr : %d\n", net_addr);
    /*
    ip_addr  ==> "192.168.13.20"
        对应的二进制位数据内容为
            1100 0000
            1010 1000
            0000 1101
            0001 0100

    net_addr ==> 336439488 32位 十进制
         对应的二进制位数据内容为
            0001 0100
            0000 1101
            1010 1000
            1100 0000

    将本机 IP 点分十进制地址【小端字节序】,转换为 32 位 int 类型网络所需的
    大端字节序数据.
    */

    char addr_buffer[ADDR_BUFFER_SIZE] = "";

    inet_ntop(
        AF_INET,         // 协议要求为 IPv4 协议
        &net_addr,       // 网络 32位 int IP 地址变量
        addr_buffer,     // 存储 点分十进制 IP 字符串缓冲区
        ADDR_BUFFER_SIZE // 缓冲区大写
    );

    printf("addr_buffer : %s\n", addr_buffer);
    printf("inet_ntop : %s\n", inet_ntop(
                                     AF_INET,         // 协议要求为 IPv4 协议
                                     &net_addr,       // 网络 32位 int IP 地址变量
                                     addr_buffer,     // 存储 点分十进制 IP 字符串缓冲区
                                     ADDR_BUFFER_SIZE // 缓冲区大小
                                     ));

    printf("addr_buffer : %p\n", addr_buffer);
    printf("inet_ntop : %p\n", inet_ntop(
                                     AF_INET,         // 协议要求为 IPv4 协议
                                     &net_addr,       // 网络 32位 int IP 地址变量
                                     addr_buffer,     // 存储 点分十进制 IP 字符串缓冲区
                                     ADDR_BUFFER_SIZE // 缓冲区大小
                                     ));
    return 0;
}

4. socket

4.1 概述

socket 是一个网络通信数据【管道】,要求通信的主机必须支持 socket 协议,才可以通过 socket 完成两个或者多个主机之间的数据通信。

socket 中文称之为 套接字。socket 需要占用进程的【文件描述符 fd】,同时也支持使用系统调用(System Calls)的 write read close 相关函数。

4.2 socket 创建函数

函数文档

#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>

int socket(int family, int type, int protocol);
  • 函数功能:
    • 创建一个 socket 网络通信管道,得到对应的 socket 文件描述符(fd)
  • 函数参数:
    • int family : 协议族,包括 IPv4 AF_INET, IPv6 AF_INET6
    • int type: socket 类型,
      • 满足 TCP 要求的 socket SOCK_STREAM
      • 满足 UDP 要求 的 socket SOCK_DGRAM
      • 原始 socket 套接字 SOCK_RAW
    • int protocol
      • 协议类型 0,自动选择默认协议
      • IPPROTO_TCP 要求是 TCP 协议
      • IPPROTO_UDP 要求为 UDP 协议
  • 返回类型
    • 创建成功,得到当前 Socket 对应的文件描述符
    • 创建失败返回 -1
#include <stdio.h>
#include <arpa/inet.h>
#include <sys/types.h>          /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>

// int socket(int family, int type, int protocol);
int main(int argc, char const *argv[])
{
    int fd = socket(AF_INET, SOCK_DGRAM, 0);

    if (fd < 0)
    {
        perror("Socket Failed!");
    }
    else
    {
        printf("scoket fd : %d\n", fd);
    }

    close(fd);
  
    return 0;
}

4.3 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);
  • 函数功能:
    • 将用户提供的 len 长度的 buf 数据,利用当 sockfd 对应的 socket 网络通信管道,发送到目标 sockaddr 结构体对应的主机地址,同时当前函数完成 UDP 协议要求的数据打包。
  • 函数参数:
    • sockfd:表示套接字描述符,是通过 socket 函数创建的套接字的标识符。它指定了要使用哪个套接字来发送数据。
    • buf:指向要发送的数据的缓冲区的指针。可以是任意类型的数据,如字符串、结构体等。
    • len:指定要发送的数据的长度(以字节为单位)。
    • flags:用于指定发送操作的一些标志。通常可以将其设置为 0,表示使用默认行为。
    • dest_addr:指向目标地址的 struct sockaddr 结构体指针。对于 IPv4,通常使用 struct sockaddr_in;对于 IPv6,使用 struct sockaddr_in6。该结构体包含了目标主机的 IP 地址和端口号等信息。
    • addrlen:表示 dest_addr 结构体的长度,以字节为单位。可以使用 sizeof 运算符来获取该结构体的大小。
  • 返回类型
    • ssize_t 有符号 int 类型数据,发送成功,返回发送数据的字节数
    • 失败返回 -1

补充说明

#include <sys/socket.h>

struct sockaddr {
    sa_family_t sa_family;  /* 地址族,如 AF_INET(IPv4)、AF_INET6(IPv6)等 */
    char        sa_data[14]; /* 具体的地址数据 */
};
#include <netinet/in.h>

struct sockaddr_in {
    sa_family_t    sin_family; /* 地址族,必须为 AF_INET */
    in_port_t      sin_port;   /* 端口号,网络字节序 */
    struct in_addr sin_addr;   /* IPv4 地址 */
    char           sin_zero[8]; /* 填充字节,使 struct sockaddr_in 和 struct sockaddr 大小相同 */
};

struct in_addr {
    in_addr_t s_addr; /* 32 位 IPv4 地址,网络字节序 */
};

函数中所需参数存在 const struct sockaddr *dest_addr, 用于描述当前目标 UDP 接收端的相关网络数据内容,实际上提供给当前参数的的是 sockaddr_in 结构体数据,内容包括【协议族,端口号,IP地址】,IP 地址要求是网络字节序 ,需要使用 htonl

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <arpa/inet.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>

int main(int argc, char const *argv[])
{
    struct sockaddr_in dst_addr;
    int fd = -1;
    char *str = "Hello World!";

    // 1. 创建 socket
    fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == fd)
    {
        perror("socket failed!");
    }

    // 2. 【重点】初始化 sockaddr_in 结构体数据
    /*
    struct sockaddr_in {
        sa_family_t    sin_family; 地址族,必须为 AF_INET
        in_port_t      sin_port;   端口号 short,网络字节序
        struct in_addr sin_addr;   IPv4 地址 int ,网络字节序
    };
    */
    dst_addr.sin_family = AF_INET;   // 设置为 IPv4 IP地址协议
    dst_addr.sin_port = htons(9000); // 设置为 IPv4 IP地址协议
    // 【重点】将点分十进制 IP 地址数据,转换为网络字节序所需 32位 IP 地址数据
    inet_pton(AF_INET, "192.168.13.20", &(dst_addr.sin_addr));

    // 3 sendto 操作
    int ret = sendto(fd,
                     str,
                     strlen(str) + 1,
                     0,
                     (struct sockaddr *)&dst_addr,
                     sizeof(dst_addr));

    if (ret > 0)
    {
        printf("send bytes : %d\n", ret);
    }

    // 4. 【重点】关闭 socket 资源
    close(fd);
    return 0;
}
4.4 bind 函数

函数文档

#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • 函数功能:
    • 当前进程根据 socket 和 网络连接对应的相关信息结构体 绑定指定的 IP 和 端口号,可以从对应的 IP 和 端口号中获取数据
  • 函数参数
    • int sockfd : 打开 socket 对应的文件描述符
    • const struct sockaddr *addr : 网络 IP 信息结构体数据,主要包括协议族,IP地址和端口号,数据要求网络字节序/大端字节序
    • socklen_t addrlen : 对应网络 IP 信息结构体数据字节个数
  • 返回值
    • 绑定成功 返回 0
    • 绑定失败 返回 -1
4.5 recvfrom 函数

函数文档

#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
              struct sockaddr *src_addr, socklen_t *addrlen);
  • 函数功能
    • 从 UDP 面向无连接的 socket 中获取数据,要求必须使用 bind 函数执行之后,才可以使用 recvfrom
  • 函数参数:
    • int sockfd : 满足 UDP 条件的 socket 对应的文件描述
    • void *buf : 接受数据的缓冲区空间
    • size_t len : 缓冲区字节数
    • int flags : 标志位,通常为 0
    • struct sockaddr *src_addr : 保存发送者对应 IP 地址信息的 struct sockaddr 结构体
    • socklen_t *addrlen : struct sockaddr 结构体字节数
  • 返回值:
    • 成功时:返回实际接收到的数据字节数。如果返回值为 0,表示连接正常关闭(对于 TCP 套接字)。
    • 失败时:返回 -1,并设置 errno 来指示具体的错误类型。常见的错误包括:
      • EAGAINEWOULDBLOCK:在非阻塞模式下,没有数据可用。
      • ECONNRESET:连接被对方重置。
      • EINTR:系统调用被信号中断。