一、什么是通信协议

通信协议是指双方实体完成通信或服务所必须遵循的规则和约定。

二、diydb的协议格式

1、协议格式是通信协议中*重要的部分之一,diydb采用的是自定义格式和BSON格式的混合格式

2、协议格式总览

%title插图%num

协议主要分三个大段

(1)协议头:包括消息长度(用于读取一个完整的协议包)和消息类型

(2)协议主体:不同的消息类型对应不同的协议主体,协议主体主要保存的是每种消息类型独有的信息

(3)附加信息:一个或多个bson格式的对象,表示实际操作和返回的数据库中的数据对象

3、协议格式主体

%title插图%num

返回消息:一、返回值,用于表示返回成功或失败;二、返回记录数,用于表示返回的数据对象的个数,即附加消息中bson对象的个                     数。

插入消息:插入记录数,表示要插入的数据对象的个数。

删除消息:因为每次只能删除一个数据,所以这里没有特殊信息。

查询消息:因为每次只能查询一个数据,所以这里没有特殊信息。

注:1)diydb的数据删除和查询时,客户端必须在附加信息中以bson格式传递一个数据对象的_id信息。

2)另外,quit命令只需要协议头就可以了。

4、附加信息

%title插图%num

每个附加记录或插入记录实际上就是一个bson对象,删除条件或查询条件实际上就是{_id:XXX}格式的json对象对应的bson对象(因为diydb实际上没有复杂的条件查询功能,当然可以以后增加)。

注:1)、在直接传递一个结构体对象时,要注意结构体里面的属性的字节对齐。

2)、跨操作系统通信时要注意字节序的问题。

三、源码分析

通讯协议的主要实现(这里主要是数据打包解包)在msg.hpp和msg.cpp两个文件中,比如客户端用msgBuildInsert封装表示插入数据的数据包并存入数据流中,然后服务器端代理线程收到这个数据流后用msgExtractInsert解包这个数据流中的数据。下面是msg.hpp的所有代码。

#ifndef MSG_HPP__
#define MSG_HPP__

#include “bson.h”

#define OP_REPLY 1//返回消息
#define OP_INSERT 2//插入消息
#define OP_DELETE 3//删除消息
#define OP_QUERY 4//查询消息
#define OP_DISCONNECT 6//断开连接消息
#define OP_CONNECT 7//连接消息

#define RETURN_CODE_STATE_OK 1

struct MsgHeader
/*数据包的头*/
{
int messageLen ;//数据包的长
int opCode ;//数据包类型
} ;

struct MsgReply
/*返回数据包*/
{
MsgHeader header ;//数据包的头
int returnCode ;//返回值
int numReturn ;//返回的记录数量
char data[0] ;//标记数具体的开始位置
} ;

struct MsgInsert
/*插入数据包*/
{
MsgHeader header ;
int numInsert ;//插入记录数
char data[0] ;
} ;

struct MsgDelete
/*删除数据包*/
{
MsgHeader header ;
char key[0] ;//删除条件
} ;

struct MsgQuery
/*查询数据包*/
{
MsgHeader header ;
char key[0] ;//查询条件
} ;

/*返回消息的封装*/
int msgBuildReply ( char **ppBuffer, int *pBufferSize,
int returnCode, bson::BSONObj *objReturn ) ;

/*解消息*/
int msgExtractReply ( char *pBuffer, int &returnCode, int &numReturn,
const char **ppObjStart ) ;

/*做一个插入obj的数据包*/
int msgBuildInsert ( char **ppBuffer, int *pBufferSize, bson::BSONObj &obj ) ;

/*做一个插入多个obj的数据包*/
int msgBuildInsert ( char **ppBuffer, int *pBufferSize, vector<bson::BSONObj*> &obj ) ;

/*解插入*/
int msgExtractInsert ( char *pBuffer, int &numInsert, const char **ppObjStart ) ;

/*删除*/
int msgBuildDelete ( char **ppBuffer, int *pBufferSize, bson::BSONObj &key ) ;

/*解删除*/
int msgExtractDelete ( char *pBuffer, bson::BSONObj &key ) ;

/*查询*/
int msgBuildQuery ( char **ppBuffer, int *pBufferSize, bson::BSONObj &key ) ;

/*解查询*/
int msgExtractQuery ( char *pBuffer, bson::BSONObj &key ) ;

int msgMultiInsert ( char **ppBuffer, int *pBufferSize, bson::BSONObj &obj ) ;

/*做一个插入多个obj的数据包*/
int msgBuildInsert ( char **ppBuffer, int *pBufferSize, vector<bson::BSONObj*> &obj ) ;

#endif

对上面函数的实现,我们以插入数据包的封装和解包为例来分析

int msgBuildInsert ( char **ppBuffer, int *pBufferSize, BSONObj &obj )
{//只插入一个数据
int rc = DIY_OK ;
int size = sizeof(MsgInsert) + obj.objsize() ;//表示插入数据的数据包的长度
MsgInsert *pInsert = NULL ;
rc = msgCheckBuffer ( ppBuffer, pBufferSize, size ) ;//如果*ppBuffer指向的堆空间不够用,则重新分配一个足够的字节数组空间
if ( rc )
{
PD_LOG ( PDERROR, “Failed to realloc buffer for %d bytes, rc = %d”,
size, rc ) ;
goto error ;
}

pInsert = (MsgInsert*)(*ppBuffer) ;//将字节流划分成包格式
// 构建协议头
pInsert->header.messageLen = size ;
pInsert->header.opCode = OP_INSERT ;
// 构建协议主体
pInsert->numInsert = 1 ;
// 构建附加消息,即填bson对象
memcpy ( &pInsert->data[0], obj.objdata(), obj.objsize() ) ;
done :
return rc ;
error :
goto done ;
}

int msgExtractInsert ( char *pBuffer, int &numInsert, const char **ppObjStart )
{
int rc = DIY_OK ;
MsgInsert *pInsert = (MsgInsert*)pBuffer ;
// 检测数据包的长度是否合法
if ( pInsert->header.messageLen < (int)sizeof(MsgInsert) )
{
PD_LOG ( PDERROR, “Invalid length of insert message” ) ;
rc = DIY_INVALIDARG ;
goto error ;
}
// 检测数据包的类型对不对
if ( pInsert->header.opCode != OP_INSERT )
{
PD_LOG ( PDERROR, “non-insert code is received: %d, expected %d”,
pInsert->header.opCode, OP_INSERT ) ;
rc = DIY_INVALIDARG ;
goto error ;
}
// 解析附加信息,即取出bson对象
numInsert = pInsert->numInsert ;
// object
if ( 0 == numInsert )
{
*ppObjStart = NULL ;
}
else
{
*ppObjStart = &pInsert->data[0] ;
}
done :
return rc ;
error :
goto done ;
}
注:客户端得到的bson对象的由来:用户在客户端输入命令,这个命令如果包含json格式的文本,则通过json库根据表示json格式的文本构建一个json对象,然后通过bson库将这个json对象转换成一个bson对象。

四、总结

1、因为tcp通信是字节流,并没有边界标识,所以应用层在传数据时,必须通过增加数据包长度字段或者增加数据包边界标识来区分一个完整的包。diydb中用的是数据包长度的方式,这也是实际应用中常用的方式。
2、如果在通信时直接传结构体数据,则要注意字节对齐机制,所以要合理安排结构体中属性的长度的排列顺序。