22. UDP网络编程

一、什么是UDP协议

  相对于 TCP 协议,UDP 协议则是面向无连接的协议。使用 UDP 协议时,不需要建立连接,只需要知道对象的 IP 地址和端口号,就可以直接发数据包。但是,数据无法保证一定到达。虽然用 UDP 传输数据不可靠,但它的优点是比 TCP 协议的速度快。对于不要求可靠到达的数据而言,就可以使用 UDP 协议。

UDP通信

UDP 每次发送数据的时候,都需要写上接收方的 IP 和 PORT;

二、TCP编程的API

2.1、创建套接字

  我们可以使用 socket() 函数 创建一个套接字。在 TCP 连接中,客户端和服务端都需要创建对应的套接字。

/**
 * @brief 创建一个套接字
 * 
 * @param __domain 指定要创建套接字的通信域
 * @param __type 指定要创建的套接字类型
 * @param __protocol 指定要与socket一起使用的特定协议
 * @return int 成功返回文件描述符,失败返回-1
 */
int socket(int __domain, int __type, int __protocol);

  参数 __domain 用来 指定要创建套接字的通信域。这里我们使用 AF_INET 表示 使用 IPv4 互联网协议,如果要 使用 IPv6 协议,我们可以指定为 AF_INET6

  参数 __type 用来 指定要创建的套接字类型。如果我们要 使用 TCP 协议,可以指定为 SOCK_STREAM。如果我们要 使用 UDP 协议,可以指定为 SOCK_DGRAM

  参数 __protocol 用来 指定要与 socket 一起使用的特定协议。如果我们将指定协议设置为 0,会导致 socket() 函数的使用会自动适用于所请求的 socket 类型的未指定的默认协议。

2.2、绑定地址和端口号

  创建完套接字之后,客户端和服务端都使用 bind() 函数 绑定地址和端口号

/**
 * @brief 绑定地址和端口号
 * 
 * @param __fd 套接字文件描述符
 * @param __addr 指定的地址
 * @param __len __addr指向地址结构的大小(以字节为单位)
 * @return int 成功返回0,失败返回-1
 */
int bind(int __fd, __CONST_SOCKADDR_ARG __addr, socklen_t __len);

  当我们使用 socket() 函数创建套接字时,它存在于一个名称空间(地址族)中,但没有为其分配地址。bind() 函数将有 __addr 指定的地址分配给文件描述符 __fd 所一i你用的套接字。__len 指定了 __addr 指向的地址结构的大小(以字节为单位)。

  在网络编程中,地址族(Address Family)指定了套接字使用网络协议类型以及地址的格式。简而言之,地址族决定了网络通信的范围和方式。每种地址族都支持特定类型的通信协议和地址格式。

2.3、发送数据

  在客户端连接上服务器之后,客户端和服务端都可以使用 sendto() 函数向对方 发送数据

/**
 * @brief 向指定地址发送缓冲区中的数据
 * 
 * @param __fd 套接字文件描述符
 * @param __buf 缓冲区指针
 * @param __n 要发送的字节数
 * @param __flags 通信标志
 * @param __addr 目标地址
 * @param __addr_len 目标地址长度
 * @return ssize_t 发送的消息大小,失败返回-1
 */
ssize_t sendto(int __fd, const void *__buf, size_t __n, int __flags, __CONST_SOCKADDR_ARG __addr, socklen_t __addr_len);

2.4、接收数据

  当一端发送数据之后,另一端可以使用 recvfrom() 函数来 接收数据

/**
 * @brief 将接收到数据存入缓冲区中
 * 
 * @param __fd 套接字文件描述符
 * @param __buf 缓冲区指针
 * @param __n 缓冲区大小
 * @param __flags 通信标志
 * @param __addr 保存发送方地址信息的结构体指针,可以为NULL
 * @param __addr_len 保存发送方地址信息的结构体大小的指针,可以为NULL
 * @return ssize_t 实际收到的消息大小,如果失败返回-1
 */
ssize_t recvfrom (int __fd, void *__restrict __buf, size_t __n, int __flags, __SOCKADDR_ARG __addr, socklen_t *__restrict __addr_len);

2.5、关闭连接

  我们可以使用 close() 函数直接 关闭套接字的所有连接

/**
 * @brief 关闭套接字全部连接
 * 
 * @param __fd 套接字文件描述符
 * @return int 成功返回0,失败返回-1
 */
int close(int __fd);

2.6、网络字节序和主机字节序的转换

  字节序 指的是多字节数据在内存中的存储顺序,主要有两种字节序。

  • 大端字节序(Big Endian):高位字节存储在内存的低地址处,低位字节存储在高位地址处。这种字节序也称为 网络字节序
  • 小端字节序(Little Endian):低位字节存储在内存的低地址处,高位字节存储在高地址处。这种字节序也称为 主机字节序

  我们可以使用 htonl() 函数或 htons() 函数 将主机字节序转换为网络字节序

uint32_t htonl(uint32_t __hostlong);
uint16_t htons(uint16_t __hostshort);

  如果我们想要 从网络字节序转换为主机字节序,可以使用 ntohl() 函数或 htohs() 函数。

uint32_t ntohl(uint32_t __netlong);
uint16_t ntohs(uint16_t __netshort));

  上面的函数只是将字节转换好了,后面我们还需要手动填入 struct in_addr 结构体中。此时我们可以使用 inet_aton() 函数直接将 IPv4 点分十进制表示法的主机地址的字符串转换为二进制形式(以网络字节序),并将其存储在 __inp 指向的结构体中。该函数转换成功返回 1,失败则返回 0。

int inet_aton(const char *__cp, struct in_addr *__inp);

  inet_aton() 函数只能用于 IP 协议,如果我们想要使用其它协议,可以使用 inet_pton() 函数。

/**
 * @brief 将字符串转换为 sockadd_in 结构体格式
 * 
 * @param __af 指定协议类型, AF_INET 表示 IPv4 协议,AF_INET6 表示 IPv6 协议
 * @param __cp 包含IP地址字符串的字符数组,
 *              如果是 IPv4 协议,格式为点分十进制(如 "192.168.1.1"),
 *              如果是 IPv6 协议,格式为冒号分隔的16进制数(如 "2001:0db8:85a3:0000:0000:8a2e:0370:7334")
 * @param __buf 指向一个缓冲区,
 *              如果是 IPv4 协议,该缓冲区应该是一个 struct in_addr 结构体指针,
 *              如果是 IPv6 协议,该缓冲区应该是一个 struct in6_addr 结构体指针
 * @return int 成功转换返回0,输入地址错误返回1,发生错误返回-1
 */
int inet_pton (int __af, const char *__restrict __cp, void *__restrict __buf);

  如果我们想要返过来,可以使用 inet_ntoa() 函数。

char *inet_ntoa (struct in_addr __in);

三、UDP网络编程

3.1、创建UDP服务器

  创建 UDP 服务器的伪代码如下:

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define UDP_SERVER_IP           INADDR_ANY
#define UDP_SERVER_PORT         8000

int main(void)
{
    struct sockaddr_in server_address = {0}, client_address = {0};
    int server_socket_fd = 0, client_socket_fd = 0;
    socklen_t client_address_length = sizeof(client_address);

    char data[1024] = {0};
    ssize_t length = 0;

    // 填写服务端地址
    server_address.sin_family = AF_INET;                        // 使用IPv4协议
    server_address.sin_addr.s_addr = htonl(UDP_SERVER_IP);      // 填写服务端的IP地址为本地的IP地址
    server_address.sin_port = htons(UDP_SERVER_PORT);           // 填写服务端的端口号

    // 创建socket
    server_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    // 绑定地址
    bind(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address));

    while (1)
    {
        // 服务端接收客户端发送的数据
        length = recvfrom(server_socket_fd, data, sizeof(data), 0, (struct sockaddr *)&client_address, &client_address_length );
        // 服务端向客户端返回的数据
        sendto(server_socket_fd, data, strlen(data), 0, (struct sockaddr *)&client_address, client_address_length );
    }

    // 关闭服务端
    close(server_socket_fd);

    return 0;
}

  UDP 和 TCP 服务器之间的一个显著差异是,因为数据报套接字是无连接的,所以就没有为了通信成功而使一个客户端连接到一个独立的套接字 “转换” 的操作。这些服务器仅仅接收消息,并有可能回复数据。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <time.h>
#include <string.h>

#define UDP_SERVER_IP           INADDR_ANY
#define UDP_SERVER_PORT         8000

int main(void)
{
    struct sockaddr_in server_address = {0}, client_address = {0};
    int server_socket_fd = 0;
    socklen_t client_address_length  = sizeof(client_address);

    char data[1024] = {0};
    ssize_t length = 0;

    time_t raw_time = 0;
    struct tm *time_info= NULL;

    // 填写服务端地址
    server_address.sin_family = AF_INET;                        // 使用IPv4协议
    server_address.sin_addr.s_addr = htonl(UDP_SERVER_IP);      // 填写服务端的IP地址为本地的IP地址
    server_address.sin_port = htons(UDP_SERVER_PORT);           // 填写服务端的端口号

    // 创建socket
    if ((server_socket_fd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
    {
        perror("create client socket failed.");
        exit(EXIT_FAILURE);
    }

    // 绑定地址
    if (bind(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
    {
        perror("bind server address failed.");
        exit(EXIT_FAILURE);
    }

    while (1)
    {
        // 服务端接收客户端发送的数据
        length = recvfrom(server_socket_fd, data, sizeof(data), 0, (struct sockaddr *)&client_address, &client_address_length);
         // 在Linux系统中,一旦data收到空,意味着是一种异常的行为:客户端非法断开连接
        if (length == 0)
        {
            printf("client (%s:%d) requested to disconnect.\n",
                    inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
            break;
        }

        data[length] = '\0';
        printf("receiving the data sent by the client (%s:%d): \n", 
                inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
        printf("%s\n", data);

        time(&raw_time);
        time_info = localtime(&raw_time);
        char date[128] = {0};
        sprintf(date, 
                "(%d-%d-%d %d:%d:%d)\n",
                time_info->tm_year + 1900, time_info->tm_mon + 1, time_info->tm_mday, 
                time_info->tm_hour, time_info->tm_min, time_info->tm_sec);
        int date_length = strlen(date);
        for (int i = length; i >= 0; i--)
        {
            data[i + date_length] = data[i];
        }
        memcpy(data, date, strlen(date));

        // 服务端向客户端返回的数据
        sendto(server_socket_fd, data, strlen(data), 0, (struct sockaddr *)&client_address, client_address_length);
    }

    // 关闭服务端
    close(server_socket_fd);

    return 0;
}

  对 socket() 的调用的不同之处仅仅在于,我们现在需要一个 数据报/UDP 套接字类型,但是 bind() 的调用方式与 TCP 服务器版本的相同。因为 UDP 是无连接的,所以这里没有调用 “监听传入的连接”。

3.2、创建UDP客户端

  创建 UDP 服务器的伪代码如下:

#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#define UDP_SERVER_IP           INADDR_ANY
#define UDP_SERVER_PORT         8000

int main(void)
{
    struct sockaddr_in server_address = {0}, client_address = {0};
    int server_socket_fd = 0, client_socket_fd = 0;
    socklen_t server_address_length = sizeof(server_address);

    char data[1024] = {0};
    ssize_t length = 0;

    // 创建socket
    client_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    // 绑定地址
    bind(client_socket_fd, (struct sockaddr *)&client_address, sizeof(client_address));

    while (1)
    {
        // 服务端接收客户端发送的数据
        length = recvfrom(client_socket_fd, data, sizeof(data), 0, (struct sockaddr *)&server_address, &server_address_length);
        // 服务端向客户端返回的数据
        sendto(client_socket_fd, data, strlen(data), 0, (struct sockaddr *)&server_address, server_address_length);
    }

    // 关闭客户端
    close(server_socket_fd);

    return 0;
}

  UDP 客户端循环工作方式几乎和 TCP 客户端一样。唯一的区别是,事先不需要建立与 UDP 服务器的连接,只是简单的发送一条消息并等待服务器的回复。

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

#define UDP_SERVER_IP           INADDR_ANY
#define UDP_SERVER_PORT         8000

int main(void)
{
    struct sockaddr_in server_address = {0}, client_address = {0};
    int server_socket_fd = 0, client_socket_fd = 0;
    socklen_t server_address_length = sizeof(server_address);

    char data[1024] = {0};
    ssize_t length = 0;

    // 填写服务端地址
    server_address.sin_family = AF_INET;                        // 使用IPv4协议
    server_address.sin_addr.s_addr = htonl(UDP_SERVER_IP);      // 填写服务端的IP地址为本地的IP地址
    server_address.sin_port = htons(UDP_SERVER_PORT);           // 填写服务端的端口号

    // 创建socket
    client_socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    // 客户端可以不绑定,系统会自动分配一个空闲的端口给客户端

    while (1)
    {
        memset(data, 0, sizeof(data));
        printf("please enter the data to be send.\n");
        fgets(data, sizeof(data), stdin);
        if (data == NULL || data == "")
        {
            break;
        }
        if (data[0] == '\n')
        {
            continue;
        }

        // 客户端向服务端发送数据
        sendto(client_socket_fd, data, strlen(data), 0, (struct sockaddr *)&server_address, server_address_length);

        memset(data, 0, sizeof(data));
        // 客户端接收服务端返回的数据
        recvfrom(client_socket_fd, data, sizeof(data), 0, NULL, NULL);
        printf("received data returned by the server (%s:%d):\n",
                inet_ntoa(server_address.sin_addr), ntohs(server_address.sin_port));
        printf("%s\n", data);
    }

    // 关闭客户端
    close(client_socket_fd);

    return 0;
}

3.3、执行UDP服务器和客户端

  如果先运行客户端,那么将无法进行任何连接,因为没有服务器等待接受请求。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动的等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。换句话说,首先启动服务器(在任何客户端试图连接之前)。

  我们在终端中输入 gcc 命令编译文件,生成对应的可执行程序。

gcc udp_server.c -o udp_server
gcc udp_client.c -o udp_client

  然后,我们在新建一个终端,在第一个终端中先运行服务端的可执行程序。

./udp_server

  然后,我们在第二个终端中先运行客户端的可执行程序。

./udp_client
posted @   星光映梦  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
历史上的今天:
2024-02-20 08. µCOS-Ⅲ的内嵌消息队列和信号量
点击右上角即可分享
微信分享提示