《深入理解计算机系统》实验八Proxy Lab

前言

新年快乐。欢迎来到本书的最后一个实验。

《深入理解计算机系统》实验八Proxy Lab下载和官方文档机翻请看:
https://blog.csdn.net/weixin_43362650/article/details/122770330

我觉得这个文档对整个实验很有帮助。

实验任务

编写一个简单的HTTP代理服务器

代理服务器英文全称是Proxy Server,其功能就是代理网络用户去取得网络信息。形象的说:它是网络信息的中转站。在一般情况下,我们使用网络浏览器直接去连接其他Internet站点取得网络信息时,须送出Request信号来得到回答,然后对方再把信息以bit方式传送回来。代理服务器是介于浏览器和Web服务器之间的一台服务器,有了它之后,浏览器不是直接到Web服务器去取回网页而是向代理服务器发出请求,Request信号会先送到代理服务器,由代理服务器来取回浏览器所需要的信息并传送给你的浏览器。–百度百科

原本客户端与服务器的关系。
请添加图片描述
加上代理后就是
请添加图片描述
代理服务器要做的就是接收客户端发送的请求,经过自己的处理后发送请求到服务端,服务端响应的数据到代理服务器后转发回给客户端。

本实验的代理服务器分为3个阶段

  1. 第一部分:实现顺序web代理
  2. 第二部分:处理多个并发请求
  3. 第三部分:缓存web对象

第一部分:实现顺序web代理

代码如下(详情看注释)

#include <stdio.h>
#include "csapp.h"

/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

/* You won't lose style points for including this long line in your code */
static const char *user_agent_hdr = "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3\r\n";

void doit(int clientfd);
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head);
void read_requesthdrs(rio_t *rp,int fd);
void return_content(int serverfd, int clientfd);


int main(int argc,char **argv)
{
    int listenfd,connfd;
    char hostname[MAXLINE],port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;

    if(argc != 2){
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }

    /* 代理创建一个监听描述符,准备好接收连接请求 */
    listenfd = Open_listenfd(argv[1]);
    while(1){
        clientlen = sizeof(clientaddr);

        /* 等待来着客户端的连接请求到侦听描述符listenfd,
        然后在addr中填写客户端的套接字地址,并返回一个已连接描述符 
        */
        connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);

        /* 将套接字地址结构clientaddr转化成对应的主机和服务名字符串,
        并将他们复制到hostname和port缓冲区
         */
        Getnameinfo((SA *)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
        printf("Accepted connection from (%s, %s)\n",hostname,port);
        doit(connfd);
        Close(connfd);
    }
}

/* 处理客户端HTTP事务 */
void doit(int clientfd){

    char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];
    char hostname[MAXLINE],path[MAXLINE],port[MAXLINE],request_head[MAXLINE];
    int serverfd;
    rio_t rio;

    /* 读取请求行和请求头 */
    Rio_readinitb(&rio,clientfd);
    Rio_readlineb(&rio,buf,MAXLINE);
    sscanf(buf,"%s %s %s",method,uri,version);
    if(strcasecmp(method,"GET")){
        printf("Not implemented");
        return;
    }

    /* 解析uri取hostname和path和port。生成request_head */
    parse_uri(uri,hostname,path,port,request_head);

    /* 建立与服务器的连接 */
    serverfd = Open_clientfd(hostname,port);

    /* 将请求头传递给服务器 */
    Rio_writen(serverfd,request_head,strlen(request_head));
    read_requesthdrs(&rio,serverfd);

    /* 将服务端读取的数据返回给客户端 */
    return_content(serverfd,clientfd);


}

/*  解析uri中的hostname和path和port。并生成request_head
 *  uri例子:http://www.cmu.edu:8080/hub/index.html
 *  hostname:www.cmu.edu
 *  path:/hub/index.html
 *  port:8080
 */
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head){

    sprintf(port,"80");     //默认值

    char *end,*bp;
    char *tail = uri+strlen(uri);   //uri的最后一个字符,不是'\0'。

    char *bg = strstr(uri,"//");

    bg = (bg!=NULL ? bg+2 : uri);   //取hostname的开头。

    end = bg;                        
    //取hostname的结尾。
    while(*end != '/' && *end != ':') end++;
    strncpy(hostname,bg,end-bg);

    bp = end + 1;                   //取port的开头
    if(*end == ':'){                //==':'说明uri中有port
        end++;
        bp = strstr(bg,"/");        //取port的结尾

        strncpy(port,end,bp-end);   
        end = bp;                   //取uri的开头
    }
    strncpy(path,end,(int)(tail-end)+1);

    /* 请求的开头行:GET /hub/index.html HTTP/1.0。 */
    sprintf(request_head,"GET %s HTTP/1.0\r\nHost: %s\r\n",path,hostname);
    
    return 1;
}

/*
 *  读取HTTP请求头
 *  Host,User-Agent,Connection和Proxy-Connection用指定的的
 *  保留其他的头(header)
 */
void read_requesthdrs(rio_t *rp,int fd){
    
    char buf[MAXLINE];

    sprintf(buf, "%s", user_agent_hdr);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Connection: close\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Proxy-Connection: close\r\n");
    Rio_writen(fd, buf, strlen(buf));

    /* 保留其他的头(header) */
    for(Rio_readlineb(rp,buf,MAXLINE);strcmp(buf,"\r\n");Rio_readlineb(rp,buf,MAXLINE)){
        if(strncmp("Host",buf,4) == 0 || strncmp("User-Agent",buf,10) == 0
            || strncmp("Connection",buf,10) == 0 || strncmp("Proxy-Connection",buf,16) == 0)
                continue;
        printf("%s",buf);
        Rio_writen(fd,buf,strlen(buf));
    }
    Rio_writen(fd,buf,strlen(buf));
    return;
}

/*
 * 将服务端读取的数据返回给客户端
 */
void return_content(int serverfd, int clientfd){

    size_t n;
    char buf[MAXLINE];
    rio_t srio;

    Rio_readinitb(&srio,serverfd);
    while((n = Rio_readlineb(&srio,buf,MAXLINE)) != 0){
        Rio_writen(clientfd,buf,n);
    }
    
}

编译
在这里插入图片描述
可能有些环境没安装好,我第一次就是没有“curl”。

linux> apt-get install  curl 

运行
在这里插入图片描述
可以看到第一部分已经完成。

第二部分:处理多个并发请求

参考《CS:APP3e》12.3.8 基于线程的并发服务器修改第一部分的代码(修改了main函数和添加了thread函数)

#include <stdio.h>
#include "csapp.h"

/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400

/* You won't lose style points for including this long line in your code */
static const char *user_agent_hdr = "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3\r\n";

void doit(int clientfd);
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head);
void read_requesthdrs(rio_t *rp,int fd);
void return_content(int serverfd, int clientfd);
void *thread(void *vargp);


int main(int argc,char **argv)
{
    int listenfd;
    int *connfd;        /* 使用指针,避免竞争 */
    char hostname[MAXLINE],port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    pthread_t tid;

    if(argc != 2){
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }

    /* 代理创建一个监听描述符,准备好接收连接请求 */
    listenfd = Open_listenfd(argv[1]);
    while(1){
        clientlen = sizeof(clientaddr);

        connfd = Malloc(sizeof(int));

        /* 等待来着客户端的连接请求到侦听描述符listenfd,
        然后在addr中填写客户端的套接字地址,并返回一个已连接描述符 
        */
        *connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);

        /* 将套接字地址结构clientaddr转化成对应的主机和服务名字符串,
        并将他们复制到hostname和port缓冲区
         */
        Getnameinfo((SA *)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
        printf("Accepted connection from (%s, %s)\n",hostname,port);

        /* 调用pthread_create函数来创建其他线程 */
        Pthread_create(&tid,NULL,thread,connfd);
    }
}

void *thread(void *vargp){
    int connfd = *((int *)vargp);
    Pthread_detach(pthread_self());
    Free(vargp);
    doit(connfd);
    Close(connfd);
    return NULL;
}

/* 处理客户端HTTP事务 */
void doit(int clientfd){

    char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];
    char hostname[MAXLINE],path[MAXLINE],port[MAXLINE],request_head[MAXLINE];
    int serverfd;
    rio_t rio;

    /* 读取请求行和请求头 */
    Rio_readinitb(&rio,clientfd);
    Rio_readlineb(&rio,buf,MAXLINE);
    sscanf(buf,"%s %s %s",method,uri,version);
    if(strcasecmp(method,"GET")){
        printf("Not implemented");
        return;
    }

    /* 解析uri取hostname和path和port。生成request_head */
    parse_uri(uri,hostname,path,port,request_head);

    /* 建立与服务器的连接 */
    serverfd = Open_clientfd(hostname,port);

    /* 将请求头传递给服务器 */
    Rio_writen(serverfd,request_head,strlen(request_head));
    read_requesthdrs(&rio,serverfd);

    /* 将服务端读取的数据返回给客户端 */
    return_content(serverfd,clientfd);


}

/*  解析uri中的hostname和path和port。并生成request_head
 *  uri例子:http://www.cmu.edu:8080/hub/index.html
 *  hostname:www.cmu.edu
 *  path:/hub/index.html
 *  port:8080
 */
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head){

    sprintf(port,"80");     //默认值

    char *end,*bp;
    char *tail = uri+strlen(uri);   //uri的最后一个字符,不是'\0'。

    char *bg = strstr(uri,"//");

    bg = (bg!=NULL ? bg+2 : uri);   //取hostname的开头。

    end = bg;                        
    //取hostname的结尾。
    while(*end != '/' && *end != ':') end++;
    strncpy(hostname,bg,end-bg);

    bp = end + 1;                   //取port的开头
    if(*end == ':'){                //==':'说明uri中有port
        end++;
        bp = strstr(bg,"/");        //取port的结尾

        strncpy(port,end,bp-end);   
        end = bp;                   //取uri的开头
    }
    strncpy(path,end,(int)(tail-end)+1);

    /* 请求的开头行:GET /hub/index.html HTTP/1.0。 */
    sprintf(request_head,"GET %s HTTP/1.0\r\nHost: %s\r\n",path,hostname);
    
    return 1;
}

/*
 *  读取HTTP请求头
 *  Host,User-Agent,Connection和Proxy-Connection用指定的的
 *  保留其他的头(header)
 */
void read_requesthdrs(rio_t *rp,int fd){
    
    char buf[MAXLINE];

    sprintf(buf, "%s", user_agent_hdr);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Connection: close\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Proxy-Connection: close\r\n");
    Rio_writen(fd, buf, strlen(buf));

    /* 保留其他的头(header) */
    for(Rio_readlineb(rp,buf,MAXLINE);strcmp(buf,"\r\n");Rio_readlineb(rp,buf,MAXLINE)){
        if(strncmp("Host",buf,4) == 0 || strncmp("User-Agent",buf,10) == 0
            || strncmp("Connection",buf,10) == 0 || strncmp("Proxy-Connection",buf,16) == 0)
                continue;
        printf("%s",buf);
        Rio_writen(fd,buf,strlen(buf));
    }
    Rio_writen(fd,buf,strlen(buf));
    return;
}

/*
 * 将服务端读取的数据返回给客户端
 */
void return_content(int serverfd, int clientfd){

    size_t n;
    char buf[MAXLINE];
    rio_t srio;

    Rio_readinitb(&srio,serverfd);
    while((n = Rio_readlineb(&srio,buf,MAXLINE)) != 0){
        Rio_writen(clientfd,buf,n);
    }

}

运行
在这里插入图片描述
可以看到第二部分已经完成

第三部分:缓存web对象

采用读者优先,代码如下

#include <stdio.h>
#include "csapp.h"

/* Recommended max cache and object sizes */
#define MAX_CACHE_SIZE 1049000
#define MAX_OBJECT_SIZE 102400
#define MAX_CACHE 10

/* You won't lose style points for including this long line in your code */
static const char *user_agent_hdr = "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3\r\n";

void doit(int clientfd);
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head);
void read_requesthdrs(rio_t *rp,int fd);
void return_content(int serverfd, int clientfd,char *url);
void *thread(void *vargp);
int maxlrucache();

/*读写者锁的结构体*/
struct RWLOCK_T{
    sem_t lock;         //基本锁
    sem_t writeLock;    //写着锁
    int readcnt;        //读者个数
};

/*LRU缓存的结构体*/
struct CACHE{
    int lruNumber;                  //引用次数,根据大小排位。大的表示最近引用过
    char url[MAXLINE];              //通过url唯一标识对应content
    char content[MAX_OBJECT_SIZE];
};

struct CACHE cache[MAX_CACHE];      //缓存,最多有MAX_CACHE个
struct RWLOCK_T* rw;                //读写者锁指针

void rwlock_init();                     //初始化读写者锁指针
char *readcache(char *url);             //读缓存
void writecache(char *buf,char *url);   //写缓存


int main(int argc,char **argv)
{
    int listenfd;
    int *connfd;        /* 使用指针,避免竞争 */
    char hostname[MAXLINE],port[MAXLINE];
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    pthread_t tid;

    if(argc != 2){
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(1);
    }

    rw = Malloc(sizeof(struct RWLOCK_T));
    rwlock_init();

    /* 代理创建一个监听描述符,准备好接收连接请求 */
    listenfd = Open_listenfd(argv[1]);
    while(1){
        clientlen = sizeof(clientaddr);

        connfd = Malloc(sizeof(int));

        /* 等待来着客户端的连接请求到侦听描述符listenfd,
        然后在addr中填写客户端的套接字地址,并返回一个已连接描述符 
        */
        *connfd = Accept(listenfd,(SA *)&clientaddr,&clientlen);

        /* 将套接字地址结构clientaddr转化成对应的主机和服务名字符串,
        并将他们复制到hostname和port缓冲区
         */
        Getnameinfo((SA *)&clientaddr,clientlen,hostname,MAXLINE,port,MAXLINE,0);
        printf("Accepted connection from (%s, %s)\n",hostname,port);

        /* 调用pthread_create函数来创建其他线程 */
        Pthread_create(&tid,NULL,thread,connfd);
    }
}

void *thread(void *vargp){
    int connfd = *((int *)vargp);
    Pthread_detach(pthread_self());
    Free(vargp);
    doit(connfd);
    Close(connfd);
    return NULL;
}

/* 处理客户端HTTP事务 */
void doit(int clientfd){

    char buf[MAXLINE],method[MAXLINE],uri[MAXLINE],version[MAXLINE];
    char hostname[MAXLINE],path[MAXLINE],port[MAXLINE],request_head[MAXLINE];
    int serverfd;
    rio_t rio;

    /* 读取请求行和请求头 */
    Rio_readinitb(&rio,clientfd);
    Rio_readlineb(&rio,buf,MAXLINE);
    sscanf(buf,"%s %s %s",method,uri,version);
    if(strcasecmp(method,"GET")){
        printf("Not implemented");
        return;
    }

    char *content = readcache(uri);
    if(content != NULL){
        Rio_writen(clientfd,content,strlen(content));
        free(content);
    }else{
        /* 解析uri取hostname和path和port。生成request_head */
        parse_uri(uri,hostname,path,port,request_head);

        /* 建立与服务器的连接 */
        serverfd = Open_clientfd(hostname,port);

        /* 将请求头传递给服务器 */
        Rio_writen(serverfd,request_head,strlen(request_head));
        read_requesthdrs(&rio,serverfd);

        /* 将服务端读取的数据返回给客户端 */
        return_content(serverfd,clientfd,uri);

    }


}

/*  解析uri中的hostname和path和port。并生成request_head
 *  uri例子:http://www.cmu.edu:8080/hub/index.html
 *  hostname:www.cmu.edu
 *  path:/hub/index.html
 *  port:8080
 */
int parse_uri(char *uri,char *hostname,char *path,char *port,char *request_head){

    sprintf(port,"80");     //默认值

    char *end,*bp;
    char *tail = uri+strlen(uri);   //uri的最后一个字符,不是'\0'。

    char *bg = strstr(uri,"//");

    bg = (bg!=NULL ? bg+2 : uri);   //取hostname的开头。

    end = bg;                        
    //取hostname的结尾。
    while(*end != '/' && *end != ':') end++;
    strncpy(hostname,bg,end-bg);

    bp = end + 1;                   //取port的开头
    if(*end == ':'){                //==':'说明uri中有port
        end++;
        bp = strstr(bg,"/");        //取port的结尾

        strncpy(port,end,bp-end);   
        end = bp;                   //取uri的开头
    }
    strncpy(path,end,(int)(tail-end)+1);

    /* 请求的开头行:GET /hub/index.html HTTP/1.0。 */
    sprintf(request_head,"GET %s HTTP/1.0\r\nHost: %s\r\n",path,hostname);
    
    return 1;
}

/*
 *  读取HTTP请求头
 *  Host,User-Agent,Connection和Proxy-Connection用指定的的
 *  保留其他的头(header)
 */
void read_requesthdrs(rio_t *rp,int fd){
    
    char buf[MAXLINE];

    sprintf(buf, "%s", user_agent_hdr);
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Connection: close\r\n");
    Rio_writen(fd, buf, strlen(buf));
    sprintf(buf, "Proxy-Connection: close\r\n");
    Rio_writen(fd, buf, strlen(buf));

    /* 保留其他的头(header) */
    for(Rio_readlineb(rp,buf,MAXLINE);strcmp(buf,"\r\n");Rio_readlineb(rp,buf,MAXLINE)){
        if(strncmp("Host",buf,4) == 0 || strncmp("User-Agent",buf,10) == 0
            || strncmp("Connection",buf,10) == 0 || strncmp("Proxy-Connection",buf,16) == 0)
                continue;
        printf("%s",buf);
        Rio_writen(fd,buf,strlen(buf));
    }
    Rio_writen(fd,buf,strlen(buf));
    return;
}

/*
 * 将服务端读取的数据返回给客户端
 */
void return_content(int serverfd, int clientfd,char *uri){

    size_t n,size = 0;
    char buf[MAXLINE],content[MAX_OBJECT_SIZE];
    rio_t srio;

    Rio_readinitb(&srio,serverfd);
    while((n = Rio_readlineb(&srio,buf,MAXLINE)) != 0){
        Rio_writen(clientfd,buf,n);

        if(n + size <= MAX_OBJECT_SIZE){
            sprintf(content + size,"%s",buf);
            size += n;
        }else{
            size = MAX_OBJECT_SIZE + 1;
        }

    }

    writecache(content,uri);

}


/*-----cache start-----*/
void rwlock_init(){
    rw->readcnt = 0;
    sem_init(&rw->lock,0,1);
    sem_init(&rw->writeLock,0,1);
}

void writecache(char *buf,char *url){
    sem_wait(&rw->writeLock);           //等待获得写者锁
    int index;
    /*看看缓存有没有空位*/
    for(index = 0;index < MAX_CACHE;index++){
        if(cache[index].lruNumber == 0){
            break;
        }
    }

    /*没有空位,根据LRU政策驱逐*/
    if(index == MAX_CACHE){
        int minlru = cache[0].lruNumber;

        /*找到最后访问的缓存*/
        for(int i = 1;i < MAX_CACHE;i++){
            if(cache[i].lruNumber < minlru){
                minlru = cache[i].lruNumber;
                index = i;
            }
        }
    }

    cache[index].lruNumber = maxlrucache()+1;
    strcpy(cache[index].url,url);
    strcpy(cache[index].content,buf);
    sem_post(&rw->writeLock);           //释放锁

    return;

}

char *readcache(char *url){
    sem_wait(&rw->lock);            //读者等待并获取锁
    if(rw->readcnt == 1)            
        sem_wait(&rw->writeLock);   //读者在读,不允许有写者
    rw->readcnt++;                  
    sem_post(&rw->lock);            //释放锁

    char *content = NULL;
    for(int i = 0;i < MAX_CACHE;i++){
        /*找到了对应的缓存*/
        if(strcmp(url,cache[i].url) == 0){
            content = (char *)Malloc(strlen(cache[i].content));
            strcpy(content,cache[i].content);
            int maxlru = maxlrucache();     //获取最大的lru
            cache[i].lruNumber = maxlru+1;  //+1成最大的lru
            break;
        }
    }

    sem_wait(&rw->lock);            //等待并获取锁  
    rw->readcnt--;
    if(rw->readcnt == 0)            //没有读者了,释放写者锁
        sem_post(&rw->writeLock);
    sem_post(&rw->lock);            //释放锁
    return content;
}

int maxlrucache(){
    int i;
    int max=0;
    for(i = 0;i<MAX_CACHE;i++){
        if(cache[i].lruNumber > max){
            max = cache[i].lruNumber;
        }
    }
    return max;
}
/*-----cache end-----*/

运行结果
在这里插入图片描述
可以看到第三部分已经完成

结语

本实验的检测分数不是太严格,实现基本功能就可以满分了。比如我把锁部分的代码删除一样也是满分,所有上面的代码不一定是严格正确的。

  • 6
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
proxylab实验是计算机网络课程中非常重要的一部分,它旨在帮助学生理解代理服务器的工作原理和实现方法。在这个实验中,学生需要根据要求完善一个代理服务器的代码,使其能够正确地处理来自客户端和服务器HTTP请求和响应。 在实验的开始阶段,学生需要理解代理服务器的基本功能和工作流程,包括如何接收、解析和转发HTTP请求,以及如何接收、解析和转发HTTP响应。接着,他们需要根据课程提供的框架代码,实现代理服务器的各个功能模块,例如HTTP请求的解析、目标服务器的连接、响应的转发等。 在实验过程中,学生需要面对各种挑战,可能包括处理不同类型的HTTP请求、处理多个并发连接、正确处理各种错误情况等。通过完成这些挑战,他们能够加深对代理服务器工作原理的理解,提高自己的编程能力和调试能力。 最终,完成proxylab实验的学生将能够掌握代理服务器的基本原理和实现方法,理解计算机网络HTTP协议的具体应用,提高自己的网络编程技能。这对他们未来的学习和工作都将是极大的帮助。同时,通过这个实验,他们也能够更好地理解课程中的理论知识,加深对计算机网络课程的学习效果。proxylab实验是一个很有挑战性和收获丰富的实践环节,对于培养学生的实际动手能力和解决问题能力有着重要的意义。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值