【嵌入式Linux应用开发】网络编程(Socket编程)

一、前言

  • 本文系统使用: Ubuntu 20.04.6 LTS
  • Linux 内核版本为:5.15.0-136-generic
  • 嵌入式Linux学习交流群:1005210698
  • 更多免费资料可加群获取

二、概述

什么是Socket

  • Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发TCP/IP网络上的应用程序。要学Internet上的TCP/IP网络编程,必须理解Socket接口。
  • Socket接口设计者最先是将接口放在Unix操作系统里面的。如果了解Unix系统的输入和输出的话,就很容易了解Socket了。
  • 网络的 Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。
  • 常用的Socket类型有两种:流式Socket (SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。
    • 流式是一种面向连接的Socket,针对于面向连接的TCP服务应用。
    • 数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

TCP/UDP通信交互图

  • 面向连接的 TCP 流模式
    在这里插入图片描述

  • UDP用户数据包模式
    在这里插入图片描述


三、函数介绍

1.Socket建立

  • 为了建立 Socket,程序可以调用Socket 函数,该函数返回一个类似于文件描述符的句柄。

函数原型:

int socket(int domain, int type, int protocol);

参数说明:

  • domain :说明我们网络程序所在的主机采用的通讯协族( AF_UNIXAF_INET 等)
    • AF_UNIX 只能够用于单一的 Unix 系统进程间通信
    • AF_INET 是针对 Internet 的,因而可以允许在远程
  • type :是网络程序所采用的通讯协议( SOCK_STREAM , SOCK_DGRAM 等)
    • SOCK_STREAM 表明我们用的是 TCP 协议,这样会提供按顺序的、可靠、双向、面向连接的比特流.
    • SOCK_DGRAM 表明我们用的是 UDP 协议,这样只会提供定长的、不可靠,无连接的通信
  • protocol:由于指定了 type ,所以这个地方一般只要用 0 来代替就可以了 socket 为网络通讯做基本的准备
  • Socket 描述符是一个指向内部数据结构的指针,它指向描述符表入口。
  • 调用 Socket 函数时,socket 执行体将建立一个 Socket,实际上”建立一个 Socket “意味着为一个 Socket 数据结构分配存储空间
  • Socket 执行体为你管理描述符表。

2.Socket配置

  • 面向连接的 socket 客户端通过调用 connect 函数在 socket 数据结构中保存本地和远端信息
  • 无连接 socket 的客户端和服务端以及面向连接 socket 的服务端通过调用 bind 函数来配置本地信息
  • 自动获得本机IP地址和随机获取一个没有被占用的端口号

函数原型:

 int bind(int sockfd, struct sockaddr *my_addr, int addrlen);

参数说明:

  • sockfd:是由 socket 函数调用返回的文件描述符
  • my_addr:是一个指向包含有本机IP地址及端口号等信息的 sockaddr 类型的指针
  • addrlen:是 sockaddr 结构的长度

涉及结构体:

struct sockaddr{  
	unisgned short  as_family;        /* 地址族, AF_xxx */
	char            sa_data[14];      /* 14 字节的协议地址 */
};
  • 不过由于系统的兼容性,一般不用这个头文件,而使用另外一个结构(struct sockaddr_in) 来代替
struct sockaddr_in{  
	unsigned short          sin_family;    /* 地址族 */
	unsigned short int      sin_port;      /* 端口号 */
	struct in_addr          sin_addr;      /* IP地址 */
	unsigned char           sin_zero[8];   /* 填充0 以保持与struct sockaddr同样大小 */
}

如果使用 Internet 所以 sin_family 一般为 AF_INET

  • sin_addr 设置为 INADDR_ANY 表示可以和任何的主机通信
  • sin_port 是要监听的端口号
  • sin_zero 用来将 sockaddr_in 结构填充到与 struct sockaddr 同样的长度,可以用 bzero()memset() 函数将其置为零
  • 计算机数据存储有两种字节优先顺序:高位字节优先和低位字节优先。
  • Internet 上数据以高位字节优先顺序在网络上传输
  • 在内部是以低位字节优先方式存储数据的机器
  • Internet 上传输数据时就需要进行转换,否则就会出现数据不一致

3.连接建立

  1. 使 socket 处于被动的监听模式,并为该 socket 建立一个输入数据队列,将到达的服务请求保存在此队列中,直到程序处理它们

函数原型:

int listen(int sockfd, int backlog);

参数说明:

  • sockfd:是 bind 后的文件描述符.
  • backlog
    • 指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待 accept() 它们
    • 对队列中等待服务的请求的数目进行了限制,大多数系统缺省值为20
    • 如果一个服务请求到来时,输入队列已满,该 socket 将拒绝连接请求,客户将收到一个出错信息

  1. 建立好输入队列后,服务器睡眠并等待客户的连接请求

函数原型:

int accept(int fd, struct sockaddr *restrict addr, socklen_t *restrict addrlen)

参数说明:

  • sockfd:是 listen 后的文件描述符
  • addr:是一个指向 sockaddr_in 变量的指针,用来存放提出连接请求服务的主机的信息(某台主机从某个端口发出该请求)
  • addrlen:通常为一个指向值为 sizeof(struct sockaddr_in) 的整型指针变量
  • 成功时返回最后的服务器端的文件描述符,此时服务器端可以向该描述符写信息了
  • addraddrlen 是用来给客户端的程序填写的,服务器端只要传递指针就可以了
  • accept 调用时,服务器端的程序会一直阻塞到有一个 客户程序发出了连接
  • accept 函数监视的 socket 收到连接请求时,socket 执行体将建立一个新的 socket,执行体将这个新 socket 和请求连接进程的地址联系起来,收到服务请求的初始 socket 仍可以继续在以前的 socket 上监听,同时可以在新的 socket 描述符上进行数据传输操作

  1. 客户程序与远端服务器建立一个连接

函数原型:

int connect(int sockfd, struct sockaddr *serv_addr, int addrlen);

参数说明:

  • sockfd:是由 socket 函数调用返回的文件描述符
  • serv_addr:储存了服务器端的连接信息,其中 sin_add 是服务端的地址
  • addrlenserv_addr 的长度

[!info]+ 信息

  • 进行客户端程序设计无须调用 bind(),因为这种情况下只需知道目的机器的IP地址,而客户通过哪个端口与服务器建立连接并不需要关心
  • socket 执行体为你的程序自动选择一个未被占用的端口,并通知你的程序数据什么时候到端口
  • connect 函数启动和远端主机的直接连接
  • 只有面向连接的客户程序使用 socket 时才需要将此 socket 与远端主机相连,无连接协议从不建立直接连接
  • 面向连接的服务器也从不启动一个连接,它只是被动的在协议端口监听客户的请求

4.数据传输

  • 发送数据

函数原型:

ssize_t send(int sockfd, const void *buf, size_t len, int flags);

参数说明:

  • sockfd:指定发送端套接字描述符
  • buf:是一个指向要发送数据的指针
  • len:是以字节为单位的数据的长度
  • flags:一般情况下置为 0

函数原型:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
              const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

  • sockfd:指定发送端套接字描述符
  • buf:是一个指向要发送数据的指针
  • len:是以字节为单位的数据的长度
  • flags:一般情况下置为 0
  • dest_addr:表示目地机的IP地址和端口号信息
  • addrlen:常常被赋值为 sizeof (struct sockaddr)

[!help]+ 说明
sendtosend 相似,区别在于 sendto 允许在无连接的套接字上指定一个目标地址


  • 接收数据

函数原型:

 ssize_t recv(int sockfd, void *buf, size_t len, int flags);

参数说明:

  • sockfd:指定接收端套接字描述符
  • buf:指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据
  • len:指明 buf 的长度
  • flags:一般情况下置为 0

函数原型:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  • sockfd:指定接收端套接字描述符
  • buf:指明一个缓冲区,该缓冲区用来存放 recv 函数接收到的数据
  • len:指明 buf 的长度
  • flags:一般情况下置为 0
  • src_addr:是一个 struct sockaddr 类型的变量,该变量保存源机的 IP 地 址及端口号
  • addrlen 常置为 sizeof (struct sockadd)

recvrecvfrom 相似,recvfrom 通常用于无连接套接字,因为此函数可以获得发送者的地址


5.结束传输

  • 当所有的数据操作结束以后,可以调用 close() 函数来释放该 socket,从而停止在该 socket 上的任何数据操作
  • 也可以调用 shutdown() 函数来关闭该 socket
  • 该函数允许你只停止在某个方向上的数据传输,而一个方向上的数据传输继续进行
  • 如可以关闭某 socket 的写操作而允许继续在该 socket 上接受数据,直至读入所有数据

函数原型:

 int shutdown(int sockfd, int how);

参数说明:

  • sockfd:需要关闭的socket的描述符
  • how
    • 0:不允许继续接收数据
    • 1:不允许继续发送数据
    • 2:不允许继续发送和接收数据
    • 均为允许则调用 close ()

6.数据转换

1.字节转换函数
  • 在网络上面有着许多类型的机器,这些机器在表示数据的字节顺序是不同的,比如i386芯片是低字节在内存地址的低端
  • 高字节在高端,而 alpha 芯片却相反,为了统一起来,在 Linux 下面,有专门的字节转换函数
unsigned long  int htonl(unsigned long  int hostlong);
unsigned short int htons(unisgned short int hostshort);
unsigned long  int ntohl(unsigned long  int netlong); 
unsigned short int ntohs(unsigned short int netshort);
  • 在这四个转换函数中,h 代表 host,n 代表 network,s 代表short,l 代表long
2.IP和域名的转换
struct hostent *gethostbyname(const char *hostname);
struct hostent *gethostbyaddr(const char *addr,int len,int type);
  • gethostbyname:可以将机器名(如 linux.yessun.com )转换为一个结构指针,在这个结构里面储存了域名的信息
  • gethostbyaddr:可以将一个32位的IP地址(C0A80001)转换为结构指针
  • hostent 结构体:
struct hostent{
  char *h_name;			/* 主机的官方域名 */
  char **h_aliases;		/* 别名列表(以 NULL 结尾) */
  int h_addrtype;		/* 返回的地址类型,在Internet环境下为AF-INET */
  int h_length;			/* 地址的字节长度 */
  char **h_addr_list;	/* 一个以0结尾的数组,包含该主机的所有地址*/
#ifdef __USE_MISC
# define	h_addr	h_addr_list[0]  /* 兼容旧版本的快捷访问方式 */
#endif
};
3.字符串的IP和32位的IP转换
  • 在网络上面我们用的IP都是数字加点 (192.168.0.1) 构成的,而在 struct in_addr 结构中用的是32位的IP
int inet_aton(const char *cp, struct in_addr *inp);
char *inet_ntoa(struct in_addr in);
  • inet_aton:将点分十进制 IPv4 地址(如 192.168.1.1)转换为网络字节序的二进制格式,并存入 inp 指向的结构体
  • inet_ntoa:将网络字节序的二进制 IPv4 地址转换为点分十进制字符串

四、代码编写

TCP编程

服务端
  • 首先调用 socket 函数创建一个 Socket ,然后调用 bind 函数将其与本机地址以及一个本地端口号绑定,然后调用 listen 在相应的 socket 上监听,当 accpet 接收到一个连接服务请求时,将生成一个新的 socket
  • 服务器显示该客户机的IP地址,并通过新的 socket 向客户端发送字符串"您好,您已接通!"
  • 循环接收来之客户机的消息
/******************************************************************
 * 个人博客:https://blog.csdn.net/2302_80277720?type=blog
 * 嵌入式Linux学习交流群:1005210698
 * 欢迎各位大佬和萌新来加入交流学习
 * Change Logs:
 * Date           Author       Notes
 * 2025-03-02     喝呜昂黄    first version
 ******************************************************************/

#include "stdio.h"
#include "sys/socket.h"
#include <arpa/inet.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

#define SERVER_PORT 8888
#define BACKLOG     10

/* socket
 * bind
 * listen
 * accept
 * send/recv
 */

int main(int argc, char **argv){
    int ret;
    int server_socket, client_socket;               // 监听套接字和客户端连接套接字
    struct sockaddr_in server_addr, client_addr;    // 服务器和客户端地址结构体

    socklen_t sin_size;                             // 客户端地址结构体长度
    int recv_len;                                   // 接收数据的长度

    const char *welcome = "您好,您已接通!";
    uint8_t recv_buf[1000];                         // 接收数据缓冲区

    int client_num = 0;                             // 客户端连接计数器
    pid_t pid;                                      // 进程ID

    // 创建监听套接字
    // AF_INET: IPv4协议族, SOCK_STREAM: TCP流式套接字, 0: 默认协议(TCP)
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if(server_socket == -1){
        printf("socket error\n");
        exit(EXIT_FAILURE);
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;                           // 设置协议族
    server_addr.sin_port = htons(SERVER_PORT);       			// 端口号(主机字节序转网络字节序)
    server_addr.sin_addr.s_addr = INADDR_ANY;                   // 监听所有本地IP地址
    // sin_zero: 填充字段(仅用于兼容旧版sockaddr结构体,必须置零)
    memset(server_addr.sin_zero, 0, 8);

    // 绑定地址到套接字
    ret = bind(server_socket, (const struct sockaddr *)&server_addr, sizeof(server_addr));
    if(ret == -1){
        perror("bind error\n");
        exit(EXIT_FAILURE);
    }

    // 开始监听连接
    ret = listen(server_socket, BACKLOG);
    if(ret == -1){
        perror("listen error\n");
        exit(EXIT_FAILURE);
    }

    // 忽略SIGCHLD信号,系统自动回收僵尸进程
    signal(SIGCHLD, SIG_IGN);

    while (1) {
        // 获得连接请求,并建立连接
        // accept()从等待队列中取出一个连接,返回新的套接字用于通信
        sin_size = sizeof(struct sockaddr_in);
        client_socket = accept(server_socket, (struct sockaddr *)&client_addr , &sin_size);
        if(client_socket == -1){
            perror("accept error");
            continue;
        }

        client_num++;
        printf("接受来自客户端 %d 的连接:%s %d\n", 
            client_num, 
            inet_ntoa(client_addr.sin_addr),        // 转换IP地址为点分十进制字符串
            ntohs(client_addr.sin_port));     // 网络字节序转主机字节序

        // 创建子进程处理客户端
        pid = fork();
        if(pid < 0){
            perror("fork error");
            close(client_socket);
            continue;
        }
        if(pid > 0){
            close(client_socket);
            continue;
        }

        // 子进程不需要监听套接字
        close(server_socket);

        // 向客户端发送欢迎消息
        ret = send(client_socket, welcome, strlen(welcome), 0);
        if(ret == -1){
            perror("send error");
            close(client_socket);
            exit(EXIT_FAILURE);
        }

        // 循环接收客户端消息
        while (1) {
            recv_len = recv(client_socket, recv_buf, sizeof(recv_buf), 0);
            if(recv_len <= 0){
	            printf("客户端 %d(%s:%d)已断开连接\n",
                    client_num,
                    inet_ntoa(client_addr.sin_addr),
                    ntohs(client_addr.sin_port));
                    
                close(client_socket);
                exit(EXIT_FAILURE);
            }
            else {
                recv_buf[recv_len] = '\0';
                printf("来自客户端 %d(%s %d)的发送的信息:%s\n", 
                    client_num, 
                    inet_ntoa(client_addr.sin_addr), 
                    ntohs(client_addr.sin_port),
                    recv_buf);
            }
        }

        close(client_socket);
        exit(0);
    }

    close(server_socket);

    return 0;
}
客户端
  • 首先通过服务器域名获得服务器的IP地址,然后创建一个 socket ,调用 connect 函数与服务器建立连接,连接成功之后接收从服务器发送过来的数据,然后循环等待键盘输入数据,按下回车发送到服务端
/******************************************************************
 * 个人博客:https://blog.csdn.net/2302_80277720?type=blog
 * 嵌入式Linux学习交流群:1005210698
 * 欢迎各位大佬和萌新来加入交流学习
 * Change Logs:
 * Date           Author       Notes
 * 2025-03-02     喝呜昂黄    first version
 ******************************************************************/

#include <arpa/inet.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>

/* socket
 * connect
 * send/recv
 */

// 服务器端口号(需与服务器端一致)
#define SERVER_PORT 8888

int main(int argc, char **argv){
    int client_socket;                  // 客户端套接字描述符
    struct sockaddr_in server_addr;     // 服务器地址结构体

    int ret;
    char send_buf[1000];
    uint8_t recv_buf[1000];

    size_t send_len;
    ssize_t recv_len;

    if(argc != 2){
        printf("Usage: \n");
        printf("%s <server_ip>\n", argv[0]);
        exit(-1);
    }


    // 创建TCP套接字
    // AF_INET: IPv4协议, SOCK_STREAM: TCP流式套接字
    client_socket = socket(AF_INET, SOCK_STREAM, 0);
    if(client_socket == -1){
        perror("Socket error");
        exit(1);
    }

    // 初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;                       // 设置协议族
    ret = inet_aton(argv[1], &server_addr.sin_addr);        // 将点分十进制 IPv4 地址(如 `192.168.1.1`)转换为网络字节序的二进制格式
    if(ret == 0){
        perror("invalid server ip");
        exit(1);
    }
    server_addr.sin_port = htons(SERVER_PORT);           	// 端口号转网络字节序
    memset(server_addr.sin_zero, 0, 8);                     // sin_zero: 填充字段(必须置零,兼容旧版API)

    // 连接到服务器
    ret = connect(client_socket, (const struct sockaddr *)&server_addr, sizeof(server_addr));
    if(ret == -1){
        perror("Connect error");
        exit(1);
    }

    // 接收服务器消息
    recv_len = recv(client_socket, recv_buf, 100, 0);
    if(ret == -1){
        perror("recv error");
        exit(1);
    }
    recv_buf[recv_len] = '\0';
    printf("Received: %s\n", recv_buf);

    //  发送数据到服务器
    while (1) {
        if(fgets(send_buf, sizeof(send_buf), stdin) == NULL){
            printf("输入结束(EOF),退出程序\n");
            break;
        }

        send_len = send(client_socket, send_buf, strlen(send_buf), 0);
        if(send_len <= 0){
            printf("服务器已断开连接\n");
            close(client_socket);
            exit(1);
        }
        
        printf("发送成功\n");
    }

    printf("客户端已退出\n");
    close(client_socket);

    return 0;
}

UDP编写

服务端
/******************************************************************
 * 个人博客:https://blog.csdn.net/2302_80277720?type=blog
 * 嵌入式Linux学习交流群:1005210698
 * 欢迎各位大佬和萌新来加入交流学习
 * Change Logs:
 * Date           Author       Notes
 * 2025-03-02     喝呜昂黄    first version
 ******************************************************************/

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

#define SERVER_PORT 8888

int main(int argc, char **argv){
    int ret;
    int server_socket;                              // 监听套接字套接字
    struct sockaddr_in server_addr, client_addr;    // 服务器和客户端地址结构体

    socklen_t sin_size;                             // 客户端地址结构体长度
    int recv_len;                                   // 接收数据的长度
    uint8_t recv_buf[1000];                         // 接收数据缓冲区

    // 创建监听套接字
    server_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if(server_socket == -1){
        perror("socket error");
        exit(1);
    }

    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(SERVER_PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    // sin_zero: 填充字段(仅用于兼容旧版sockaddr结构体,必须置零)
    memset(server_addr.sin_zero, 0, 8);
    
    // 绑定地址到套接字
    sin_size = sizeof(server_addr);
    ret = bind(server_socket, (const struct sockaddr *)&server_addr, sin_size);
    if(ret == -1){
        perror("bind error");
        exit(1);
    }

    // 循环接收客户端消息
    while (1) {
        recv_len = recvfrom(server_socket, recv_buf,
                        sizeof(recv_buf), 0, 
                        (struct sockaddr *)&client_addr, &sin_size);

        if(recv_len > 0) {
            recv_buf[recv_len] = '\0';
            printf("来自客户端 %s %d)的发送的信息:%s\n", 
                inet_ntoa(client_addr.sin_addr), 
                ntohs(client_addr.sin_port),
                recv_buf);
        }
    }

    close(server_socket);

    return 0;
}
客户端
/******************************************************************
 * 个人博客:https://blog.csdn.net/2302_80277720?type=blog
 * 嵌入式Linux学习交流群:1005210698
 * 欢迎各位大佬和萌新来加入交流学习
 * Change Logs:
 * Date           Author       Notes
 * 2025-03-02     喝呜昂黄    first version
 ******************************************************************/

#include <arpa/inet.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>


// 服务器端口号(需与服务器端一致)
#define SERVER_PORT 8888

int main(int argc, char **argv){
    int client_socket;                  // 客户端套接字描述符
    struct sockaddr_in server_addr;     // 服务器地址结构体

    int ret;
    char send_buf[1000];
    uint8_t recv_buf[1000];

    size_t send_len;
    ssize_t recv_len;
    socklen_t sin_size;

    if(argc != 2){
        printf("Usage: \n");
        printf("%s <server_ip>\n", argv[0]);
        exit(-1);
    }

    // 创建TCP套接字
    // AF_INET: IPv4协议, SOCK_STREAM: TCP流式套接字
    client_socket = socket(AF_INET, SOCK_DGRAM, 0);
    if(client_socket == -1){
        perror("Socket error");
        exit(1);
    }

    // 初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    ret = inet_aton(argv[1], &server_addr.sin_addr);
    if(ret == 0){
        perror("invalid server ip");
        exit(1);
    }
    server_addr.sin_port = htons(SERVER_PORT);
    memset(server_addr.sin_zero, 0, 8); 
    sin_size = sizeof(server_addr);

    //  发送数据到服务器
    while (1) {
        if(fgets(send_buf, sizeof(send_buf), stdin) == NULL){
            printf("输入结束(EOF),退出程序\n");
            break;
        }

        send_len = sendto(client_socket, send_buf,
                          strlen(send_buf), 0,
                          (struct sockaddr *)&server_addr, sin_size);
        if(send_len <= 0){
            printf("服务器已断开连接\n");
            close(client_socket);
            exit(1);
        }
        
        printf("发送成功\n");
    }

    printf("客户端已退出\n");
    close(client_socket);

    return 0;
}

五、参考

本文参考《Linux网络编程入门 (转载)》、《Linux下Socket编程》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

喝呜昂_黄

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

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

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

打赏作者

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

抵扣说明:

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

余额充值