网络编程-read()/write()/assert()函数

本文深入讲解了TCP网络编程中read和write函数的工作原理及其在不同场景下的应用,包括阻塞和非阻塞模式下的行为差异,并探讨了发送和接收缓冲区的概念及read/write调用的阻塞情况。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

序言

基于TCP的网络编程中,使用read/recv接收数据,使用write/send发送数据。sendto/recvfrom也可用于TCP数据传输


1. read函数
  • [1] 头文件

    • unistd.h
  • [2] 函数原型

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

    • 功能:从描述符fd所指向的文件中读取count个字符到buf所指向的缓存区中

    • 参数:

      • fd:文件描述符
      • buf:存放读取数据的地址
      • count:所要读取的字符
  • [3] 函数返回值:分几种情况

    • 1)从普通文件中read数据
      • 成功:返回读到的字节数
      • 已到文件尾或无数据可读:返回0
      • 出错:返回-1
      • 若在读到指定字节数之前读到文件末尾,例如,若在到达文件末端之前还有30个字节,而要求读100个字节,则read返回30,下一次再调用read时,它将返回0(文件末端)
    • 2)从设备文件中read数据

      • 以终端这一设备来讲,通常一次读一行,读到换行符返回
      • 调用read时睡眠等待,直到终端输入换行符才从read一定字符返回,剩下的数据仍保存在内核终端设备缓冲区
    • 3)read用于套接字

      • 若对方结束了连接,返回0
      • 读取过程中信号中断:如果已读取了一部分数据则返回已读取数据,没有读取返回-1且errno为EINTR
  • [4] 补充:socket read中的阻塞和非阻塞

    • 阻塞socket:read一直阻塞等待有数据到来返回,不管数据量多少返回其实际读取的字符数

    • 非阻塞socket:不管socket接收缓冲区是否有数据都会立即返回,有数据返回实际读取的字符长度,没有数据返回-1,并errno为EWOULDBLOCK或者EAGAIN (表示应该设置为阻塞的)


2. write函数
  • [1] 头文件

    • unistd.h
  • [2] 函数原型

    • ssize_t write(int fd, void *buf, size_t length);

    • 功能:把length个字节从buf所指缓冲区中写到描述符fd所指文件中

  • [3] 函数返回值

    • 成功:返回写入的字符数

    • 失败:返回-1

    • 阻塞和非阻塞返回值

      • 阻塞:与read不同(返回实际读取到的字符数),write只有在缓冲区足以放下整个buffer时才返回

      • 非阻塞:返回能够放下的字节数,之后调用则返回-1,并errno = EAGAIN或EWOULDBLOCK


3. 发送和接收缓冲区 && read/write调用阻塞
  • write成功

    • write成功返回,只是buf中的数据被复制到了kernel中的TCP发送缓冲区。至于数据什么时候被发往网络,什么时候被对方主机接收,什么时候被对方进程读取,系统调用层面不会给予任何保证和通知
  • 发送端和接收端缓存

    • 对于每个socket都有自己的发送和接收缓冲区,不管TCP还是UDP

    • send buffer:已经发送到网络的数据依然需要暂存在send buffer中,只有收到对方的ack后,kernel才从buffer中清除这一部分数据,为后续发送数据腾出空间

    • receive buffer:接收端将收到的数据暂存在receive buffer中,自动进行确认。但如果socket所在的进程不及时将数据从receive buffer中取出,最终导致receive buffer填满,由于TCP的滑动窗口拥塞控制,接收端会阻止发送端向其发送数据

  • write和read调用阻塞

    • write调用阻塞:一般来说,由于接收端进程从socket读数据的速度跟不上发送端进程向socket写数据的速度,最终导致发送端write调用阻塞

    • read调用阻塞:从socket的receive buffer拷贝数据到应用程序的buffer中,read调用阻塞,通常是发送端的数据没有到达


4. assert()函数
  • [1] 头文件

    • assert.h
  • [2] 实现

    • 在C标准库中,assert是用来实现的,而不是函数

    • 表达式:int assert(int expression);

    • 功能:先计算表达式 expression ,如果其值为假为0,那么它先向stderr打印一条出错信息,然后通过调用 abort() 来终止程序运行

  • [3] 用法总结

    • 在函数开始时检验传入参数的合理性
    int resetBufferSize(int nNewSize)
    {
      assert(nNewSize >= 0);
      assert(nNewSize <= MAX_BUFFER_SIZE);
      ...
    }
    • 一次只检验一个条件,如果检验多个,断言失败无法直观判断是哪个条件出错

    • 不能使用改变环境的语句

      assert(i++ < 100)
      先断言i是否小于100,再++

      如果出错,比如i在执行之前就为100,那么i++这条命令就没有执行

  • [4] assert的缺点及避免

    • 频繁调用会极大影响程序的性能,增加额外的开销

    • 避免:在调试结束后,在assert.h头文件之前添加 #define NDEBUG 来禁止 assert 调用

#include <stdio.h>

#define NDEBUG
#include <assert.h>



参考文章
http://www.cnblogs.com/xiehongfeng100/p/4619451.html
http://www.cnblogs.com/Dscxy/p/4103833.html
http://www.cnblogs.com/ggzss/archive/2011/08/18/2145017.html

2017.11.03

include "zf_common_debug.h" #include "zf_common_interrupt.h" #include "zf_common_clock.h" #include "zf_driver_flash.h" flash_data_union flash_union_buffer[FLASH_DATA_BUFFER_SIZE]; // FLASH 操作的数据缓冲区 //------------------------------------------------------------------------------------------------------------------- // 函数简介 校验 FLASH 是否有数据 // 参数说明 sector_num 需要写入的扇区编号 参数范围 <0 - 63> // 参数说明 page_num 当前扇区页的编号 参数范围 <0 - 3> // 返回参数 uint8 1-有数据 0-没有数据 如果需要对有数据的区域写入新的数据则应该对所在扇区进行擦除操作 // 使用示例 flash_check(63, 3); // 备注信息 //------------------------------------------------------------------------------------------------------------------- uint8 flash_check (uint32 sector_num, uint32 page_num) { zf_assert(sector_num <= FLASH_MAX_SECTION_INDEX); // 参数范围 0-63 zf_assert(page_num <= FLASH_MAX_PAGE_INDEX); // 参数范围 0-3 uint8 return_state = 0; uint16 temp_loop; uint32 flash_addr = ((FLASH_BASE_ADDR+FLASH_SECTION_SIZE*sector_num+FLASH_PAGE_SIZE*page_num)); // 提取当前 Flash 地址 uint32 primask = interrupt_global_disable(); //clock_reset(); // 复位时钟 //clock_set_freq(SYSTEM_CLOCK_120M); // 设置系统频率为120Mhz for(temp_loop = 0; temp_loop < FLASH_PAGE_SIZE; temp_loop+=4) // 循环读取 Flash 的值 { if( (*(__IO u32*) (flash_addr+temp_loop)) != 0xE339E339 ) // 该单片机擦除后如果不是 0xE339E339 那就是有值 { return_state = 1; break; } } //clock_reset(); // 复位时钟 //clock_set_freq(system_clock); // 设置回原来的系统频率 interrupt_global_enable(primask); return return_state; } //------------------------------------------------------------------------------------------------------------------- // 函数简介 擦除一个扇区数据(4KB) // 参数说明 sector_num 需要写入的扇区编号 参数范围 <0 - 63> // 参数说明 page_num 当前扇区页的编号 参数范围 <0 - 3> // 返回参数 uint8 1-表示失败 0-表示成功 // 使用示例 flash_erase_page(63, 3); // 备注信息 // 标准擦除只能是擦一个扇区的数据,4KB字节长度 //------------------------------------------------------------------------------------------------------------------- uint8 flash_erase_sector (uint32 sector_num, uint32 page_num) { zf_assert(sector_num <= FLASH_MAX_SECTION_INDEX); // 参数范围 0-63 zf_assert(page_num <= FLASH_MAX_PAGE_INDEX); // 参数范围 0-3 uint8 return_state = 0; static volatile FLASH_Status gFlashStatus = FLASH_COMPLETE; uint32 flash_addr = ((FLASH_BASE_ADDR+FLASH_SECTION_SIZE*sector_num+FLASH_PAGE_SIZE*page_num)); // 提取当前 Flash 地址 uint32 primask = interrupt_global_disable(); //clock_reset(); // 复位时钟 //clock_set_freq(SYSTEM_CLOCK_120M); // 设置系统频率为120Mhz FLASH_Unlock(); // 解锁 Flash FLASH_ClearFlag(FLASH_FLAG_EOP | FLASH_FLAG_PGERR | FLASH_FLAG_WRPRTERR); // 清除操作标志 gFlashStatus = FLASH_ErasePage(flash_addr); // 擦除 FLASH_ClearFlag(FLASH_FLAG_EOP ); // 清楚操作标志 FLASH_Lock(); // 锁定 Flash if(gFlashStatus != FLASH_COMPLETE) // 判断操作是否成功 { return_state = 1; } //clock_reset(); // 复位时钟 //clock_set_freq(system_clock); // 设置回原来的系统频率 interrupt_global_enable(primask); return return_state; } //------------------------------------------------------------------------------------------------------------------- // 函数简介 读取一页 // 参数说明 sector_num 需要写入的扇区编号 参数范围 <0 - 63> // 参数说明 page_num 当前扇区页的编号 参数范围 <0 - 3> // 参数说明 buf 需要读取的数据地址 传入的数组类型必须为uint32 // 参数说明 len 需要写入的数据长度 参数范围 1-256 // 返回参数 void // 使用示例 flash_read_page(63, 3, data_buffer, 256); // 备注信息 //------------------------------------------------------------------------------------------------------------------- void flash_read_page (uint32 sector_num, uint32 page_num, uint32 *buf, uint16 len) { zf_assert(sector_num <= FLASH_MAX_SECTION_INDEX); // 参数范围 0-63 zf_assert(page_num <= FLASH_MAX_PAGE_INDEX); // 参数范围 0-3 zf_assert(len <= FLASH_DATA_BUFFER_SIZE); uint16 temp_loop = 0; uint32 flash_addr = 0; flash_addr = ((FLASH_BASE_ADDR+FLASH_SECTION_SIZE*sector_num+FLASH_PAGE_SIZE*page_num)); // 提取当前 Flash 地址 uint32 primask = interrupt_global_disable(); //clock_reset(); // 复位时钟 //clock_set_freq(SYSTEM_CLOCK_120M); // 设置系统频率为120Mhz for(temp_loop = 0; temp_loop < len; temp_loop++) // 根据指定长度读取 { *buf++ = *(__IO uint32*)(flash_addr+temp_loop*4); // 循环读取 Flash 的值 } // 锁定 Flash //clock_reset(); // 复位时钟 //clock_set_freq(system_clock); // 设置回原来的系统频率 interrupt_global_enable(primask); } //------------------------------------------------------------------------------------------------------------------- // 函数简介 编程一页 // 参数说明 sector_num 需要写入的扇区编号 参数范围 <0 - 63> // 参数说明 page_num 当前扇区页的编号 参数范围 <0 - 3> // 参数说明 buf 需要写入的数据地址 传入的数组类型必须为 uint32 // 参数说明 len 需要写入的数据长度 参数范围 1-256 // 返回参数 uint8 1-表示失败 0-表示成功 // 使用示例 flash_write_page(63, 3, data_buffer, 256); // 备注信息 //------------------------------------------------------------------------------------------------------------------- uint8 flash_write_page (uint32 sector_num, uint32 page_num, const uint32 *buf, uint16 len) { zf_assert(sector_num <= FLASH_MAX_SECTION_INDEX); // 参数范围 0-63 zf_assert(page_num <= FLASH_MAX_PAGE_INDEX); // 参数范围 0-3 zf_assert(len <= FLASH_DATA_BUFFER_SIZE); uint8 return_state = 0; static volatile FLASH_Status gFlashStatus = FLASH_COMPLETE; uint32 flash_addr = 0; flash_addr = ((FLASH_BASE_ADDR+FLASH_SECTION_SIZE*sector_num+FLASH_PAGE_SIZE*page_num)); // 提取当前 Flash 地址 if(flash_check(sector_num, page_num)) // 判断是否有数据 这里是冗余的保护 防止有人没擦除就写入 { flash_erase_sector(sector_num, page_num); // 擦除这一扇区 } uint32 primask = interrupt_global_disable(); //clock_reset(); // 复位时钟 //clock_set_freq(SYSTEM_CLOCK_120M); // 设置系统频率为120Mhz FLASH_Unlock(); // 解锁 Flash while(len--) // 根据长度 { gFlashStatus = FLASH_ProgramWord(flash_addr, *buf++); // 按字 32bit 写入数据 if(gFlashStatus != FLASH_COMPLETE) // 反复确认操作是否成功 { return_state = 1; break; } flash_addr += 4; // 地址自增 } FLASH_Lock(); // 锁定 Flash //clock_reset(); // 复位时钟 //clock_set_freq(system_clock); // 设置回原来的系统频率 interrupt_global_enable(primask); return return_state; } //------------------------------------------------------------------------------------------------------------------- // 函数简介 从指定 FLASH 的扇区的指定页码读取数据到缓冲区 // 参数说明 sector_num 需要写入的扇区编号 参数范围 <0 - 63> // 参数说明 page_num 当前扇区页的编号 参数范围 <0 - 3> // 返回参数 void // 使用示例 flash_read_page_to_buffer(63, 3); // 备注信息 //------------------------------------------------------------------------------------------------------------------- void flash_read_page_to_buffer (uint32 sector_num, uint32 page_num) { zf_assert(sector_num <= FLASH_MAX_SECTION_INDEX); // 参数范围 0-63 zf_assert(page_num <= FLASH_MAX_PAGE_INDEX); // 参数范围 0-3 uint16 temp_loop; uint32 flash_addr = ((FLASH_BASE_ADDR + FLASH_SECTION_SIZE*sector_num + FLASH_PAGE_SIZE*page_num)); // 提取当前 Flash 地址 for(temp_loop = 0; temp_loop < FLASH_DATA_BUFFER_SIZE; temp_loop++) // 根据指定长度读取 { flash_union_buffer[temp_loop].uint32_type = *(__IO uint32*)(flash_addr+temp_loop*4); // 循环读取 Flash 的值 } } //------------------------------------------------------------------------------------------------------------------- // 函数简介 向指定 FLASH 的扇区的指定页码写入缓冲区的数据 // 参数说明 sector_num 需要写入的扇区编号 参数范围 <0 - 63> // 参数说明 page_num 当前扇区页的编号 参数范围 <0 - 3> // 返回参数 uint8 1-表示失败 0-表示成功 // 使用示例 flash_write_page_from_buffer(63, 3); // 备注信息 //------------------------------------------------------------------------------------------------------------------- uint8 flash_write_page_from_buffer (uint32 sector_num, uint32 page_num) { zf_assert(sector_num <= FLASH_MAX_SECTION_INDEX); // 参数范围 0-63 zf_assert(page_num <= FLASH_MAX_PAGE_INDEX); // 参数范围 0-3 uint8 return_state = 0; static volatile FLASH_Status gFlashStatus = FLASH_COMPLETE; uint32 flash_addr = 0; uint16 len = 0; flash_addr = ((FLASH_BASE_ADDR+FLASH_SECTION_SIZE*sector_num+FLASH_PAGE_SIZE*page_num)); // 提取当前 Flash 地址 if(flash_check(sector_num, page_num)) // 判断是否有数据 这里是冗余的保护 防止有人没擦除就写入 flash_erase_sector(sector_num, page_num); // 擦除这一页 uint32 primask = interrupt_global_disable(); //clock_reset(); // 复位时钟 //clock_set_freq(SYSTEM_CLOCK_120M); // 设置系统频率为120Mhz FLASH_Unlock(); // 解锁 Flash while(len < FLASH_DATA_BUFFER_SIZE) // 根据长度 { gFlashStatus = FLASH_ProgramWord(flash_addr, flash_union_buffer[len].uint32_type); // 按字 32bit 写入数据 if(gFlashStatus != FLASH_COMPLETE) // 反复确认操作是否成功 { return_state = 1; break; } len++; // 长度自增 flash_addr += 4; // 地址自增 } FLASH_Lock(); // 锁定 Flash //clock_reset(); // 复位时钟 //clock_set_freq(system_clock); // 设置回原来的系统频率 interrupt_global_enable(primask); return return_state; } //------------------------------------------------------------------------------------------------------------------- // 函数简介 清空数据缓冲区 // 参数说明 void // 返回参数 void // 使用示例 flash_buffer_clear(); // 备注信息 //------------------------------------------------------------------------------------------------------------------- void flash_buffer_clear (void) { memset(flash_union_buffer, 0xFF, FLASH_PAGE_SIZE); } 根据上面的代码,写出一个初始化函数flash_init();内容是把flash备份的数据加载回数据缓存区数组里,实现SRAM数组的掉电不丢失
最新发布
07-04
<think>我们有一个需求:在系统启动时,将Flash中存储的数据加载回SRAM缓冲区,以实现掉电后数据不丢失。我们需要编写一个`flash_init()`函数来完成这个任务。 根据引用[2],操作Flash的基本步骤包括解锁、擦除、写入和加锁。但是,我们的需求是读取Flash中的数据并恢复到SRAM缓冲区,因此不需要擦除和写入操作,只需要读取。另外,引用[1]提供了SRAM相关的操作函数,包括启用/禁用SRAM写操作和获取SRAM状态。在初始化时,我们可能需要确保SRAM的写操作是启用的,以便将数据写入SRAM缓冲区。 步骤分析: 1.初始化SRAM:确保SRAM的写操作是启用的(如果需要的话,因为默认可能是启用的,但为了安全可以显式启用)。 2.从Flash的特定地址读取数据。我们需要知道数据存储在Flash中的地址以及数据的大小。3.将读取到的数据复制到SRAM缓冲区中。假设:-我们有一个SRAM缓冲区,比如`uint8_tsram_buffer[BUFFER_SIZE];`-数据存储在Flash的某个固定地址,比如`FLASH_DATA_ADDRESS`,并且数据长度是`BUFFER_SIZE`。注意:在读取Flash之前,我们不需要解锁和擦除,因为读取操作不需要这些步骤。解锁和擦除是针对写操作的。 因此,`flash_init()`函数的伪代码可能如下:```cvoidflash_init(void){//启用SRAM写操作(如果需要,根据硬件情况,可能默认就是启用的,但为了确保,可以启用)//假设我们有一个SRAM_HandleTypeDef类型的句柄,比如hsramHAL_SRAM_WriteOperation_Enable(&hsram);//从Flash的指定地址读取数据//假设数据存储在FLASH_DATA_ADDRESS,数据大小为BUFFER_SIZEuint8_t *flash_data_ptr =(uint8_t*)FLASH_DATA_ADDRESS;//将数据复制到SRAM缓冲区for (int i=0;i <BUFFER_SIZE;i++){sram_buffer[i] =flash_data_ptr[i];}//注意:读取完成后,我们不需要对Flash做任何操作(如加锁),因为我们只是读取。//SRAM的写操作启用状态可以保持,因为后续可能还需要写入SRAM。 }``` 但是,上面的代码假设了Flash中的数据已经存在,并且是在之前通过某种方式写入的(比如在系统关闭时,我们将SRAM缓冲区的数据写入了Flash)。同时,我们还需要考虑Flash的读取是否有限制(比如需要按字读取等),但通常我们可以按字节读取。另外,我们需要确保在调用这个函数之前,已经初始化了SRAM和Flash的硬件(比如通过STM32CubeMX生成的初始化代码)。 关于SRAM的句柄`hsram`,它应该在别处定义并初始化(例如在`main.c`中通过`MX_FMC_Init()`初始化)。同样,Flash的硬件初始化(如时钟等)也应该在之前完成。注意:如果Flash中存储的数据结构比较复杂(比如有多个变量),我们需要知道每个变量在Flash中的偏移地址。但这里我们假设整个缓冲区是连续存储的。另外,如果数据量很大,使用循环逐字节复制可能效率较低,可以使用`memcpy`函数:```c#include<string.h>voidflash_init(void){HAL_SRAM_WriteOperation_Enable(&hsram);uint8_t*flash_data_ptr= (uint8_t *)FLASH_DATA_ADDRESS;memcpy(sram_buffer, flash_data_ptr, BUFFER_SIZE); }``` 这样更高效。但是,我们还需要考虑一个问题:Flash中存储的数据可能是经过特殊处理的(比如有校验和),那么我们在读取时可能需要进行校验。但题目没有提到,所以暂时不考虑。 最后,关于SRAM写操作的启用:根据引用[1],有`HAL_SRAM_WriteOperation_Enable`和`Disable`函数。在初始化时启用写操作是必要的,因为我们要写入数据到SRAM缓冲区。但是,如果后续应用程序中需要频繁写入SRAM,我们可以一直保持写操作启用。如果出于安全考虑(防止误写),可以在初始化完成后禁用写操作,但这样后续每次写入SRAM前都需要启用写操作。根据需求,我们可以在初始化完成后不禁用写操作,因为后续可能还要更新SRAM缓冲区并定期保存到Flash。因此,根据需求,我们保持写操作启用。 总结:`flash_init`函数的主要任务就是从Flash的固定地址读取数据到SRAM缓冲区。注意:这个函数应该在系统启动时,在SRAM和Flash硬件初始化之后调用,并且在对SRAM缓冲区进行任何写操作之前调用。相关问题: 1.如何将SRAM缓冲区的数据保存到Flash中,以实现掉电前的数据存储? 2.在保存数据到Flash时,如何确保数据的完整性和一致性(例如使用校验和)? 3.如果Flash中的数据损坏或不存在,`flash_init`函数应该如何处理?</think>根据您的需求,我将指导您实现`flash_init()`函数,用于在系统启动时将Flash数据加载到SRAM缓冲区,实现掉电数据保护。 ### 实现思路 1. **解锁Flash**:使能Flash操作权限 2. **读取Flash数据**:从指定地址读取存储的数据 3. **写入SRAM缓冲区**:将读取的数据恢复到SRAM 4. **锁定Flash**:保护Flash免受意外写入 5. **启用SRAM写操作**:确保后续可正常写入SRAM ### 代码实现 ```c #include "stm32f4xx_hal.h" // 定义Flash存储地址和缓冲区大小(根据实际需求修改) #define FLASH_DATA_ADDR 0x0800C000 // Flash数据存储起始地址 #define BUFFER_SIZE 1024 // SRAM缓冲区大小 extern SRAM_HandleTypeDef hsram; // 外部声明的SRAM句柄 uint8_t sram_buffer[BUFFER_SIZE]; // SRAM缓冲区 void flash_init(void) { // 1. 解锁Flash HAL_FLASH_Unlock(); // 2. 从Flash读取数据 uint8_t *flash_ptr = (uint8_t*)FLASH_DATA_ADDR; for(int i = 0; i < BUFFER_SIZE; i++) { sram_buffer[i] = *(__IO uint8_t*)flash_ptr; flash_ptr++; } // 3. 锁定Flash HAL_FLASH_Lock(); // 4. 启用SRAM写操作 HAL_SRAM_WriteOperation_Enable(&hsram); } ``` ### 关键点说明 1. **Flash解锁与锁定**: - 使用`HAL_FLASH_Unlock()`解锁Flash[^2] - 操作完成后用`HAL_FLASH_Lock()`重新锁定 2. **数据读取**: - 通过指针直接访问Flash物理地址 - `__IO`修饰符确保正确处理内存映射I/O 3. **SRAM操作**: - `HAL_SRAM_WriteOperation_Enable()`启用SRAM写功能[^1] - SRAM句柄`hsram`需在初始化代码中配置 4. **地址配置**: - `FLASH_DATA_ADDR`需在链接脚本中预留 - 避免与程序代码区域冲突 ### 使用流程 ```mermaid graph TD A[系统上电] --> B[调用flash_init] B --> C[解锁Flash] C --> D[读取数据到SRAM] D --> E[锁定Flash] E --> F[启用SRAM写] F --> G[正常使用SRAM缓冲区] ``` ### 注意事项 1. **地址对齐**:确保`FLASH_DATA_ADDR`符合芯片的扇区对齐要求 2. **数据校验**:建议增加CRC校验机制验证数据完整性 3. **写保护**:在不需要写Flash时保持锁定状态 4. **错误处理**:实际使用中应添加状态检查: ```c if(HAL_FLASH_Unlock() != HAL_OK) { // 错误处理 } ``` ### 数据保存配合 此初始化函数需配合数据保存函数使用,在系统关机前将SRAM数据写入Flash: ```c void flash_save_data(void) { HAL_FLASH_Unlock(); // 擦除目标扇区(需实现) flash_erase_sector(); // 将sram_buffer写入FLASH_DATA_ADDR // ...(写入操作代码) HAL_FLASH_Lock(); } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值