本周翻出了以前做过的一个项目,重新做了一些改进,主要做的是针对长短信的编解码工作,发现以前做项目的时候有很多没有理解透彻的地方,现在特地补上。
首先介绍下长短信的PDU的特点,普通短信UD区长度为140字节,超过140字节就得拆分成多条短信,拆分后的短信经过短信网关、短信中心的存储转发后到达终端的顺序可能和原发送顺序不同(此处已在实际工作中验证),为了解决接收方对于信息内容的理解出现困扰,1996年颁布GSM03.40V4.13.0中定义了长短信,所谓的长短信就是由一组相互独立的不超过普通短信长度的子短信组成,在网络传输中被视为多条普通短信,而在终端上被合并显示。
一、在PDU-Type字段中,协议数据单元类型是用1个字节表示的8个位图,具体含义如下:
Bit No. | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
发送方 | RP | UDHI | SRR | VPF | VPF | RD | MTI | MTI |
接收方 | SRI | \ | \ | MMS |
其中,UDHI标记,1bit,用户数据头标示,0——用户数据没有头信息,1——有,一般短信为0,但是短信为长短信是, UDHI=1.
当UDHI=1时,在用户数据长度之后,具体短信数据之前,会加入6个字节或者7个字节的udhi头做为前缀。
6个字节的TP_udhi协议头:05 00 03 XX MM NN
byte 1 : 05, 表示剩余协议头的长度
byte 2 : 00, 这个值在GSM 03.40规范9.2.3.24.1中规定,表示随后的这批超长短信的标识位长度为1(格式中的XX值)。
byte 3 : 03, 这个值表示剩下短信标识的长度
byte 4 : XX,这批短信的唯一标志,事实上,SME(手机或者SP)把消息合并完之后,就重新记录,所以这个标志是否唯一并不是很重要。
byte 5 : MM, 这批短信的数量。如果一个超长短信总共5条,这里的值就是5。
byte 6 : NN, 这批短信的数量。如果当前短信是这批短信中的第一条,值是1,第二条值是2。
例如:05 00 03 39 02 01
7个字节的TP_udhi协议头:06 08 04 XX XX MM NN
byte 1 : 06, 表示剩余协议头的长度
byte 2 : 08, 这个值在GSM 03.40规范9.2.3.24.1中规定,表示随后的这批超长短信的标识位长度为2(格式中的XX值)。
byte 3 : 04, 这个值表示剩下短信标识的长度
byte 4-5 : XX XX,这批短信的唯一标志,事实上,SME(手机或者SP)把消息合并完之后,就重新记录,所以这个标志是否唯 一并不是很重要。
byte 6 : MM, 这批短信的数量。如果一个超长短信总共5条,这里的值就是5。
byte 7 : NN, 这批短信的数量。如果当前短信是这批短信中的第一条,值是1,第二条值是2。
例如:06 08 04 00 39 02 01
可能,还有包括5字节协议头的,目前没有遇到过。
二、在用户数据长度处,
1 如果用户用默认7位编码
1>没有用户数据头,长度数字标示7bit的字符个数(是指短信内容字符数,而不是编码后的字节数)
2>有用户数据头,长度数字表示包括用户数据头包含补丁在内在内的7bit个数(是指头+短信内容字符数,而不是编码后的字节数)
2 如果用户用8位编码
表示用户数据区的字节数,有数据头信息则包括在内
3 如果为UCS2编码则是用户数据区的字节数,有数据头信息则包括在内
三、详细讲一下7bit编解码
编码原理是,ASCII码的第八位是0,所以一个8bit的数据,可以压缩成7bit的数据。压缩后,平均8个byte可以压缩为7个byte。7bit编码模式短信内容经过压缩,恢复后数据长度可以超过140字节。如果是7bit编码则从前向后每个字节从低到高位使用最后不足的一个字节的各位全部用0补全最多可以有160个字符。
如8个字母分别为ABCDEFGH,则编码如下:
B1 | A7 | A6 | A5 | A4 | A3 | A2 | A1 |
C2 | C1 | B7 | B6 | B5 | B4 | B3 | B2 |
D3 | D2 | D1 | C7 | C6 | C5 | C4 | C3 |
E4 | E3 | E2 | E1 | D7 | D6 | D5 | D4 |
F5 | F4 | F3 | F2 | F1 | E7 | E6 | E5 |
G6 | G5 | G4 | G3 | G2 | G1 | F7 | F6 |
H7 | H6 | H5 | H4 | H3 | H2 | H1 | G7 |
网上有关于7bit短信编解码的代码,但是大多针对普通短信,而不适用于长短信。如果仔细观察数据,你会发现同一段数据,不带Header的7bit编码和带Header的7bit编码是不相同的,原因是不带Header,7bit是从第一个字节的第0位开始填充;而带Header的内容,Header之后的部分,要留给bit位填充0值来保证兼容性。例如:Header的总长为6字节,呈现给用户的数据时从第7字节开始的,编码要把前六个字节偏移空位给补上。
int gsmDecode7bit2(const unsigned char* pSrc, char* pDst, int iSrcLen, int iLeft)
{
int nSrc; // 源字符串的计数值
int nDst; // 目标解码串的计数值
int nByte; // 当前正在处理的组内字节的序号,范围是0-6
unsigned char nLeft; // 上一字节残余的数据
// 计数值初始化
nSrc = 0;
nDst = 0;
// 组内字节序号和残余数据初始化
nByte = iLeft; //此处为修改后增加的偏移值
nLeft = 0;
int start_flag=1;
// 将源数据每7个字节分为一组,解压缩成8个字节
// 循环该处理过程,直至源数据被处理完
// 如果分组不到7字节,也能正确处理
while(nSrc<iSrcLen)
{
// 将源字节右边部分与残余数据相加,去掉最高位,得到一个目标解码字节
*pDst = ((*pSrc << nByte) | nLeft) & 0x7f;
// 将该字节剩下的左边部分,作为残余数据保存起来
nLeft = *pSrc >> (7-nByte);
// 修改目标串的指针和计数值
if(start_flag!=1)
{
pDst++;
nDst++;
}
else
start_flag=0;
// 修改字节计数值
nByte++;
// 到了一组的最后一个字节
if(nByte == 7)
{
// 额外得到一个目标解码字节
*pDst = nLeft;
// 修改目标串的指针和计数值
pDst++;
nDst++;
// 组内字节序号和残余数据初始化
nByte = 0;
nLeft = 0;
}
// 修改源串的指针和计数值
pSrc++;
nSrc++;
}
// 输出字符串加个结束符
*pDst = '\0';
// 返回目标串长度
return nDst;
}
我在网上找的编码代码,也存在BUG,编码后可能会丢失最后几个字节数据,修改后如下:
int gsmEncode7bit(const char* pSrc, unsigned char* pDst, int nSrcLength)
{
int nSrc; // 源字符串的计数值
int nDst; // 目标编码串的计数值
int nChar; // 当前正在处理的组内字符字节的序号,范围是0-7
unsigned char nLeft; // 上一字节残余的数据
// 计数值初始化
nSrc = 0;
nDst = 0;
// 将源串每8个字节分为一组,压缩成7个字节
// 循环该处理过程,直至源串被处理完
// 如果分组不到8字节,也能正确处理
while (nSrc < nSrcLength)
{
// 取源字符串的计数值的最低3位
nChar = nSrc & 7;
// 处理源串的每个字节
if(nChar == 0)
{
// 组内第一个字节,只是保存起来,待处理下一个字节时使用
nLeft = *pSrc;
}
else
{
// 组内其它字节,将其右边部分与残余数据相加,得到一个目标编码字节
*pDst = (*pSrc << (8-nChar)) | nLeft;
// 将该字节剩下的左边部分,作为残余数据保存起来
nLeft = *pSrc >> nChar;
// 修改目标串的指针和计数值
pDst++;
nDst++;
}
// 修改源串的指针和计数值
pSrc++;
nSrc++;
}
//补充了如下代码,解决数据丢失问题
if(nChar==0)
{
*pDst = nLeft;
pDst++;
nDst++;
}
else if(nChar!=0)
{
*pDst = (0 << (8-nChar)) | nLeft;
pDst++;
nDst++;
}
// 返回目标串长度
return nDst;
}