5

我最近在 Stack Overflow 上问了一个问题,关于如何将我的数据从 16 位整数,后跟未确定数量的 void*-cast 内存转换为无符号字符的 std::vector,以便使用已知的套接字库作为 NetLink,它使用一个签名看起来像这样的函数来发送原始数据:

void rawSend(const vector<unsigned char>* data);

(作为参考,这是那个问题:Casting an unsigned int + a string to an unsigned char vector

问题已成功回答,我感谢那些回答的人。Mike DeSimone 给出了一个 send_message() 函数的示例,该函数将数据转换为 NetLink 接受的格式(std::vector),如下所示:

void send_message(NLSocket* socket, uint16_t opcode, const void* rawData, size_t rawDataSize)
{
    vector<unsigned char> buffer;
    buffer.reserve(sizeof(uint16_t) + rawDataSize);
    buffer.push_back(opcode >> 8);
    buffer.push_back(opcode & 0xFF);
    const unsigned char* base(reinterpret_cast<const unsigned char*>(rawData));
    buffer.insert(buffer.end(), base, base + rawDataSize);
    socket->rawSend(&buffer);
}

这看起来正是我所需要的,所以我开始编写一个随附的 receive_message() 函数......

...但是我很尴尬地说我并不完全理解所有的位移和诸如此类的东西,所以我在这里遇到了障碍。在过去近十年中我编写的所有代码中,我的大部分代码都是使用高级语言编写的,而我的其余代码从未真正需要低级内存操作。

回到编写 receive_message() 函数的主题,正如你所想象的,我的出发点是 NetLink 的 rawRead() 函数,其签名如下所示:

vector<unsigned char>* rawRead(unsigned bufferSize = DEFAULT_BUFFER_SIZE, string* hostFrom = NULL);

看起来我的代码将像这样开始:

void receive_message(NLSocket* socket, uint16_t* opcode, const void** rawData)
{
    std::vector<unsigned char, std::allocator<unsigned char>>* buffer = socket->rawRead();
    std::allocator<unsigned char> allocator = buffer->get_allocator(); // do I even need this allocator?  I saw that one is returned as part of the above object, but...
    // ...
}

在第一次调用 rawRead() 之后,看来我需要遍历向量,从中检索数据并反转位移操作,然后将数据返回到 *rawData 和 *opcode。同样,我对移位不是很熟悉(我做了一些谷歌搜索来理解语法,但我不明白为什么上面的 send_message() 代码需要移位),所以我对下一步感到茫然这里。

有人可以帮我理解如何编写这个随附的 receive_message() 函数吗?作为奖励,如果有人可以帮助解释原始代码,以便我将来知道它是如何工作的(特别是在这种情况下如何进行转换以及为什么它是必要的),那将有助于加深我对未来的理解。

提前致谢!

4

2 回答 2

3

库的函数签名……

    void rawSend( const vector<unsigned char>* data );

迫使您构建std::vector数据,这实质上意味着它会带来不必要的低效率。要求客户端代码构建std::vector. 设计者不知道他们在做什么,不使用他们的软件是明智的。

库函数签名……

    vector<unsigned char>* rawRead(unsigned bufferSize = DEFAULT_BUFFER_SIZE, string* hostFrom = NULL);

std::string更糟糕的是:如果你想指定一个“hostFrom”(无论它的真正含义是什么),它不仅不必要地要求你构建一个,而且它还不必要地要求你释放结果vector。至少如果对函数结果类型有任何意义。当然,可能没有。

您不应该使用具有如此令人作呕的函数签名的库。可能任何随机挑选的图书馆都会好得多。即,更容易使用。


现有的使用代码如何……

void send_message(NLSocket* socket, uint16_t opcode, const void* rawData, size_t rawDataSize)
{
    vector<unsigned char> buffer;
    buffer.reserve(sizeof(uint16_t) + rawDataSize);
    buffer.push_back(opcode >> 8);
    buffer.push_back(opcode & 0xFF);
    const unsigned char* base(reinterpret_cast<const unsigned char*>(rawData));
    buffer.insert(buffer.end(), base, base + rawDataSize);
    socket->rawSend(&buffer);
}

作品:

  • reserve调用是过早优化的情况。它试图vector只分配一个缓冲区(此时执行)而不是可能的两个或更多。一个更好的解决方法vector是使用一个更健全的库。

  • buffer.push_back(opcode >> 8)(假定的)16 位量的高 8 位opcode放在向量的开头。首先放置高位,最显着的部分,称为大端格式。您在另一端的阅读代码必须采用大端格式。同样,如果此发送代码使用小端格式,那么读取代码将不得不采用小端格式。所以,这只是一个数据格式的决定,但考虑到这个决定,两端的代码必须遵守它。

  • buffer.push_back(opcode & 0xFF)调用将低 8 位放在opcode高位之后,这对于大端是正确的。

  • const unsigned char* base(reinterpret_cast<const unsigned char*>(rawData))声明只是为您的数据命名了一个适当类型的指针,称之为base. 该类型const unsigned char*是合适的,因为它允许字节级地址算术。原始形式参数类型const void*不允许地址算术。

  • buffer.insert(buffer.end(), base, base + rawDataSize)数据添加到向量中。表达式base + rawDataSize是先前声明启用的地址算术。

  • socket->rawSend(&buffer)是对 SillyLibraryrawSend方法的最终调用。


如何包装对 SillyLibraryrawRead函数的调用。

首先,为字节数据类型定义一个名称(命名总是一个好主意):

typedef unsigned char Byte;
typedef ptrdiff_t Size;

请参阅有关如何释放/销毁/删除(如有必要)SillyLibrary 函数结果的文档:

void deleteSillyLibVector( vector<Byte> const* p )
{
    // perhaps just "delete p", but it depends on the SillyLibrary
}

现在,涉及到发送操作std::vector只是一种痛苦。对于接收操作,则相反。创建一个动态数组并将其作为函数结果安全有效地传递,std::vector正是设计的目的。

但是,发送操作只是一个调用。

对于接收操作,根据SillyLibrary的设计,您需要循环执行接收调用次数,直到您收到所有数据。您没有提供足够的信息来执行此操作。但是下面的代码显示了一个底层读取,您的循环代码可以调用它,在 a 中累积数据vector

Size receive_append( NLSocket& socket, vector<Byte>& data )
{
    vector<Byte> const* const result = socket.raw_read();

    if( result == 0 )
    {
        return 0;
    }

    struct ScopeGuard
    {
        vector<Byte>* pDoomed;
        explicit ScopeGuard( vector<Byte>* p ): pDoomed( p ) {}
        ~ScopeGuard() { deleteSillyLibVector( pDoomed ); }
    };

    Size const nBytesRead = result->size();
    ScopeGuard cleanup( result );

    data.insert( data.end(), result->begin(), result->end() );
    return nBytesRead;
}

请注意使用析构函数进行清理,这使得这个异常更安全。在这种特殊情况下,唯一可能的例外是 a std::bad_alloc,无论如何这都是非常致命的。但是为了异常安全,使用析构函数进行清理的一般技术非常值得了解和使用(虽然通常不需要定义任何新类,但是在处理 SillyLibrary 时)可能必须这样做)。

最后,当您的循环代码确定所有数据都在手边时,它可以解释vector. 我把它作为一个练习,尽管这主要是你要求的。那是因为我已经在这里写了几乎整篇文章。

免责声明:即用型代码。

干杯&hth.,

于 2011-10-15T04:59:04.967 回答
0

把比特摆弄成非比特摆弄术语,opcode >> 8等价于opcode / 256opcode & 0xFF等价于opcode - ((opcode / 256) * 256)。注意舍入/截断。

opcode其视为由两个块组成,ophi并且oplo,每个块的值都为 0..255。opcode == (ophi * 256) + oplo.

一些额外的线索...

0xFF  == 255 == binary  11111111 == 2^8 - 1
0x100 == 256 == binary 100000000 == 2^8

              opcode
         /              \
Binary : 1010101010101010
         \      /\      /
           ophi    oplo

这样做的原因基本上是用于将 16 位值写入字节数据流的 endian-fix。网络流有它自己的规则,其中必须首先发送值的“大端”,而与任何特定平台上的默认处理方式无关。那 send_message 基本上是解构 16 位值来发送它。您需要读取两个块,然后重建十六位值。

无论您将重构编码为opcode = (ophi * 256) + oplo;还是原样,opcode == (ophi << 8) | oplo;这主要取决于口味 - 优化器将理解等价性并找出最有效的方法。

另外,不,我认为您不需要分配器。vector鉴于您正在使用参数,我什至不确定 using是一个好主意const void** rawData,但可能是这样,您应该reserve在阅读它之前先做一个。然后额外的相关块(重建操作码的两个字节,加上数组内容)。

我看到的一个大问题——你怎么知道你将要读取的原始数据的大小?它似乎既不是 的参数receive_message,也不是数据流本身提供的。

于 2011-10-15T04:28:19.210 回答