Linux网络编程 ——TCP套接字通信

网络编程系列文章

第1章 Linux系统编程入门(上)
第1章 Linux系统编程入门(下)

第2章 Linux多进程开发(上)
第2章 Linux多进程开发(下)

第3章 Linux多线程开发

第4章 Linux网络编程


第5章 Web服务器

7. TCP 三次握手

    TCP 是一种 面向连接的 单播协议,在发送数据前,通信双方必须在彼此间建立一条连接。所谓的“ 连接”,其实是 客户端服务器 的内存里保存的一份关于对方的信息,如 IP 地址端口号等。

    TCP 可以看成是一种字节流,它会处理 IP 层以下的层丢包重复 以及 错误问题。在连接的建立过程中,双方需要交换一些连接的参数。这些参数可以放在 TCP 头部

    TCP 提供了一种 可靠面向连接字节流传输层的服务,采用 三次握手 建立一个连接采用 四次挥手 来关闭一个连接

    三次握手 的目的是保证双方互相之间 建立了连接三次握手 发生在 客户端连接 的时候,当调用 connect(),底层会通过TCP协议进行 三次握手

在这里插入图片描述

三次握手

在这里插入图片描述

TCP 三次握手状态转换不携带数据):

在这里插入图片描述

  • 第一次握手

    1. 客户端将 SYN 标志位 置为 1
    2. 生成一个随机的32位的序号seq=J , 这个序号后边是可以携带数据(数据的大小)
  • 第二次握手

    1. 服务器端接收客户端的连接: ACK=1
    2. 服务器会回发一个确认序号ack = 客户端的序号 + 数据长度 + SYN/FIN(按 一个字节 算)
    3. 服务器端会向客户端发起连接请求: SYN=1
    4. 服务器会生成一个随机序号seq = K
  • 第三次握手

    1. 客户单应答服务器的连接请求:ACK=1
    2. 客户端回复收到了服务器端的数据:ack = 服务端的序号 + 数据长度 + SYN / FIN(按 一个字节 算)

为什么要三次握手,而不是两次握手:确认客户端服务器 端都能 收发数据,以保证建立的 连接是可靠的

TCP 头部结构
在这里插入图片描述

  • 16 位端口号port number):告知主机报文段是来自哪里(源端口)以及传给哪个上层协或应用程序(目的端口)的。进行 TCP 通信时,客户端通常使用系统 自动选择临时端口号

  • 32 位序号sequence number):一次 TCP 通信(从 TCP 连接建立到断开)过程中某一个传输方向上的字节流的每个字节的编号。⭐️

    • 假设 主机 A主机 B 进行 TCP 通信,A 发送给 B 的第一个 TCP 报文段中,序号值被系统初始化为某个 随机值 ISNInitial Sequence Number初始序号值)。那么在该传输方向上(从 A -> B),后续的 TCP 报文段中 序号值 将被系统设置成 ISN 加上 该报文段所携带数据的 第一个字节在整个字节流中偏移

      例如,某个 TCP 报文段传送的数据是字节流中的第 1025 ~ 2048 字节,那么该报文段的序号值就是 ISN + 1025。另外一个传输方向(从 BA)的 TCP 报文段的序号值也具有相同的含义。

  • 32 位确认号acknowledgement number):用作对另一方发送来的 TCP 报文段 的响应。其值是收到的 TCP 报文段的序号值 + 数据长度 / (数据长度+1)。⭐️

    • 假设 主机 A主机 B 进行TCP 通信,那么 A 发送出TCP 报文段 不仅携带自己的序号,而且包含对 B 发送来TCP 报文段确认号
    • 反之,B 发送出TCP 报文段 也同样携带自己的序号 和 对 A 发送来的 报文段确认序号
      在这里插入图片描述

    注意:只有收到 标志位 SYN=1FIN=1 时,确认序号ack才会再 +1;其余的都是加上具体 数据长度

  • 4 位头部长度head length):标识该 TCP 头部有多少个 32 bit(4 字节)。因为 4 位最大能表示 15,所以 TCP 头部最长是 60 字节

  • 6 位标志位 包含如下几项:

    • URG 标志,表示 紧急指针urgent pointer)是否有效。
    • ACK 标志,表示 确认号 是否有效。我们称携带 ACK 标志的 TCP 报文段为 确认报文段。⭐️
    • PSH 标志,提示接收端应用程序应该 立即TCP 接收缓冲区读走数据,为接收后续数据腾出空间(如果应用程序不将接收到的数据读走,它们就会一直停留在 TCP 接收缓冲区中)。
    • RST 标志,表示要求对方 重新建立连接。我们称携带 RST 标志的 TCP 报文段为 复位报文段
    • SYN 标志,表示 请求建立一个连接。我们称携带 SYN 标志的 TCP 报文段为 同步报文段。⭐️
    • FIN 标志,表示通知对方本端要 关闭连接了。我们称携带 FIN 标志的 TCP 报文段为 结束报文段。⭐️
  • 16 位窗口大小window size):是 TCP 流量控制的一个手段。这里说的窗口,指的是 接收通告窗口Receiver WindowRWND)。它告诉对方本端的 TCP 接收缓冲区 还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。⭐️

  • 16 位校验和TCP checksum):由发送端填充,接收端对 TCP 报文段执行 CRC 算法 以校验TCP 报文段在传输过程中是否损坏。注意,这个校验 不仅包括 TCP 头部,也包括数据部分。这也是 TCP 可靠传输的一个重要保障。

  • 16 位紧急指针urgent pointer):是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一个字节的序号。因此,确切地说,这个字段是紧急指针相对当前序号的偏移,不妨称之为 紧急偏移。TCP 的紧急指针是发送端向接收端发送紧急数据的方法。

8. TCP 滑动窗口

    滑动窗口(Sliding window)是一种 流量控制技术。早期的网络通信中,通信双方不会考虑网络的拥挤情况直接发送数据。由于大家不知道网络拥塞状况,同时发送数据,导致中间节点阻塞掉包,谁也发不了数据,所以就有了滑动窗口机制来解决此问题。

  • 滑动窗口协议是用来 改善吞吐量 的一种技术,即容许 发送方接收任何应答之前 传送 附加的包接收方 告诉 发送方 在某一时刻 能送多少包(称 窗口尺寸)。


    TCP 中采用 滑动窗口 来进行 传输控制,滑动窗口的大小意味着 接收方 还有多大的 缓冲区 可以用于接收数据。发送方 可以通过滑动窗口的大小来确定 应该发送多少字节的数据。当滑动窗口0 时,发送方一般不能再发送数据报。

    滑动窗口是 TCP 中实现诸如 ACK 确认流量控制拥塞控制的承载结构。

窗口 理解为 缓冲区的大小

  • 滑动窗口的大小会随着发送数据和接收数据而变化。
  • 通信的双方都有 发送缓冲区接收数据的缓冲区

服务器

  • 发送缓冲区(发送缓冲区的窗口)
  • 接收缓冲区(接收缓冲区的窗口)

客户端:

  • 发送缓冲区(发送缓冲区的窗口)
  • 接收缓冲区(接收缓冲区的窗口)

在这里插入图片描述

发送方的缓冲区

  • 白色格子:空闲的空间
  • 灰色格子:数据已经被发送出去了,但是还没有被接收
  • 紫色格子:还没有发送出去的数据

接收方的缓冲区

  • 白色格子:空闲的空间
  • 紫色格子:已经接收到的数据,还未被消化

举例
在这里插入图片描述

# mss: Maximum Segment Size(一条数据的最大的数据量)
# win: 滑动窗口

1. 客户端向服务器发起连接,客户单的滑动窗口是4096,一次发送的最大数据量是1460
2. 服务器接收连接情况,告诉客户端服务器的窗口大小是6144,一次发送的最大数据量是1024
3. 第三次握手
4. 4-9 客户端连续给服务器发送了6k的数据,每次发送1k
5. 第10次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了2k,窗口大小是2k
6. 第11次,服务器告诉客户端:发送的6k数据以及接收到,存储在缓冲区中,缓冲区数据已经处理了4k,窗口大小是4k
7. 第12次,客户端给服务器发送了1k的数据
8. 第13次,客户端主动请求和服务器断开连接,并且给服务器发送了1k的数据
9. 第14次,服务器回复ACK 8194, a:同意断开连接的请求 b:告诉客户端已经接受到方才发的2k的数据 c:滑动窗口2k
10.第15、16次,通知客户端滑动窗口的大小
11.第17次,第三次挥手,服务器端给客户端发送FIN,请求断开连接
12.第18次,第四次回收,客户端同意了服务器端的断开请求

9. TCP 四次挥手

    四次挥手 发生在 断开连接 的时候,在程序中当调用了 close() 会使用TCP协议进行四次挥手。
    客户端和服务器端 都可以主动发起断开连接,谁先调用 close() 谁就是发起。
    因为在TCP连接的时候,采用 三次握手 建立的的 连接是双向的,在断开的时候需要 双向断开

在这里插入图片描述

四次挥手状态转换
在这里插入图片描述

在这里插入图片描述

10. TCP 通信并发

    要实现TCP通信服务器 处理 并发的任务,使用 多线程 或者 多进程 来解决。

(1)多进程思路

  1. 一个父进程,多个子进程
  2. 父进程负责等待并接受客户端的连接
  3. 子进程完成通信,接受一个客户端连接,就创建一个子进程用于通信。

⭐️ 服务器端server_process.c

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <wait.h>
#include <errno.h>

void recyleChild(int arg) {  // 使用信号释放子进程资源
    while(1) { // 处理同时结束多个子进程的情况
        int ret = waitpid(-1, NULL, WNOHANG);
        if(ret == -1) {
            // 所有的子进程都回收了
            break;
        }else if(ret == 0) {
            // 还有子进程活着
            break;
        } else if(ret > 0){
            // 被回收了
            printf("子进程 %d 被回收了\n", ret);
        }
    }
}

int main() {

    struct sigaction act; 
    act.sa_flags = 0; 
    sigemptyset(&act.sa_mask);      // 清空临时阻塞信号掩码
    act.sa_handler = recyleChild;   // 回调函数
    // 注册信号捕捉
    sigaction(SIGCHLD, &act, NULL);
    

    // 1. 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 2. 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3. 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 不断循环等待客户端连接
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 4. 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);
        if(cfd == -1) {
            if(errno == EINTR) { // 产生中断
                continue;
            }
            perror("accept");
            exit(-1);
        }

        // 5. 每一个连接进来,创建一个子进程跟客户端通信
        pid_t pid = fork();
        if(pid == 0) {  // 先不考虑错误的情况
            // 子进程
            // 获取客户端的信息
            char cliIp[16];
            inet_ntop(AF_INET, &cliaddr.sin_addr.s_addr, cliIp, sizeof(cliIp));
            unsigned short cliPort = ntohs(cliaddr.sin_port);
            printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

            // 接收客户端发来的数据
            char recvBuf[1024];
            while(1) {
                int len = read(cfd, &recvBuf, sizeof(recvBuf));

                if(len == -1) {
                    perror("read");
                    exit(-1);
                }else if(len > 0) {
                    printf("recv client : %s\n", recvBuf);
                } else if(len == 0) {
                    printf("client closed....\n");
                    break;
                }
                write(cfd, recvBuf, strlen(recvBuf) + 1);
            }
            close(cfd);
            exit(0);    // 退出当前子进程
        }else{
            close(cfd);
        }

    }

    // 6. 关闭文件描述符
    close(lfd);
    return 0;
}

⭐️ 客户端client.c

// TCP通信的客户端
#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main() {

    // 1.创建套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if(fd == -1) {
        perror("socket");
        exit(-1);
    }

    // 2.连接服务器端
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    inet_pton(AF_INET, "192.168.216.129", &serveraddr.sin_addr.s_addr);
    serveraddr.sin_port = htons(9999);
    int ret = connect(fd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));

    if(ret == -1) {
        perror("connect");
        exit(-1);
    }
    
    // 3. 通信
    char recvBuf[1024];
    int i = 0;
    while(1) {
        
        sprintf(recvBuf, "data : %d\n", i++);
        
        // 给服务器端发送数据
        write(fd, recvBuf, strlen(recvBuf)+1);  // 加上字符串结束符

        int len = read(fd, recvBuf, sizeof(recvBuf));
        if(len == -1) {
            perror("read");
            exit(-1);
        } else if(len > 0) {
            printf("recv server : %s\n", recvBuf);
        } else if(len == 0) {
            // 表示服务器端断开连接
            printf("server closed...");
            break;
        }

        sleep(1);
    }

    // 关闭连接
    close(fd);

    return 0;
}
  • 运行结果:
    在这里插入图片描述
  • 中断子进程
    在这里插入图片描述

(2)多线程实现并发服务器

#include <stdio.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

// 把所需要参数放到一个结构体中
struct sockInfo {
    int fd; // 通信的文件描述符
    struct sockaddr_in addr;    // 客户端信息
    pthread_t tid;  // 线程号
};

struct sockInfo sockinfos[128];  // 可以支持128个客户端同时连接

void * working(void * arg) { 
    // 子线程和客户端通信   cfd 客户端的信息 线程号
    // 获取客户端的信息
    struct sockInfo * pinfo = (struct sockInfo *)arg;

    char cliIp[16];
    inet_ntop(AF_INET, &pinfo->addr.sin_addr.s_addr, cliIp, sizeof(cliIp));
    unsigned short cliPort = ntohs(pinfo->addr.sin_port);
    printf("client ip is : %s, prot is %d\n", cliIp, cliPort);

    // 接收客户端发来的数据
    char recvBuf[1024];
    while(1) {
        int len = read(pinfo->fd, &recvBuf, sizeof(recvBuf));

        if(len == -1) {
            perror("read");
            exit(-1);
        }else if(len > 0) {
            printf("recv client : %s\n", recvBuf);
        } else if(len == 0) {
            printf("client closed....\n");
            break;
        }
        write(pinfo->fd, recvBuf, strlen(recvBuf) + 1);
    }
    close(pinfo->fd);
    return NULL;
}

int main() {

    // 1. 创建socket
    int lfd = socket(PF_INET, SOCK_STREAM, 0);
    if(lfd == -1){
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(9999);
    saddr.sin_addr.s_addr = INADDR_ANY;

    // 2. 绑定
    int ret = bind(lfd,(struct sockaddr *)&saddr, sizeof(saddr));
    if(ret == -1) {
        perror("bind");
        exit(-1);
    }

    // 3. 监听
    ret = listen(lfd, 128);
    if(ret == -1) {
        perror("listen");
        exit(-1);
    }

    // 初始化数据
    int max = sizeof(sockinfos) / sizeof(sockinfos[0]);
    for(int i = 0; i < max; i++) {
        bzero(&sockinfos[i], sizeof(sockinfos[i]));
        sockinfos[i].fd = -1;
        sockinfos[i].tid = -1;
    }

    // 循环等待客户端连接,一旦一个客户端连接进来,就创建一个子线程进行通信
    while(1) {

        struct sockaddr_in cliaddr;
        int len = sizeof(cliaddr);
        // 4. 接受连接
        int cfd = accept(lfd, (struct sockaddr*)&cliaddr, &len);

        struct sockInfo * pinfo;
        for(int i = 0; i < max; i++) {
            // 从这个数组中找到一个可以用的sockInfo元素
            if(sockinfos[i].fd == -1) {
                pinfo = &sockinfos[i];
                break;
            }
            if(i == max - 1) {
                sleep(1);
                i--;
            }
        }

        pinfo->fd = cfd;
        memcpy(&pinfo->addr, &cliaddr, len);

        // 5. 创建子线程,通信
        pthread_create(&pinfo->tid, NULL, working, pinfo);
        // 设置线程分离,子线程结束后,自动回收资源
        pthread_detach(pinfo->tid);
    }

    // 6. 关闭文件描述符
    close(lfd);
    return 0;
}
  • 运行结果:
    在这里插入图片描述
    在这里插入图片描述

11. TCP 状态转换

在这里插入图片描述
在这里插入图片描述

黑色实线:异常(可以先不看
红色实现:客户端
绿色虚线:服务器

  • 2MSL(Maximum Segment Lifetime)
    • TCP 连接 主动关闭方 接收到 被动关闭方 发送的 FIN 和最终的 ACK 后,连接的主动关闭方必须处于 TIME_WAIT 状态并持续 2MSL 时间。
    • 这样就能够让 TCP 连接主动关闭方 在它发送的 ACK 丢失 的情况下 重新发送最终ACK
    • 主动关闭方 重新发送的最终 ACK 并不是因为 被动关闭方 重传了 ACK(它们并不消耗序列号,被动关闭方也不会重传),而是因为 被动关闭方 重传了它的 FIN事实上,被动关闭方总是重传 FIN 直到它收到一个最终的 ACK

主动断开 连接的一方, 最后进出入一个 TIME_WAIT 状态, 这个状态会持续: 2msl

  • msl: 官方建议: 2分钟, Linux中实际30s


半关闭
    当 TCP 链接AB 发送 FIN 请求关闭,另一端 B 回应 ACK 之后(A 端进入 FIN_WAIT_2 状态),并没有立即发送 FINAA 方处于 半连接状态半开关),此时 A 可以接收 B 发送的数据,但是 A 已经不能再向 B 发送数据。

从程序的角度,可以使用 API 来控制实现 半连接状态

#include <sys/socket.h>
int shutdown(int sockfd, int how);
	sockfd: 需要关闭的socket的描述符
	how: 允许为shutdown操作选择以下几种方式:
		SHUT_RD(0): 关闭sockfd上的读功能,此选项将不允许sockfd进行读操作。(不能接收数据)
		该套接字不再接收数据,任何当前在套接字接受缓冲区的数据将被无声的丢弃掉。
		SHUT_WR(1): 关闭sockfd的写功能,此选项将不允许sockfd进行写操作。进程不能在对此套接字发 写操作。 (不能发送数据)
		SHUT_RDWR(2):关闭sockfd的读写功能。相当于调用shutdown两次:首先是以SHUT_RD,然后以 SHUT_WR。

    使用 close 中止一个连接,但它只是 减少描述符的 引用计数,并不直接关闭连接,只有当 描述符的引用计数0 时才关闭连接。shutdown 不考虑 描述符的引用计数,直接关闭描述符。也可选择中止一个方向的连接,只中止读只中止写

注意:

  1. 如果有多个进程共享一个套接字close 每被调用一次,计数 减1 ,直到计数为 0 时,也就是所用进程都调用了 close套接字将被释放
  2. 在多进程中如果一个进程 调用shutdown(sfd, SHUT_RDWR) 后,其它的进程将无法进行通信。但如果一个进程 close(sfd)不会影响 到其它进程

12 端口复用

端口复用 最常用的用途是:

  • 防止 服务器重启 时之前绑定的端口还未释放
    • 当服务器主动断开连接,进入FIN_WAIT_2TIME_WAIT 状态时,其端口号被占用,还未释放,不能再次启动服务器
  • 程序突然退出而系统没有释放端口
##include <sys/types.h>
#include <sys/socket.h>

// 设置套接字的属性(不仅仅能设置端口复用)
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
	参数:
		- sockfd : 要操作的文件描述符
		- level : 级别 - SOL_SOCKET (端口复用的级别)
		- optname : 选项的名称
			- SO_REUSEADDR  (允许重用本地地址)
			- SO_REUSEPORT  (允许重用本地端口)
		- optval : 端口复用的值(整形 / 结构体)
			- 1 : 可以复用
			- 0 : 不可以复用
		- optlen : optval参数的大小

// 端口复用,设置的时机是在服务器绑定端口之前。
setsockopt();
bind();


常看 网络相关信息 的命令 netstat -anp

  • 参数:
    -a 所有的 socket
    -p 显示正在使用 socket 的程序的名称
    -n 直接使用IP地址,而不通过域名服务器

注:仅供学习参考,如有不足,欢迎指正!

  • 34
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Vivado是一款由Xilinx开发的FPGA设计软件,用于实现面向硬件的开发流程。Vivado软件本身并不直支持TCP,它主要用于FPGA设计、验证和实现等功能。但是,通过在FPGA设计中添加相应的IP核或外设模块,可以实现TCP的功能。 在Vivado中,我们可以使用AXI Ethernet Lite IP核来实现TCP。AXI Ethernet Lite是Xilinx官方提供的一个轻量级以太网IP核,通过该IP核可以实现FPGA与以太网之间的数据传输。我们可以将AXI Ethernet Lite IP核添加到Vivado工程中,并配置相关参数,然后通过软件编程的方式,在FPGA中实现TCP协议的功能,实现与其他设备之间的数据交互。 另一种实现TCP的方法是使用Xilinx提供的开发板或模块中已经集成的以太网口。这些开发板或模块通常提供了以太网口,并且已经在硬件上实现了TCP/IP协议栈。在Vivado中,我们可以使用相关的板级支持包(BSP)来集成这些以太网口,并进行相关的配置。然后,我们可以在Vivado中进行FPGA设计,使其与已经实现了TCP/IP协议栈的硬件进行通信,实现TCP的功能。 综上所述,Vivado本身不直支持TCP,但可以通过添加IP核或使用已经实现了TCP/IP协议栈的硬件口来实现TCP的功能。这样可以使FPGA与其他设备进行数据交互,拓展Vivado在网络通信方面的应用。 ### 回答2: Vivado是一种FPGA设计和开发工具,用于实现和验证硬件电路。Vivado本身并不直支持TCP,因为它更专注于硬件设计和验证领域。然而,Vivado可以通过一些其他工具和方法实现TCP。 一种常见的方法是使用嵌入式软件来实现TCP。Vivado支持使用嵌入式处理器,如MicroBlaze或ARM Cortex-A9等。这些处理器可以与FPGA设计集成,并且可以运行基于操作系统(如FreeRTOS或Linux)的软件。通过在嵌入式软件中使用TCP/IP协议栈,可以在FPGA设计中实现TCP。 另一种方法是在FPGA设计中使用专门的IP核来实现TCP。Vivado提供了许多可用于构建网络口的IP核,如以太网MAC核、TCP/IP协议栈核等。通过使用这些IP核,可以在FPGA设计中实现TCP。 此外,可以使用外部电路或外部设备来实现FPGA与TCP通信。例如,可以通过使用以太网PHY芯片和以太网线缆将FPGA连到局域网,并使用TCP/IP协议进行通信。在这种情况下,Vivado本身并不直参与TCP,而是通过FPGA设计与外部设备进行通信。 总结来说,Vivado本身并不直支持TCP,但可以通过使用嵌入式软件、专门的IP核或外部设备来在FPGA设计中实现TCP。具体的方法和实现取决于具体的需求和应用场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

酷酷的懒虫

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

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

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

打赏作者

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

抵扣说明:

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

余额充值