ET---TBuffer学习笔记

本文深入剖析了TBuffer的设计原理与实现细节,重点介绍了双向链表LinkedList的高效应用,以及如何通过TBuffer实现数据的发送与接收。

TBuffer

请大家关注我的微博:@NormanLin_BadPixel坏像素


3.0已经把TBuffer整合到CircularBuffer。不过建议大家继续看下去吧,毕竟作者的思路大致是没变化的,这篇我可能讲的比较详细。(毕竟我花心思写了T_T)


大致浏览了一下,难道这里用到了类似这篇文章提到的技术?(需要安全上网工具)我只是随便一猜,大家还是继续看代码吧。

public const int ChunkSize = 8192;

这里是定义了一个常量,决定了每个缓存区块的大小为8KB。为什么是8KB呢?可能是8KB的数据传输比较快吧。

private readonly LinkedList<byte[]> bufferList = new LinkedList<byte[]>();

哇,看到这里我才发现自己的知识量是多么的浅薄。我竟然连LinkedList都不知道。羞于见人。赶紧去学习一下

这真的是一个很方便的类型。这里我忍不住想要为大家介绍一下。我们平常都在使用List<>。当我们在遍历List的时候,我们遍历的是里面的每一个值。而当我们在遍历LinkedList<>的时候,我们遍历的是它的节点LinkedListNode<>。这个节点可就神奇了,它就像里面的节点,储存了与它相连的数据,不过它储存的不是左子树右子树,而是它的前节点后节点。储存了这些信息有什么用呢?这样方便我们插入啊!普通的链表List,当我们插入一个新数据的时候,我们需要把插入的地方后面的数据全部都往后移,而这个LinkedList只要修改插入节点的前节点的后节点,插入节点的前节点就好了。(有点绕哈哈,不过很好理解,无非是一个顺序问题。)而且,这个可以很方便的删除第一个节点或者最后一个节点,用来做有限撤销操作的储存非常合适。

看到这,我发现我其实对这个类型并不是很陌生,在大学里数据结构课程上老师就有对这种结构进行过描述。只不过我不知道C#里面就是LinkedList。(其实就是没记起来:_))

复习了LinkedList<>(厚颜无耻),我们继续往下看。

public int LastIndex { get; set; }
public int FirstIndex { get; set; }

这俩个变量我们现在还不知道他们是做啥的,大概是对这个缓冲区有效信息的范围下标的储存吧。因为我们的数据不一定刚好就填充满8KB的区块,可能只填充了一半,这个时候,就需要标识有效数据的下标了。

public TBuffer()
{
    this.bufferList.AddLast(new byte[ChunkSize]);
}

OK,我们在构造函数里就先给了双向链表一个值,虽然是空的数组,但保证bufferList不为空嘛。

public int Count{get;}

这个Count属性的获取器代码很容易看懂。
后面的AddLastRemoveFirstbyte[] Firstbyte[] Last也一样,不需要我讲的吧。我们来看看重要的收发数据。我们先来看看发送数据,这里的发送数据不是说向远端发送数据,而是往这个缓冲区发送数据。

public void SendTo(byte[] buffer)
{
    int alreadyCopyCount = 0;
    while (alreadyCopyCount < buffer.Length)
    {
        if (this.LastIndex == ChunkSize)
        {
            this.bufferList.AddLast(new byte[ChunkSize]);
            this.LastIndex = 0;
        }

        int n = buffer.Length - alreadyCopyCount;
        if (ChunkSize - this.LastIndex > n)
        {
            Array.Copy(buffer, alreadyCopyCount, this.bufferList.Last.Value, this.LastIndex, n);
            this.LastIndex += buffer.Length - alreadyCopyCount;
            alreadyCopyCount += n;
        }
        else
        {
            Array.Copy(buffer, alreadyCopyCount, this.bufferList.Last.Value, this.LastIndex, ChunkSize - this.LastIndex);
            alreadyCopyCount += ChunkSize - this.LastIndex;
            this.LastIndex = ChunkSize;
        }
    }
}

首先,alreadyCopyCount这个变量记录的是已经往缓冲区里面存入的数据量。while循环我就不多说了。
if (this.LastIndex == ChunkSize),这种情况是到了缓冲区里面的小区块满了的时候,需要对缓冲区进行扩增,并把LastIndex归0,因为是开始往新的区块里存了。

n代表剩余的数据量,如果剩余的数据量小于区块剩余的量,则把buffer的[alreadyCopyCount,alreadyCopyCount + n)区间内的数据拷贝到最新的区块也就是bufferList的最后一个节点的值LastIndex下标之后。这是Array.Copy的方法,大家了解一下就理解了。此后,对LastIndexalreadyCopyCount的值进行更新。

如果剩余的数据量还是很大,超过了每个区块的最大8KB,那就要对数据进行分区块处理,也就是else里面的代码做的处理。

好的,我们继续来看接收数据,当然,这里的接收数据是从缓冲区中接收也就是读取数据。

public void RecvFrom(byte[] buffer)
{
    if (this.Count < buffer.Length || buffer.Length == 0)
    {
        throw new Exception($"bufferList size < n, bufferList: {this.Count} buffer length: {buffer.Length}");
    }
    int alreadyCopyCount = 0;
    while (alreadyCopyCount < buffer.Length)
    {
        int n = buffer.Length - alreadyCopyCount;
        if (ChunkSize - this.FirstIndex > n)
        {
            Array.Copy(this.bufferList.First.Value, this.FirstIndex, buffer, alreadyCopyCount, n);
            this.FirstIndex += n;
            alreadyCopyCount += n;
        }
        else
        {
            Array.Copy(this.bufferList.First.Value, this.FirstIndex, buffer, alreadyCopyCount, ChunkSize - this.FirstIndex);
            alreadyCopyCount += ChunkSize - this.FirstIndex;
            this.FirstIndex = 0;
            this.bufferList.RemoveFirst();
        }
    }
}

经过了前面的解释,这里无非就是对上面存的操作进行一次逆操作。不过需要注意的是,用来存读取得到的数据的byte[] 长度不能为0也不能超过缓冲区中原有的数据长度。这样可能是为了避免出现空的字节导致后续反序列化出现问题吧。

<think>我们被要求将uint32_t类型的IP地址转换为字符串形式的IP地址(点分十进制字符串)。在ESP-IDF中,IP地址通常使用lwIP库(轻量级IP协议栈)来处理。lwIP提供了IP地址类型和转换函数。根据引用[2]中提到的ESP32网络编程,我们可以知道ESP-IDF使用lwIP,因此我们可以查找lwIP的相关函数。在lwIP中,IP地址类型为`ip_addr_t`,实际上是一个结构体。但是,我们有时也会看到将IP地址存储为`uint32_t`(网络字节序)。注意,网络字节序是大端序。在ESP-IDF中,我们通常使用以下步骤:1.将`uint32_t`(网络字节序)转换为`ip4_addr_t`结构(用于IPv4)。2.使用lwIP提供的函数将`ip4_addr_t`转换为字符串。然而,我们也可以直接通过操作字节来转换,但使用lwIP提供的函数更为标准和安全。lwIP中提供了`ip4addr_ntoa`函数,该函数可以将一个指向`ip4_addr_t`的指针转换为点分十进制字符串。但是注意,这个函数返回一个静态分配的缓冲区,所以它不可重入(不能在中断中使用,且连续调用会覆盖之前的转换结果)。另外,我们还可以使用`inet_ntoa`函数(POSIX风格),它也是lwIP提供的,用于将`in_addr`结构转换为字符串。注意:`in_addr`结构中的`s_addr`字段就是网络字节序的`uint32_t`。因此,我们可以有两种方法:方法一:使用`ip4addr_ntoa`步骤:1.定义一个`ip4_addr_t`类型的变量。2.将`uint32_t`类型的IP地址(网络字节序)赋值给`ip4_addr_t`的`addr`字段。3.调用`ip4addr_ntoa`函数将其转换为字符串。方法二:使用`inet_ntoa`步骤:1.定义一个`structin_addr`类型的变量。2.将`uint32_t`值赋给`in_addr`的`s_addr`字段(因为`in_addr`实际上就是一个包含`s_addr`成员的结构)。3.调用`inet_ntoa`函数将其转换为字符串。注意:两个函数都返回指向静态缓冲区的指针,所以如果需要保存结果,应该立即复制该字符串。下面我们演示这两种方法。首先,需要包含必要的头文件:-对于`ip4addr_ntoa`:`#include"lwip/ip4_addr.h"`-对于`inet_ntoa`:`#include<lwip/inet.h>`示例代码:方法一:```c#include"lwip/ip4_addr.h"voidconvert_ip_uint32_to_string(uint32_tip_addr,char*buffer,size_tbuffer_size){ip4_addr_taddr;addr.addr=ip_addr;//ip_addr是网络字节序char*str=ip4addr_ntoa(&addr);if(str){strncpy(buffer,str,buffer_size);buffer[buffer_size-1]='\0';//确保字符串结束}else{buffer[0]='\0';//转换失败,返回空字符串}}```方法二:```c#include<lwip/inet.h>voidconvert_ip_uint32_to_string(uint32_tip_addr,char*buffer,size_tbuffer_size){structin_addraddr;addr.s_addr=ip_addr;//ip_addr是网络字节序char*str=inet_ntoa(addr);if(str){strncpy(buffer,str,buffer_size);buffer[buffer_size-1]='\0';}else{buffer[0]='\0';}}```注意:在实际使用中,我们通常从网络接口获取到的是网络字节序的IP地址(大端序),所以直接使用即可。另一种不使用lwIP函数的方法(手动转换):我们可以将32位的IP地址按字节拆分成4个部分(注意字节序),然后格式化成字符串。但是需要注意的是,网络字节序是大端序,所以高位字节在低地址,低位字节在高地址。而我们的32位整数在内存中的表示,如果是大端序,那么直接取每个字节(从高地址到低地址)的顺序应该是:第一个字节是最高位的8位(即点分十进制的第一个数),但是实际上,我们通常将网络字节序的32位整数看作是大端序,所以在转换时,应该从整数的高8位开始取。由于我们不知道ESP32-S3的平台字节序(虽然它是小端机),但ip地址是网络字节序(大端),所以我们按大端序来解析:```cvoidconvert_ip_uint32_to_string(uint32_tip_addr,char*buffer,size_tbuffer_size){uint8_tbytes[4];bytes[0]=(ip_addr>>24)&0xFF;//最高位的8位,对应点分十进制的第一个数bytes[1]=(ip_addr>>16)&0xFF;bytes[2]=(ip_addr>>8)&0xFF;bytes[3]=ip_addr&0xFF;//最低位,对应点分十进制的最后一个数snprintf(buffer,buffer_size,"%d.%d.%d.%d",bytes[0],bytes[1],bytes[2],bytes[3]);}```但是请注意,如果我们的`ip_addr`是主机字节序(小端序),那么上面这种移位方式就是错误的。因此,我们必须确保传入的`ip_addr`是网络字节序(大端序)。在ESP-IDF的很多网络函数中,返回的IP地址通常是网络字节序。因此,我们使用这种移位方式,前提是`ip_addr`是网络字节序。所以,为了保险起见,我们最好使用lwIP提供的函数,这样可避免字节序问题。综上所述,推荐使用第一种或第二种方法(使用lwIP提供的函数)。注意:在使用lwIP函数时,需要确保lwIP已经初始化,并且包含相应的头文件。另外,由于这些函数可能不是线程安全的(因为它们使用静态缓冲区),所以在多线程环境中使用时需要加锁。但在ESP32中,如果是在同一任务中调用,则不会出现问题(因为静态缓冲区在同一个任务中连续调用会被覆盖,但不会同时被其他任务覆盖,因为任务切换后其他任务调用时也会覆盖,但如果你在转换后立即使用该字符串并复制出来,则没有问题)。如果是在不同的任务中同时调用,那么可能会导致结果错误。此时,可以考虑使用互斥锁或者使用线程安全的函数`inet_ntoa_r`(但是lwIP的API中并没有提供`inet_ntoa_r`,而`ip4addr_ntoa_r`是提供的)。因此,我们可以使用`ip4addr_ntoa_r`,它是可重入版本,可以指定一个缓冲区。使用`ip4addr_ntoa_r`的示例:```c#include"lwip/ip4_addr.h"voidconvert_ip_uint32_to_string(uint32_tip_addr,char*out_str,size_tout_str_len){ip4_addr_taddr;addr.addr=ip_addr;//需要一个足够大的缓冲区,IP地址点分十进制字符串最多15字节(不包括结束符),例如"255.255.255.255"为15个字符chartmp_buf[16];//16字节的缓冲区char*result=ip4addr_ntoa_r(&addr,tmp_buf,sizeof(tmp_buf));if(result!=NULL){strncpy(out_str,tmp_buf,out_str_len);out_str[out_str_len-1]='\0';}else{out_str[0]='\0';}}```但注意,这里我们使用了临时缓冲区,然后复制给输出缓冲区。也可以直接让调用者提供一个足够大的缓冲区,这样避免二次拷贝。实际上,我们可以要求调用者提供16字节的缓冲区,然后直接使用`ip4addr_ntoa_r`将结果写入该缓冲区:```c#include"lwip/ip4_addr.h"voiduint32_to_ip_string(uint32_tip,char*out_buf){ip4_addr_taddr;addr.addr=ip;ip4addr_ntoa_r(&addr,out_buf,16);//确保out_buf至少16字节}```这样更直接。注意,`ip4addr_ntoa_r`的第三个参数是缓冲区大小,至少应该是16。因此,我们更推荐使用`ip4addr_ntoa_r`,因为它是可重入的,并且更安全。总结回答:在ESP-IDF中,将`uint32_t`(网络字节序)表示的IPv4地址转换为点分十进制字符串,可以使用lwIP的`ip4addr_ntoa_r`函数(可重入版本),这样可以避免静态缓冲区的不可重入问题。步骤:1.包含头文件:`#include"lwip/ip4_addr.h"`2.将`uint32_t`值赋值给`ip4_addr_t`结构体的`addr`成员。3.使用`ip4addr_ntoa_r`进行转换,并提供一个至少16字节的缓冲区来存储结果。示例代码:```c#include"lwip/ip4_addr.h"voidip_to_string(uint32_tip,char*out_str){ip4_addr_taddr;addr.addr=ip;ip4addr_ntoa_r(&addr,out_str,16);}```调用示例:```cuint32_tip=...;//从某处获取的网络字节序IP地址charip_str[16];//缓冲区大小为16ip_to_string(ip,ip_str);printf("IPAddress:%s\n",ip_str);```注意:如果IP地址是主机字节序(在ESP32上为小端序),则需要先转换为网络字节序(大端序)。但通常情况下,我们从网络接口中获取的IP地址已经是网络字节序。因此,在转换之前,请确保`ip`是网络字节序。如果`ip`已经是主机字节序,那么我们可以使用宏`htonl`将其转换为网络字节序:```c#include<lwip/netdb.h>//用于htonluint32_tip_network_byte_order=htonl(ip_host_byte_order);```但在ESP-IDF的网络编程中,从结构体如`tcpip_adapter_ip_info_t`中获取的IP地址,其存储形式是网络字节序(大端序),所以直接使用即可。[^2]中提到ESP32接入网络,以及IP地址的处理,所以这里使用的IP地址通常是从网络接口信息中获取的,因此是网络字节序。因此,按照上述方法转换即可。相关问题:§§相关问题§§1.ESP-IDF中如何获取ESP32的IP地址?2.如何在ESP32中将点分十进制字符串形式的IP地址转换为uint32_t?3.在ESP-IDF的lwIP中,如何安全地处理IP地址转换以避免多任务问题?</think> ### ESP32-S3 ESP-IDF中将`uint32_t`类型的IP地址转换为字符串形式 在ESP-IDF框架中,将`uint32_t`类型的IP地址(网络字节序)转换为点分十进制字符串(如"192.168.1.100")的标准方法是使用`inet_ntoa()`函数。以下是详细实现步骤: #### 方法实现 ```c #include <lwip/inet.h> // 包含网络转换函数 void uint32_to_ip_string(uint32_t ip, char* buffer) { struct in_addr addr; addr.s_addr = ip; // 将uint32_t赋给in_addr结构 strcpy(buffer, inet_ntoa(addr)); // 转换为字符串 } ``` #### 使用示例 ```c #include "esp_netif.h" void app_main() { esp_netif_ip_info_t ip_info; esp_netif_get_ip_info(ESP_NETIF_STA_DEFAULT, &ip_info); char ip_str[16]; // 缓冲区大小(15+1) uint32_to_ip_string(ip_info.ip.addr, ip_str); printf("IP Address: %s\n", ip_str); // 输出: "192.168.1.100" } ``` #### 关键说明 1. **网络字节序**:`uint32_t` IP地址必须是大端序(网络字节序)。ESP-IDF网络API(如`esp_netif_get_ip_info()`)返回的地址已自动处理字节序。 2. **缓冲区大小**:输出缓冲区至少需**16字节**(包括终止符`\0`),对应IPv4地址的最大长度15字符。 3. **线程安全性**: - `inet_ntoa()`使用静态缓冲区,**非线程安全** - 替代方案:使用可重入版本`inet_ntoa_r()`: ```c void uint32_to_ip_string_safe(uint32_t ip, char* buffer) { struct in_addr addr; addr.s_addr = ip; inet_ntoa_r(addr, buffer, 16); // 线程安全版本 } ``` #### 字节操作备选方案(手动转换) ```c void manual_ip_convert(uint32_t ip, char* buffer) { // 按网络字节序解析四段IP地址 snprintf(buffer, 16, "%d.%d.%d.%d", (int)(ip & 0xFF), (int)((ip >> 8) & 0xFF), (int)((ip >> 16) & 0xFF), (int)((ip >> 24) & 0xFF) ); } ``` > ⚠️ 此方法仅适用于网络字节序数据,不推荐常规使用[^2]。 #### 引用说明 [^1]: 函数`nvs_get_i32()`用于从NVS存储读取32位整数,与IP转换无关,但展示ESP-IDF常用数据类型。 [^2]: ESP-IDF网络API返回的IP地址已处理字节序,直接使用即可避免转换错误。 [^3]: 动态修改蓝牙名称示例说明ESP-IDF对网络参数的操作一致性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值