socket编程UDP程序

目录

一.使用函数详解

1.socket函数

2.bind函数 

3.IP地址转化函数 

4. recvfrom函数

5.sendto函数 

二.测试代码

1.本地环回测试

2.绑定INADDR_ANY的回复服务器

3.简易xshell


一.使用函数详解

1.socket函数

头文件:  #include <sys/types.h>  ,  #include <sys/socket.h>


函数:  int socket(int domain, int type, int protocol);

参数:
①domain:

  •  AF_INET 这是大多数用来产生socket的协议,使用TCP或UDP来传输,用IPv4的地址
  •  AF_INET6 与上面类似,不过是来用IPv6的地址
  •  AF_UNIX 本地协议,使用在Unix和Linux系统上,一般都是当客户端和服务器在同一台及其上的时候使用

②type:

  •  SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接. 这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
  •  SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的,使用UDP来进行它的连接。

③protocol:   传0 表示使用默认协议。


返回值:  成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno

        

socket接口底层做了什么

创建套接字本质就是打开了一个文件

创建套接字的过程其实就是在底层创建:

  1. 通信需要的网卡相关的资源
  2. 需要和文件系统挂钩的相关文件系统资源

                 

①进程调用socket函数前的结构

②调用后的结构 

                         

③其中每一个struct file结构体中包含的就是对应打开文件各种信息,比如文件的属性信息、操作方法以及文件缓冲区等。其中文件对应的属性在内核当中是由struct inode结构体来维护的,而文件对应的操作方法实际就是一堆的函数指针(比如read*和write*)在内核当中就是由struct file_operations结构体来维护的。而文件缓冲区对于打开的普通文件来说对应的一般是磁盘,但对于现在打开的“网络文件”来说,这里的文件缓冲区对应的就是网卡。

  •  对于一般的普通文件来说,当用户通过文件描述符将数据写到文件缓冲区,然后再把数据刷到磁盘上就完成了数据的写入操作。而对于现在socket函数打开的“网络文件”来说,当用户将数据写到文件缓冲区后,操作系统会定期将数据刷到网卡里面,而网卡则是负责数据发送的,因此数据最终就发送到了网络当中。
     

                         

2.bind函数 

头文件: #include <sys/types.h>    #include <sys/socket.h>

函数:     int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • ①sockfd:socket文件描述符,就是我们创建套接字时获取到的文件描述符。
  • ②addr:网络相关的属性信息,包括协议家族、IP地址、端口号等
  • ③addrlen:传入的addr结构体的长度 , sizeof(addr)长度

返回值:成功返回0,失败返回-1, 设置errno

补充:

  • 服务器程序所监听的网络地址和端口号通常是固定不变的,客户端程序得知服务器程序的地址和端口号后就可以向服务器发起连接,因此服务器需要调用bind绑定一个固定的网络地址和端口号。
  • bind()的作用是将参数sockfd和addr绑定在一起,使sockfd这个用于网络通讯的文件描述符监听addr所描述的地址和端口号。前面讲过,struct sockaddr *是一个通用指针类型,addr参数实际上可以接受多种协议的sockaddr结构体,而它们的长度各不相同,所以需要第三个参数addrlen指定结构体的长度。

                 

(1)struct sockaddr_in 结构体

struct sockaddr_in当中的成员如下: 

  • sin_family:表示协议家族。
  • sin_port:表示端口号,是一个16位的整数。
  • sin_addr:表示IP地址,是一个32位的整数。

                

(2)如何理解绑定?

  • 所谓的绑定本质上是服务器为了让自己的文件信息和网络信息关联起来 
  • 在进行绑定的时候需要将IP地址和端口号告诉对应的网络文件,此时就可以改变网络文件当中文件操作函数的指向,将对应的操作函数改为对应网卡的操作方法,此时读数据和写数据对应的操作对象就是网卡了,所以绑定实际上就是将文件和网络关联起来

                 

(3)整数IP vs 字符串IP

  • 在计算机中网络通信时,我们尽可能传输少的数据但是能够说明问题。
  • IP地址实际可以划分为四个区域,其中每一个区域的取值都是0~255,而这个范围的数字只需要用8个比特位就能表示,因此我们实际只需要32个比特位就能够表示一个IP地址。其中这个32位的整数的每一个字节对应的就是IP地址中的某个区域,我们将IP地址的这种表示方法称之为整数IP,此时表示一个IP地址只需要4个字节。
  • 因为采用整数IP的方案表示一个IP地址只需要4个字节,并且在网络通信也能表示同样的含义,因此在网络通信时就没有用字符串IP而用的是整数IP,因为这样能够减少网络通信时数据的传送。

                 

(4)字符串IP和整数IP相互转换的方式

由于联合体的空间是成员共享的因此我们设置IP和读取IP的方式如下:

  • 当我们想以整数IP的形式设置IP时,直接将其赋值给联合体的第一个成员就行了。
  • 当我们想以字符串IP的形式设置IP时,先将字符串分成对应的四部分,然后将每部分转换成对应的二进制序列依次设置到联合体中第二个成员当中的p1、p2、p3和p4.
  • 当我们想取出整数IP时,直接读取联合体的第一个成员就行了。
  • 当我们想取出字符串IP时,依次获取联合体中第二个成员当中的p1、p2、p3和p4,然后将每一部分转换成字符串后拼接到一起

注意: 在操作系统内部实际用的就是位段和枚举,来完成字符串IP和整数IP之间的相互转换的
 

                                 

3.IP地址转化函数 

(1)inet_addr函数

  • 实际在进行字符串IP和整数IP的转换时,我们不需要自己编写转换逻辑,系统已经为我们提供了相应的转换函数,我们直接调用即可。
  • 函数原型  : in_addr_t   inet_addr(const char *cp);
  • 我们只需传入待转换的字符串IP,该函数返回的就是转换后的整数IP。除此之外,inet_aton函数也可以将字符串IP转换成整数IP,不过该函数使用起来没有inet_addr简单。

(2)inet_ntoa函数

  • 将整数IP转换成字符串IP的函数叫做inet_ntoa
  • 函数原型:   char *   inet_ntoa(struct in_addr in);
  • 需要注意的是,传入inet_ntoa函数的参数类型是in_addr,因此我们在传参时不需要选中in_addr结构当中的32位的成员传入,直接传入in_addr结构体即可。

4. recvfrom函数

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

参数:

  • sockfd:对应操作的文件描述符。表示从该文件描述符索引的文件当中读取数据。
  • buf:读取数据的存放位置。
  • len:期望读取数据的字节数。
  • flags:读取的方式。一般设置为0,表示阻塞读取。
  • src_addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等(谁发给你的)
  • addrlen:调用时传入期望读取的src_addr结构体的长度,返回时代表实际读取到的src_addr结构体的长度,这是一个输入输出型参数。

返回值说明:

读取成功返回实际读取到的字节数,读取失败返回-1,同时错误码会被设置。


注意:

  • 由于UDP是不面向连接的,因此我们除了获取到数据以外还需要获取到对端网络相关的属性信息,包括IP地址和端口号等。
  • 在调用recvfrom读取数据时,必须将addrlen设置为你要读取的结构体对应的大小。
  • 由于recvfrom函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转。

如何甄别你是应用层的一个攻击程序?

服务器能拿到客户端的ip , 对ip进行统计,如果次数大于某一特定值,可能是攻击程序。

 

                

5.sendto函数 

函数:     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:传入dest_addr结构体的长度。

返回值说明:

写入成功返回实际写入的字节数,写入失败返回-1,同时错误码会被设置。


注意:

  • 由于UDP不是面向连接的,因此除了传入待发送的数据以外还需要指明对端网络相关的信息,包括IP地址和端口号等。
  • 由于sendto函数提供的参数也是struct sockaddr*类型的,因此我们在传入结构体地址时需要将struct sockaddr_in*类型进行强转

                

二.测试代码

1.本地环回测试

(1)代码

①Makefile ,一次生成两个可执行程序

CC=g++

.PHONY:all
all:server client

server:udp_server.cc 
		$(CC) -o $@ $^ -std=c++11

client:udp_client.cc
		$(CC) -o $@ $^ -std=c++11

.PHONY:clean
clean:
		rm -f server client

                 

②udp_server.hpp

#pragma once 

#include<iostream>
#include<string>
#include<sys/socket.h>
#include<cstring>
#include<sys/types.h>
#include<arpa/inet.h>

#define DEFAULT 8080
#define SIZE 128

class UdpServer{
  private:
    std::string ip;
    int port;
    int sockfd;

  public:
    UdpServer(std::string _ip,int _port = DEFAULT):ip(_ip),port(_port)
    {}

    ~UdpServer()
    {
    }

  public:
    bool InitUdpServer()
    {
      sockfd = socket(AF_INET,SOCK_DGRAM,0);
      if(sockfd < 0){
        std::cerr << "socket error" << std::endl;
        return false;
      }

      std::cout << "socket create success, sockfd: " << sockfd << std::endl;

      struct sockaddr_in local; //创建变量初始化
      memset(&local , '\0' ,sizeof(local));

      local.sin_family = AF_INET;
      local.sin_port = htons(port); //主机序列转网络序列16为端口号
      local.sin_addr.s_addr = inet_addr(ip.c_str()); //将点分十进制的IP转化为 整数ip

      if(bind(sockfd,(struct sockaddr*)&local ,sizeof(local)) < 0){
        std::cerr << "bind error" << std::endl;
        return false;
      }
      std::cout << "bind success " << std::endl;

      return true;
    }

    void Start()
    {
      char buffer[SIZE]; 
      for(;;){
        struct sockaddr_in peer; //哪一端发送的数据
        socklen_t len = sizeof(peer);
        ssize_t size = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);

        if(size > 0){
          buffer[size] = 0 ;
          int _port = ntohs(peer.sin_port);
          std::string _ip = inet_ntoa(peer.sin_addr);
          std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
        }
        else{
          std::cerr << "recvfrom error!" << std::endl;
        }
      }
    }

};

                         

③udp_server.cc

#include "udp_server.hpp"


int main(int argc , char* argv[])
{
  if(argc != 2){
    std::cout << "Usage: " << argv[0] << " port " << std::endl;
    return 1;
  }

  std::string ip = "127.0.0.1";
  int port = atoi(argv[1]); //字符串转化为整数

  UdpServer* svr = new UdpServer(ip,port);
  svr->InitUdpServer();
  svr->Start();
 

  return 0;
}
  •  IP地址为127.0.0.1实际上等价于localhost表示本地主机,我们将它称之为本地环回 , 通常用来进行网络通信代码的本地测试。—般跑通的情况下,本地环境以及代码基本没有大问题。

                                 

④udp_client.hpp

#pragma once 

#include<iostream>
#include<string>
#include<sys/types.h>
#include<arpa/inet.h>
#include<cstring>



class UdpClient{
   private:
     std::string ip;
     int port;
     int sockfd;

   public:
    UdpClient(std::string _ip,int _port):ip(_ip),port(_port)
    {}

    ~UdpClient()
    {}

   public:
    bool InitUdpClient()
    {
      sockfd = socket(AF_INET ,SOCK_DGRAM ,0);
      if(sockfd < 0){
        std::cerr << "socket error!" << std::endl;
        return false;
      }

      //客户端不需要port? 客户端不需要绑定?
      return true;
    }

    void Start()
    {
      struct sockaddr_in peer; //往哪发
      memset(&peer , 0 ,sizeof(peer));

      peer.sin_family = AF_INET;
      peer.sin_port = htons(port);
      peer.sin_addr.s_addr = inet_addr(ip.c_str()); //点分十进制转整形

      std::string msg;
      for(;;){
        std::cout << "Please Enter# ";
        std::cin >> msg;
        sendto(sockfd ,msg.c_str(),msg.size(), 0 ,(struct sockaddr*)&peer ,sizeof(peer));

      }
    }
};

                 

客户端是否需要bind的问题 

1.Server端对应的尤其是IP和端口号,是不能轻易被更改的。必须是确定的,众所周知的,而且永远不会改。

2.为什么server端口号不能改?
因为服务器面对的客户是成百上千的,服务器—旦把port改了,那么客户端就找不到了,就没办法访问了, 只有绑定之后这个端口号才真正属于自己,因为一个端口只能被一个进程所绑定,服务器绑定一个端口就是为了独占这个端口。

3.客户端不需要bind ?

  • 一个主机来讲会使用许多客户端,甚至可能同时跑起来,你在进行bind的时候很容易造成冲突(一个客户端已经绑定了一个port,但是另一个客户端想要绑定相同port就会失败,导致客户端起不来,一个port只能绑定一个客户端),无法启动的问题。
  • 客户端需要唯一性,但是不需要明确必须是哪个端口号,我们不关心,但是需要lP和port。client udp在recv,send的时候,系统会自动进行IP和端口号的绑定。

4.为什么系统自动bind的时候不会出错,冲突?
只有OS最清楚port的使用情况(port也是有资源上限的,OS会把port管理起来)

                         

⑤udp_client.cc 

#include"udp_client.hpp"



int main(int argc ,char* argv[])
{
  if(argc != 3){
    std::cout << "Usage: " << argv[0] << " ip port " << std::endl;
    return 1;
  }

  std::string ip = argv[1];
  int port = atoi(argv[2]);

  UdpClient* ucli = new UdpClient(ip,port);
  ucli->InitUdpClient();

  ucli->Start();
}

                

(2)结果

①server端运行,套接字是创建成功的,对应获取到的文件描述符就是3 

                         

 ②使用netstat命令查看网络状态:

常用选项: 

  • -n:直接使用IP地址,而不通过域名服务器。
  • -l:显示监控中的服务器的Socket。
  • -t:显示TCP传输协议的连线状况。
  • -u:显示UDP传输协议的连线状况。
  • -p:显示正在使用Socket的程序识别码和程序名称。

 属性信息:

  • 其中netstat命令显示的信息中,Proto表示协议的类型,Recv-Q表示网络接收队列,Send-Q表示网络发送队列,Local Address表示本地地址,Foreign Address表示外部地址,State表示当前的状态,PID表示该进程的进程ID,Program name表示该进程的程序名称。
  • 其中Foreign Address写成0.0.0.0:*表示任意IP地址、任意的端口号的程序都可以访问当前进程。

③多个client可以和server通信, 客户端也已经动态绑定成功了

 

                 

2.绑定INADDR_ANY的回复服务器

INADDR_ANY

①.服务器的公网IP是49.232.80.153,这里使用ping命令,显示网络良好

                 

 ②.将服务端设置的本地环回改为我的公网IP,此时当我们重新编译程序再次运行服务端的时候会发现服务端绑定失败。

③.由于云服务器的IP地址是由对应的云厂商提供的,这个IP地址并不一定是真正的公网IP,这个IP地址是不能直接被绑定的,如果需要让外网访问,此时我们需要bind 0。系统当当中提供的一个INADDR_ANY,这是一个宏值,它对应的值就是0。

因此如果我们需要让外网访问,那么在云服务器上进行绑定时就应该绑定INADDR_ANY,此时我们的服务器才能够被外网访问。

                        

绑定INADDR_ANY的优点

1..网络地址为INADDR_ANY,这个宏表示本地的任意IP地址,因为服务器可能有多个网卡,每个网卡也可能绑定多个IP地址,这样设置可以在所有的IP地址上监听,直到与某个客户端建立了连接时才确定下来到底用哪个IP地址

2.如果绑定的是确定的IP,意味着只会接受特定绑定IP发送过来的数据,其他发送过来的数据就接受不到了。IP本质是标定主机的,所有发给这台主机上的数据到IP层都应该给我。所以经常使用INADDR_ANY来表示任意绑定。

3.在Linux内核当中,这个INADDR ANY选项本质在内核的逻辑上起了一个判断的作用;如果IP== INADDR ANY收上来的所有IP报文都交给我,如果绑定的是具体的IP,只有从这个IP上上来的报文才会给你。

(1)代码

①udp_server.hpp , 绑定INADDR_ANY , 发送数据到client

#pragma once 

#include<iostream>
#include<string>
#include<sys/socket.h>
#include<cstring>
#include<sys/types.h>
#include<arpa/inet.h>
#include<unistd.h>

#define DEFAULT 8080
#define SIZE 128

class UdpServer{
  private:
    int port;
    int sockfd;

  public:
    UdpServer(int _port = DEFAULT):port(_port)
    {}

    ~UdpServer()
    {
      if(sockfd >= 0)
        close(sockfd);
    }

  public:
    bool InitUdpServer()
    {
      sockfd = socket(AF_INET,SOCK_DGRAM,0);
      if(sockfd < 0){
        std::cerr << "socket error" << std::endl;
        return false;
      }

      std::cout << "socket create success, sockfd: " << sockfd << std::endl;

      struct sockaddr_in local; //创建变量初始化
      memset(&local , '\0' ,sizeof(local));

      local.sin_family = AF_INET;
      local.sin_port = htons(port); //主机序列转网络序列16为端口号
      local.sin_addr.s_addr = INADDR_ANY; //接收任意IP发来的消息

      if(bind(sockfd,(struct sockaddr*)&local ,sizeof(local)) < 0){
        std::cerr << "bind error" << std::endl;
        return false;
      }
      std::cout << "bind success " << std::endl;

      return true;
    }

    void Start()
    {

      char buffer[SIZE]; 
      for(;;){
        struct sockaddr_in peer; //哪一端发送的数据
        socklen_t len = sizeof(peer);
        ssize_t size = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);

        if(size > 0){
          buffer[size] = 0 ;
          int _port = ntohs(peer.sin_port);
          std::string _ip = inet_ntoa(peer.sin_addr);
          std::cout << _ip << ":" << _port << "# " << buffer << std::endl;

          std::string echo_msg = "server get -> ";
          echo_msg += buffer;
          sendto(sockfd , echo_msg.c_str(),echo_msg.size(),0,(struct sockaddr*)&peer,len);

        }
        else{
          std::cerr << "recvfrom error!" << std::endl;

        }
      }
    }

};
  • 服务器端收到了客户端发来的消息,此时Server是知道那个IP发来的消息。有一万个人连我我就知道了一万个人的struct socketaddr,可以把每个人的套接字信息push到vector中。以后凡是人和人给我发消息,我用for循环遍历vector,你发送的消息再推送给所有人,我们就完成了一个简单的群聊功能。

                 

②udp_client.hpp , 接收数据 ,打印出来

#pragma once 

#include<iostream>
#include<string>
#include<sys/types.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>


class UdpClient{
   private:
     std::string ip;
     int port;
     int sockfd;

   public:
    UdpClient(std::string _ip,int _port):ip(_ip),port(_port)
    {}

    ~UdpClient()
    {
      if(sockfd >= 0){
        close(sockfd);
      }
    }


   public:
    bool InitUdpClient()
    {
      sockfd = socket(AF_INET ,SOCK_DGRAM ,0);
      if(sockfd < 0){
        std::cerr << "socket error!" << std::endl;
        return false;
      }

      //客户端不需要port? 客户端不需要绑定?
      return true;
    }

    void Start()
    {
      struct sockaddr_in peer; //往哪发
      memset(&peer , 0 ,sizeof(peer));

      peer.sin_family = AF_INET;
      peer.sin_port = htons(port);
      peer.sin_addr.s_addr = inet_addr(ip.c_str()); //点分十进制转整形


      std::string msg;// 存储发送给server的消息
      for(;;){
        std::cout << "Please Enter# ";
        getline(std::cin,msg);
        sendto(sockfd ,msg.c_str(),msg.size(), 0 ,(struct sockaddr*)&peer ,sizeof(peer));

        char buffer[128];
        struct sockaddr_in temp;
        socklen_t len =sizeof(temp);
        ssize_t size = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&temp,&len);
        if(size > 0){
          buffer[size] = 0;
          std::string _ip = inet_ntoa(temp.sin_addr);
          int _port = ntohs(temp.sin_port);
          std::cout << _ip << ":" << _port << "# " << buffer << std::endl;
        }
      }
    }
};

                 

 (2)结果

①两个IP都能访问server

                 

②该服务器的本地IP地址变成了0.0.0.0,这就意味着该UDP服务器可以在本地读取任何一张网卡里面端口号为8081的数据。 

                 

3.简易xshell

(1)代码

只需要修改udp_server.hpp的代码

#pragma once 

#include<iostream>
#include<string>
#include<sys/socket.h>
#include<cstring>
#include<sys/types.h>
#include<arpa/inet.h>
#include<string.h>
#include<unistd.h>
#include<sys/wait.h>

#define DEFAULT 8080
#define SIZE 128

class UdpServer{
  private:
    int port;
    int sockfd;

  public:
    UdpServer(int _port = DEFAULT):port(_port)
    {}

    ~UdpServer()
    {
      if(sockfd >= 0)
        close(sockfd);
    }

  public:
    bool InitUdpServer()
    {
      sockfd = socket(AF_INET,SOCK_DGRAM,0);
      if(sockfd < 0){
        std::cerr << "socket error" << std::endl;
        return false;
      }

      std::cout << "socket create success, sockfd: " << sockfd << std::endl;

      struct sockaddr_in local; //创建变量初始化
      memset(&local , '\0' ,sizeof(local));

      local.sin_family = AF_INET;
      local.sin_port = htons(port); //主机序列转网络序列16为端口号
      local.sin_addr.s_addr = INADDR_ANY; //接收任意IP发来的消息

      if(bind(sockfd,(struct sockaddr*)&local ,sizeof(local)) < 0){
        std::cerr << "bind error" << std::endl;
        return false;
      }
      std::cout << "bind success " << std::endl;

      return true;
    }

    void Start()
    {

      char buffer[SIZE]; 
      for(;;){
        struct sockaddr_in peer; //哪一端发送的数据
        socklen_t len = sizeof(peer);
        ssize_t size = recvfrom(sockfd,buffer,sizeof(buffer)-1,0,(struct sockaddr*)&peer,&len);

        if(size > 0){
          buffer[size] = 0 ;
          int _port = ntohs(peer.sin_port);
          std::string _ip = inet_ntoa(peer.sin_addr);
          std::cout << _ip << ":" << _port << "# " << buffer << std::endl;

       
        int pipes[2];
        pipe(pipes);//匿名管道

        pid_t id = fork();
        if(id == 0){
            //child
            close(pipes[0]);
            dup2(pipes[1], 1);
            execlp(buffer, buffer,nullptr);//进程替换
            exit(1);
        }
        
        //father
        std::string result;
        close(pipes[1]);
        char c;
        while(1){
            if(read(pipes[0], &c, 1) > 0){
                result.push_back(c);
            }
            else{
                break;
            }
        }
        wait(nullptr);


       std::string echo_msg;
       if(result.empty()){
           echo_msg = "server get!->";
           echo_msg += buffer;
       }
       else{
           echo_msg = result;
       }
       sendto(sockfd, echo_msg.c_str(), echo_msg.size(), 0, (struct sockaddr*)&peer, len);

      }
      else{
          std::cerr << "recvfrom error!" << std::endl;

      }

      
    }

    } 


};

                         

(2)结果 , 只能执行不带选项的简单命令

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值