套接字:在没有memcpy的情况下,使用recvfrom将UDP数据获取到字对齐的缓冲区

Sockets: getting UDP data into a word-aligned buffer using recvfrom without memcpy?

本文关键字:UDP 数据获取 缓冲区 对齐 recvfrom memcpy 套接字 情况下 使用      更新时间:2023-10-16

我承认对套接字编程一无所知。

实际上,我只是想为一个有工作UDP接口的硬件编写一个非常简单的测试工具。测试工具应该能够向硬件发送UDP数据包,并从中接收UDP数据包。将接收到的UDP数据包立即返回设备,并可能重复。

关键的一点是,设备期望数据为32位字。这意味着UDP数据包的实际数据内容需要进行字对齐,而且在我的测试工具的接收端,我需要将数据缓冲区处理为32位字对齐缓冲区。

同时,UDP标头的大小意味着,从硬件中出来,在实际数据之前,数据字段的前面有2个字节的填充,因为当你添加所有不同的标头时,你最终会得到一个数据偏移的起始位置,它不是32位字对齐的,它偏离了半个字。

我认为可行的方法是在UDP接收函数中定义一个字对齐的缓冲区,然后从一个指针传递给recvfrom,该指针转换为一个字符,然后偏移2(对应于半字未对齐)。在这种情况下,实际的数据字应该在返回给用户的缓冲区中对齐,填充到缓冲区第一个字的后半字中。但它在recvfrom函数中出现了故障。就好像recvfrom决定将数据缓冲区的开头放在32位字的边界上,这是它绝对不能做的

一般来说,这是内部行为吗?如果是这样的话,人们似乎真的别无选择,只能做一个荒谬而低效的记忆;在我看来,没有其他解决方案似乎并不特别可信。那么,我如何才能在单词边界上正确复制数据单词呢?

这是接收功能。请注意,如果用注释行替换活动行,函数不会segfault,所以我可以肯定地将这些行作为问题隔离开来。(这样做只是一个调试步骤,对解决问题没有帮助,因为如果这样做了,那么稍后当我去读单词时,由于预期的原因,它就不起作用了)

bool EthernetSoftwareIF::receiveUDP(rx_entry_t &rxdata)
{        
uint32_t *data_w = new uint32_t[350]; // need a word-aligned buffer
//char *data = (char *)data_w; 
char *data = ((char *)data_w)+sizeof(uint16_t); // adjust for 2-byte padding
#ifdef SIMULATION
// simulation mode
int len = sizeof(this->remoteServAddr);
int bytecount = this->socket_if.recvfrom(data, sizeof(data_w)-sizeof(uint16_t), MSG_DONTWAIT, (sockaddr*)&(this->remoteServAddr), &len );
//int bytecount = this->socket_if.recvfrom(data, sizeof(data_w), MSG_DONTWAIT, (sockaddr*)&(this->remoteServAddr), &len );
#else
socklen_t len = sizeof(this->remoteServAddr);
int bytecount = recvfrom(this->udp_socket, data, sizeof(data_w)-sizeof(uint16_t), MSG_DONTWAIT, (sockaddr*)&(this->remoteServAddr), &len );
//int bytecount = recvfrom(this->udp_socket, data, sizeof(data_w), MSG_DONTWAIT, (sockaddr*)&(this->remoteServAddr), &len );
#endif
if (bytecount < 0)
{
#ifdef SIMULATION
printf("EthernetSoftwareIF::receiveUDP: error during packet reception (error code: %d).n",this->socket_if.lasterror());
#else
printf("EthernetSoftwareIF::receiveUDP: error during packet reception.n");
#endif
return false;
}
rxdata.uiBytes = bytecount;
rxdata.uiSourceIP = htonl(this->remoteServAddr.sin_addr.s_addr);
rxdata.uiSourcePort = htons(this->remoteServAddr.sin_port);
rxdata.uiDestPort = REMOTE_SERVER_PORT;
rxdata.pData = (void*)data_w;
return true; 
}  

(针对移民的回答)

是的,我知道它没有给我头球,我也不需要看。但问题是,一旦通过所有标头,缓冲区的开头就不会对齐。报头的大小使得报头中的最后一个数据字节位于半字边界而不是字边界。

因此,数据缓冲区将从半字边界开始。这意味着数据缓冲区中的前半个字需要填充,以便缓冲区中实际数据是字对齐的。然后我需要从缓冲区中读出单词,从第一个真正的单词开始。我将在我实际收到的UDP数据包中看到的是:

<halfword_padding><first_word><word>...<last_word>

因此,我想做的是确保我设置recvfrom写入的接收缓冲区在其分配的空间开始时是字对齐的,但作为将来自接收到的udp数据包的数据的第一部分的半字填充被放置到该缓冲区的第二半字中,以便我的数据的后续实际第一个字将在分配的接收缓冲器的第二个字中,并且我可以在字对齐的边界上直接从中读取。因此,数据缓冲区一旦填满,应该看起来像:

<first_halfword(ignored)><halfword_padding><first_word><word>...<last_word>

这有道理吗?


2014年2月26日更新

我一直在对recvfrom行以及如何设置缓冲区data_w和缓冲区指针数据进行一些实验。似乎很清楚的是:

如果您在recvfrom的buffer参数中提供的缓冲区指针指向已分配缓冲区的开始,则recvfrom将正常进行。但是,如果您给它一个指针,该指针与分配的缓冲区的起始地址有一个偏移量,则结果是不可预测。指定偏移量和缓冲区长度的各种不同方式导致了截然不同的结果。

因此,在我的情况下,如果我给recvfrom一个指向data_w的指针(我似乎可以将其转换为任何类型),那么recvfrom就会成功。但是,如果数据是从转换为类型和偏移量派生的,那么recvfrom会以各种不同且表面上不相关的方式中断。

我不明白recvfrom怎么可能对外部声明的缓冲区中的偏移量敏感,但我看到的事实就是事实。也许有人可以揭示recvfrom的内部原因,从而解释这种行为。

与此同时,如果真的是这样的话,那么结论似乎是:如果你需要读取在整个UDP数据包的字边界上对齐的数据,因此在数据包的数据部分的开头至少有2个字节的填充,你别无选择,只能使用memcpy来重新对齐数据。这似乎有点令人难以置信——当然还有其他选择不需要在两个不同的缓冲区之间进行转换吗?

recvfrom不会为您提供标头。它只将数据包中的数据复制到缓冲区中。如果缓冲区是字对齐的,那么数据的开头将是字对齐。

在使用套接字时,您根本不需要关心UDP(或IP,或以太网)标头。

好的,所以硬件生成<pad><pad><32位字><32位字>。。。我理解正确吗?

不如做一些类似的事情:

union tp {
uint32_t w;
uint8_t b[4];
};
union tp rxbuffer[number of words + 1];
uint32_t *payload_ptr = &rxbuffer[1].w;
uint8_t * recvptr = &rxbuffer[0].b[2];

或者是一个类似的双关语?然后将recvptr传递给recvfrom,并使用payload_ptr访问有效负载,payload_ptr32位对齐,因为rxbuffer是.

根据标准,类型punning并不完全安全,但在实践中几乎在任何地方都可以(通过并集实现这一点意味着它甚至可以使用gcc和-fstrict别名)。

我也遇到了同样的问题,搜索其他有同样问题的人,找到了这个条目。我有一个简单的解决方案,但首先有一点解释:UDP创建于国外世纪80年代,其标准是并且应该是长期稳定的。在其他地方可能会出现许多不兼容问题。但是不幸的是,20世纪80年代的开发人员没有考虑内存中的32位对齐。

我有一个问题,头和有效载荷在同一个内存中一个接一个。我不使用标准的硬件以太网适配器,而是使用一个特殊的解决方案,在具有快速数据交换的普通控制器上使用单对以太网。看见https://vishia.org/spe/index.html由于对齐有问题,我将标头的所有数据都放在一个结构中,例如UDP:

typedef struct UdpHeader_Telg_Ethn_T {
uint16 dstMacAddr[3];
uint16 senderMacAddr[3];
uint16 typeId;                     //: 0x0800 for UDP See https://de.wikipedia.org/wiki/Ethernet#Das_Typ-Feld_(EtherType)[]
//union UDPorIP_T {
//struct UDP_T {
uint16 version_lenHdr_Flags;    //: 0x4500 for UDP 5=5*4 Byte header
uint16 lenIPdata;               //: @0x10 length incl. IP header, from version_lenHdr ... excl. FCS-CRC
uint16 seq;                     //: currently number
uint16 flg;
uint16 TTL_Type;                //: Time to live 0x80 for 128 hops, and Type 0x01 for UDP
uint16 chkIPdata;                     //: check code
uint16 senderIp[2];             //: @0x1a first word MSW 192.168, then LSword, note: It is not aligned at a 32 bit position!
uint16 dstIp[2];                //: @0x1e first word MSW 192.168, then LSword, note: It is not aligned at a 32 bit position!
// end IP header
uint16 senderPort;                  //: @22 src or sender port
uint16 dstPort;
uint16 lenUDPdata;
uint16 checkUDPdata;
/**After head, some udpData: Here only one word is defined, which also assures the 32-bit alignment. */
uint16 udpData[1];                   //: One can use this array with free indexing to access the payload as universal uint16.
} UdpHeader_Telg_Ethn_s;

你可以在这里看到评论不足的东西,构建头部的想法没有奏效。

最后一个元素是一个uint16作为有效载荷。正如所评论的那样,它可以自己用作有效负载,取指向数组的指针。当然,我有一个完整的特定结构用于有效载荷。我的简单解决方案是:为第一个16位字重新选择这个特定的结构,并将前16位写入udpData[0]上的UDP标头结构中。通常情况下,有效负载从公共数据开始,而不是从细节开始,所以这是合理的。

当然,现在我可以解决问题了,我的特定数据没有对齐。对于我现在的任务,我使用德州仪器公司的TMS320,它将分离结构对齐为32位,但结构中的数据元素也对齐为16位。如果我使用memcpy,就不会出现问题。您通常可以在16位对齐的数据报的有效负载中复制来自特定有效负载的数据。但我对两者的记忆范围实际上只有一个。

这就是为什么我考虑了一个通用的解决方案:从UDP的有效负载字节地址0x002开始定义一个特定于用户的有效负载,总是手动处理前16位。因此,该方法解决了UDP头未32位对齐的错误。相比之下,21世纪的现代UDP数据报具有特定于UDP有效载荷的前2个字节,其余的是32位可对齐的。

因此,在接收器端(PC),我仅从0x02位置使用有效载荷进行数据评估。位置0x00,01可以用于公共信息。在反转方向上足够。