如何设计出高性能的通信协议——从struct到TLV思想.md

  1. 如何设计出高性能的通信协议——从struct到TLV思想.md
    原创 张小方 CppGuide 2024-01-10 10:00 发表于上海
    关注我,更多的优质内容第一时间阅读

https://mp.weixin.qq.com/s/6uiRqOTWVcWg23ZAxGvYeg

3.1 协议的演化
假设现在 A 与 B 之间要传输一个关于用户信息的数据包,可以将该数据包格式定义成如下形式:

#pragma pack(push, 1)
struct userinfo
{
//命令号
int32_t cmd;
//用户性别
char gender;
//用户昵称
char name[8];
};
#pragma pack(pop)
相信很多开发者曾经都定义过这样的协议,这种数据结构简单明了,对端只要直接拷贝按字段解析就可以了。但是,需求总是不断变化的,某一天根据新的需求需要在这个结构中增加一个字段表示用户的年龄,于是修改协议结构成:

#pragma pack(push, 1)
struct userinfo
{
//命令号
int32_t cmd;
//用户性别
char gender;
//用户昵称
char name[8];
//用户年龄
int32_t age;
};
#pragma pack(pop)
问题并没有直接增加一个字段那么简单,新修改的协议格式导致旧的客户端无法兼容(旧的客户端已经分发出去),这个时候我们升级服务器端的协议格式成新的,会导致旧的客户端无法使用。所以我们在最初设计协议的时候,我们需要增加一个版本号字段,针对不同的版本来做不同的处理,即:

/**

  • 旧的协议,版本号是 1
    */
    #pragma pack(push, 1)
    struct userinfo
    {
    //版本号
    short version;
    //命令号
    int32_t cmd;
    //用户性别
    char gender;
    //用户昵称
    char name[8];
    };
    #pragma pack(pop)

/**

  • 新的协议,版本号是 2
    */
    #pragma pack(push, 1)
    struct userinfo
    {
    //版本号
    short version;
    //命令号
    int32_t cmd;
    //用户性别
    char gender;
    //用户昵称
    char name[8];
    //用户年龄
    int32_t age;
    };
    #pragma pack(pop)
    这样我们可以用以下伪码来兼容新旧协议:

//从包中读取一个 short 型字段
short version = <从包中读取一个 short 型字段>;
if (version == 1)
{
//当旧的协议格式进行处理
}
else if (version == 2)
{
//当新的协议格式进行处理
}
上述方法是一个兼容旧版协议的常见做法。但是这样也存在一个问题,如果我们的业务需求变化快,我们可能需要经常调整协议字段(增、删、改),这样我们的版本号数量会比较多,我们的代码会变成类似下面这种形式:

//从包中读取一个 short 型字段
short version = <从包中读取一个 short 型字段>;
if (version == 版本号1)
{
//对版本号1格式进行处理
}
else if (version == 版本号2)
{
//对版本号2格式进行处理
}
else if (version == 版本号3)
{
//对版本号3格式进行处理
}
else if (version == 版本号4)
{
//对版本号4格式进行处理
}
else if (version == 版本号5)
{
//对版本号5格式进行处理
}
…省略更多…
这只是考虑了协议顶层结构,还没有考虑更多复杂的嵌套结构,不管怎样,这样的代码会变得越来越难以维护。

这里只是为了说明问题,实际开发中,建议读者在设计协议时尽量考虑周全,避免反复修改协议结构。

上述协议格式还存在另外一个问题,对于 name 字段,其长度为 8 个字节,这种定长的字段,长度大小不具有伸缩性,太长很多情况都用不完则造成内存和网络带宽的浪费,太短则某些情况下不够用。那么有没有什么方法来解决呢?

方法是有的,对于字符串类型的字段,我们可以在该字段前面加一个表示字符串长度(length)的标志,那么上面的协议在内存中的状态可以表示成如下图示:

图片

这种方法解决了定义字符串类型时太长浪费太短不够用的问题,但是没有解决修改协议(如新增字段)需要兼容众多旧版本的问题,对于这个问题,我们可以通过在每个字段前面加一个 type 类型来解决,我们可以使用一个 char 类型来表示常用的类型,规定如下:

类型 Type值 类型描述
bool 0 布尔值
char 1 char 型
int16 2 16 位整型
int32 3 32 位整型
int64 4 64 位整型
string 5 字符串或二进制序列
list 6 列表
map 7 map
更多自定义类型省略…

那么对于上述协议,其内存格式变成:

图片

这样,每个字段的类型就是自解释了。这就是所谓的 TLV(Type-Length-Value,有的资料也称 Tag-Length-Value,其设计思想来源于 ANS.1 规范中一种叫 BER(Basic Encoding Rules)的编码格式)。这种格式的协议,我们可以方便地增删和修改字段类型,程序解析时根据每个字段的 type 来得到字段的类型。

这里再根据笔者的经验多说几句,实际开发中 TLV 类型虽然易于扩展,但是也存在如下缺点:

TLV 格式因为每个字段增加了一个 type 类型,导致所占空间增大;

我们在解析字段时需要额外增加一些判断 type 的逻辑,去判断字段的类型,做相应的处理,即:

//读取第一个字节得到 type
if (type == Type::BOOL)
{
//bool型处理
}
else if (type == Type::CHAR)
{
//char型处理
}
else if (type == Type::SHORT)
{
//short型处理
}
…更多类型省略…
如上代码所示,每个字段我们都需要有这样的逻辑判断,这样的编码方式是非常麻烦的。

即使我们知道了每个字段的技术类型(相对业务来说),每个字段的业务含义仍然需要我们制定文档格式,也就是说 TLV 格式只是做到了技术上自解释。

所以,在实际的开发中,完全遵循 TLV 格式的协议并不多,尤其是针对一些整型类型的字段,例如整型字段的大小一旦在知道类型后,其长度就是固定下来的,例如 short 类型占 2 个字节,int32 类型占 4 个字节,因此不必专门浪费一段空间去存储其长度信息。

TLV 格式还可以嵌套,如下图所示:

图片

有的项目在 TLV 格式的基础上还扩展了一种叫 TTLV 格式的协议,即 Tag-Type-Length-Value,每个字段前面再增加一个 Tag 类型,这个时候 Type 表示数据类型,Tag 的含义由协议双方协定。

3.2 协议的分类
根据协议的内容是否是文本格式(即人为可读格式),我们将协议分为文本协议和二进制协议,像 http 协议的包头部分和 FTP 协议等都是典型的文本协议的例子。

3.3 协议设计工具
虽然 TLV 很简单,但是每搞一套新的协议都要从头编解码、调试,写编解码是一个毫无技术含量的枯燥体力活。在大量复制粘贴过程中,容易出错。

因此出现了一种叫 IDL(Interface Description Language)的语言规范,它是一种描述语言,也是一个中间语言,IDL 规范协议的使用类型,提供跨语言特性。可以定义一个描述协议格式的 IDL 文件,然后通过 IDL 工具分析 IDL 文件,就可以生成各种语言版本的协议代码。Google Protobuf 库自带的工具 protoc 就是这样一个工具。

3.4 一个自定义协议示例
现在我们结合前面的理论知识,演示如何自定义一个灵活的通信协议类。在我的开源即时通讯 Flamingo (https://github.com/balloonwj/flamingo)中我自定义了一个协议格式,这个协议也分为包头和包体两部分,其中包头的定义如下:

#pragma pack(push, 1)
//协议头
struct chat_msg_header
{
char compressflag; //压缩标志,如果为1,则启用压缩,反之不启用压缩
int32_t originsize; //包体压缩前大小
int32_t compresssize; //包体压缩后大小
char reserved[16];
};
#pragma pack(pop)
包体的内容长度无论是否设置了 compressflag 压缩标志,最后其实际长度都是 originsize,在得到了包体内容后,我们可以按通信两端规定好的协议来解析一个个的业务字段。

假设是一个聊天内容协议,发送方示例代码如下:

//发送方组装包体的格式
std::string outbuf;
net::BinaryStreamWriter writeStream(&outbuf);
writeStream.WriteInt32(msg_type_chat);
writeStream.WriteInt32(m_seq);
//senderId
writeStream.Write(senderId);
//消息内容
writeStream.WriteString(chatMsg);
//receiverId
writeStream.WriteInt32(receiverId);
writeStream.Flush();
上述代码开始处定义一个自动扩展的字符串缓冲区 outbuf(这里使用了 std::string),然后依次写入如下信息:

字段标号 字段名 类型/字节数目 说明
字段 1 msgType int32/4 字节 消息类型
字段 2 seq int32/4 字节 消息序号
字段 3 senderId int32/4 字节 发送者 id
字段 4 chatMsg string/长度由消息内容定 聊天消息,可以定义成一个 json 字符串
字段 5 receiverId int32/4 字节 接收者 id
写入上述几个字段后,消息体结构示意图如下:

图片

上图中有一个小的细节,即对于 string 类型的消息,在写入实际的字符串内容之前会先写入这个字符串的长度,writeStream.WriteString(chatMsg); 函数的实现如下:

//WriteString实际上调用了WriteCString方法
bool BinaryStreamWriter::WriteString(const string& str)
{
return WriteCString(str.c_str(), str.length());
}

bool BinaryStreamWriter::WriteCString(const char* str, size_t len)
{
std::string buf;
write7BitEncoded(len, buf);

m_data->append(buf);
m_data->append(str, len);
return true;

}
这里的 m_data 是前面介绍的 outbuf 的指针,也就是说使用一个 std::string 来存放二进制流, BinaryStreamWriter::WriteCString() 方法会先将字符串的长度写入流中,再写入字符串本身的内容,对于字符串的长度会根据其长度值压缩成 1 ~ 5 个字节,这将在后面文章《协议中对整型数值的压缩》中介绍。

//将一个4字节的整型数值压缩成1~5个字节
void write7BitEncoded(uint32_t value, std::string& buf)
{
do
{
unsigned char c = (unsigned char)(value & 0x7F);
value >>= 7;
if (value)
c |= 0x80;

    buf.append(1, c);
} while (value);

}
再写入上述 5 个字段后会调用 writeStream.Flush() 方法,该方法的实现如下:

void BinaryStreamWriter::Flush()
{
char* ptr = &(*m_data)[0];
unsigned int ulen = htonl(m_data->length());
memcpy(ptr, &ulen, sizeof(ulen));
}
这个函数的作用是在流的前 4 个字节处存放流数据的长度,存储长度使用的是网络字节序,这 4 个字节在创建 BinaryStreamWriter 对象时被预留出来。

std::string outbuf;
net::BinaryStreamWriter writeStream(&outbuf);
上述代码调用 BinaryStreamWriter 的构造函数:

enum
{
//4字节头长度
BINARY_PACKLEN_LEN_2 = 4,
CHECKSUM_LEN = 2,
};

BinaryStreamWriter::BinaryStreamWriter(string* data) :
m_data(data)
{
m_data->clear();
char str[BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN];
m_data->append(str, sizeof(str));
}
实际上在 m_data 指向流起始处一共预留了 6 个字节,前 4 个字节是放置将来整个流数据的长度(网络字节序),后 2 个字节存放数据的校验和(checksum,这里未使用)。

因此调用 writeStream.Flush() 方法后,流对象的结构变成如下所示:

图片

图中 4 字节的 streamLength 表示包体长度。

至此,这个二进制流虽然在我们这里的含义是包体部分,但是已经可以做到自我分界和解析了。在简单的业务中,我们先读取 4 个字节的 streamLength,然后根据 streamLength 转换成本机字节序后的长度来获取实际的内容长度。但是,我觉得这个不够方便,我没有在这个流的基础上继续扩展,而是选择在这个流的前面再加一个包头定义(即上述代码中的 chat_msg_header struct)。chat_msg_header struct(包头)和这里的流(包体)组装成一个完整的包:

//p 即是包体流的指针
void TcpSession::sendPackage(const char* p, int32_t length)
{
string srcbuf(p, length);
string destbuf;
//按需压缩
if (m_bNeedCompress)
{
if (!ZlibUtil::compressBuf(srcbuf, destbuf))
{
LOGE(“compress buf error”);
return;
}
}

string strPackageData;
chat_msg_header header;   
if (m_bNeedCompress)
{
 //设置压缩标志
 header.compressflag = PACKAGE_COMPRESSED;
 //设置压缩后的包体大小
 header.compresssize = destbuf.length();
}
else
 header.compressflag = PACKAGE_UNCOMPRESSED;
         
//设置压缩前的包体大小            
header.originsize = length;

//插入真正的包头
strPackageData.append((const char*)&header, sizeof(header));
strPackageData.append(destbuf);

//将整个包发到网络上去
conn->send(strPackageData);
}
实际上可以基于 BinaryStreamWriter 这个流对象进行扩展,而不用单独再定义一个 chat_msg_header 结构体作为包头。

在 BinaryStreamWriter 对于浮点型的处理,是先将浮点数按一定的精度转换成字符串,然后将字符串写入流中:

// isNULL 参数表示可以写入一个 double 类型占位符
bool BinaryStreamWriter::WriteDouble(double value, bool isNULL)
{
char doublestr[128];
if (isNULL == false)
{
sprintf(doublestr, “%f”, value);
WriteCString(doublestr, strlen(doublestr));
}
else
WriteCString(doublestr, 0);

return true;

}
上面是对这个自定义协议的封包过程,解包过程我实现了一个 BinaryStreamReader 类,该类的操作对象是去除了包头 chat_msg_header 结构后拿到的包体流。上述聊天协议的解包代码如下:

bool CRecvMsgThread::HandleMessage(const std::string& strMsg)
{
//strMsg是包体流,如何得到包体流在《02. 网络编程中正确的解包方式》一节已经介绍过了
net::BinaryStreamReader readStream(strMsg.c_str(), strMsg.length());

//读取消息类型
int32_t msgType;
if (!readStream.ReadInt32(msgType))
{
return false;
}

//读取消息序列号
if (!readStream.ReadInt32(m_seq))
{
return false;
}

//根据消息类型做处理
switch (msgType)
{
//聊天消息
case msg_type_chat:
{
//从流中读取发送者id
int32_t senderId;
if (!readStream.ReadInt32(senderId))
{
break;
}

//从流中读取聊天消息本身
std::string chatMsg;
size_t chatMsgLength;
if (!readStream.ReadString(&chatMsg, 0, chatMsgLength))
{
return false;
}

//从流中读取接收者id
int32_t receiverId;
if (!readStream.ReadInt32(receiverId))
{
break;
}

//对聊天消息进行处理
HandleChatMessage(senderId, receiverId, data);
}
break;

//对其他消息的处理
}// end switch

return false;
}
上述代码根据写入的流的字段类型顺序依次读出相应的字段值,在拿到各个字段值后就可以进行相应的业务处理了。

BinaryStreamReader 和 BinaryStreamWriter 类完整实现如下:

ProtocolStream.h(文件位置:flamingoserver/net/ProtocolStream.h)

/**

  • 一个强大的协议类, protocolstream.h
  • zhangyl 2017.05.27
    */

#ifndef PROTOCOL_STREAM_H
#define PROTOCOL_STREAM_H

#include <stdlib.h>
#include <sys/types.h>
#include
#include
#include <stdint.h>

//二进制协议的打包解包类,内部的服务器之间通讯,统一采用这些类
namespace net
{
enum
{
TEXT_PACKLEN_LEN = 4,
TEXT_PACKAGE_MAXLEN = 0xffff,
BINARY_PACKLEN_LEN = 2,
BINARY_PACKAGE_MAXLEN = 0xffff,

    TEXT_PACKLEN_LEN_2 = 6,
    TEXT_PACKAGE_MAXLEN_2 = 0xffffff,

    BINARY_PACKLEN_LEN_2 = 4,               //4字节头长度
    BINARY_PACKAGE_MAXLEN_2 = 0x10000000,   //包最大长度是256M,足够了

    CHECKSUM_LEN = 2,
};

//计算校验和
unsigned short checksum(const unsigned short* buffer, int size);
//将一个4字节的整型数值压缩成1~5个字节
void write7BitEncoded(uint32_t value, std::string& buf);
//将一个8字节的整型值编码成1~10个字节
void write7BitEncoded(uint64_t value, std::string& buf);

//将一个1~5个字节的字符数组值还原成4字节的整型值
void read7BitEncoded(const char* buf, uint32_t len, uint32_t& value);
//将一个1~10个字节的值还原成4字节的整型值
void read7BitEncoded(const char* buf, uint32_t len, uint64_t& value);

class BinaryStreamReader final
{
public:
    BinaryStreamReader(const char* ptr, size_t len);
    ~BinaryStreamReader() = default;

    virtual const char* GetData() const;
    virtual size_t GetSize() const;
    bool IsEmpty() const;
    bool ReadString(std::string* str, size_t maxlen, size_t& outlen);
    bool ReadCString(char* str, size_t strlen, size_t& len);
    bool ReadCCString(const char** str, size_t maxlen, size_t& outlen);
    bool ReadInt32(int32_t& i);
    bool ReadInt64(int64_t& i);
    bool ReadShort(short& i);
    bool ReadChar(char& c);
    size_t ReadAll(char* szBuffer, size_t iLen) const;
    bool IsEnd() const;
    const char* GetCurrent() const { return cur; }

public:
    bool ReadLength(size_t& len);
    bool ReadLengthWithoutOffset(size_t& headlen, size_t& outlen);

private:
    BinaryStreamReader(const BinaryStreamReader&) = delete;
    BinaryStreamReader& operator=(const BinaryStreamReader&) = delete;

private:
    const char* const ptr;
    const size_t      len;
    const char* cur;
};

class BinaryStreamWriter final
{
public:
    BinaryStreamWriter(std::string* data);
    ~BinaryStreamWriter() = default;

    virtual const char* GetData() const;
    virtual size_t GetSize() const;
    bool WriteCString(const char* str, size_t len);
    bool WriteString(const std::string& str);
    bool WriteDouble(double value, bool isNULL = false);
    bool WriteInt64(int64_t value, bool isNULL = false);
    bool WriteInt32(int32_t i, bool isNULL = false);
    bool WriteShort(short i, bool isNULL = false);
    bool WriteChar(char c, bool isNULL = false);
    size_t GetCurrentPos() const { return m_data->length(); }
    void Flush();
    void Clear();

private:
    BinaryStreamWriter(const BinaryStreamWriter&) = delete;
    BinaryStreamWriter& operator=(const BinaryStreamWriter&) = delete;

private:
    std::string* m_data;
};

}// end namespace

#endif //!PROTOCOL_STREAM_H
ProtocolStream.cpp (文件位置:flamingoserver/net/ProtocolStream.cpp)

#ifndef _WIN32
#include <arpa/inet.h>
#else
#include <Winsock2.h>
#pragma comment(lib, “Ws2_32.lib”)
#endif

#include “ProtocolStream.h”
#include <string.h>
#include <stdio.h>
#include <sys/types.h>
#include
#include
#include <stdio.h>

using namespace std;

namespace net
{
//计算校验和
unsigned short checksum(const unsigned short* buffer, int size)
{
unsigned int cksum = 0;
while (size > 1)
{
cksum += *buffer++;
size -= sizeof(unsigned short);
}
if (size)
{
cksum += (unsigned char)buffer;
}
//将32位数转换成16位数
while (cksum >> 16)
cksum = (cksum >> 16) + (cksum & 0xffff);

    return (unsigned short)(~cksum);
}

//将一个4字节的整型数值压缩成1~5个字节
void write7BitEncoded(uint32_t value, std::string& buf)
{
    do
    {
        unsigned char c = (unsigned char)(value & 0x7F);
        value >>= 7;
        if (value)
            c |= 0x80;

        buf.append(1, c);
    } while (value);
}

//将一个8字节的整型值编码成1~10个字节
void write7BitEncoded(uint64_t value, std::string& buf)
{
    do
    {
        unsigned char c = (unsigned char)(value & 0x7F);
        value >>= 7;
        if (value)
            c |= 0x80;

        buf.append(1, c);
    } while (value);
}

//将一个1~5个字节的字符数组值还原成4字节的整型值
void read7BitEncoded(const char* buf, uint32_t len, uint32_t& value)
{
    char c;
    value = 0;
    int bitCount = 0;
    int index = 0;
    do
    {
        c = buf[index];
        uint32_t x = (c & 0x7F);
        x <<= bitCount;
        value += x;
        bitCount += 7;
        ++index;
    } while (c & 0x80);
}

//将一个1~10个字节的值还原成4字节的整型值
void read7BitEncoded(const char* buf, uint32_t len, uint64_t& value)
{
    char c;
    value = 0;
    int bitCount = 0;
    int index = 0;
    do
    {
        c = buf[index];
        uint64_t x = (c & 0x7F);
        x <<= bitCount;
        value += x;
        bitCount += 7;
        ++index;
    } while (c & 0x80);
}

BinaryStreamReader::BinaryStreamReader(const char* ptr_, size_t len_)
    : ptr(ptr_), len(len_), cur(ptr_)
{
    cur += BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN;
}

bool BinaryStreamReader::IsEmpty() const
{
    return len <= BINARY_PACKLEN_LEN_2;
}

size_t BinaryStreamReader::GetSize() const
{
    return len;
}

bool BinaryStreamReader::ReadCString(char* str, size_t strlen, /* out */ size_t& outlen)
{
    size_t fieldlen;
    size_t headlen;
    if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
        return false;
    }

    // user buffer is not enough
    if (fieldlen > strlen) {
        return false;
    }

    // 偏移到数据的位置
    //cur += BINARY_PACKLEN_LEN_2; 
    cur += headlen;
    if (cur + fieldlen > ptr + len)
    {
        outlen = 0;
        return false;
    }
    memcpy(str, cur, fieldlen);
    outlen = fieldlen;
    cur += outlen;
    return true;
}

bool BinaryStreamReader::ReadString(string* str, size_t maxlen, size_t& outlen)
{
    size_t headlen;
    size_t fieldlen;
    if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
        return false;
    }

    // user buffer is not enough
    if (maxlen != 0 && fieldlen > maxlen) {
        return false;
    }

    // 偏移到数据的位置
    //cur += BINARY_PACKLEN_LEN_2; 
    cur += headlen;
    if (cur + fieldlen > ptr + len)
    {
        outlen = 0;
        return false;
    }
    str->assign(cur, fieldlen);
    outlen = fieldlen;
    cur += outlen;
    return true;
}

bool BinaryStreamReader::ReadCCString(const char** str, size_t maxlen, size_t& outlen)
{
    size_t headlen;
    size_t fieldlen;
    if (!ReadLengthWithoutOffset(headlen, fieldlen)) {
        return false;
    }
    // user buffer is not enough
    if (maxlen != 0 && fieldlen > maxlen) {
        return false;
    }

    // 偏移到数据的位置
    //cur += BINARY_PACKLEN_LEN_2; 
    cur += headlen;

    //memcpy(str, cur, fieldlen);
    if (cur + fieldlen > ptr + len)
    {
        outlen = 0;
        return false;
    }
    *str = cur;
    outlen = fieldlen;
    cur += outlen;
    return true;
}

bool BinaryStreamReader::ReadInt32(int32_t& i)
{
    const int VALUE_SIZE = sizeof(int32_t);

    if (cur + VALUE_SIZE > ptr + len)
        return false;

    memcpy(&i, cur, VALUE_SIZE);
    i = ntohl(i);

    cur += VALUE_SIZE;

    return true;
}

bool BinaryStreamReader::ReadInt64(int64_t& i)
{
    char int64str[128];
    size_t length;
    if (!ReadCString(int64str, 128, length))
        return false;

    i = atoll(int64str);

    return true;
}

bool BinaryStreamReader::ReadShort(short& i)
{
    const int VALUE_SIZE = sizeof(short);

    if (cur + VALUE_SIZE > ptr + len) {
        return false;
    }

    memcpy(&i, cur, VALUE_SIZE);
    i = ntohs(i);

    cur += VALUE_SIZE;

    return true;
}

bool BinaryStreamReader::ReadChar(char& c)
{
    const int VALUE_SIZE = sizeof(char);

    if (cur + VALUE_SIZE > ptr + len) {
        return false;
    }

    memcpy(&c, cur, VALUE_SIZE);
    cur += VALUE_SIZE;

    return true;
}

bool BinaryStreamReader::ReadLength(size_t& outlen)
{
    size_t headlen;
    if (!ReadLengthWithoutOffset(headlen, outlen)) {
        return false;
    }

    //cur += BINARY_PACKLEN_LEN_2;
    cur += headlen;
    return true;
}

bool BinaryStreamReader::ReadLengthWithoutOffset(size_t& headlen, size_t& outlen)
{
    headlen = 0;
    const char* temp = cur;
    char buf[5];
    for (size_t i = 0; i < sizeof(buf); i++)
    {
        memcpy(buf + i, temp, sizeof(char));
        temp++;
        headlen++;

        //if ((buf[i] >> 7 | 0x0) == 0x0)
        if ((buf[i] & 0x80) == 0x00)
            break;
    }
    if (cur + headlen > ptr + len)
        return false;

    unsigned int value;
    read7BitEncoded(buf, headlen, value);
    outlen = value;

    /*if ( cur + BINARY_PACKLEN_LEN_2 > ptr + len ) {
    return false;
    }

    unsigned int tmp;
    memcpy(&tmp, cur, sizeof(tmp));
    outlen = ntohl(tmp);*/
    return true;
}

bool BinaryStreamReader::IsEnd() const
{
    assert(cur <= ptr + len);
    return cur == ptr + len;
}

const char* BinaryStreamReader::GetData() const
{
    return ptr;
}

size_t BinaryStreamReader::ReadAll(char* szBuffer, size_t iLen) const
{
    size_t iRealLen = min(iLen, len);
    memcpy(szBuffer, ptr, iRealLen);
    return iRealLen;
}

//=================class BinaryStreamWriter implementation============//
BinaryStreamWriter::BinaryStreamWriter(string* data) :
    m_data(data)
{
    m_data->clear();
    char str[BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN];
    m_data->append(str, sizeof(str));
}

bool BinaryStreamWriter::WriteCString(const char* str, size_t len)
{
    std::string buf;
    write7BitEncoded(len, buf);

    m_data->append(buf);

    m_data->append(str, len);

    //unsigned int ulen = htonl(len);
    //m_data->append((char*)&ulen,sizeof(ulen));
    //m_data->append(str,len);
    return true;
}

bool BinaryStreamWriter::WriteString(const string& str)
{
    return WriteCString(str.c_str(), str.length());
}

const char* BinaryStreamWriter::GetData() const
{
    return m_data->data();
}

size_t BinaryStreamWriter::GetSize() const
{
    return m_data->length();
}

bool BinaryStreamWriter::WriteInt32(int32_t i, bool isNULL)
{
    int32_t i2 = 999999999;
    if (isNULL == false)
        i2 = htonl(i);
    m_data->append((char*)& i2, sizeof(i2));
    return true;
}

bool BinaryStreamWriter::WriteInt64(int64_t value, bool isNULL)
{
    char int64str[128];
    if (isNULL == false)
    {

#ifndef _WIN32
sprintf(int64str, “%ld”, value);
#else
sprintf(int64str, “%lld”, value);
#endif
WriteCString(int64str, strlen(int64str));
}
else
WriteCString(int64str, 0);
return true;
}

bool BinaryStreamWriter::WriteShort(short i, bool isNULL)
{
    short i2 = 0;
    if (isNULL == false)
        i2 = htons(i);
    m_data->append((char*)& i2, sizeof(i2));
    return true;
}

bool BinaryStreamWriter::WriteChar(char c, bool isNULL)
{
    char c2 = 0;
    if (isNULL == false)
        c2 = c;
    (*m_data) += c2;
    return true;
}

bool BinaryStreamWriter::WriteDouble(double value, bool isNULL)
{
    char   doublestr[128];
    if (isNULL == false)
    {
        sprintf(doublestr, "%f", value);
        WriteCString(doublestr, strlen(doublestr));
    }
    else
        WriteCString(doublestr, 0);
    return true;
}

void BinaryStreamWriter::Flush()
{
    char* ptr = &(*m_data)[0];
    unsigned int ulen = htonl(m_data->length());
    memcpy(ptr, &ulen, sizeof(ulen));
}

void BinaryStreamWriter::Clear()
{
    m_data->clear();
    char str[BINARY_PACKLEN_LEN_2 + CHECKSUM_LEN];
    m_data->append(str, sizeof(str));
}

}// end namespace
该协议类提供了对 char、short、int32、int64、string 等常用的字段类型的读写,功能非常强大,免去了定义各种结构体的麻烦。但是从业务上来讲,在实际开发中,每个字段的含义以及读写字段的顺序需要通信的双方提前协商好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值