如果发送端仅仅发送一段字符串,可以使用《muduo网络编程分包和解包(一)》介绍的长度+字符串的格式发送数据,但是如果想发送一个结构体或对象,需要对对象进行序列化把它转变成字节序才能发送给接收端,而不能直接把结构体或对象本身发送给接收端,原因见《jsoncpp学习》。
muduo使用protobuf进行序列化。由于protobuf打包的数据没有自带长度和类型信息,需要在protobuf打包的数据前面加上长度和消息的类型名。
长度信息的作用是:接收端接收到数据后根据长度信息截取一个完整的消息。类型名的作用是:接收端根据接收到的类型名和对象序列化后产生的字节序进行反序列化,产生该类型的对象。
len(4个字节)
message Type nameLen(4个字节)
message Type name (nameLen byte,end with \0)
protobuf data ( len-nameLen-8 byte)
checkSum (4个字节)
以上数据表示的含义为:
len:占4个字节,它表示除len占据的4个字节外整个消息的长度,即nameLen + name + probobuf data + checkSum占的字节数;
message Type nameLen:占4个字节,表示消息类型名的长度
message Type name:占nameLen个字节,表示消息类型名
protobuf data:占 len-nameLen-8个字节,表示对象序列化后的字节序
checkSum:占4个字节,表示使用adler32算法对消息中除len外其他所有数据计算的校验值。使用校验的原因为:虽然tcp是可靠传输协议,但陈硕写的《Tcp的可靠性有多高》指出如果路由器出现硬件故障,有些错误无法用链路层、网路层、传输层的checksum查出来,只能通过应用层的checksum来检测。
(1)首先通过protobuf把要发送的数据放到消息对象中。例子见https://github.com/chenshuo/muduo/blob/master/examples/protobuf/codec/client.cc 中的
muduo::Query query;
query.set_id(1);
query.set_questioner("Chen Shuo");
query.add_question("Running?");
这里的消息对象muduo::Query是google::protobuf::Message的子类对象。
(2)调用codec.cc的send函数把消息对象进行序列化并以第2节介绍的格式进行打包。其中send函数会调用fillEmptyBuffer函数对消息对象进行打包。
fillEmptyBuffer的代码为:
void ProtobufCodec::fillEmptyBuffer(Buffer* buf, const google::protobuf::Message& message)
{
// buf->retrieveAll();
assert(buf->readableBytes() == 0);
const std::string& typeName = message.GetTypeName();
int32_t nameLen = static_cast(typeName.size()+1);
buf->appendInt32(nameLen);
buf->append(typeName.c_str(), nameLen);
// code copied from MessageLite::SerializeToArray() and MessageLite::SerializePartialToArray().
GOOGLE_DCHECK(message.IsInitialized()) << InitializationErrorMessage("serialize", message);
int byte_size = message.ByteSize();
buf->ensureWritableBytes(byte_size);
uint8_t* start = reinterpret_cast(buf->beginWrite());
uint8_t* end = message.SerializeWithCachedSizesToArray(start);
if (end - start != byte_size)
{
ByteSizeConsistencyError(byte_size, message.ByteSize(), static_cast<int>(end - start));
}
buf->hasWritten(byte_size);
int32_t checkSum = static_cast(
::adler32(1,
reinterpret_cast<const Bytef*>(buf->peek()),
static_cast<int>(buf->readableBytes())));
buf->appendInt32(checkSum);
assert(buf->readableBytes() == sizeof nameLen + nameLen + byte_size + sizeof checkSum);
int32_t len = sockets::hostToNetwork32(static_cast(buf->readableBytes()));
buf->prepend(&len, sizeof len);
}
6-9行代码为:获取message的类型名和类型名的长度,然后存放到Buffer中。
12-22行的代码来源于protobuf中SerializeToArray的代码,它的作用是对message的内容进行序列化,并把序列化的内容存放到buffer中。(SerializeToArray的代码位于protobuf-master/src/google/protobuf/message_lite.cc中的MessageLite::SerializeToArray。代码位置https://github.com/google/protobuf/blob/master/src/google/protobuf/message_lite.cc)
25-29行代码为:使用adler32算法对消息数据计算校验值,然后存放到Buffer中。虽然tcp是可靠传输协议,但《Tcp的可靠性有多高》指出如果路由器出现硬件故障,有些错误无法用链路层、网路层、传输层的checksum查出来,只能通过应用层的checksum来检测。
30-32行代码为:计算nameLen+name+protobuf data + checkSum占据的长度,然后对计算的数据由本地字节序转换成网络字节序,并存放到整个消息的最前面。
(4)message打包好后,就可以通过send函数把数据发送到接收端了。
muduo接收消息的代码见https://github.com/chenshuo/muduo/blob/master/examples/protobuf/codec/codec.cc 的ProtobufCodec::onMessage。
接收步骤:
(1)onMessage首先判断接收到的数据是否大于或等于kMinMessageLen+kHeaderLen(消息格式长度的最小值),如果小于直接返回。
(2)否则获得len的大小,并根据len判断buffer里的数据是否大于或等于len+kHeaderLen(kHeaderLen为4代表len所占的字节数),如果小于直接返回。
(3)否则调用parse函数对消息数据进行解析,并对protobuf data反序列化获得google::protobuf::Message对象。
parse函数的关键代码为:
MessagePtr ProtobufCodec::parse(const char* buf, int len, ErrorCode* error)
{
MessagePtr message;
// check sum
int32_t expectedCheckSum = asInt32(buf + len - kHeaderLen);
int32_t checkSum = static_cast(
::adler32(1,
reinterpret_cast<const Bytef*>(buf),
static_cast<int>(len - kHeaderLen)));
if (checkSum == expectedCheckSum)
{
// get message type name
int32_t nameLen = asInt32(buf);
if (nameLen >= 2 && nameLen <= len - 2*kHeaderLen)
{
std::string typeName(buf + kHeaderLen, buf + kHeaderLen + nameLen - 1);
// create message object
message.reset(createMessage(typeName));
if (message)
{
// parse from buffer
const char* data = buf + kHeaderLen + nameLen;
int32_t dataLen = len - nameLen - 2*kHeaderLen;
if (message->ParseFromArray(data, dataLen))
{
*error = kNoError;
}
else
{
*error = kParseError;
}
}
。。。。
}
}
return message;
}
parse函数的解释为:
1)6-10行代码为:获取消息里的校验值,然后对获取的消息数据使用adler32算法计算校验值,如果两个校验值不等回调errorCallback。
2)14-17行代码为:如果校验值相等,获取nameLen,然后读取nameLen字节的数据获取消息类型名。
3)第19行代码为:调用google::protobuf::Message* ProtobufCodec::createMessage(const std::string& typeName),该函数可以根据消息类型名来创建该类型的对象。
4)20-28行代码为:调用message对象的ParseFromArray,该函数会对接收到的序列化字节序protobuf data进行反序列化,从而获得message所指对象每个数据成员的值。
可见19-28行代码是对protobuf data反序列化最重要的步骤
(4)从parse获取google::protobuf::Message对象后,调用messageCallback_函数执行用户回调函数。
因为ProtobufCodec::onMessage函数获得的是google::protobuf::Message,messageCallback_ 传给用户的消息也是google::protobuf::Message,但用户期望的对象实际上是该类的子类,比如3.(1)中的muduo::Query对象。所以用户还需要在回调函数messageCallback_中对google::protobuf::Message对象做一次向下转换把它转换为实际需要的子类对象。为了简化用户操作,muduo的ProtobufDispatcher帮用户做了这个操作,这样用户在回调函数中获得的就是实际需要操作的子类对象,而不是Message对象。
ProtobufDispatcher究竟如何与多个未知的消息合作,将message 向下转换为那些未知的消息类型的呢?muduo将多态和模板结合,利用templated derived class来提供类型上的灵活性。
protobufDispatcher有一个模板成员函数registerMessageCallback,为了让protobufDispatcher做向下转换,用户需要调用该函数把实际的消息类型作为模板参数传给它,该消息的回调函数也要传给它。registerMessageCallback获得消息类型后会创建一个模板化的类CallbackT< T >,这样消息的类型信息就保存在CallbackT< T >了。
ProtobufDispatcher位于 https://github.com/chenshuo/muduo/blob/master/examples/protobuf/codec/dispatcher.h 。
(1)《muduo网络编程分包和解包(一)》用于仅简单的发送字符串这种情况, 发送数据以“长度+字符串”的格式发送给接收端,是将std::string转换成muduo::net::Buffer的过程;接收数据时是对接收到的muduo::net::Buffer数据去掉长度并转换成字符串的过程。
(2)《muduo网络编程分包和解包(二)》用于发送复杂的结构体和对象的情况,发送数据时用protobuf对数据序列化并在序列化字节序的前面加上长度和类型名后面加上校验值(感觉也可以不加),是将google::protobuf::Message子类对象转换成muduo::net::Buffer的过程;接收数据则是对接收到muduo::net::Buffer存储的数据反序列化并转换成google::protobuf::Message子类对象的过程。