我最近在Stack Overflow上提出了一个问题,关于如何将我的数据从一个16位整数后跟一个未确定数量的void * -cast内存转换为一个std :: vector of unsigned chars,以便使用称为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和*操作码。再一次,我不太熟悉bitshifting(我做了一些谷歌搜索来理解语法,但我不明白为什么上面的send_message()代码需要转移),所以我是在这里我的下一步不知所措。
有人可以帮我理解如何编写这个伴随的receive_message()函数吗?作为奖励,如果有人可以帮助解释原始代码,以便我知道它的未来如何运作(特别是,在这种情况下如何转移以及为什么有必要),这将有助于加深我对未来的理解。
提前致谢!
答案 0 :(得分:3)
图书馆的功能签名......
void rawSend( const vector<unsigned char>* data );
强制您构建std::vector
数据,这实质上意味着它会造成不必要的低效率。要求客户端代码构建std::vector
没有任何优势。如果设计的人不知道他们在做什么,那么不使用他们的软件是明智的。
库函数签名......
vector<unsigned char>* rawRead(unsigned bufferSize = DEFAULT_BUFFER_SIZE, string* hostFrom = NULL);
更糟糕的是:如果你想指定一个“hostFrom”(无论真正意味着什么),它不仅不必要地要求你构建一个std::string
,但它不必要地要求你释放结果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位数量opcode
的高8位置于向量的开头。放置高部分,最重要的部分,首先称为 big endian 格式。您在另一端的阅读代码必须采用大端格式。同样,如果此发送代码使用 little endian 格式,则读取代码必须采用小端格式。所以,这只是一种数据格式的决定,但鉴于决定,两端的代码必须遵守它。
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)
是最后一次调用SillyLibrary的rawSend
方法。
如何将调用包装到SillyLibrary rawRead
函数。
首先,为byte数据类型定义一个名称(总是一个好主意命名):
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的设计,您需要循环,执行接收呼叫的次数,直到您收到所有数据。您没有提供足够的信息来执行此操作。但是下面的代码显示了循环代码可以调用的底层读取,在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;
}
注意使用析构函数进行清理,这使得这更加安全。在这个特殊情况下,唯一可能的例外是std::bad_alloc
,无论如何这都非常致命。但是,为了异常安全,使用析构函数进行清理的一般技术非常值得了解和使用(当然,通常不需要定义任何新类,但是在处理SillyLibrary时)可能必须这样做。)
最后,当您的循环代码确定所有数据都在手边时,它可以解释vector
中的数据。我把它留作练习,尽管这主要是你要求的。那是因为我已经在这里写了几乎整篇文章。
免责声明:袖口外码。
干杯&amp;第h。,
答案 1 :(得分:0)
为了将比特摆弄成非比特小词,opcode >> 8
相当于opcode / 256
而opcode & 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位值写入字节数据流的字节序修复。网络流具有自己的规则,其中必须首先发送值的“大端”,而不依赖于在任何特定平台上默认处理的方式。 send_message基本上解构了16位值来发送它。您需要读取两个块,然后重建16位值。
无论您将重建编码为opcode = (ophi * 256) + oplo;
还是opcode == (ophi << 8) | oplo;
,主要是品味问题 - 优化人员会理解等效性并找出最有效的方法。
另外,不,我认为你不需要分配器。我甚至不确定使用vector
是个好主意,因为你使用的是const void** rawData
参数,但可能是这样,你应该在阅读之前先做reserve
。然后额外添加相关的块(两个字节重构操作码,加上数组内容)。
我看到的一个大问题 - 您如何知道您将要阅读的原始数据的大小?它似乎不是receive_message
的参数,也不是数据流本身提供的。