让数据解析能够做到向前向后完全兼容(最近做项目总结)
Published on 三月 11, 2010
最近在做项目的时候,遇到一个问题,即结构体内的字段可能会在未来的时间内不停的增加(不会减少或者删除),所以在打包解包的时候就会涉及到版本兼容的问题,并且是向前和向后同时兼容。
我们先来看一下,如果结构体的内容永远不变,那么我们用结构体自解析的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | typedef struct _farmbase_land1 {/*{{{*/ unsigned char ID; unsigned char bitmap; _farmbase_land1() { ID = 0; bitmap = 0; } int Output(unsigned int /*ver*/,char*& buff,int& iLen,int iMaxLen) {/*{{{*/ int needLen = sizeof(unsigned char)*2; if(needLen>iMaxLen) { return FBErrSystemNoMem; } char *t_Buff = buff; *(unsigned char*)t_Buff = ID; t_Buff+=sizeof(unsigned char); *(unsigned char*)t_Buff = bitmap; t_Buff+=sizeof(unsigned char); iLen = t_Buff - buff; return 0; }/*}}}*/ int Input(unsigned int /*ver*/,char *buff,int& iLen,int iMaxLen) {/*{{{*/ int needLen = sizeof(unsigned char)*2; if(needLen>iMaxLen) { return FBErrSystemNoMem; } char *t_Buff = buff; ID = *(unsigned char*)t_Buff; t_Buff+=sizeof(unsigned char); bitmap = *(unsigned char*)t_Buff; t_Buff+=sizeof(unsigned char); iLen = t_Buff - buff; return 0; }/*}}}*/ }CFarmBaseLand1;/*}}}*/ |
可以看出,结构体能够自己在打包/解包的时候,返回使用了的buff的长度,所以,如果我们是处理上述结构体的一个数组,那么代码可以这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | static int Output(unsigned int ver,char *buff,int& iLen,int iMaxLen,map<unsigned int,T>* ptrMap) {/*{{{*/ if(ptrMap==NULL) { return -1; } char *t_Buff = buff; int t_Len=0; int t_MaxLen=iMaxLen; *(unsigned short*)t_Buff = ptrMap->size(); t_Buff += sizeof(unsigned short); for(typename map<unsigned int,T>::iterator it=ptrMap->begin();it!=ptrMap->end();++it) { t_MaxLen = iMaxLen - (t_Buff-buff); int ret = it->second.Output(ver,t_Buff,t_Len,t_MaxLen); if(ret) { return -3; } t_Buff += t_Len; } iLen = t_Buff - buff; return 0; }/*}}}*/ static int Input(unsigned int ver,char *buff,int& iLen,int iMaxLen,map<unsigned int,T>* ptrMap) {/*{{{*/ if(ptrMap==NULL) { return -1; } if(iMaxLen == 0) { //这个字段暂时没有数据 iLen = 0; return 100; } int t_Len=0; int t_MaxLen=iMaxLen; char *t_Buff = buff; if(sizeof(unsigned short)>(unsigned int)t_MaxLen) { return -2; } unsigned short sCount = *(unsigned short*)t_Buff; t_Buff += sizeof(unsigned short); for(unsigned int i = 0;i<sCount;++i) { T t_data; t_MaxLen = iMaxLen - (t_Buff - buff); int ret = t_data.Input(ver,t_Buff,t_Len,t_MaxLen); if(ret) { return -3; } t_Buff += t_Len; (*ptrMap)[t_data.ID] = t_data; } iLen = t_Buff - buff; return 0; }/*}}}*/ |
但是这样自解析带来的最大问题就是,我们会控制大量的版本,并且每次升级版本都要重发所有程序,这个成本是非常大的。
所以,我们需要对这种解析方式进行更改,需要考虑两个问题:
1.当新的API读到旧的格式的数据的时候,并写回的时候,怎么做。(即兼容旧数据)
2.当旧的已经发布的API读到新的数据的时候,并写回的时候,怎么做。(即兼容新数据)
答案是:
1.当新API读到旧的数据的时候,新API多的参数用默认值填充,写回的时候按照新API的格式写回。
2.当旧API读到新数据,自己不认识的那段buff,要保存起来,写回的时候,将这段buff原样memcpy。
所以output和input函数将会升级成这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | typedef struct _farmbase_land1 {/*{{{*/ unsigned short precId; string extrabuff; _farmbase_land1() { precId = 0; } int ExtraOutput(unsigned int /*ver*/,char*& buff,int& iLen,int iMaxLen) {/*{{{*/ int needLen = extrabuff.size()+sizeof(unsigned short)+0+sizeof(unsigned char);//这个地方要加上最新的字段 if(needLen>iMaxLen) { return FBErrSystemNoMem; } char *t_Buff = buff; *(unsigned char *)t_Buff = extrabuff.size()+sizeof(unsigned short)+0; //这里的0代表以后扩展字段的sizeof t_Buff+=sizeof(unsigned char); /*在这里添加字段*/ *(unsigned short *)t_Buff = precId; t_Buff+=sizeof(unsigned short); if(extrabuff.size()>0) { memcpy(t_Buff,extrabuff.c_str(),extrabuff.size()); } t_Buff+=extrabuff.size(); iLen = t_Buff - buff; return 0; }/*}}}*/ int ExtraInput(unsigned int /*ver*/,char *buff,int& iLen,int iMaxLen) {/*{{{*/ char *t_Buff = buff; unsigned char t_size=0; unsigned char allsize = (unsigned char)*t_Buff; t_Buff+=sizeof(unsigned char); /* //在这里可以任意的添加字段了 //这里这样写,主要是为了当新的api读到老数据的时候 unsigned char testdata; t_size = allsize - (t_Buff-buff-sizeof(unsigned char)); if(t_size != 0) { testdata = (unsigned char)*t_Buff; t_Buff+=sizeof(unsigned char); } else { testdata = 0; } unsigned char testdata2; t_size = allsize - (t_Buff-buff-sizeof(unsigned char)); if(t_size != 0) { testdata2 = (unsigned char)*t_Buff; t_Buff+=sizeof(unsigned char); } else { testdata2 = 0; } */ t_size = allsize - (t_Buff-buff-sizeof(unsigned char)); if(t_size != 0) { precId = *(unsigned short*)t_Buff; t_Buff+=sizeof(unsigned short); } else { precId = 0; } t_size = allsize - (t_Buff-buff-sizeof(unsigned char)); extrabuff.resize(t_size); if(extrabuff.size()>0) { memcpy((char*)extrabuff.c_str(),t_Buff,extrabuff.size()); } t_Buff+=extrabuff.size(); iLen = t_Buff - buff; return 0; }/*}}}*/ }CFarmBaseLand1;/*}}}*/ |
而当解析上面的结构体数组时,则和原来的函数没有什么区别,所以保证了对外的接口统一。
最终问题完美解决~~
原创文章,版权所有。转载请注明:转载自Vimer的程序世界 [ http://www.vimer.cn ]
本文链接地址: http://www.vimer.cn/?p=951
东西很不错,不过每个公司都有相应的解决方案吧。
好久没看c/c++的东西了,感觉有点啃的吃力,呵呵。
[回复]
Dante 回复:
三月 12th, 2010 at 4:39 下午
呃,据我所知,互娱那边就是通过这种方式来保证各个版本兼容的~~
[回复]
kid 回复:
二月 23rd, 2012 at 1:22 上午
may be protobuf or messagepack can help~
[回复]