TCP数据包无边界性问题与解决方案实现

一、引子

从数据从socket缓冲区和数据的过程可知,数据的接收和发送是无关的,recv()/read()函数不管数据发送了多少次都会尽可能的接收更多的数据, 也就是说recv()/read(),send()/write()的执行次数可能不同。

例如write()/send()重复执行了三次,每次都发送字符串''xxzzff'',那么对等方可分三次接收,也可分两次接收"xxzzffxxzzff", 可能能一次就接受到了"xxzzffxxzzffxxzzff"。

假设我们希望客户端每次发送一位学生的学号,让服务器返回该学生的姓名、住址、成绩等信息,这时候就会出现问题,服务器不能区分学生的学号。例如第一次发送1,第二次发送3。服务器可能把它当作学号是13来处理,返回的信息也必然是错误的。

这便是TCP的"粘包问题",也称数据的无边界性。因为TCP的头部不包含长度字段,如图所示:

相比于UDP就不是,UDP不是流式协议,他有数据长度字段,对等方知道从哪儿开始到哪儿结束;但是双方不管你接没接收到,反正我是发给你了 要么全部接收到,要么没接收到,不会出现像TCP那样的粘包,丢包等问题。所以UDP的速度方面是远超与TCP的。当然这些必须基于物理层与通信链路层无差错的情况下。

以下用代码来解释TCP粘包问题:

limit_serv.c (服务器程序)

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

#define PORT 8888
#define BUF_SIZE 100
#define Err_exit(m) \
    do\
    {\
    perror(m);\
    exit(EXIT_FAILURE);\
    }while(0);

int serv_sock, clnt_sock = 0;

static void handle(int signo)
{
    close(clnt_sock);
    close(serv_sock);
    exit(-1);
}

int main(int ac, char **av)
{
    int  recvlen;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t socklen = sizeof(clnt_addr);
    char buf[BUF_SIZE] = {0};

    if((serv_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	Err_exit("socket");

    memset(&serv_addr, 0, socklen);
    serv_addr.sin_family = AF_INET;
    if(*(av + 1))
	serv_addr.sin_addr.s_addr = inet_addr(*(av + 1));
    else
        serv_addr.sin_addr.s_addr = INADDR_ANY;
    if(*(av + 2))
	serv_addr.sin_port = htons(atoi(*(av + 2)));
    else
	serv_addr.sin_port = htons(PORT);

    if(bind(serv_sock, (struct sockaddr *)&serv_addr, socklen) < 0)
	Err_exit("bind");

    if(listen(serv_sock, 10) < 0)
	Err_exit("listen");
	
    clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &socklen);
	
    if(clnt_sock != 0)
    {
        printf("User ip:%s port:%d has online\n", inet_ntoa(clnt_addr.sin_addr),    ntohs(clnt_addr.sin_port));
    }
	
    sleep(10);//注意这里让程序停止10秒

    recvlen = recv(clnt_sock, buf, BUF_SIZE, 0);
    fprintf(stdout, "Message from client %s\n", buf);
    shutdown(clnt_sock, SHUT_RD);
    send(clnt_sock, buf, recvlen, 0);
	
    close(clnt_sock);
    close(serv_sock);
    return 0;
}

limit_clnt.c (客户端程序)

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

#define PORT 8888
#define BUF_SIZE 245

#define Err_exit(m) \
	do\
	{\
		perror(m);\
		exit(EXIT_FAILURE);\
	}while(0);

int clnt_sock;
static void handle(int signo)
{
	close(clnt_sock);
	exit(-1);
}
int main(int ac, char **av)
{
	int  i;
	struct sockaddr_in clnt_addr;
	socklen_t socklen = sizeof(clnt_addr);
	char buf[BUF_SIZE] = {0};
	char echo[BUF_SIZE] = {0};//用两个不同的buf以示区分

	signal(SIGINT, handle);
	if((clnt_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		Err_exit("socket");

	memset(&clnt_addr, 0, socklen);
	clnt_addr.sin_family = AF_INET;
	if(*(av + 1))
		clnt_addr.sin_addr.s_addr = inet_addr(*(av + 1));
	else
		clnt_addr.sin_addr.s_addr = INADDR_ANY;
	if(*(av + 2))
		clnt_addr.sin_port = htons(atoi(*(av + 2)));
	else
		clnt_addr.sin_port = htons(PORT);

	if(connect(clnt_sock, (struct sockaddr *)&clnt_addr, sizeof(clnt_addr)) < 0)
		Err_exit("connect");

	strcpy(buf, "xxzzff");
	printf("i will send to server>>>>:%s\n", buf);

	for(i = 0; i < 20; i++){
		send(clnt_sock, buf, strlen(buf) + 1, 0);}

	puts("11111111");
	recv(clnt_sock, echo, BUF_SIZE, 0);
	printf("Message form server echo is %s\n", echo);

	close(clnt_sock);
	return 0;
}

 运行结果为:

.........

Message feom server echo is: xxzzffxxzzffxxzzff

解析:客户端发送字符串xxzzff,服务器等待10s后才开始接收 从缓冲区中一次性读取所有积压的数据,然后回射发送给客户端。

 

二、解决方案

①接收定长包

②包尾加\t\n 适用于ftp

③包头定义包体长度

④使用更加复杂的应用层程序

 

三、具体实现

接收定长包:

1>重写write()/send()函数为writen()  和read()/recv()函数为readn() 让其定向的接收:

readn() 代码如下:

ssize_t readn(int fd, void *buf, size_t count)
{
    char *bufp = (char *)buf;
    size_t nleft = count;//还剩下的字节数
    ssize_t nread;//TCP读到的字节数

    while(nleft > 0)
    {
	if((nread = read(fd, bufp, nleft)) < 0)
	{
	    if(errno == EINTR)//如果捕捉到的异常信号将忽略
	        continue;
	    else//函数调用失败
		return (-1);
        }
	else if(nread == 0)//对等方关闭了
	    return (count - nleft);//返回实际从底层读取的字节数

	bufp += nread;
	nleft -= nread;
    }

    return count;
}

 writen()代码如下:

ssize_t writen(int fd, void *buf, size_t count)
{
    char *bufp = (char *)buf;
    size_t nleft = count;
    ssize_t nwrite;

    while(nleft > 0)
    {
	if((nwrite = write(fd, bufp, nleft)) < 0)
	{
	    if(errno == EINTR)
	        continue;
	    else
		return (-1);
	}
	else if(nwrite == 0)
	    return (count - nleft);//返回实际已经交付底层的字节数

	bufp += nwrite;
	nleft -= nwrite;
    }

    return count;
}

2> 定义一个头部协议

//定义头部协议 用于接收定长包
typedef struct packet
{
	int len;//包头
	char buf[BUF_SIZE];//包体
}Packet;

 代码实现:

dcjs_serv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define BUF_SIZE 245
#define PORT 8888

#define Err_exit(m) \
    do\
    {\
	perror(m);\
	exit(EXIT_FAILURE);\
    }while(0);


int semid;//信号量标识符

//定义头部协议 用于接收定长包
typedef struct packet
{
    int len;//包头
    char buf[BUF_SIZE];//包体
}Packet;

union semnum
{
    int val;/*value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux specific) */
}sem_num;


struct sembuf p = {0, -1, SEM_UNDO}; 
struct sembuf v = {0, 1, SEM_UNDO};

int O_Response(int com_sock, struct sockaddr_in *peer);
int O_Service(int com_sock, struct sockaddr_in *peer);//服务器提供的服务

//这里封装read()和write()函数 仅用于Linux socket
ssize_t readn(int fd, void *buf, size_t count);//接收定长消息
ssize_t writen(int fd, void *buf, size_t count);//发送定长消息

static void handle(int signo)
{
    printf("I have attach the signal num(%d)\n", signo);
    printf("i will exit process after 3 secs!\n");
    sleep(3);
    semctl(semid, 0, IPC_RMID);
    exit(0);
}

int main(void)
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    socklen_t socklen = sizeof(clnt_addr);
    int on = 1, ret;//设置套接字选项结构,因不同选项而异 后者为函数返回值
    pid_t pid;
    key_t key;
	
    signal(SIGINT, handle);//收到终止信号^c
	
    key = ftok("../", 'b');
    if((semid = semget(key, 1, IPC_CREAT | 0666)) < 0)
	Err_exit("semget");

    sem_num.val = 1;
    semctl(semid, 0, SETVAL, sem_num);
	
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_addr.s_addr = INADDR_ANY;
    serv_addr.sin_port = htons(PORT);

    if((serv_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
	Err_exit("socket");
	
    //将套接字设置为地址可复用类型
    if(setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
	Err_exit("setsockopt");

    if(bind(serv_sock, (struct sockaddr *)&serv_addr, socklen) < 0)
	Err_exit("bind");

    if(listen(serv_sock, SOMAXCONN) < 0)
	Err_exit("listen");

    while(1)
    {
	printf("i am wait peer connecting!\n");
	if((clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_addr, &socklen)) < 0)
	    Err_exit("accept");
		
	fprintf(stdout, "User ip:%s port:%d has online\n", inet_ntoa(clnt_addr.sin_addr), ntohs(clnt_addr.sin_port));
	pid = fork();

	if(pid < 0)
        {
            Err_exit("fork");
        }
	else if(pid > 0)//parent process
        {
	    ret = O_Response(clnt_sock, &clnt_addr);
	    waitpid(pid, NULL, WNOHANG);//WNOHANG参数表明如果没有收集到子进程waitpid()不会在这里等待s
	}
	else{//child parent
	    ret = O_Service(clnt_sock, &clnt_addr);
	    exit(EXIT_SUCCESS);
        }
		
    }
    close(clnt_sock);
    close(serv_sock);//异常退出
    return 0;
}


ssize_t readn(int fd, void *buf, size_t count)
{
    char *bufp = (char *)buf;
    size_t nleft = count;//还剩下的字节数
    ssize_t nread;//TCP读到的字节数

    while(nleft > 0)
    {
        if((nread = read(fd, bufp, nleft)) < 0)
	{
	    if(errno == EINTR)//如果捕捉到的异常信号将忽略
		continue;
	    else//函数调用失败
		return (-1);
        }
	else if(nread == 0)//对等方关闭了
	    return (count - nleft);//返回实际从交付底层取到的字节数
    
        bufp += nread;
	nleft -= nread;
    }

    return count;
}

ssize_t writen(int fd, void *buf, size_t count)
{
    char *bufp = (char *)buf;
    size_t nleft = count;
    ssize_t nwrite;

    while(nleft > 0)
    {
	if((nwrite = write(fd, bufp, nleft)) < 0)
	{
	    if(errno == EINTR)
		continue;
	    else
		return (-1);
	}
	else if(nwrite == 0)
	    return (count - nleft);//返回实际已经交付底层的字节数

	bufp += nwrite;
	nleft -= nwrite;
    }

    return count;
}


//专门用于服务器子进程来接收对等方子进程的服务请求
int O_Service(int com_sock, struct sockaddr_in *peer)
{
    Packet recvbuf;
    bzero(&recvbuf, sizeof(recvbuf));
    int ret, real;

//规则:先接收4个字节,再接收real个字节	
    while(1)
    {
        semop(semid, &p, 1);//P
	if((ret = readn(com_sock, &recvbuf.len, sizeof(int))) < 0)//首先接收包头
	    Err_exit("readn");//函数调用失败
	if((ret >= 0) && (ret < sizeof(int)))//对等方中途断开连接
	{
	    printf("User ip:%s, port:%d has offline\n", inet_ntoa(peer->sin_addr), ntohs(peer->sin_port));
	    break;
	}
		
	real = ntohl(recvbuf.len);
	if((ret = readn(com_sock, recvbuf.buf, real)) < 0)
		Err_exit("readn");
	if((ret >= 0) && (ret < real))//对等方下线或中途下线
	{
	    printf("User ip:%s, port:%d has offline\n", inet_ntoa(peer->sin_addr), ntohs(peer->sin_port));
	    break;	
	}
	fprintf(stdout, "Message from client is :%s\n", recvbuf.buf);
	ret = semctl(semid, 0, GETVAL, sem_num.val);
        printf("\t当前信号量值为:%d\n", ret);

	writen(com_sock, &recvbuf, sizeof(int) + real);//Echo

	semop(semid, &v, 1);//v
	ret = semctl(semid, 0, GETVAL, sem_num.val);
	printf("\t当前信号量值为:%d\n", ret);

	if(strcmp(recvbuf.buf, "quit") == 0)//客户端主动发起断开请求
	    return 0;
	bzero(&recvbuf, sizeof(recvbuf));
    }
	
    return 0;
}


//用于父进程,实现双工通信
int O_Response(int com_sock, struct sockaddr_in *peer)
{
    Packet sendbuf;
    Packet recvbuf;//Client Echo 
    int ret, real;
    bzero(&sendbuf, sizeof(sendbuf));
    bzero(&recvbuf, sizeof(recvbuf));
	
    while(1)
    {
        printf("server send Message to client\n>>>>:");
	fgets(sendbuf.buf, BUF_SIZE, stdin);
	real = strlen(sendbuf.buf)+1;
	sendbuf.len = htonl(real);
		
	writen(com_sock, &sendbuf, real+sizeof(int));
		
	//下面为接收客户端回射,父进程只能等子进程接收对等方的子进程发送;接收完了才能接收回射
	semop(semid, &p, 1);//P
	if((ret = readn(com_sock, &recvbuf.len, sizeof(int))) < 0)//首先接收包头
	    Err_exit("readn");//函数调用失败
	if((ret >= 0) && (ret < sizeof(int)))//对等方中途断开连接
	{
	    printf("User ip:%s, port:%d has offline\n", inet_ntoa(peer->sin_addr), ntohl(peer->sin_port));
	    break;
	}
		
	real = ntohl(recvbuf.len);
	if((ret = readn(com_sock, recvbuf.buf, real)) < 0)
	    Err_exit("readn");
	if((ret >= 0) && (ret < real))//对等方下线或中途下线
	{
	    printf("User ip:%s, port:%d has offline\n", inet_ntoa(peer->sin_addr), ntohs(peer->sin_port));
	    break;	
	}

	fprintf(stdout, "\tMessage from client echo is :%s(This Message was mine just now!)\n", recvbuf.buf);
	ret = semctl(semid, 0, GETVAL, sem_num.val);
        printf("\t当前信号量值为:%d\n", ret);
		
	semop(semid, &v, 1);//v
	ret = semctl(semid, 0, GETVAL, sem_num.val);
        printf("\t当前信号量值为:%d\n", ret);
		
	if(strcmp(recvbuf.buf, "quit") == 0)//服务器主动提出拒绝服务
	    return 0;
		
	bzero(&sendbuf, sizeof(sendbuf));
	bzero(&recvbuf, sizeof(recvbuf));
    }
	
    return 0;
}

dcsj_clnt.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define BUF_SIZE 245
#define PORT 8888

#define Err_exit(m) \
	do\
	{\
		perror(m);\
		exit(EXIT_FAILURE);\
	}while(0);


int clnt_sock;
int semid;//信号量标识符

//定义头部协议 用于接收定长包
typedef struct packet
{
	int len;//包头
	char buf[BUF_SIZE];//包体
}Packet;

union semnum
{
	int val;/*value for SETVAL */
    struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
    unsigned short  *array;  /* Array for GETALL, SETALL */
    struct seminfo  *__buf;  /* Buffer for IPC_INFO(Linux specific) */
}sem_num;

#if 0 
sem_num.val = 1;
共用体不能全局为某个成员赋值
#endif

struct sembuf p = {0, -1, SEM_UNDO}; 
struct sembuf v = {0, 1, SEM_UNDO};

//这里封装read()和write()函数 仅用于Linux socket
ssize_t readn(int fd, void *buf, size_t count);//接收定长消息
ssize_t writen(int fd, void *buf, size_t count);//发送定长消息

static void handle(int signo)
{
	printf("I have attach the signal num(%d)\n", signo);
	printf("i will exit process after 3 secs!\n");
	sleep(3);
	close(clnt_sock);
    semctl(semid, 0, IPC_RMID);
    exit(0);
}

int main(int ac, char **av)
{
	int ret, real;
	pid_t pid;
	key_t key;
	struct sockaddr_in clnt_addr;
	Packet sendbuf;
	Packet recvbuf;
	signal(SIGINT, handle);//收到终止信号^c
	
	key = ftok("../../", 'b');
	if((semid = semget(key, 1, IPC_CREAT | 0666)) < 0)
		Err_exit("semget");

	sem_num.val = 1;
	semctl(semid, 0, SETVAL, sem_num);

	if((clnt_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		Err_exit("socket");

	bzero(&sendbuf, sizeof(sendbuf));
	bzero(&recvbuf, sizeof(recvbuf));
	bzero(&clnt_addr, sizeof(clnt_addr));

	clnt_addr.sin_family = AF_INET;
	if(*(av + 1))
		clnt_addr.sin_addr.s_addr = inet_addr(*(av+ 1));
	else
		clnt_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
	if(*(av + 2))
		clnt_addr.sin_port = htons(atoi(*(av + 2)));
	else
		clnt_addr.sin_port = htons(PORT);

	if(connect(clnt_sock, (struct sockaddr *)&clnt_addr, sizeof(clnt_addr)) < 0)
		Err_exit("connect");

	pid = fork();

	if(pid < 0){
		Err_exit("fork");}
	else if(pid > 0){//parent process
		while(1)
		{
    		semop(semid, &p, 1);//P
    		if((ret = readn(clnt_sock, &recvbuf.len, sizeof(int))) < 0)//首先接收包头
    			Err_exit("readn");//函数调用失败
    			
    		if((ret >= 0) && (ret < sizeof(int)))//对等方中途断开连接
    			break;
    		
    		real = ntohl(recvbuf.len);
			
    		if((ret = readn(clnt_sock, recvbuf.buf, real)) < 0)
    			Err_exit("readn");
    		if((ret >= 0) && (ret < real))//对等方下线或中途下线
    			break;
			
    		fprintf(stdout, "Message from server is :%s\n", recvbuf.buf);
    		ret = semctl(semid, 0, GETVAL, sem_num.val);
            printf("\t当前信号量值为:%d\n", ret);
    
    		writen(clnt_sock, &recvbuf, sizeof(int) + real);//Echo to server
    
    		semop(semid, &v, 1);//v
    		ret = semctl(semid, 0, GETVAL, sem_num.val);
    		printf("\t当前信号量值为:%d\n", ret);
    
    		if(strcmp(recvbuf.buf, "quit") == 0){//客户端主动发起断开请求的特殊字符判别
				close(clnt_sock);
				break;
			}
    		bzero(&recvbuf, sizeof(recvbuf));
		}
	}
	else{//child process
		while(1)
		{
			printf("client send Message to server\n>>>>:");
     		fgets(sendbuf.buf, BUF_SIZE, stdin);
     		real = strlen(sendbuf.buf)+1;
     		sendbuf.len = htonl(real);
     		
     		writen(clnt_sock, &sendbuf, real+sizeof(int));
     		
     		//下面是子进程定长接收服务器的回射
     		semop(semid, &p, 1);//P
     		if((ret = readn(clnt_sock, &recvbuf.len, sizeof(int))) < 0)//首先接收包头
     			Err_exit("readn");//函数调用失败
     		if((ret >= 0) && (ret < sizeof(int)))//对等方中途断开连接
     			break;
     		
     		real = ntohl(recvbuf.len);
     		if((ret = readn(clnt_sock, recvbuf.buf, real)) < 0)
     			Err_exit("readn");
			
     		if((ret >= 0) && (ret < real))//对等方下线或中途下线
     			break;	
     
			//打印回射
     		fprintf(stdout, "\tMessage from client echo is :%s(This Message was mine just now!)\n", recvbuf.buf);
     		ret = semctl(semid, 0, GETVAL, sem_num.val);
            printf("\t当前信号量值为:%d\n", ret);
     		
     		semop(semid, &v, 1);//v
     		ret = semctl(semid, 0, GETVAL, sem_num.val);
            printf("\t当前信号量值为:%d\n", ret);

			if(strcmp(recvbuf.buf, "quit") == 0)
			{
				close(clnt_sock);
				break;
			}

			bzero(&sendbuf, sizeof(sendbuf));
			bzero(&recvbuf, sizeof(recvbuf));
		}
	}

	close(clnt_sock);
	return 0;
}


ssize_t readn(int fd, void *buf, size_t count)
{
	char *bufp = (char *)buf;
	size_t nleft = count;//还剩下的字节数
	ssize_t nread;//TCP读到的字节数

	while(nleft > 0)
	{
		if((nread = read(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)//如果捕捉到的异常信号将忽略
				continue;
			else//函数调用失败
				return (-1);
		}
		else if(nread == 0)//对等方关闭了
			return (count - nleft);//返回实际从交付底层取到的字节数

		bufp += nread;
		nleft -= nread;
	}

	return count;
}

ssize_t writen(int fd, void *buf, size_t count)
{
	char *bufp = (char *)buf;
	size_t nleft = count;
	ssize_t nwrite;

	while(nleft > 0)
	{
		if((nwrite = write(fd, bufp, nleft)) < 0)
		{
			if(errno == EINTR)
				continue;
			else
				return (-1);
		}
		else if(nwrite == 0)
			return (count - nleft);//返回实际已经交付底层的字节数

		bufp += nwrite;
		nleft -= nwrite;
	}

	return count;
}

实现结果:

代码分析:

本代码中加入了信号量,将原来的双工通信变成了单工;服务器/客户端都能够发送,也能够接收。但是同时也证明了用两个不同程序产生竞争条件必须使用信号量来解决这种问题,使用二元信号量 就将本来可以接收多客户端的程序变成了一个客户端,能接收是能接收,但是接收了 信号量的值无法为第三个用户服务。再者本程序成为实现了两个程序使用同一个信号量的例子。只要使用相同的键值,我们就能找到相应的信号量标识符(在共享内存,与消息队列中同样适用)ps:我原来是不知道的,这东西必须手动杀死 相应的Linux命令为:ipcs -s  (查看系统中所有信号量集)ipcrm -s <semid> (杀死系统中当前指定的信号量集)。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值