短信猫软件的实现(C#)<十三>超长短信
超长短信:长度超过一条,而分多条发送的短信,通过用户数据头标识在接收端进行组合的短信(接收的短信在手机或其他终端上看到的是一条)。GSM_03.40规范中是Concatenated Short Messages :This facility allows short messages to be concatenated to form a longer message.
此种短信理论上最长可以将255条短信合成一条,名副其实的超长短信。
有关超长短信可以参考GSM_03.40规范和CMPP有关超长短信的内容:GSM_03.40规范中的 9.2.3.23 TP-User-Data-Header-Indicator (TP-UDHI) 和9.2.3.24 TP-User Data (TP-UD)
本文的程序是在原来基础上添加的,详细请参考:短信猫软件的实现(C#)系列博客索引PDU字符串中与超长短信有关的只有TP-UDHI位(在PDU字串中的PDUType的D6位),有关PDU编码请参考:短信猫软件的实现(C#)<三>PDU格式短信解析。TP-UDHI位为1,则在User Data中含有消息头,用来表示各种不同的其他形式短信,其中包括长短信。
消息头是User Data的开头部分,有两种格式:6位格式和 7位格式。6位:05 00 03 XX MM NN;7位格式:06 08 04 XX XX MM NN。
各字节含义:
byte 1:剩余协议头长度。
byte 2:00/08 这个字节在GSM 03.40规范9.2.3.24中规定,00:代表长短信,8位参考标识;08:代表长短信,16位参考标识;还规定了其他数值,与长短信无关,详细参考GSM 03.40规范9.2.3.24。
byte 3:代表剩下短信标识的长度:03,三个字节;04,四个字节。
byte 4:XX 这批短信的唯一标志,事实上,SME(手机或者SP)把消息合并完之后,就重新记录,所以这个标志是否唯一并不是很 重要。7位格式的 和byte 5一起作为16位标志。
byte 5:MM 这批短信的数量,超长短信分成几条,值即是几。7位 XX和byte 4共同作为16位标识。
byte 6:NN 本条短信在超长短信中是第几条。7位格式 MM 同6位格式的 MM。
byte 7:NN 7位格式中,同6位格式中的NN。长短信消息头规律:第一个字节:消息头剩余长度;第二字节:消息类型;第三字节:剩余消息头长度;后面一个或两个字节根据标识位数作为这批短信的唯一标识,是否唯一不重要,但同批短信标志位必须相同,否则将被解析成多条短信。后面两个字节分别是总数量和序号。
- 编码实现:
 
此次编码是在之前编码基础上添加长短信编解码部分而实现的,添加时不对原来程序做过多修改;这次添加长短信深感这个类库的可扩展性太差,以致程序有点乱,添加长短信费了一番功夫,而且功能实现不尽合理;由于这段时间比较忙,暂时不对程序做大的改动,仅仅添加长短信编码部分。
对编解码类的更改:
属性更改:
长短信发送时需将TP-UDHI位置为1,而这位位于PDU-type 这个8位组,普通短信这个八位组发送时值为“11” 接收时为“24”,长短信 分别为: “51”、“64”。之前程序对应的属性只能读到“11”,字段值也为“11”没有更改。为使其支持长短信编解码将其中属性、字段更改为:
1: private string protocolDataUnitType = "11";2: /// <summary>3: /// 协议数据单元类型(1个8位组)4: /// </summary>5: public string ProtocolDataUnitType6: {7: set8: {9: protocolDataUnitType = value;10: }11: get12: {13: return protocolDataUnitType;14: }15: }这样编解码时只需正确设置属性值,即可完成长短信的编解码。
方法更改:
编码:(USC2/7位):
只需把原来程序 字符数超过最大字符数时 抛出异常改为 对应长短信编码即可;为了改动的地方比较少,返回值:长短信返回逗号分隔的PDU串。7bit编码须做一定处理,规范中要求添加填充位,让后面userData符合7bit的格式;6byte消息头共占48bit 填充一位补成49bit,相当于后面第一个ASCII符做一定特殊处理,后面直接调用之前的编码函数即可,通过验证发现 第一个只需左移一位,即完成这一位编码,放入PDU传即可。
1: /// <summary>2: /// PDU编码器,完成PDU编码(USC2编码,超过70个字时 分多条发送,PDU各个串之间逗号分隔)3: /// </summary>4: /// <param name="phone">目的手机号码</param>5: /// <param name="Text">短信内容</param>6: /// <returns>编码后的PDU字符串 长短信时 逗号分隔</returns>7: public string PDUUSC2Encoder(string phone, string Text)8: {9: DestinationAddress = phone;10:11: if (Text.Length > 70)12: {13: //长短信设TP-UDHI位为1 PDU-type = “51”14: ProtocolDataUnitType = "51";15:16: //计算长短信条数17: int count = Text.Length / 67 + 1;18:19: //长短信格式字符串,格式 每条之间 逗号分隔20: string result = string.Empty;21:22: for (int i = 0; i < count; i++)23: {24: //如果不是最后一条25: if (i != count - 1)26: {27: UserData = Text.Substring(i * 67, 67);28:29: result += serviceCenterAddress + protocolDataUnitType30: + messageReference + destinationAddress + protocolIdentifer31: + dataCodingScheme + validityPeriod + (userData.Length / 2 + 6).ToString("X2")32: + "05000339" + count.ToString("X2") + (i + 1).ToString("X2") + userData + ",";33: }34: else35: {36: UserData = Text.Substring(i * 67);37:38: if (userData != null || userData.Length != 0)39: {40:41: result += serviceCenterAddress + protocolDataUnitType42: + messageReference + destinationAddress + protocolIdentifer43: + dataCodingScheme + validityPeriod + (userData.Length / 2 + 6).ToString("X2")44: + "05000339" + count.ToString("X2") + (i + 1).ToString("X2") + userData;45: }46: else47: {48: result = result.TrimEnd(',');49: }50: }51: }52:53: return result;54: }55:56: //不是长短信57: UserData = Text;58: return serviceCenterAddress + protocolDataUnitType59: + messageReference + destinationAddress + protocolIdentifer60: + dataCodingScheme + validityPeriod + userDataLenghth + userData;61: }62:63: /// <summary>64: /// 7bit 编码(超过160个字时 分多条发送,PDU各个串之间逗号分隔)65: /// </summary>66: /// <param name="phone">手机号码</param>67: /// <param name="Text">短信内容</param>68: /// <returns>编码后的字符串 长短信时 逗号分隔</returns>69: public string PDU7BitEncoder(string phone, string Text)70: {71: dataCodingScheme = "00";72: DestinationAddress = phone;73:74: if (Text.Length > 160)75: {76: //长短信设TP-UDHI位为1 PDU-type = “51”77: ProtocolDataUnitType = "51";78:79: //计算长短信条数80: int count = Text.Length / 153 + 1;81:82: //长短信格式字符串,格式 每条之间 逗号分隔83: string result = string.Empty;84:85: for (int i = 0; i < count; i++)86: {87: //如果不是最后一条88: if (i != count - 1)89: {90: UserData = Text.Substring(i * 153 + 1, 152);91:92: result += serviceCenterAddress + protocolDataUnitType93: + messageReference + destinationAddress + protocolIdentifer94: + dataCodingScheme + validityPeriod + (160).ToString("X2")95: + "05000339" + count.ToString("X2") + (i + 1).ToString("X2")96: +((int)(new ASCIIEncoding().GetBytes(Text.Substring(i*153,1))[0]<<1)).ToString("X2") + userData + ",";97: }98: else99: {100: UserData = Text.Substring(i * 153+1);101:102: int len = Text.Substring(i * 153).Length;103:104: if (userData != null || userData.Length != 0)105: {106:107: result += serviceCenterAddress + protocolDataUnitType108: + messageReference + destinationAddress + protocolIdentifer109: + dataCodingScheme + validityPeriod + (len + 7).ToString("X2")110: + "05000339" + count.ToString("X2") + (i + 1).ToString("X2")111: + ((int)(new ASCIIEncoding().GetBytes(Text.Substring(i * 153, 1))[0] << 1)).ToString("X2")112: + userData;113: }114: else115: {116: result = result.TrimEnd(',');117: }118: }119: }120:121: return result;122: }123:124: UserData = Text;125:126: return serviceCenterAddress + protocolDataUnitType127: + messageReference + destinationAddress + protocolIdentifer128: + dataCodingScheme + validityPeriod + userDataLenghth + userData;129: }这样,调用时 非长短信调用方式不变 长短信 返回值为逗号分隔的各PDU串、很方便调用方更改。
解码(USC2/7位):
解码函数只需添加对TP-UDHI位判断,为1则根据消息头解出本条短信在本批次短信中的位置 及本批次短信的总条数。
PDUDecoder完成PDU解码,PDU7bitDecoder仅完成7位PDU的用户数据编码,供UserData属性调用,不需改动;只需更改PDUDecoder即完成长短信编码:根据TP-udhi位取出消息头,消息体赋给userData即可正常解码。方法返回格式改为:MMNNXX,中心号码,手机号码,发送时间,短信内容 MM这批短信总条数 NN本条所在序号 XX为这条短信的唯一标识;
1: /// <summary>2: /// 重载 解码,返回信息字符串 格式3: /// </summary>4: /// <param name="strPDU">短信PDU字符串</param>5: /// <returns>信息字符串(MMNN,中心号码,手机号码,发送时间,短信内容 MM这批短信总条数 NN本条所在序号)</returns>6: public string PDUDecoder(string strPDU)7: {8: int lenSCA = Convert.ToInt32(strPDU.Substring(0, 2), 16) * 2 + 2; //短消息中心占长度9: serviceCenterAddress = strPDU.Substring(0, lenSCA);10:11: //PDU-type位组12: protocolDataUnitType = strPDU.Substring(lenSCA, 2);13:14: int lenOA = Convert.ToInt32(strPDU.Substring(lenSCA + 2, 2), 16); //OA占用长度15: if (lenOA % 2 == 1) //奇数则加1 F位16: {17: lenOA++;18: }19: lenOA += 4; //加号码编码的头部长度20: originatorAddress = strPDU.Substring(lenSCA + 2, lenOA);21:22: dataCodingScheme = strPDU.Substring(lenSCA + lenOA + 4, 2); //DCS赋值,区分解码7bit23:24: serviceCenterTimeStamp = strPDU.Substring(lenSCA + lenOA + 6, 14);25:26: userDataLenghth = strPDU.Substring(lenSCA + lenOA + 20, 2);27: int lenUD = Convert.ToInt32(userDataLenghth, 16) * 2;28:29: if (protocolDataUnitType != "24")30: {31: if (dataCodingScheme == "08" || dataCodingScheme == "18") //USC2 长短信 去掉消息头32: {33: userDataLenghth = (Convert.ToInt16(strPDU.Substring(lenSCA + lenOA + 20, 2), 16) - 6).ToString("X2");34: userData = strPDU.Substring(lenSCA + lenOA + 22 + 6 * 2);35:36: return strPDU.Substring(lenSCA + lenOA + 22 + 4 * 2, 2 * 2)37: + strPDU.Substring(lenSCA + lenOA + 22 + 3 * 2, 2) + "," + ServiceCenterAddress + ","38: + OriginatorAddress + "," + ServiceCenterTimeStamp + "," + UserData;39: }40: else41: {42: userData = strPDU.Substring(lenSCA + lenOA + 22 + 6 * 2 + 1 * 2); //消息头六字节,第一字节特殊译码 >>743:44: //首字节译码45: byte byt = Convert.ToByte(strPDU.Substring(lenSCA + lenOA + 22 + 6 * 2, 2), 16);46: char first = (char)(byt >> 1);47:48: return strPDU.Substring(lenSCA + lenOA + 22 + 4 * 2, 2 * 2)49: + strPDU.Substring(lenSCA + lenOA + 22 + 3 * 2, 2) + "," + ServiceCenterAddress + ","50: + OriginatorAddress + "," + ServiceCenterTimeStamp + "," + first + UserData;51: }52: }53:54: userData = strPDU.Substring(lenSCA + lenOA + 22);55: return "010100," + ServiceCenterAddress + "," + OriginatorAddress + "," + ServiceCenterTimeStamp + "," + UserData;56: }这样,程序返回值字符串中有长短信的有关消息,前两个8位组 分别指示这批短信的总条数和这条所在的序号,调用方只需对这两个8位组和这批短信的唯一标识判断处理即可解码出长短信,拼接长短信。
注释掉:public void PDUDecoder(string strPDU, out string msgCenter, out string phone, out string msg, out string time)方法。对应对其的调用都改为对刚修改的函数的调用。
2 . GSMMODEM类更改:
接收短信,读取短信先读出刚才信息字符串(MMNN,中心号码,手机号码,发送时间,短信内容 MM这批短信总条数 NN本条所在序号)然后在处理(现在程序对此不做处理,需要的话 自己先改动)
发送短信:只需添加长短信的编码的发送,用foreach语句遍历发送各条PDU串即可:
1: /// <summary>2: /// 发送短信3: /// 发送失败将引发异常4: /// </summary>5: /// <param name="phone">手机号码</param>6: /// <param name="msg">短信内容</param>7: public void SendMsg(string phone, string msg)8: {9: PDUEncoding pe = new PDUEncoding();10: pe.ServiceCenterAddress = msgCenter; //短信中心号码 服务中心地址11:12: string temp = pe.PDUUSC2Encoder(phone, msg);13:14: string tmp = string.Empty;15:16: foreach (string str in temp.Split(','))17: {18: int len = (str.Length - Convert.ToInt32(str.Substring(0, 2), 16) * 2 - 2) / 2; //计算长度19:20: try21: {22: //注销事件关联,为发送做准备23: sp.DataReceived -= sp_DataReceived;24:25: sp.Write("AT+CMGS=" + len.ToString() + "\r");26: sp.ReadTo(">");27: sp.DiscardInBuffer();28:29: //事件重新绑定 正常监视串口数据30: sp.DataReceived += sp_DataReceived;31:32: tmp = SendAT(str + (char)(26)); //26 Ctrl+Z ascii码33: }34: catch (Exception)35: {36: throw new Exception("短信发送失败");37: }38: finally39: {40: }41:42: if (tmp.Substring(tmp.Length - 4, 3).Trim() == "OK")43: {44: continue;45: }46:47: throw new Exception("短信发送失败");48: }49: }50:51: /// <summary>52: /// 发送短信 (重载)53: /// </summary>54: /// <param name="phone">手机号码</param>55: /// <param name="msg">短信内容</param>56: /// <param name="msgType">短信类型</param>57: public void SendMsg(string phone, string msg, MsgType msgType)58: {59: if (msgType == MsgType.AUSC2)60: {61: SendMsg(phone, msg);62: }63: else64: {65:66: PDUEncoding pe = new PDUEncoding();67: pe.ServiceCenterAddress = msgCenter; //短信中心号码 服务中心地址68:69: string temp = pe.PDU7BitEncoder(phone, msg);70:71: string tmp = string.Empty;72:73: foreach (string str in temp.Split(','))74: {75:76: int len = (str.Length - Convert.ToInt32(str.Substring(0, 2), 16) * 2 - 2) / 2; //计算长度77: try78: {79: tmp = SendAT("AT+CMGS=" + len.ToString() + "\r" + str + (char)(26)); //26 Ctrl+Z ascii码80: }81: catch (Exception)82: {83: throw new Exception("短信发送失败");84: }85:86: if (tmp.Substring(tmp.Length - 4, 3).Trim() == "OK")87: {88: continue;89: }90:91: throw new Exception("短信发送失败");92: }93: }94: }直接调用函数即可发送,长度超过最大字符数,自动以长短信发送;调用方式和以前完全一样。
读取短信的函数直接调用解码函数,返回格式同解码函数,调用时需要根据字符串自己组合短信,函数没有太大变化,这里不再给出具体函数了,详细参考附件源程序。
- 总结:长短信的发送就是把超过协议最大长度的短信分成多条发送,在接收终端(如手机)端看到的是一条短信。置TP-udhi位为1,添加消息头;USC2的编码只需添加消息头,剩下的134个字节可以发送67个字符,7位短信需要加上填充位 6byte消息头占48位,需添加一位填充(0或1)填充位置在本字节的最低位,我的程序把字节左移一位(相当于填充0);接收解码时只需右移一位即可。
 
附件:工程项目文件