[系统架构]一种裸机代码/系统架构-基于“客户端-服务器”的系统设计方法

一、动机

  1. 模块化设计 ,四个典型的降低系统复杂度的有效方案:模块化、抽象、层以及层次结构。显然模块化设计是其中之一,并且也很常见。
  2. 模块的自治性,希望一个模块的运行尽可能少地依赖其他模块,甚至做到完全不依赖。模块具有一定的容错性,在一定程度上,当它依赖的资源失效时,模块应当有合理的处置方案,并尽到停止服务以及通知的义务。

二、使用“客户端-服务器”模型强制模块化

该段落引用自《Principle Of Computer System Design An Introduction》, Jerome H. Saltzer ,M. Frans Kaashoek.
操作系统的一个重要任务是实现“客户端-服务器”组织结构,在这种组织结构中,模块之间仅通过发送消息进行交互,这种组织有三个好处。
1)消息是程序员请求模块提供服务的唯一途径,这将使得程序员更难以违反模块化的约定。
2)消息是错误在模块之间传播的唯一途径,如果客户端/服务器二者之一任意一个失效,只要它们有各自的消息检查机制,那么就能够限制错误从一个模块传输到另一个模块。
3)消息是攻击者侵入模块的唯一途径,因此,如果模块检查输入消息,那么就能够规避大部分攻击。

三、arm cc工具链支持将资源指定到特定的代码段

观察下面的两个代码片段,考虑使用module_register_taskFunc(“task1”,task1StepFunction)生成一个module_task_item_t结构体类型的变量,它会被存放到名为task.item.1的字段中,并且驻留于ROM空间(因为是用const修饰的),arm cc工具链会将task.item.0,task.item.1,task.item.2三个字段自动按顺序排列,那么在运行module_task函数时,我们新建的task1StepFunction函数会被运行。

这个特性意味着:我们在工程中新增或删除一个文件时,无需修改工程中的主框架(如main函数的内容,module_task函数的内容)就可以完成功能的增删。事实上,这个套路在很多地方都可以见到,比如linux内核,uboot中都可以见到,并且笔者的思路也源于此。

注:下面的以阿拉伯数字为代码段定顺序的方案目前可以在arm cc上正确得到结果,但是运用在gcc中时需要进行改动,笔者目前也不知道为什么,谨在此指出以强调不同情况具有差异,不适合完全照搬。

static void nop_process(void){}

//第一个任务项目,段入口
static const module_task_item_t task_table_start __attribute__((section("task.item.0"))) = {"",nop_process};
//最后一个任务项目,段结尾
static const module_task_item_t task_table_end   __attribute__((section("task.item.2"))) = {"",nop_process};

void module_task(void){
	const module_task_item_t* pTaskFunc;
	for(pTaskFunc = &task_table_start + 1; pTaskFunc < &task_table_end; pTaskFunc++){
		if(pTaskFunc->taskHandler != NULL)
			pTaskFunc->taskHandler();
	}
}
#ifndef _MODULE_FRAME_H_
#define _MODULE_FRAME_H_

/*任务处理项*/
typedef struct {
	const char* name;
	void (*taskHandler)(void);
}module_task_item_t;

#define __module_task(name,func)	\
	__attribute__((used)) const module_task_item_t task_module_##func##__LINE__ \
	__attribute__((section("task.item.1"))) = {name,func}

//注册模块任务函数
#define module_register_taskFunc(name,func) __module_task(name,func)
extern void module_task(void);

#endif

四、拓展方法的适用范围

本质上,存入内存中的是数据。结构体中的内容可以是函数指针,它同样也可以是变量指针!来看下面的例子,考虑构建一个受信任的存储服务。

“受信任的存储服务”

1)什么是受信任的服务?笔者认为这和做人是一样的,可以信任的人做事情,在他界定的服务与对未知事物掌控度的范围内,能做就会告诉你能做,不能做(比如违反安全协议,超出能力)就是不能做,正常提供服务的过程中如果遇到错误应该及时报错,客户可以据此来决定下一步该怎么走。当然,人有主观能动性,这是远远优于机器的地方,对于机器来说,必须要仔细界定服务的边界

2)第一点描述的内容中隐含了一个条件:一个提供受信任的服务的模块必须是自主的。客户端只能通过发消息请求服务(读服务,写服务等等)的方式来获取数据读写服务,否则如果只是提供对内存的读写过程函数,那就无所谓什么受不受信任的说法了。

事实上,linux系统中通过system call(系统调用)来获取内核服务,freeRTOS中使用信号量、消息队列等方式来请求其他任务提供的服务。它们在使用上看起来只是调用了一个函数,但事实上这是远程过程调用(remote process call)的实现,目的在于降低应用程序的复杂度,让请求服务的行为看起来和调用一个常规函数是一样的,这有利于不同水平的开发人员异步维护同一个系统。

3)下面的代码中的数据存储服务接收来自客户端的数据读请求以及数据写请求,存储服务进行合法性检查,紧接着处理请求,最后向客户端发送响应,这些响应可以是读成功响应,可以是写成功响应,当然也可以是非法地址响应以及内存错误响应。

存储服务c文件。

/*********************************************************************
 * INCLUDES
 */
#include "trusted_storage_service.h"
#include <string.h>
#include "module_frame.h"
#include "stm32f10x_flash.h"

/*********************************************************************
 * LOCAL
 */
//机器特性
#define FLASH_BLOCK_SIZE	1024
#define FLASH_BLOCK_NUM		32
#define FLASH_WORD_LEN		(4)
#define FLASH_START_ADDR	((uint32_t)0x8000000)
#define FLASH_END_ADDR		((uint32_t)(0x8000000 + FLASH_BLOCK_NUM * FLASH_BLOCK_SIZE))
//人为定义(同样要根据机器特性来确定)
#define FLASH_PAGE_SIZE		128		//128字节
#define FLASH_PAGE_NUM		256		//256页

#define FLASH_REDO_TIMES	3		//当内存失效时,重复操作次数

const storageService_client_port_t storage_client_table_start __attribute__((section("storage.client.0"))) = {"",0,0,0,NULL,NULL,NULL,NULL,NULL};
const storageService_client_port_t storage_client_table_end   __attribute__((section("storage.client.2"))) = {"",0,0,0,NULL,NULL,NULL,NULL,NULL};

static uint8_t flashData_writeCache[FLASH_BLOCK_SIZE];	//flash数据写缓冲,驻留在ram中
static uint8_t flashData_readCache[FLASH_BLOCK_SIZE];	//flash数据读缓冲,驻留在ram中

static void trustedStorageService(void){
	const storageService_client_port_t* pClientPort = NULL;
	
	uint32_t operateAddr = 0, commonTempVariable = 0;
	uint16_t remainOperateBytes = 0, flashBlockUsableBytes = 0, operateBytes = 0; //剩余待操作的字节数,当前块可用字节数,正在操作的字节数
	uint8_t  flashBlockCoordinate = 0;	//当前操纵的块坐标
	uint16_t flashByteCoordinate = 0;	//当前操纵的块内字节坐标
	uint16_t index = 0, operateTimesCounter = 0;
	uint8_t  responseMsgType = 0;	//0:操作成功,1:无效地址,2:存储器故障
			
	for(pClientPort = &storage_client_table_start + 1; pClientPort < &storage_client_table_end; pClientPort++){
		//提取请求
//客户端写请求
		if(!memcmp(pClientPort->pRequestMsg,"write",5)){
			//无效的地址:超出用户私有存储空间范围的,起始地址不是字(4字节)对齐的。
			//无效的写入字节数:写入字节数不是字(4字节)对齐的
			if((*(pClientPort->pStartAddr) < pClientPort->storageStartAddr) || 
			   ((*(pClientPort->pStartAddr) + *(pClientPort->pBytes)) > pClientPort->storageEndAddr + 1) ||
			   (*(pClientPort->pStartAddr) % FLASH_WORD_LEN != 0) || (*(pClientPort->pBytes) % FLASH_WORD_LEN != 0)){
				//响应无效的地址与写入字节数输入(安全裕度原则与鲁棒性原则共存)			
				responseMsgType = 1;
			}else{
				//step1: 初始化操作辅助信息
				operateTimesCounter = 0;	//执行前复位
				responseMsgType = 0;		//执行前复位
				remainOperateBytes = *(pClientPort->pBytes);
				operateAddr = *(pClientPort->pStartAddr);
				flashBlockCoordinate = (operateAddr - FLASH_START_ADDR) / FLASH_BLOCK_SIZE;
				flashByteCoordinate  = (operateAddr - FLASH_START_ADDR) % FLASH_BLOCK_SIZE;
				flashBlockUsableBytes = FLASH_BLOCK_SIZE - flashByteCoordinate;
				while(remainOperateBytes){				
					//step2: 读取块并校验读取数据
					for(operateTimesCounter = 0; operateTimesCounter < FLASH_REDO_TIMES; operateTimesCounter++){
						for (index = 0; index < FLASH_BLOCK_SIZE / FLASH_WORD_LEN; index++){
							commonTempVariable = *(uint32_t*)(FLASH_START_ADDR + flashBlockCoordinate * FLASH_BLOCK_SIZE + index * 4);
							flashData_readCache[index * FLASH_WORD_LEN + 0] = commonTempVariable >>  0;
							flashData_readCache[index * FLASH_WORD_LEN + 1] = commonTempVariable >>  8;
							flashData_readCache[index * FLASH_WORD_LEN + 2] = commonTempVariable >> 16;
							flashData_readCache[index * FLASH_WORD_LEN + 3] = commonTempVariable >> 24;
						}
						if(0 == memcmp(flashData_readCache,(uint8_t*)(FLASH_START_ADDR + flashBlockCoordinate * FLASH_BLOCK_SIZE),FLASH_BLOCK_SIZE)){
							break;	//数据校验通过,跳出当前for循环
						}
					}
					if(operateTimesCounter >= FLASH_REDO_TIMES){
						responseMsgType = 2;	//存储器故障,直接退出
						break;
					}
					//step3: 更新数据
					memcpy(flashData_writeCache,flashData_readCache,FLASH_BLOCK_SIZE);
					operateBytes = remainOperateBytes;
					if(operateBytes > flashBlockUsableBytes){
						operateBytes = flashBlockUsableBytes;					
					}
					memcpy(&flashData_writeCache[flashByteCoordinate],
						   &(pClientPort->clientBuf)[*(pClientPort->pBytes)-remainOperateBytes],operateBytes);
					//step4: 写回flash并校验写入数据
						//应用程序与BootLoader区域的最后一道安全屏障
					if((flashBlockCoordinate <= FLASH_FORBIDDEN_STOP_BLOCK_1) ||
					   (flashBlockCoordinate >= FLASH_FORBIDDEN_START_BLOCK_2 && flashBlockCoordinate <= FLASH_FORBIDDEN_STOP_BLOCK_2)){
						//非法入侵BootLoader与应用程序区域
						responseMsgType = 1;
						break;		//跳出while循环				   
					}else if(0 != memcmp(flashData_writeCache,flashData_readCache,FLASH_BLOCK_SIZE)){	//即将写入的数据与读出数据不同才写入
						//至少执行一次,至多执行3次,如果flash发生块损坏不能一直在这里停着
						for(operateTimesCounter = 0; operateTimesCounter < FLASH_REDO_TIMES; operateTimesCounter++){
							__disable_irq();
							FLASH_Unlock();
							FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
							FLASH_ErasePage(FLASH_START_ADDR + flashBlockCoordinate * FLASH_BLOCK_SIZE);
							FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);	
							for (index = 0; index < FLASH_BLOCK_SIZE / FLASH_WORD_LEN; index++){
								commonTempVariable = 0U;
								commonTempVariable |= (uint32_t)flashData_writeCache[index * FLASH_WORD_LEN + 0] <<  0;
								commonTempVariable |= (uint32_t)flashData_writeCache[index * FLASH_WORD_LEN + 1] <<  8;
								commonTempVariable |= (uint32_t)flashData_writeCache[index * FLASH_WORD_LEN + 2] << 16;
								commonTempVariable |= (uint32_t)flashData_writeCache[index * FLASH_WORD_LEN + 3] << 24;				
								FLASH_ProgramWord(FLASH_START_ADDR + flashBlockCoordinate * FLASH_BLOCK_SIZE + index * FLASH_WORD_LEN, commonTempVariable);
								FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR);
							}				 
							FLASH_Lock();
							__enable_irq();
							//读出
							for (index = 0; index < FLASH_BLOCK_SIZE / FLASH_WORD_LEN; index++){
								commonTempVariable = *(uint32_t*)(FLASH_START_ADDR + flashBlockCoordinate * FLASH_BLOCK_SIZE + index * 4);
								flashData_readCache[index * FLASH_WORD_LEN + 0] = commonTempVariable >>  0;
								flashData_readCache[index * FLASH_WORD_LEN + 1] = commonTempVariable >>  8;
								flashData_readCache[index * FLASH_WORD_LEN + 2] = commonTempVariable >> 16;
								flashData_readCache[index * FLASH_WORD_LEN + 3] = commonTempVariable >> 24;
							}
							if(0 == memcmp(flashData_writeCache,flashData_readCache,FLASH_BLOCK_SIZE)){
								break;	//数据校验通过,跳出当前for循环
							}
						}
					}
					//调整下一个待操作块坐标与字节坐标
					if(operateTimesCounter >= FLASH_REDO_TIMES){
						responseMsgType = 2;	//存储器故障,直接退出
						break;
					}else{
						operateAddr += operateBytes;
						flashBlockCoordinate = (operateAddr - FLASH_START_ADDR) / FLASH_BLOCK_SIZE;
						flashByteCoordinate  = (operateAddr - FLASH_START_ADDR) % FLASH_BLOCK_SIZE;
						flashBlockUsableBytes = FLASH_BLOCK_SIZE - flashByteCoordinate;
						remainOperateBytes -= operateBytes;
					}
				}
			}
			//发送响应
			switch(responseMsgType){
				case 0:
					memcpy((void*)(pClientPort->pResponseMsg),"w_operation_ok",14);
					break;
				case 1:
					memcpy((void*)(pClientPort->pResponseMsg),"w_invalid_addr",14);
					break;
				case 2:
					memcpy((void*)(pClientPort->pResponseMsg),"w_mem_break",11);
					break;
				default:
					break;
			}			
			memset((void*)(pClientPort->pRequestMsg),0,5);
		}
//客户端读请求
		else if(!memcmp(pClientPort->pRequestMsg,"read",4)){
			//无效的地址:超出用户私有存储空间范围的,起始地址不是字(4字节)对齐的。
			//无效的写入字节数:写入字节数不是字(4字节)对齐的
			if((*(pClientPort->pStartAddr) < pClientPort->storageStartAddr) || 
			   ((*(pClientPort->pStartAddr) + *(pClientPort->pBytes)) > (pClientPort->storageEndAddr + 1)) ||
			   (*(pClientPort->pStartAddr) % FLASH_WORD_LEN != 0) || (*(pClientPort->pBytes) % FLASH_WORD_LEN != 0)){
				//响应无效的地址与写入字节数输入(安全裕度原则与鲁棒性原则共存)			
				responseMsgType = 1;
			}else{
				//step1: 初始化操作辅助信息
				operateTimesCounter = 0;	//执行前复位
				responseMsgType = 0;		//执行前复位
				remainOperateBytes = *(pClientPort->pBytes);
				operateAddr = *(pClientPort->pStartAddr);
				flashBlockCoordinate = (operateAddr - FLASH_START_ADDR) / FLASH_BLOCK_SIZE;
				flashByteCoordinate  = (operateAddr - FLASH_START_ADDR) % FLASH_BLOCK_SIZE;
				flashBlockUsableBytes = FLASH_BLOCK_SIZE - flashByteCoordinate;
				while(remainOperateBytes){				
					//step2: 读取块并校验读取数据
					if((flashBlockCoordinate <= FLASH_FORBIDDEN_STOP_BLOCK_1) ||
					   (flashBlockCoordinate >= FLASH_FORBIDDEN_START_BLOCK_2 && flashBlockCoordinate <= FLASH_FORBIDDEN_STOP_BLOCK_2)){
						//非法入侵BootLoader与应用程序区域
						responseMsgType = 1;
						break;		//跳出while循环				   
					}
					
					for(operateTimesCounter = 0; operateTimesCounter < FLASH_REDO_TIMES; operateTimesCounter++){
						for (index = 0; index < FLASH_BLOCK_SIZE / FLASH_WORD_LEN; index++){
							commonTempVariable = *(uint32_t*)(FLASH_START_ADDR + flashBlockCoordinate * FLASH_BLOCK_SIZE + index * 4);
							flashData_readCache[index * FLASH_WORD_LEN + 0] = commonTempVariable >>  0;
							flashData_readCache[index * FLASH_WORD_LEN + 1] = commonTempVariable >>  8;
							flashData_readCache[index * FLASH_WORD_LEN + 2] = commonTempVariable >> 16;
							flashData_readCache[index * FLASH_WORD_LEN + 3] = commonTempVariable >> 24;
						}
						if(0 == memcmp(flashData_readCache,(uint8_t*)(FLASH_START_ADDR + flashBlockCoordinate * FLASH_BLOCK_SIZE),FLASH_BLOCK_SIZE)){
							break;	//数据校验通过,跳出当前for循环
						}
					}
					if(operateTimesCounter >= FLASH_REDO_TIMES){
						responseMsgType = 2;	//存储器故障
						break;	//跳出while循环
					}
					//将数据写入用户缓冲
					operateBytes = remainOperateBytes;
					if(operateBytes > flashBlockUsableBytes){
						operateBytes = flashBlockUsableBytes;					
					}
					memcpy((uint8_t*)&(pClientPort->clientBuf)[*(pClientPort->pBytes)-remainOperateBytes],
						   &flashData_readCache[flashByteCoordinate],operateBytes);
					//调整下一个待操作块坐标与字节坐标
					operateAddr += operateBytes;
					flashBlockCoordinate = (operateAddr - FLASH_START_ADDR) / FLASH_BLOCK_SIZE;
					flashByteCoordinate  = (operateAddr - FLASH_START_ADDR) % FLASH_BLOCK_SIZE;
					flashBlockUsableBytes = FLASH_BLOCK_SIZE - flashByteCoordinate;
					remainOperateBytes -= operateBytes;
				}
			}
			//发送响应
			switch(responseMsgType){
				case 0:
					memcpy((void*)(pClientPort->pResponseMsg),"r_operation_ok",14);
					break;
				case 1:
					memcpy((void*)(pClientPort->pResponseMsg),"r_invalid_addr",14);
					break;
				case 2:
					memcpy((void*)(pClientPort->pResponseMsg),"r_mem_break",11);
					break;
				default:
					break;
			}
			memset((void*)(pClientPort->pRequestMsg),0,4);
		}		
	}
}module_register_taskFunc("storage_service",trustedStorageService);

存储服务头文件片段。

#ifndef _TRUSTED_STORAGE_SERVICE_H_
#define _TRUSTED_STORAGE_SERVICE_H_

#include <stdint.h>

/************************************************************************
 * CONSTANT-MACROS AND ENUMERATIONS
 */
/*
-----------------------------------------------------------------

	0x08000000 -----------------------------------
			   |		6kB bootload			 |
    0x08001800 -----------------------------------
			   |	    2KB update msg		     |
	0x08002000 -----------------------------------
			   |        22kB app area			 |
	0x08007800 -----------------------------------
			   |        2KB  NV data area        | <-
			   -----------------------------------

-----------------------------------------------------------------
*/
//禁止用户操作的区域
#define FLASH_FORBIDDEN_START_AREA_1	(uint32_t*)0x08000000		//BootLoader程序区域
#define FLASH_FORBIDDEN_STOP_AREA_1		(uint32_t*)0x080017FF

#define FLASH_FORBIDDEN_START_BLOCK_1	0	//上面的6KB区域产生6个块索引
#define FLASH_FORBIDDEN_STOP_BLOCK_1	5

#define FLASH_FORBIDDEN_START_AREA_2	(uint32_t*)0x08002000		//应用程序区域
#define FLASH_FORBIDDEN_STOP_AREA_2		(uint32_t*)0x080077FF

#define FLASH_FORBIDDEN_START_BLOCK_2	8
#define FLASH_FORBIDDEN_STOP_BLOCK_2	29

//存储器服务对象应当提供符合下面结构描述的端口
typedef struct {
	const char* clientName;						//客户名称	
	
	const uint32_t storageStartAddr;			//客户独占的存储器起始地址
	const uint32_t storageEndAddr;				//客户独占的存储器结束地址
	const uint16_t storagePageNums;				//客户独占的存储器页数
	
	const unsigned char* clientBuf;				//指向客户负载数据缓冲:要写入的内容,要装载读取数据的容器,校验码
	const uint32_t*      pStartAddr;			//客户请求操作数据的起始地址指针
	const uint16_t*      pBytes;				//客户请求操作的字节数指针
	const unsigned char* pRequestMsg;			//更新存储器请求指针: write,read
	const unsigned char* pResponseMsg;			//更新存储器响应指针:(w_,r_)operation_ok,invalid_addr,mem_break
}storageService_client_port_t;

//注册存储服务对象,客户名称请固定到15字节
#define storageService_register_client(clientName,privateStartAddr,privateEndAddr,privatePageNum, \
									   clientBuf,pOptionStartAddr,pOptionBytes,pRequestMsg,pResponseMsg) \
		__attribute__((used)) const storageService_client_port_t storage_client_##privateStartAddr##__LINE__ \
		__attribute__((section("storage.client.1"))) = {clientName,privateStartAddr,privateEndAddr,privatePageNum, \
		clientBuf,pOptionStartAddr,pOptionBytes,pRequestMsg,pResponseMsg}

#endif

客户端代码段。

//客户端代码段
#define NET_STORAGE_START_ADDR	0x08007800
#define NET_STORAGE_END_ADDR	0x0800797F
#define NET_STORAGE_PAGES		3	//128字节/页

#define NET_STORAGE_CACHE_SIZE	400

uint8_t  netStorageLoadBuf[NET_STORAGE_CACHE_SIZE];
uint32_t netStorageOptionStartAddr;
uint16_t netStorageOptionBytes;
uint8_t  netStorageRequestMsg[6];	//write,read,check
uint8_t  netStorageResponseMsg[15];	//option_ok,invalid_addr,mem_break

storageService_register_client("net____business",NET_STORAGE_START_ADDR,NET_STORAGE_END_ADDR,
							   NET_STORAGE_PAGES,
							   netStorageLoadBuf,&netStorageOptionStartAddr,&netStorageOptionBytes,
							   netStorageRequestMsg,netStorageResponseMsg);
void netManagementInit(void){
//初始化期间向存储服务发起读mac地址请求
	netStorageOptionStartAddr = NET_STORAGE_START_ADDR;
	netStorageOptionBytes = NET_STORAGE_PAGES * 128;
	memset(netStorageResponseMsg,0,15);		
	memcpy(netStorageRequestMsg,"read",4);
}module_server_register_initFunc("net_manager",netManagementInit);

void netManagementTask(void){
//无线参数存储事务-读响应
	if(!memcmp(netStorageResponseMsg,"r_operation_ok",14)){
		//to do
		memset(netStorageResponseMsg,0,14);
	}
}module_register_taskFunc("net_manager",netManagementTask);

4)利弊。

  1. 笔者在裸机实现中需要将请求过程显式地表达出来,因此相对于RPC实现必然是有不足的,但是对于程序员自己维护整个工程而言,是有利的。
  2. 上面的程序是裸机程序,以轮询的方式运行,因此客户端与服务器是异步运行的。在操作系统中,无论是system call内核陷入,还是信号量与消息队列的任务立即同步,其实时性是有保障的。特别是如果在轮询过程中,对按键检测的消抖不释放cpu的干等延时操作会导致系统实时性变差,因此需要仔细设计系统的组织方式并严格规避干等延时操作。但是相对而言,这样的组织方式足够简单清晰明了,程序员在组织系统的过程中会有意识地考量系统自主、自治以及系统边界的内容,对于提升系统稳定性是很有帮助的。

五、写在最后

上面的内容是笔者在实践应用中学习积累的经验之谈,受限于本人水平,必然会有错误以及不当之处,所以在分享个人经验的同时,也欢迎广大读者勘误讨论,发表自己的意见看法,共同交流,共同进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值