Liinux——(网络)socket编程

预备知识

源IP地址和目的IP地址

  • 在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地址

认识端口号

  • 端口号(port)是传输层协议的内容.
  • 端口号是一个2字节16位的整数;
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪个进程来处理;
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程;
  • 一个端口号只能被一个进程占用

传输层协议(TCP和UDP)的数据段中有两个端口号, 分别叫做源端口号和目的端口号. 就是在描述 "数据是谁发的, 要发给谁

简单认识TCP协议

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

简单认识UDP协议

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

补充
计算机在存储数据时是有大小端的概念的:
大端模式: 数据的高字节内容保存在内存的低地址处,数据的低字节内容保存在内存的高地址处。
小端模式: 数据的高字节内容保存在内存的高地址处,数据的低字节内容保存在内存的低地址处。
在这里插入图片描述

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.
  • TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.
  • 不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;
  • 如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

网络字节序与主机字节序之间的转换
为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换。

//常用于端口转换
#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。ntoh表示网络转主机,hton表示主机转网络
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;
  • 如果主机是大端字节序,这些 函数不做转换,将参数原封不动地返回。

socket编程接口

socket常见API

创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

参数解析:
int domain
参数 domain 用于指定一个通信域;这将选择将用于通信的协议族。下面是三个常用的协议簇

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

int type

  • SOCK_STREAM 这个协议是按照顺序的、可靠的、数据完整的基于字节流的连接。这是一个使用最多的socket类型,这个socket是使用TCP来进行传输。
  • SOCK_DGRAM 这个协议是无连接的、固定长度的传输调用。该协议是不可靠的使用UDP来进行它的连接。
  • SOCK_SEQPACKET该协议是双线路的、可靠的连接,发送固定长度的数据包进行传输。必须把这个包完整的接受才能进行读取。
  • SOCK_RAW socket类型提供单一的网络访问,这个socket类型使用ICMP公共协议。(ping、traceroute使用该协议)
  • SOCK_RDM 这个类型是很少使用的,在大部分的操作系统上没有实现,它是提供给数据链路层使用,不保证数据包的顺序

int protocol
参数 protocol 通常设置为 0,表示为给定的通信域和套接字类型选择默认协议。当对同一域和套接字类型支持多个协议时,可以使用 protocol 参数选择一个特定协议。

  • 在 AF_INET 通信域中,套接字类型为SOCK_STREAM 的默认协议是传输控制协议(TCP);
  • 在 AF_INET 通信域中,套接字类型为 SOCK_DGRAM 的默认协议时 UDP。

返回值:成功:返回指向新创建的socket的文件描述符,失败:返回-1,设置errno
在这里插入图片描述

绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);

socket上面介绍了这里不用多说

主要介绍一下这const struct sockaddr *address
这个结构体指针指向一个 struct sockaddr 类型变量,如下所示:
一般有三种结构 sockaddr sockaddr_in sockaddr_un(后面两种在使用的时候都要进行一下强制转换)

客户端不用bind,OS自动给其bind——客户端的port要随机OS分配防止出现启动冲突,
服务端要自己bind——服务器的端口不能随意改变,众所周知且不能随意改变 同一家公司的port号需要统一规范化
什么时候绑定呢?——在我们首次调用系统用发送数据的函数的时候,os会在底层随机选择clientport+自己的IP

struct sockaddr {
 sa_family_t sa_family;
 char sa_data[14];
}
struct sockaddr_in
  {
    __SOCKADDR_COMMON (sin_);//协议簇
    in_port_t sin_port;			/* 端口号  */
    struct in_addr sin_addr;		/* ip地址  */

    /* Pad to size of `struct sockaddr'.  */
    unsigned char sin_zero[sizeof (struct sockaddr) -
			   __SOCKADDR_COMMON_SIZE -
			   sizeof (in_port_t) -
			   sizeof (struct in_addr)];
  };

一般在使用时要将这个结构体的内容清零
在这里插入图片描述
这里有一个问题:在传输ip地址的时候,肯定时以字符串的风格如"1.1.1.1"但是我们这里要接收的话,是要以四字节整数为基础接收的,所以说,要进行转化(不是强转),那么该怎么办呢?
在这里插入图片描述

在这里插入图片描述
这里面的提供了解决办法

字符串ip转换
in_addr_t inet_addr(const char*cp)

这个接口的作用就是将一个字符串风格的ip地址,转换为四字节ip地址,并且默认内部已经将主机序列转换为了网络序列
在这里插入图片描述

注意事项:
云服务器:不需要bind ip地址,需要让服务器自己指定ip地址
自己本地装的虚拟机,或者是物理机是允许的

绑定的时候需要ip地址,但云服务器一般不要指明某一个确定的ip,一个服务器可能有多张网卡,或者说有多个ip,为了提高效率,我们直接使用INADDR_ANY绑定本主机上的 任意ip或者说是所有的ip,只要端口号是我们指明的端口号,都能转到机器上

UDP接收信息

在这里插入图片描述

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

返回值说明:

  • 成功则返回实际接收到的字符数,失败返回-1,错误原因会存于errno 中。

参数说明:

  • sockfd: socket描述符;

  • buf: UDP数据报缓存区(包含所接收的数据);

  • len: 缓冲区长度。

  • flags: 调用操作方式(一般设置为0)。

  • src_addr: 指向发送数据的客户端地址信息(需要知道客户端的ip和端口号)的结构体(sockaddr_in需类型转换)

addrlen:指针,指向实际输出时结构体的大小
在这里插入图片描述
在这里插入图片描述

UDP发送信息
 int sendto(int sockfd, const void *buf, ssize_t len, unsigned int flags, const struct sockaddr *dest_addr, int addrlen);

返回值说明:

  • 成功则返回实际传送出去的字符数,失败返回-1,错误原因会存于errno 中。

参数说明:

  • sockfd: socket描述符;

  • buf:UDP数据报缓存区(包含待发送数据);

  • len: UDP数据报的长度;

  • flags:调用方式标志位(一般设置为0);

  • dest_addr:  指向接收数据的主机地址信息的结构体(sockaddr_in需类型转换);

  • addrlen:发送的结构体的长度;

轮哭了,

用上面的接口应用的简单通信程序

err.hpp

#pragma once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR
};

udp_client.cc

#include "udp_client.hpp"
#include <cstring>

// 127.0.0.1: 本地环回,就表示的就是当前主机,通常用来进行本地通信或者测试

static void usage(std::string proc)
{
    std::cout << "Usage:\n\t" << proc << " serverip serverport\n" << std::endl; 
}

// ./udp_client serverip serverport
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(SOCKET_ERR);
    }
    // client 这里要不要bind呢?要的!socket通信的本质[clientip:clientport, serverip:serverport]
    // 要不要自己bind呢?不需要自己bind,也不要自己bind,OS自动给我们进行bind -- 为什么?client的port要随机让OS分配防止client出现
    // 启动冲突 -- server 为什么要自己bind?1. server的端口不能随意改变,众所周知且不能随意改变的 2. 同一家公司的port号
    // 需要统一规范化

    // 明确server是谁
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    while(true)
    {
        
        // 用户输入
        std::string message;
        std::cout << "[请输入]# ";
         std::cin >> message;

        // 什么时候bind的?在我们首次系统调用发送数据的时候,OS会在底层随机选择clientport+自己的IP,1. bind 2. 构建发送的数据报文
        //发送
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));

        //接受
        char buffer[1024];
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int n = recvfrom(sock, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << "server echo# "<<buffer << std::endl;
        }
    }

    return 0;
}

}

udp_server.cc

#include "udp_server.hpp"
#include <memory>
#include <string>
#include <cstdio>

using namespace ns_server;
using namespace std;

static void usage(string proc)
{
    std::cout << "Usage:\n\t" << proc << " port\n"<< std::endl;
}


// ./udp_server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);
    unique_ptr<UdpServer> usvr(new UdpServer(port));

    usvr->InitServer(); // 服务器的初始化
    usvr->start();

    return 0;
}


udp_client.hpp

#pragma once

#include <iostream>
#include <string>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"


udp_server.hpp

#pragma once

#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unordered_map>
#include "err.hpp"

namespace ns_server
{
    const static uint16_t default_port = 8080;
    using func_t = std::function<std::string(std::string)>;

    class UdpServer
    {
    public:
        UdpServer(uint16_t port = default_port) : port_(port)
        {
            std::cout << "server addr: " << port_ << std::endl;
        }
        void InitServer()
        {
              // 1. 创建socket接口,打开网络文件
            sock_ = socket(AF_INET, SOCK_DGRAM, 0);
            if (sock_ < 0)
            {
                std::cerr << "create socket error: " << strerror(errno) << std::endl;
                exit(SOCKET_ERR);
            }
            std::cout << "create socket success: " << sock_ << std::endl; // 3

            // 2. 给服务器指明IP地址(??)和Port
            struct sockaddr_in local; // 这个 local 在哪里定义呢?用户空间的特定函数的栈帧上,不在内核中!
            bzero(&local, sizeof(local));

            local.sin_family = AF_INET; // PF_INET
            local.sin_port = htons(port_);
            // inet_addr: 1,2
            // 1. 字符串风格的IP地址,转换成为4字节int, "1.1.1.1" -> uint32_t -> 能不能强制类型转换呢?不能,这里要转化
            // 2. 需要将主机序列转化成为网络序列
            // 3. 云服务器,或者一款服务器,一般不要指明某一个确定的IP
            local.sin_addr.s_addr = INADDR_ANY; // 让我们的udpserver在启动的时候,bind本主机上的任意IP
            if (bind(sock_, (struct sockaddr *)&local, sizeof(local)) < 0)
            {
                std::cerr << "bind socket error: " << strerror(errno) << std::endl;
                exit(BIND_ERR);
            }
            std::cout << "bind socket success: " << sock_ << std::endl; // 3
        }
        void start()
        {
            char buffer[1024];
           while (true)
            {
                // 收
                struct sockaddr_in peer;
                socklen_t len = sizeof(peer); // 这里一定要写清楚,未来你传入的缓冲区大小
                int n = recvfrom(sock_, buffer, sizeof(buffer) - 1, 0, (struct sockaddr *)&peer, &len);
                if (n > 0)
                    buffer[n] = '\0';
                else continue;

                std::string clientip = inet_ntoa(peer.sin_addr);
                uint16_t clientport = ntohs(peer.sin_port);
                std::cout << clientip<< "-"<<clientport<<"#"<<"收到信息# " << buffer << std::endl;
                //发
                sendto(sock_, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, sizeof(peer));
            }
        }

        ~UdpServer()
        {

        }

    private:
        int sock_;
        uint16_t port_;
        // std::string ip_; 
    };
} // end NS_SERVER


在这里插入图片描述

在这里插入图片描述

开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

参数说明:

  • sockfd:需要设置为监听状态的套接字对应的文件描述符。

  • backlog:全连接队列的最大长度。如果有多个客户端同时发来连接请求,此时未被服务器处理的连接就会放入连接队列,该参数代表的就是这个全连接队列的最大长度,一般不要设置太大,设置为5或10即可。
    返回值说明:

  • 监听成功返回0,监听失败返回-1,同时错误码会被设置。

接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);

参数说明:

  • sockfd:特定的监听套接字,表示从该监听套接字中获取连接。

  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。

  • addrlen:调用时传入期望读取的addr结构体的长度,返回时代表实际读取到的addr结构体的长度,这是一个输入输出型参数。
    返回值说明:

  • 获取连接成功返回接收到的套接字的文件描述符,获取连接失败返回-1,同时错误码会被设置。

建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

参数说明:

  • sockfd:特定的套接字,表示通过该套接字发起连接请求。
  • addr:对端网络相关的属性信息,包括协议家族、IP地址、端口号等。
  • addrlen:传入的addr结构体的长度。

返回值说明:

  • 连接或绑定成功返回0,连接失败返回-1,同时错误码会被设置。

补充一下TCP其他会用到的接口:
TCP服务器读取数据的函数read

ssize_t read(int fd, void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示从该文件描述符中读取数据。
  • buf:数据的存储位置,表示将读取到的数据存储到该位置。
  • count:数据的个数,表示从该文件描述符中读取数据的字节数。

返回值说明:

  • 如果返回值大于0,则表示本次实际读取到的字节个数。
  • 如果返回值等于0,则表示对端已经把连接关闭了。
  • 如果返回值小于0,则表示读取时遇到了错误

TCP服务器写入数据的函数write

ssize_t write(int fd, const void *buf, size_t count);

参数说明:

  • fd:特定的文件描述符,表示将数据写入该文件描述符对应的套接字。
  • buf:需要写入的数据。
  • count:需要写入数据的字节个数。

返回值说明:

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

注意:这里的需要写入的数据,最好用string类型的c_str(),我写

简单的tcp回声服务器
tcpserver.hpp

#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unordered_map>
#include <unistd.h>
using namespace std;

namespace mysever
{
    // using func_t = function<string(const string &)>;
    const uint16_t default_port = 8080;
    class server
    {
    public:
        server(uint16_t _port = default_port)
            : port(_port)
        {
            std::cout << "server addr: " << port << std::endl;
        }
        void serverint(uint16_t port)
        {
            listensock = socket(AF_INET, SOCK_STREAM, 0);
            if (listensock < 0)
            {
                std::cerr << "create listensocket error: " << strerror(errno) << std::endl;
                exit(-1);
            }
            std::cout << "create listensocket success: " << listensock << std::endl;

            struct sockaddr_in serveraddr;
            bzero(&serveraddr, sizeof(serveraddr));
            serveraddr.sin_addr.s_addr = INADDR_ANY;
            serveraddr.sin_family = AF_INET;
            serveraddr.sin_port = htons(port);

            if (bind(listensock, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
            {
                std::cerr << "bind socket error: " << strerror(errno) << std::endl;
                exit(-1);
            }
            std::cout << "bind socket success: " << listensock << std::endl;

            if (listen(listensock, 5) < 0)
            {
                std::cerr << "listen socket error: " << strerror(errno) << std::endl;
                exit(-1);
            }
            std::cout << "listen socket success: " << listensock << std::endl;
        }
        void start()
        {
            while (1)
            {
                struct sockaddr_in peer;
                bzero(&peer, sizeof(peer));
                socklen_t len = sizeof(peer);
                int clientsock = accept(listensock, (struct sockaddr *)&peer, &len);
                if (clientsock < 0)
                {
                    std::cerr << "accept error, continue next" << std::endl;
                    continue;
                }

                string clientip = inet_ntoa(peer.sin_addr);
                uint16_t clientport = ntohs(peer.sin_port);
                std::cout << "get a new link->" << clientsock << " [" << clientip << "]:" << clientport << std::endl;

                // 接收信息
                char buffer[1024];
                while (1)
                {
                    ssize_t ret = read(clientsock, buffer, sizeof(buffer) - 1);
                    if (ret > 0)
                    {
                        buffer[ret] = 0;
                        cout << "接收到来自客户端的信息 ->" << clientsock << " [" << clientip << "]:" << clientport <<buffer<< endl;
                        string res = buffer;
                        if(write(clientsock, res.c_str(),res.size() )>0)
                        {
                            cout<<"返回数据成功"<<endl;
                        }
                    }
                    else if (ret == 0)
                    { // 对端关闭连接
                        cout << clientip << ":" << clientport << " close!" << endl;
                        break;
                    }
                    else
                    { // 读取失败
                        cerr << clientsock << " read error!" << endl;
                        break;
                    }
                }
                close(clientsock); //归还文件描述符
		       cout << clientip << ":" << clientport << "client quit" <<endl;
            }
        }
            ~server() {close(listensock);}

        private:
            uint16_t port;
            int listensock;
            // func_t func_;
        };
    }

tcpserver.cc

#include"tcpserver.hpp"
#include <memory>
#include <string>
#include <cstdio>
using namespace mysever;

void usage(string proc)
{
      cout << "Usage:\t" << proc << " port\n"
         << endl;
}

int main(int argc,char *argv[])
{
    if(argc!=2)
    {
        usage(argv[0]);
        exit(-1);
    }
    uint16_t myport = atoi(argv[1]);
    unique_ptr<server> usr(new server(myport));
    usr->serverint(myport);
    usr->start();
}

tcpclient.hpp

#pragma once

#include <iostream>
#include<string>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unordered_map>
using namespace std;


tcpclient.cc

#include "tcpclient.hpp"
#include <unistd.h>
void usage(string proc)
{
    cout << "Usage:\t" << proc << " serverip"
         << " serverip\n"
         << endl;
}

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        usage(argv[0]);
        exit(-1);
    }

    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0)
    {
        std::cerr << "create socket error" << std::endl;
        exit(-1);
    }
     std::cerr << "create socket success" << std::endl;
    struct sockaddr_in temp;
    bzero(&temp,sizeof(temp));
    string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);

    temp.sin_addr.s_addr = inet_addr(serverip.c_str());
    temp.sin_port = htons(serverport);
    temp.sin_family = AF_INET;

    if (connect(sock, (struct sockaddr *)&temp, sizeof(temp)) == 0) // connect 连接成功返回0 失败返回-1
    {
        
        std::cout << "客户端连接成功..." << std::endl;
        // 传输数据
        char buffer[1024];
        while (1)
        {
            string message;
            cout << "请输入#";
            cin >> message;
            write(sock, message.c_str(), message.size());

            
            int size = read(sock, buffer, sizeof(buffer)- 1);
            if (size > 0)
            {
                buffer[size] = 0;
                cout << "server echo# " << buffer << endl;
            }
            else if (size == 0)
            {
                std::cout << "server close!" << std::endl;
                break;
            }
            else
            {
                std::cerr << "read error!" << std::endl;
                break;
            }
        }
    }
    else
    {
        std::cerr << "客户端连接失败..." << std::endl;
        exit(-1);
    }
}

makefile

.PHONY:ALL
ALL:tcpserver tcpclient
tcpserver:tcpserver.cc
	g++ -o $@ $^ -std=c++11
tcpclient:tcpclient.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm tcpclient tcpserver

在这里插入图片描述

在上面的的这些接口中可以发现,这里面有一个结构体指针sockaddr* ,那么这个sockaddr结构体到底是什么呢?

sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX DomainSocket. 然而, 各种网络协议的地址格式并不相同

在这里插入图片描述
这些结构体中 struct sockadd_in 表示基于网络 进行网络通信的套接字;
struct sockaddr_un叫做域间套接字,用来两个进程之间进行本地通信的这个是取代system V本地之间进行通信的解决方案

  • IPv4和IPv6的地址格式定义在netinet/in.h中,IPv4地址用sockaddr_in结构体表示,包括16位地址类型, 16
    位端口号和32位IP地址.
  • IPv4、IPv6地址类型分别定义为常数AF_INET、AF_INET6. 这样,只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容.
  • *socket API可以都用struct sockaddr 类型表示, 在使用的时候需要强制转化成sockaddr_in; 这样的好处是程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为
    参数
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

晚安刘十三

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

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

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

打赏作者

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

抵扣说明:

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

余额充值