J'essaie de lire/écrire plusieurs messages de tampons de protocole à partir de fichiers, en C++ et en Java. Google suggère d'écrire des préfixes de longueur avant les messages, mais il n'y a aucun moyen de le faire par défaut (que j'ai pu voir).
Cependant, l'API Java dans la version 2.1.0 a reçu un ensemble de fonctions d'E/S "délimitées" qui font apparemment ce travail:
parseDelimitedFrom
mergeDelimitedFrom
writeDelimitedTo
Existe-t-il des équivalents C++? Et sinon, quel est le format de fil pour les préfixes de taille des API Java attachées, donc je peux analyser ces messages en C++?
Celles-ci existent maintenant dans google/protobuf/util/delimited_message_util.h
à partir de la v3.3.0.
Je suis un peu en retard pour la fête ici, mais les implémentations ci-dessous incluent des optimisations manquantes dans les autres réponses et n'échoueront pas après 64 Mo d'entrée (bien qu'il applique toujours la limite de 64 Mo sur chaque message individuel , mais pas sur tout le flux).
(Je suis l'auteur des bibliothèques C++ et Java protobuf, mais je ne travaille plus pour Google. Désolé que ce code ne soit jamais entré dans la bibliothèque officielle. Voilà à quoi cela ressemblerait si cela a.)
bool writeDelimitedTo(
const google::protobuf::MessageLite& message,
google::protobuf::io::ZeroCopyOutputStream* rawOutput) {
// We create a new coded stream for each message. Don't worry, this is fast.
google::protobuf::io::CodedOutputStream output(rawOutput);
// Write the size.
const int size = message.ByteSize();
output.WriteVarint32(size);
uint8_t* buffer = output.GetDirectBufferForNBytesAndAdvance(size);
if (buffer != NULL) {
// Optimization: The message fits in one buffer, so use the faster
// direct-to-array serialization path.
message.SerializeWithCachedSizesToArray(buffer);
} else {
// Slightly-slower path when the message is multiple buffers.
message.SerializeWithCachedSizes(&output);
if (output.HadError()) return false;
}
return true;
}
bool readDelimitedFrom(
google::protobuf::io::ZeroCopyInputStream* rawInput,
google::protobuf::MessageLite* message) {
// We create a new coded stream for each message. Don't worry, this is fast,
// and it makes sure the 64MB total size limit is imposed per-message rather
// than on the whole stream. (See the CodedInputStream interface for more
// info on this limit.)
google::protobuf::io::CodedInputStream input(rawInput);
// Read the size.
uint32_t size;
if (!input.ReadVarint32(&size)) return false;
// Tell the stream not to read beyond that size.
google::protobuf::io::CodedInputStream::Limit limit =
input.PushLimit(size);
// Parse the message.
if (!message->MergeFromCodedStream(&input)) return false;
if (!input.ConsumedEntireMessage()) return false;
// Release the limit.
input.PopLimit(limit);
return true;
}
D'accord, je n'ai donc pas été en mesure de trouver des fonctions C++ de niveau supérieur implémentant ce dont j'ai besoin, mais certains spelunking via la référence API Java sont apparus ci-dessous, à l'intérieur de MessageLite interface:
void writeDelimitedTo(OutputStream output)
/* Like writeTo(OutputStream), but writes the size of
the message as a varint before writing the data. */
Ainsi, le préfixe de taille Java est un varint (tampons de protocole)!
Armé de ces informations, je suis allé fouiller dans l'API C++ et j'ai trouvé l'en-tête CodedStream , qui contient les éléments suivants:
bool CodedInputStream::ReadVarint32(uint32 * value)
void CodedOutputStream::WriteVarint32(uint32 value)
En les utilisant, je devrais être capable de rouler mes propres fonctions C++ qui font le travail.
Cependant, ils devraient vraiment l'ajouter à l'API Message principale; il manque des fonctionnalités compte tenu de Java l'a, tout comme l'excellent port C # de protobuf-net de Marc Gravell (via SerializeWithLengthPrefix et DeserializeWithLengthPrefix).
J'ai résolu le même problème en utilisant CodedOutputStream/ArrayOutputStream pour écrire le message (avec la taille) et CodedInputStream/ArrayInputStream pour lire le message (avec la taille).
Par exemple, le pseudo-code suivant écrit la taille du message suivant par le message:
const unsigned bufLength = 256;
unsigned char buffer[bufLength];
Message protoMessage;
google::protobuf::io::ArrayOutputStream arrayOutput(buffer, bufLength);
google::protobuf::io::CodedOutputStream codedOutput(&arrayOutput);
codedOutput.WriteLittleEndian32(protoMessage.ByteSize());
protoMessage.SerializeToCodedStream(&codedOutput);
Lors de l'écriture, vous devez également vérifier que votre tampon est suffisamment grand pour contenir le message (y compris la taille). Et lors de la lecture, vous devez vérifier que votre tampon contient un message entier (y compris la taille).
Ce serait certainement pratique s'ils ajoutaient des méthodes pratiques à l'API C++ similaires à celles fournies par l'API Java.
IsteamInputStream est très fragile aux eofs et autres erreurs qui se produisent facilement lorsqu'il est utilisé avec std :: istream. Après cela, les flux de protobuf sont endommagés de façon permanente et toutes les données de tampon déjà utilisées sont détruites. Il existe un support approprié pour la lecture à partir de flux traditionnels dans protobuf.
Implémentez google::protobuf::io::CopyingInputStream
Et utilisez-le avec CopyingInputStreamAdapter . Faites de même pour les variantes de sortie.
En pratique, un appel d'analyse se termine dans google::protobuf::io::CopyingInputStream::Read(void* buffer, int size)
où un tampon est donné. La seule chose qui reste à faire est d'y lire en quelque sorte.
Voici un exemple à utiliser avec les flux synchronisés Asio ( SyncReadStream / SyncWriteStream ):
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
using namespace google::protobuf::io;
template <typename SyncReadStream>
class AsioInputStream : public CopyingInputStream {
public:
AsioInputStream(SyncReadStream& sock);
int Read(void* buffer, int size);
private:
SyncReadStream& m_Socket;
};
template <typename SyncReadStream>
AsioInputStream<SyncReadStream>::AsioInputStream(SyncReadStream& sock) :
m_Socket(sock) {}
template <typename SyncReadStream>
int
AsioInputStream<SyncReadStream>::Read(void* buffer, int size)
{
std::size_t bytes_read;
boost::system::error_code ec;
bytes_read = m_Socket.read_some(boost::asio::buffer(buffer, size), ec);
if(!ec) {
return bytes_read;
} else if (ec == boost::asio::error::eof) {
return 0;
} else {
return -1;
}
}
template <typename SyncWriteStream>
class AsioOutputStream : public CopyingOutputStream {
public:
AsioOutputStream(SyncWriteStream& sock);
bool Write(const void* buffer, int size);
private:
SyncWriteStream& m_Socket;
};
template <typename SyncWriteStream>
AsioOutputStream<SyncWriteStream>::AsioOutputStream(SyncWriteStream& sock) :
m_Socket(sock) {}
template <typename SyncWriteStream>
bool
AsioOutputStream<SyncWriteStream>::Write(const void* buffer, int size)
{
boost::system::error_code ec;
m_Socket.write_some(boost::asio::buffer(buffer, size), ec);
return !ec;
}
Usage:
AsioInputStream<boost::asio::ip::tcp::socket> ais(m_Socket); // Where m_Socket is a instance of boost::asio::ip::tcp::socket
CopyingInputStreamAdaptor cis_adp(&ais);
CodedInputStream cis(&cis_adp);
Message protoMessage;
uint32_t msg_size;
/* Read message size */
if(!cis.ReadVarint32(&msg_size)) {
// Handle error
}
/* Make sure not to read beyond limit of message */
CodedInputStream::Limit msg_limit = cis.PushLimit(msg_size);
if(!msg.ParseFromCodedStream(&cis)) {
// Handle error
}
/* Remove limit */
cis.PopLimit(msg_limit);
J'ai rencontré le même problème en C++ et en Python.
Pour la version C++, j'ai utilisé un mélange du code que Kenton Varda a publié sur ce fil et du code de la demande d'extraction qu'il a envoyée à l'équipe de protobuf (car la version publiée ici ne gère pas EOF = tandis que celui qu'il a envoyé à github le fait).
#include <google/protobuf/message_lite.h>
#include <google/protobuf/io/zero_copy_stream.h>
#include <google/protobuf/io/coded_stream.h>
bool writeDelimitedTo(const google::protobuf::MessageLite& message,
google::protobuf::io::ZeroCopyOutputStream* rawOutput)
{
// We create a new coded stream for each message. Don't worry, this is fast.
google::protobuf::io::CodedOutputStream output(rawOutput);
// Write the size.
const int size = message.ByteSize();
output.WriteVarint32(size);
uint8_t* buffer = output.GetDirectBufferForNBytesAndAdvance(size);
if (buffer != NULL)
{
// Optimization: The message fits in one buffer, so use the faster
// direct-to-array serialization path.
message.SerializeWithCachedSizesToArray(buffer);
}
else
{
// Slightly-slower path when the message is multiple buffers.
message.SerializeWithCachedSizes(&output);
if (output.HadError())
return false;
}
return true;
}
bool readDelimitedFrom(google::protobuf::io::ZeroCopyInputStream* rawInput, google::protobuf::MessageLite* message, bool* clean_eof)
{
// We create a new coded stream for each message. Don't worry, this is fast,
// and it makes sure the 64MB total size limit is imposed per-message rather
// than on the whole stream. (See the CodedInputStream interface for more
// info on this limit.)
google::protobuf::io::CodedInputStream input(rawInput);
const int start = input.CurrentPosition();
if (clean_eof)
*clean_eof = false;
// Read the size.
uint32_t size;
if (!input.ReadVarint32(&size))
{
if (clean_eof)
*clean_eof = input.CurrentPosition() == start;
return false;
}
// Tell the stream not to read beyond that size.
google::protobuf::io::CodedInputStream::Limit limit = input.PushLimit(size);
// Parse the message.
if (!message->MergeFromCodedStream(&input)) return false;
if (!input.ConsumedEntireMessage()) return false;
// Release the limit.
input.PopLimit(limit);
return true;
}
Et voici mon implémentation python2:
from google.protobuf.internal import encoder
from google.protobuf.internal import decoder
#I had to implement this because the tools in google.protobuf.internal.decoder
#read from a buffer, not from a file-like objcet
def readRawVarint32(stream):
mask = 0x80 # (1 << 7)
raw_varint32 = []
while 1:
b = stream.read(1)
#eof
if b == "":
break
raw_varint32.append(b)
if not (ord(b) & mask):
#we found a byte starting with a 0, which means it's the last byte of this varint
break
return raw_varint32
def writeDelimitedTo(message, stream):
message_str = message.SerializeToString()
delimiter = encoder._VarintBytes(len(message_str))
stream.write(delimiter + message_str)
def readDelimitedFrom(MessageType, stream):
raw_varint32 = readRawVarint32(stream)
message = None
if raw_varint32:
size, _ = decoder._DecodeVarint32(raw_varint32, 0)
data = stream.read(size)
if len(data) < size:
raise Exception("Unexpected end of file")
message = MessageType()
message.ParseFromString(data)
return message
#In place version that takes an already built protobuf object
#In my tests, this is around 20% faster than the other version
#of readDelimitedFrom()
def readDelimitedFrom_inplace(message, stream):
raw_varint32 = readRawVarint32(stream)
if raw_varint32:
size, _ = decoder._DecodeVarint32(raw_varint32, 0)
data = stream.read(size)
if len(data) < size:
raise Exception("Unexpected end of file")
message.ParseFromString(data)
return message
else:
return None
Ce n'est peut-être pas le code le plus beau et je suis sûr qu'il peut être refactorisé un peu, mais au moins cela devrait vous montrer une façon de le faire.
Maintenant, le gros problème: c'est LENT .
Même lorsque vous utilisez l'implémentation C++ de python-protobuf, c'est un ordre de grandeur plus lent qu'en C++ pur. J'ai un point de référence où je lis des messages protobuf 10M de ~ 30 octets chacun à partir d'un fichier. Cela prend ~ 0,9s en C++ et 35s en python.
Une façon de le rendre un peu plus rapide serait de réimplémenter le décodeur varint pour le lire à partir d'un fichier et le décoder en une seule fois, au lieu de lire dans un fichier et de décoder comme ce code le fait actuellement. (le profilage montre qu'une quantité importante de temps est passée dans l'encodeur/décodeur varint). Mais inutile de dire que cela ne suffit pas à lui seul pour combler l'écart entre la version python et la version C++.
Toute idée pour l'accélérer est la bienvenue :)
Voici:
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/io/coded_stream.h>
using namespace google::protobuf::io;
class FASWriter
{
std::ofstream mFs;
OstreamOutputStream *_OstreamOutputStream;
CodedOutputStream *_CodedOutputStream;
public:
FASWriter(const std::string &file) : mFs(file,std::ios::out | std::ios::binary)
{
assert(mFs.good());
_OstreamOutputStream = new OstreamOutputStream(&mFs);
_CodedOutputStream = new CodedOutputStream(_OstreamOutputStream);
}
inline void operator()(const ::google::protobuf::Message &msg)
{
_CodedOutputStream->WriteVarint32(msg.ByteSize());
if ( !msg.SerializeToCodedStream(_CodedOutputStream) )
std::cout << "SerializeToCodedStream error " << std::endl;
}
~FASWriter()
{
delete _CodedOutputStream;
delete _OstreamOutputStream;
mFs.close();
}
};
class FASReader
{
std::ifstream mFs;
IstreamInputStream *_IstreamInputStream;
CodedInputStream *_CodedInputStream;
public:
FASReader(const std::string &file), mFs(file,std::ios::in | std::ios::binary)
{
assert(mFs.good());
_IstreamInputStream = new IstreamInputStream(&mFs);
_CodedInputStream = new CodedInputStream(_IstreamInputStream);
}
template<class T>
bool ReadNext()
{
T msg;
unsigned __int32 size;
bool ret;
if ( ret = _CodedInputStream->ReadVarint32(&size) )
{
CodedInputStream::Limit msgLimit = _CodedInputStream->PushLimit(size);
if ( ret = msg.ParseFromCodedStream(_CodedInputStream) )
{
_CodedInputStream->PopLimit(msgLimit);
std::cout << mFeed << " FASReader ReadNext: " << msg.DebugString() << std::endl;
}
}
return ret;
}
~FASReader()
{
delete _CodedInputStream;
delete _IstreamInputStream;
mFs.close();
}
};
Je cherchais également une solution pour cela. Voici le cœur de notre solution, en supposant que certains Java a écrit de nombreux messages MyRecord avec writeDelimitedTo
dans un fichier. Ouvrez le fichier et la boucle, en faisant:
if (someCodedInputStream-> ReadVarint32 (& bytes)) { CodedInputStream :: Limit msgLimit = someCodedInputStream-> PushLimit (octets); if (myRecord-> ParseFromCodedStream (someCecord)) { // faites votre travail avec l'instance MyRecord analysée } else { // gérer l'erreur d'analyse } someCodedInputStream-> PopLimit ( msgLimit); } else { // peut-être fin du fichier }
J'espère que ça aide.
Juste pour être complet, je poste ici une version à jour qui fonctionne avec la version principale de protobuf et Python3
Pour la version C++ il suffit d'utiliser les utils dans delimited_message_utils.h, ici un MWE
#include <google/protobuf/io/zero_copy_stream_impl.h>
#include <google/protobuf/util/delimited_message_util.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
template <typename T>
bool writeManyToFile(std::deque<T> messages, std::string filename) {
int outfd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC);
google::protobuf::io::FileOutputStream fout(outfd);
bool success;
for (auto msg: messages) {
success = google::protobuf::util::SerializeDelimitedToZeroCopyStream(
msg, &fout);
if (! success) {
std::cout << "Writing Failed" << std::endl;
break;
}
}
fout.Close();
close(outfd);
return success;
}
template <typename T>
std::deque<T> readManyFromFile(std::string filename) {
int infd = open(filename.c_str(), O_RDONLY);
google::protobuf::io::FileInputStream fin(infd);
bool keep = true;
bool clean_eof = true;
std::deque<T> out;
while (keep) {
T msg;
keep = google::protobuf::util::ParseDelimitedFromZeroCopyStream(
&msg, &fin, nullptr);
if (keep)
out.Push_back(msg);
}
fin.Close();
close(infd);
return out;
}
Pour la version Python3, en s'appuyant sur la réponse de @fireboot, la seule chose à modifier était le décodage de raw_varint32
def getSize(raw_varint32):
result = 0
shift = 0
b = six.indexbytes(raw_varint32, 0)
result |= ((ord(b) & 0x7f) << shift)
return result
def readDelimitedFrom(MessageType, stream):
raw_varint32 = readRawVarint32(stream)
message = None
if raw_varint32:
size = getSize(raw_varint32)
data = stream.read(size)
if len(data) < size:
raise Exception("Unexpected end of file")
message = MessageType()
message.ParseFromString(data)
return message
Puisque je ne suis pas autorisé à écrire ceci en tant que commentaire à la réponse de Kenton Varda ci-dessus; Je crois qu'il y a un bug dans le code qu'il a posté (ainsi que dans d'autres réponses qui ont été fournies). Le code suivant:
...
google::protobuf::io::CodedInputStream input(rawInput);
// Read the size.
uint32_t size;
if (!input.ReadVarint32(&size)) return false;
// Tell the stream not to read beyond that size.
google::protobuf::io::CodedInputStream::Limit limit =
input.PushLimit(size);
...
définit une limite incorrecte car elle ne prend pas en compte la taille du varint32 qui a déjà été lu depuis l'entrée. Cela peut entraîner une perte/corruption de données car des octets supplémentaires sont lus dans le flux qui peut faire partie du message suivant. La manière habituelle de gérer cela correctement est de supprimer le CodedInputStream utilisé pour lire la taille et d'en créer un nouveau pour lire la charge utile:
...
uint32_t size;
{
google::protobuf::io::CodedInputStream input(rawInput);
// Read the size.
if (!input.ReadVarint32(&size)) return false;
}
google::protobuf::io::CodedInputStream input(rawInput);
// Tell the stream not to read beyond that size.
google::protobuf::io::CodedInputStream::Limit limit =
input.PushLimit(size);
...
En travaillant avec une version objective-c des tampons de protocole, j'ai rencontré ce problème exact. Lors de l'envoi depuis le client iOS vers un serveur basé sur Java qui utilise parseDelimitedFrom, qui attend la longueur comme premier octet, je devais d'abord appeler writeRawByte vers CodedOutputStream. Publier ici pour aider les autres qui Tout en travaillant sur ce problème, on pourrait penser que les proto-bufs de Google viendraient avec un simple indicateur qui le fait pour vous ...
Request* request = [rBuild build];
[self sendMessage:request];
}
- (void) sendMessage:(Request *) request {
//** get length
NSData* n = [request data];
uint8_t len = [n length];
PBCodedOutputStream* os = [PBCodedOutputStream streamWithOutputStream:outputStream];
//** prepend it to message, such that Request.parseDelimitedFrom(in) can parse it properly
[os writeRawByte:len];
[request writeToCodedOutputStream:os];
[os flush];
}