UNIX-Linux环境编程(八):网络通信

本文详细介绍了UNIX/Linux环境中的网络通信,包括ISO/OSI七层网络协议模型、TCP/IP协议族及其与ISO/OSI模型的对比。重点讨论了套接字(Socket)的概念,如接口、模式、绑定和函数,并分别阐述了基于TCP和UDP协议的客户机/服务器模型,提供了编程示例。
摘要由CSDN通过智能技术生成

之前有写过socket网络编程的一些基本概念,最近对一些知识点进行梳理之后打算详细的、有层次地归纳一下这些基础概念。

一、基本概念

1. ISO/OSI七层网络协议模型

ISO/OSI

2. TCP/IP协议族
  1. TCP (Transmission Control Protocol, 传输控制协议) 面向连接的服务。
  2. UDP (User Datagram Protocol, 用户数据报文协议) 面向无连接的服务。
  3. IP (Internet Protocol, 互联网协议) 信息传递机制
    TCP/IP
3. TCP/IP协议与ISO/OSI模型的对比

TCP对比

4. 消息流

消息流

5. 消息包

消息包

6.IP地址
  1. IP地址是Internet中唯一地址标识
    A. 一个IP地址占32位,正在扩充至128位。
    B. 每个Internet包必须带IP地址。
  2. 点分十进制表示法
    0x01020304 -> 1.2.3.4,高数位在左,低数位在右。
  3. IP地址分级
    A级:0 + 7位网络地址 + 24位本地地址
    B级:10 + 14位网络地址 + 16位本地地址
    C级:110 + 21位网络地址 + 8位本地地址
    D级:1110 + 28位多播(Muticast)地址
  4. 子网掩码
    IP地址 & 子网掩码 = 网络地址
    IP地址: 192.168.182.48
    子网掩码:255.255.255.0
    网络地址:192.168.182
    本地地址:48

二、套接字(Socket)

1. 接口

socket接口

2. 模式
  1. 点对点(Peer-to-Peer, P2P):一对一的通信。
  2. 客户机/服务器(Client/Server, C/S):一对多的通信。
3. 绑定

先要有一个套接字描述符,还要有物理通信载体,然后将二者绑定在一起。

4. 函数
  1. 创建套接字
    套接字描述符类似于文件描述符,UNIX把网络当文件看待,
    发送数据即写文件,接收数据即读文件,一切皆文件。

     #include <sys/socket.h>
    
     int socket (int domain, int type, int protocol);
    
     domain   - 域/地址族,取值:
    
     	AF_UNIX/AF_LOCAL/AF_FILE: 本地通信(进程间通信);
     	AF_INET: 基于TCP/IPv4(32位IP地址)的网络通信;
     	AF_INET6: 基于TCP/IPv6(128位IP地址)的网络通信;
     	AF_PACKET: 基于底层包接口的网络通信。
    
     type     - 通信协议,取值:
    
     	SOCK_STREAM: 数据流协议,即TCP协议;
     	SOCK_DGRAM: 数据报协议,即UDP协议。
    
     protocol - 特别通信协议,一般不用,置0即可。
    
     成功返回套接字描述符,失败返回-1。
    
  2. 准备通信地址

     IP地址用于定位主机,端口号用于定位主机上的进程。
     A. 基本地址类型
     struct sockaddr 
     {
     	sa_family_t sa_family;   // 地址族
     	char        sa_data[14]; // 地址值
     };
    
     B. 本地地址类型
     #include <sys/un.h>
     struct sockaddr_un 
     {
     	sa_family_t sun_family; // 地址族
     	char        sun_path[]; // 套接字文件路径
     };
    
     C. 网络地址类型
     #include <netinet/in.h>
     struct sockaddr_in 
     {
     	// 地址族
     	sa_family_t sin_family;
    
     	// 端口号
     	// unsigned short, 0-65535
     	// 逻辑上表示一个参与通信的进程
     	// 使用时需要转成网络字节序
     	// 0-1024端口一般被系统占用
     	// 如:21-FTP、23-Telnet、80-WWW
     	in_port_t sin_port;
    
     	// IP地址
     	struct in_addr sin_addr;
     };
     struct in_addr
     {
     	in_addr_t s_addr;
     };
     typedef uint32_t in_addr_t;
    
    1. 将套接字和通信地址绑定在一起

       #include <sys/socket.h>
       int bind (int sockfd, const struct sockaddr* addr,socklen_t addrlen);
       成功返回0,失败返回-1。
      
    2. 建立连接

       #include <sys/socket.h>
       int connect (int sockfd, const struct sockaddr* addr,socklen_t addrlen);
       成功返回0,失败返回-1。
      
    3. 用读写文件的方式通信:read/write

    4. 关闭套接字:close

    5. 字节序转换

       #include <arpa/inet.h>
      
       // 32位无符号整数,主机字节序 -> 网络字节序
       uint32_t htonl (uint32_t hostlong);
      
       // 16位无符号整数,主机字节序 -> 网络字节序
       uint16_t htons (uint16_t hostshort);
      
       // 32位无符号整数,网络字节序 -> 主机字节序
       uint32_t ntohl (uint32_t netlong);
      
       // 16位无符号整数,网络字节序 -> 主机字节序
       uint16_t ntohs (uint16_t netshort);
      

主机字节序因处理器架构而异,有的采用小端字节序,有的采用大端字节序。网络字节序则固定采用大端字节序。

  1. IP地址转换

     #include <arpa/inet.h>
    
     // 点分十进制字符串 -> 网络字节序32位无符号整数
     in_addr_t inet_addr (const char* cp);
    
     // 点分十进制字符串 -> 网络字节序32位无符号整数
     int inet_aton (const char* cp, struct in_addr* inp);
    
     // 网络字节序32位无符号整数 -> 点分十进制字符串
     char* inet_ntoa (struct in_addr in);
    
5. 编程
  1. 本地通信
    服务器:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并绑定->接收数据->关闭套接字
    客户机:创建套接字(AF_LOCAL)->准备地址(sockaddr_un)并连接->发送数据->关闭套接字
    范例:locsvr.cloccli.c
    locsvr.c
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCK_FILE "/tmp/sock"

int main () 
{
    printf ("服务器:创建本地数据报套接字...\n");

    int sockfd = socket (AF_LOCAL, SOCK_DGRAM, 0);
    if (sockfd == -1) 
    {
        perror ("socket");
        return -1;
    }

    printf ("服务器:准备地址并绑定...\n");

    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy (addr.sun_path, SOCK_FILE);

    if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) 
    {
        perror ("bind");
        return -1;
    }

    printf ("服务器:接收数据...\n");

    for (;;) 
    {
        char buf[1024];

        ssize_t rb = read (sockfd, buf, sizeof (buf));
        if (rb == -1) 
        {
            perror ("read");
            return -1;
        }

        if (! strcmp (buf, "!!"))
           break;

       printf ("< %s\n", buf);
    }

   printf ("服务器:关闭套接字...\n");

    if (close (sockfd) == -1) 
    {
        perror ("close");
        return -1;
    }

    printf ("服务器:删除套接字文件...\n");

    if (unlink (SOCK_FILE) == -1) 
    {
        perror ("unlink");
        return -1;
    }

    printf ("服务器:大功告成!\n");

    return 0;
}

loccli.c

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/un.h>

#define SOCK_FILE "/tmp/sock"

int main () 
{
    printf ("客户机:创建本地数据报套接字...\n");

   int sockfd = socket (AF_LOCAL, SOCK_DGRAM, 0);
    if (sockfd == -1) 
    {
        perror ("socket");
        return -1;
    }

    printf ("客户机:准备地址并连接...\n");

    struct sockaddr_un addr;
    addr.sun_family = AF_LOCAL;
    strcpy (addr.sun_path, SOCK_FILE);
    if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) 
    {
        perror ("connect");
        return -1;
   }

    printf ("客户机:发送数据...\n");

    for (;;) 
    {
       printf ("> ");

        char buf[1024];
        gets (buf);

       if (! strcmp (buf, "!")) break;

        if (write (sockfd, buf, (strlen (buf) + 1) *
           sizeof (buf[0])) == -1) 
        {
           perror ("write");
            return -1;
       }

        if (! strcmp (buf, "!!"))
            break;
    }

    printf ("客户机:关闭套接字...\n");

    if (close (sockfd) == -1) 
    {
        perror ("close");
        return -1;
    }

    printf ("客户机:大功告成!\n");

    return 0;
}

  1. 网络通信
    服务器:创建套接字(AF_INET)->准备地址(sockaddr_in)并绑定->接收数据->关闭套接字
    客户机:创建套接字(AF_INET)->准备地址(sockaddr_in)并连接->发送数据->关闭套接字
    范例:netsvr.cnetcli.c
    netsvr.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main (int argc, char* argv[]) 
{
    if (argc < 2) 
    {
        fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
        return -1;
    }

    printf ("服务器:创建网络数据报套接字...\n");

    int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) 
    {
        perror ("socket");
        return -1;
    }

    printf ("服务器:准备地址并绑定...\n");

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons (atoi (argv[1]));
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) 
    {
       perror ("bind");
        return -1;
    }

    printf ("服务器:接收数据...\n");

    for (;;) 
    {
        char buf[1024];

        ssize_t rb = read (sockfd, buf, sizeof (buf));
        if (rb == -1) 
        {
            perror ("read");
            return -1;
        }

        if (! strcmp (buf, "!!"))
            break;

        printf ("< %s\n", buf);
    }

    printf ("服务器:关闭套接字...\n");

    if (close (sockfd) == -1) 
    {
        perror ("close");
        return -1;
    }

    printf ("服务器:大功告成!\n");

    return 0;
}

netcli.c

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

int main (int argc, char* argv[]) 
{
    if (argc < 3) 
    {
        fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
        return -1;
    }

    printf ("客户机:创建网络数据报套接字...\n");

    int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) 
    {
        perror ("socket");
        return -1;
    }

    printf ("客户机:准备地址并连接...\n");

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons (atoi (argv[2]));
    addr.sin_addr.s_addr = inet_addr (argv[1]);

    if (connect (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) 
    {
        perror ("connect");
        return -1;
    }

    printf ("客户机:发送数据...\n");

    for (;;) 
    {
        printf ("> ");

        char buf[1024];
        gets (buf);

        if (! strcmp (buf, "!")) break;

        if (write (sockfd, buf, (strlen (buf) + 1) *
            sizeof (buf[0])) == -1) 
        {
            perror ("write");
            return -1;
        }

        if (! strcmp (buf, "!!"))
           break;
    }

    printf ("客户机:关闭套接字...\n");

    if (close (sockfd) == -1) 
    {
        perror ("close");
        return -1;
   }

    printf ("客户机:大功告成!\n");

    return 0;
}

三、基于TCP协议的客户机/服务器模型

1. 基本特征
  1. 面向连接。

  2. 可靠,保证数据的完整性和有序性。
    A B C D E F
    基本特征
    每个发送都有应答,若在时间窗口内没有收到A的应答,则从A开始重发。

2. 编程模型

tcp编程模型
三次握手四次挥手

3. 常用函数
  1. listen

     #include <sys/socket.h>
     int listen (int sockfd, int backlog);
    
     将sockfd参数所标识的套接字标记为被动模式,使之可用于接受连接请求。
    
     backlog参数表示未决连接请求队列的最大长度,即最多允许同时有多少个未决连接请求存在。
     若服务器端的未决连接数已达此限,则客户机端的connect()函数将返回-1,且errno为ECONNREFUSED。
    
     成功返回0,失败返回-1。
    

listen
2. accept

int accept (int sockfd, struct sockaddr* addr,socklen_t* addrlen);

从sockfd参数所标识套接字的未决连接请求队列中,提取第一个连接请求。
同时创建一个新的套接字,用于在该连接中通信,返回该套接字的描述符。

addr和addrlen参数用于输出连接请求发起者的地址信息。

成功返回通信套接字描述符,失败返回-1。

accpet
3. recv

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

通过sockfd参数所标识的套接字,
期望接收len个字节到buf所指向的缓冲区中。

成功返回实际接收到的字节数,失败返回-1。
  1. send

     ssize_t send (int sockfd, const void* buf,size_t len, int flags);
    
     通过sockfd参数所标识的套接字,
     从buf所指向的缓冲区中发送len个字节。
    
     成功返回实际被发送的字节数,失败返回-1。
    

范例:tcpsvr.ctcpcli.c
tcpsvr.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

void sigchld (int signum) 
{
    for (;;) 
    {
        pid_t pid = waitpid (-1, 0, WNOHANG);
        if (pid == -1) 
        {
            if (errno != ECHILD) 
            {
                perror ("waitpid");
                exit (-1);
            }

            printf ("服务器:全部子进程都已退出。\n");
            break;
        }

        if (pid)
            printf ("服务器:发现%u子进程退出。\n", pid);
        else 
        {
            printf ("服务器:暂时没有子进程退出。\n");
            break;
        }
    }
}

int main (int argc, char* argv[]) 
{
    if (argc < 2) 
    {
        fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
        return -1;
    }

    if (signal (SIGCHLD, sigchld) == SIG_ERR) 
    {
        perror ("signal");
        return -1;
    }

    printf ("服务器:创建网络数据流套接字...\n");

    int sockfd = socket (AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) 
    {
        perror ("socket");
        return -1;
    }

    printf ("服务器:准备地址并绑定...\n");

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons (atoi (argv[1]));
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) 
    {
        perror ("bind");
        return -1;
    }
    printf ("服务器:监听套接字...\n");

    if (listen (sockfd, 1024) == -1) 
    {
        perror ("listen");
        return -1;
    }

    for (;;) 
    {
        printf ("服务器:等待连接请求...\n");
        struct sockaddr_in addrcli = {};
        socklen_t addrlen = sizeof (addrcli);
        int connfd = accept (sockfd, (struct sockaddr*)&addrcli,&addrlen);
        if (connfd == -1) 
        {
            perror ("accept");
            return -1;
        }

        printf ("服务器:接受来自%s:%u客户机的连接请求。"
            "创建子进程为其提供服务...\n",
            inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));

        pid_t pid = fork ();
        if (pid == -1) 
        {
            perror ("fork");
            return -1;
        }

        if (pid == 0) 
        {
            printf ("%u子进程:为%s:%u客户机提供服务...\n", getpid (),
                inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));

            if (close (sockfd) == -1) 
            {
                perror ("close");
                return -1;
            }

            for (;;) 
            {
                char buf[1024];

                printf ("%u子进程:接收请求...\n", getpid ());

                ssize_t rb = recv (connfd, buf, sizeof (buf), 0);
                if (rb == -1) 
                {
                    perror ("recv");
                    return -1;
                }

                if (rb == 0) 
                {
                    printf ("%u子进程:客户机已关闭连接。\n",
                        getpid ());
                    break;
                }

                printf ("%u子进程:发送响应...\n", getpid ());

                if (send (connfd, buf, rb, 0) == -1) 
                {
                    perror ("send");
                    return -1;
                }
            }

            printf ("%u子进程:关闭连接套接字...\n", getpid ());

            if (close (connfd) == -1) 
            {
                perror ("close");
                return -1;
            }

           printf ("%u子进程:即将退出。\n", getpid ());

            return 0;
        }

        if (close (connfd) == -1) 
        {
            perror ("close");
            return -1;
        }
    }

    printf ("服务器:关闭监听套接字...\n");

    if (close (sockfd) == -1) 
    {
        perror ("close");
       return -1;
    }

    printf ("服务器:大功告成!\n");

    return 0;
}

tcpcli.c

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

int main (int argc, char* argv[]) 
{
    if (argc < 3) 
    {
        fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
       return -1;
    }

    printf ("客户机:创建网络数据流套接字...\n");

    int sockfd = socket (AF_INET, SOCK_STREAM, 0);
    if (sockfd == -1) 
    {
        perror ("socket");
        return -1;
    }

    printf ("客户机:准备地址并连接...\n");

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons (atoi (argv[2]));
    addr.sin_addr.s_addr = inet_addr (argv[1]);

    if (connect (sockfd, (struct sockaddr*)&addr,
        sizeof (addr)) == -1) 
    {
        perror ("connect");
        return -1;
   }

    printf ("客户机:发送请求并接收响应...\n");

    for (;;) 
    {
        printf ("> ");

        char buf[1024];
        gets (buf);
        if (! strcmp (buf, "!")) break;

        if (send (sockfd, buf, (strlen (buf) + 1) *
            sizeof (buf[0]), 0) == -1) 
       {
            perror ("send");
            return -1;
        }

        ssize_t rb = recv (sockfd, buf, sizeof (buf), 0);
        if (rb == -1) 
        {
            perror ("recv");
           return -1;
        }

        if (rb == 0) 
        {
            printf ("客户机:服务器已宕机!\n");
            break;
        }

        printf ("< %s\n", buf);
    }

    printf ("客户机:关闭套接字...\n");

    if (close (sockfd) == -1) 
    {
       perror ("close");
        return -1;
    }

    printf ("客户机:大功告成!\n");

    return 0;
}

四、基于UDP协议的客户机/服务器模型

1. 基本特征
  1. 面向无连接。

  2. 不可靠,不保证数据的完整性和有序性。
    发送

  3. 效率高速度快。

2. 编程模型

udp模型
通信

3. 常用函数
#include <sys/socket.h>

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

通过sockfd参数所标识的套接字,期望接收len个字节到buf所指向的缓冲区中。

若src_addr和addrlen参数不是空指针,则通过这两个参数输出源地址结构及其长度。
注意在这种情况下,addrlen参数的目标应被初始化为,src_addr参数的目标数据结构的大小。

成功返回实际接收到的字节数,失败返回-1。

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

通过sockfd参数所标识的套接字,从buf所指向的缓冲区中发送len个字节。

发送目的的地址结构及其长度,通过dest_addr和addrlen参数输入。

成功返回实际被发送的字节数,失败返回-1。

范例:udpsvr.cudpcli.c
udpsvr.c

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

int main (int argc, char* argv[]) 
{
    if (argc < 2) 
    {
        fprintf (stderr, "用法:%s <端口号>\n", argv[0]);
        return -1;
    }

    printf ("服务器:创建网络数据报套接字...\n");

    int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1) 
    {
        perror ("socket");
        return -1;
    }

    printf ("服务器:准备地址并绑定...\n");

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons (atoi (argv[1]));
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind (sockfd, (struct sockaddr*)&addr, sizeof (addr)) == -1) 
    {
        perror ("bind");
        return -1;
    }

    for (;;) 
    {
        printf ("服务器:接收请求...\n");

        char buf[1024];
        struct sockaddr_in addrcli = {};
        socklen_t addrlen = sizeof (addrcli);

        ssize_t rb = recvfrom (sockfd, buf, sizeof (buf), 0,
            (struct sockaddr*)&addrcli, &addrlen);
        if (rb == -1) 
        {
            perror ("recvfrom");
            return -1;
        }

        printf ("服务器:向%s:%u客户机发送响应...\n",
            inet_ntoa (addrcli.sin_addr), ntohs (addrcli.sin_port));

        if (sendto (sockfd, buf, rb, 0,
            (struct sockaddr*)&addrcli, addrlen) == -1) 
        {
            perror ("sendto");
            return -1;
        }
    }

    printf ("服务器:关闭套接字...\n");

    if (close (sockfd) == -1) 
    {
        perror ("close");
        return -1;
    }

    printf ("服务器:大功告成!\n");

    return 0;
}

udpcli.c

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

int main (int argc, char* argv[])
{
    if (argc < 3)
    {
        fprintf (stderr, "用法:%s <服务器IP地址> <端口号>\n", argv[0]);
        return -1;
    }

    printf ("客户机:创建网络数据报套接字...\n");

    int sockfd = socket (AF_INET, SOCK_DGRAM, 0);
    if (sockfd == -1)
    {
        perror ("socket");
        return -1;
    }

    printf ("客户机:准备地址...\n");

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons (atoi (argv[2]));
    addr.sin_addr.s_addr = inet_addr (argv[1]);

    printf ("客户机:发送请求并接收响应...\n");

    for (;;)
    {
        printf ("%s:%u> ", inet_ntoa (addr.sin_addr),
            ntohs (addr.sin_port));

        char buf[1024];
        gets (buf);

        if (! strcmp (buf, "!")) break;

        if (sendto (sockfd, buf, (strlen (buf) + 1) *
            sizeof (buf[0]), 0, (struct sockaddr*)&addr,
            sizeof (addr)) == -1)
        {
            perror ("send");
            return -1;
        }

        struct sockaddr_in addrsvr = {};
        socklen_t addrlen = sizeof (addrsvr);

        if (recvfrom (sockfd, buf, sizeof (buf), 0,
            (struct sockaddr*)&addrsvr, &addrlen) == -1)
        {
            perror ("recv");
            return -1;
        }

        printf ("%s:%u< %s\n", inet_ntoa (addrsvr.sin_addr),
            ntohs (addrsvr.sin_port), buf);
    }

    printf ("客户机:关闭套接字...\n");

    if (close (sockfd) == -1)
    {
        perror ("close");
        return -1;
    }

    printf ("客户机:大功告成!\n");

    return 0;
}

tcp/udp

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值