一、cmpp协议简介
中国移动通信互联网短信网关接口协议(china mobile peer to peer cmpp),是中国移动梦网内部各sms参与节点相互交换sms的官方协议。作为梦网的参与方,移动梦网的增值服务商(service provider sp )要按照此协议规范实现sp的部分,才可以将自己的短信通过移动的gsm网络的数据通道传输到最终手机用户上。
实际上,协议规范了3个方面的内容:
sp与移动的互联网短信网关(internet short message gateway,ismg)之间的接口协议
ismg之间的接口协议(譬如移动各省、市之间的短信息交换通过ismg之间进行)
ismg与汇接网关(gateway name server gns,类似互联网上的dns服务器)之间的接口协议,譬如跨省之类的短信需要gns的帮助指出当前ismg该如何传递短信。
其中,后二方面属于移动短信息系统内部实现,对于sp来讲大概可以“透明”来看待,只要实现了sp同ismg的正确交互,就可以实现接入移动梦网短信系统。我们关心的只是sp端的开发细节。
二、cmpp交互模式
从手机用户角度讲,按短信的发起/接收路径来讲,有两个叫法:
mt(short message mobile terminated, smmt),短信接收,短信从sp发送到手机用户。
mo (short message mobile originate,smmo),短信发送,短信从手机用户端发送到目标sp。
这两类短信交互,从sp端来看,都是属于socket传输应用,cmpp的协议是以tcp/ip协议作为底层承载协议的,属于tcp/ip协议栈之上的应用。
sp同ismg的交互连接分长连接和短连接。
所谓短连接,就是一次连接,传输一个消息,然后等待回复后拆除连接,显然,效率很低,所以,基本上不被考虑(实际应用移动也不允许sp采用短连接,只是不明白移动为什么还要写入文档? ismg间会需要?)
所谓长连接,就是sp建立同ismg连接,然后不断将数据包(一个个cmpp消息)发送到ismg,此处发送不必等待某条消息的ismg回应消息返回,就接着发送下一个消息。同时,等待ismg返回信息或者等待ismg发送给sp的消息。发送同接收消息不是一定要同步的,实际采用异步(同时也时双工)模式。从效率上,显然,必须全双工的异步模式才能够满足实际应用需求。

三、sp端开发
1.消息分类
首先,图中的cmpp消息有很多种,sp同ismg之间交流这些消息。大体上这些消息发出后,对方往往需要回复一个应答(resp)类消息。注意,这些消息大多具有方向性,也就是说只能够从一端到另一端,而不可反方向进行,有些(少数)则可两端都能够发出。以下信息主要来源于移动的文档,但针对大家易混淆或源文档解释不够详细做了明确和补充。具体见下表:

2.交互阶段
整个cmpp协议交互分为验证、事务两个阶段。验证阶段,发送cmpp_connection消息进行验证,通过验证后(必须要通过才)进入cmpp事务阶段,可以发送短信数据了。上表中的cmpp_connection以下的消息都属于事务阶段的消息。
3.消息数据结构
每一个消息包含 消息头 和 消息体两个部分,头固定长度为12字节,其他消息长度各异,但是同一类型消息的长度是固定的。所有消息的各个字段基本上仅有3种类型:unsigned integer (无符号整型) 、integer(整型)、octet string(字符串),每种类型具体长度不定,网络字节顺序。
1、消息头(3个unsigned integer字段组成):
4字节的total_length (unsigned integer),包含了此消息的总计(包括了头部分)长度。
4字节的command_id(unsigned integer),指明了此消息到底是什么消息,就是上表中消息的枚举值。应用程序根据此值确定本数据包到底是什么消息,从而可以按照确定的消息类型,解析余下的消息体。
4字节的sequence_id(unsigned integer),指明了此数据包在发送此消息端的唯一编号。这个唯一编号,实际上可以看作流水操作编号。因为分析到交互模式我们看到,sp发送数据到ismg,不是每发送一个就停下来等待ismg的回复,而是“一下子”发送多个数据包过去,然后等待ismg的回应。然而,怎么知道回应的消息是到底对应之前发送过去的消息中的那一条呢?本字段就是解决此难题。sp按照编号发送消息过去,等待ismg的回应―一般情形下回应消息数据结构都有表明本消息回应的是sp发出的哪一条消息,这个对应就是依靠sequence_id。它并不要求一定要严格唯一,但是在给定的一段时间内,必须唯一(基本上只要sp发送过去的消息中没有重复就行了)。如果是需要sp回答的消息,sp也必须将ismg发送过来的消息的sequence_id填入相应字段,表明这是某个消息的回应。sp端和ismg端sequence_id都没有确定具体的算法。sp可以(但不推荐)采用数据库的唯一id作为此值。
2、消息体。消息体长度根据消息不同,长度不一。其他的参考移动的文档《中国移动通信互联网短信网关接口协议(china mobile peer to peer, cmpp)(v2.0)》,这里着重讲讲2个重要消息的消息体数据结构:
cmpp-_submit的消息体:

cmpp_submit消息长度是可变的,将sp端的消息发送给ismg,ismg将返回一个msgid给sp标示此消息,之后(48小时以内,但一般最多几分钟内就可),ismg返回关于此消息的递送报告。递送报告同mo短消息是通过另外一个重要消息cmpp-_deliver来提交给sp的:
cmpp-_deliver的各个字段:

如果是报告,那么msg_content将按照状态报告结构来解释:

关于state字段,如下解释:

4.安全验证
cmpp协议在cmpp_connect中传递验证消息。验证消息为9字节的0+移动给出的密码+当前时间戳字节数组的md5算法后的字节。时间戳为 月日时分秒,10位。代码算法如下:
private byte[] getmd5code()
{
byte[] buf=new byte[6+9+_password.length+10] ;
byte[] s_a=encoding.ascii.getbytes(_systemid); //就是企业代码
byte[] s_0={0,0,0,0,0,0,0,0,0}; //9字节的0,此处当作右补0
byte[] s_p=encoding.ascii.getbytes(_password); //密码
this._timestamp =gettimestamp(); //取得认证码时赋值字符串
byte[] s_t=encoding.ascii.getbytes(_timestamp); //10位字符串字节数组
s_a.copyto(buf,0);
s_0.copyto(buf,6);
s_p.copyto(buf,6+9);
s_t.copyto(buf,6+9+_password.length);
md5 md5= new md5cryptoserviceprovider(); //创建md5类别
return(md5.computehash(buf,0,buf.length));
}
其中gettimestamp函数为返回例如“0710125959”(7月10号12点59分59秒)这样的字符串,详细代码略过,有兴趣请查看本文的附件代码。
5.厂商api问题
笔者公司所处广东,广东移动提供了华为的以c 形式的api(smeidll.dll),来帮助大家初期熟悉cmpp协议。但是,经过开发测试,发现华为的api至少存在几个问题:
1、封装成几个api函数,但是由于cmpp自身的复杂性,导致这些函数丑陋无比,参数多,而且难以明晰含义。华为的api,内部将cmpp的验证、事务阶段分成几个函数实现,其中将发送sms到ismg功能以函数提供,竟然出现submitaexexex之类的函数说明。
2、cmpp的交互是异步的,需要多线程实现一边发送,一边接收反馈信息。此api应当是内部维护一个线程进行cmpp_submit消息发送,但是华为api却通过空循环之类的操作等待ismg返回cmpp_submit_resp得到相应的msgid再返回(从而实现消息同步返回)。经过测试,大约需要200毫秒,这个在实际sp的高性能需求场合根本无法满足系统要求。
3、接收短信必须依靠程序主动先发出函数hasdelivermessage调用 ,得到有消息才可通过getdeliversmex函数获取消息,显然,这种方式是低效率的,而且容易产生消息数据包丢失,表现为有些mo消息,sp接收不到。而且,令人疑惑的是,你还不能够新开一个线程专门来做判断并接收mo的动作,实际开发中一旦采用线程来做就回发生内存保护错误(大概属于同api自身的线程有冲突)。
4、返回错误码,往往又是华为自己定的一套错误码(大概华为设计此api为了适应smgp cmpp等多个协议),而且经常变动,很是伤脑筋。
基于以上理由,我认为自己按照cmpp协议开发一个sp端程序,比较能够满足一般sp的需求。
四、c#实现
1、cmpp协议实现类cmppclient
通过研究,笔者用c#写了一组类实现自己的cmpp sp端程序(cmppclient)。为了实现相关类,还需要编写一些辅助类,并且首先要解决cmpp协议的数据结构同c#的数据之间的转换问题。
cmpp的octet string 实际上相当于c#中的byte[],所有cmpp消息的octet string字段出了cmpp_submit和cmpp_deliver的msg_content字段外,其他的都可以认为是ascii编码,所以全部可以采用system.text.encoding.ascii进行编码和解码;对于msg_content字段,由于一般情况下存在汉字信息传输.,所以默认的编/解码应该为encoding.default,实际是什么编码还要考察msg_fmt字