神奇的产品经理总是能给我来点新花样——
背景
这次的需求是解析MMS协议。
什么是MMS(Multimedia Messaging Service
Encapsulation Protocol)协议?通俗一点来讲就是彩信。至于为啥需要解析MMS协议,哪来的数据?嘘...这都不是这次的重点。总之就是关注MMS协议中携带的信息,需要解析和展示出携带的用户信息、多媒体信息。
我们可以先借助报文来看一下,MMS协议是基于WAP或HTTP进行传输的,可以认为是在HTTP上层的协议。通过wireshark来看模拟报文,wireshark可以展示MMS协议的相关结构。
这是一个发送彩信的模拟报文,借助wireshark打开,我们可以看到它被wireshark识别为MMSE协议类型。
这是协议报文通过wireshark解析的情况:
我这里只截取了传输层以上的详情。可以看到这是一个POST方法的http请求,在http协议详情中正常展示了http协议的各项内容,在http协议之上又展示了MMS协议的内容。简单来讲,就是在http request body中传输MMS协议的报文。
通过上面直观的展示,我们可以初步体会到对MMS协议报文的解析与HTTP协议是解耦的,但是MMS协议报文的传输可能部分依赖HTTP的过程,如Restful风格的请求方法。图上还有一个m-send-conf类型的MMS协议报文,这是表示发送确认的。事实上,除了发送、确认,还有推送、已读确认和下载等许多过程。
阅读理解
下面根据一些规范与前人的铺垫来理解MMS的解析。
根据MMS协议标准(Multimedia Messaging Service
Encapsulation Protocol Approved Version 1.3 – 13 Sep 2011),MMS协议报文被称为PDU,在整个协议规范中,共有15种基本的PDU,分别表示不同过程和作用。
本规范定义了多媒体消息服务(MMS)版本1.3的协议数据单元(PDUs)的结构、内容和编码。
OMA多媒体消息服务使用WAP WSP/HTTP作为底层协议,以在终端(MS)上的MMS客户端和MMS代理中继之间传输MMS PDUs。WSP会话管理和相关的能力协商机制以及安全功能不在本文档的范围内。
本规范的第5章包含了应用于MMS PDUs的消息结构的一般描述。这一结构基于在[RFC2822]、[RFC2045]和[RFC2387]中定义的Internet电子邮件的众所周知的消息结构。[WAPWSP]提供了这种消息的二进制编码机制,并作为MMS PDUs的二进制编码的基础。
由于移动网络的空中接口带宽有限,MMS PDUs以二进制编码的消息格式在MMS客户端和MMS代理中继之间传输。这个过程称为封装。用于此传输的是包含MMS PDUs作为其主体的WSP PDUs或HTTP消息。
在MMS级别,基本上有十五种类型的PDUs:
• 向MMS代理中继发送消息(M-Send.req,M-Send.conf)
• 从MMS代理中继获取消息(WSP/HTTP GET.req,M-Retrieve.conf)
• 关于新消息的MMS通知(M-Notification.ind,M-NotifyResp.ind)
• 已发送消息的交付报告(M-Delivery.ind)
• 消息交付的确认(M-Acknowledge.ind)
• 已发送消息的阅读报告(M-Read-Rec.ind,M-Read-Orig.ind)
• 转发交易(MMS客户端发送一个请求以将消息转发给MMS代理中继,M-Forward.req和M-Forward.conf)
• 请求MMS代理中继删除(从其内部存储中)未被MMS客户端检索的MM(M-Delete.req和M-delete.conf)
• 请求MMS客户端取消已经检索的MM(M-Cancel-req和M-Cancel.conf)
除了这些基本PDUs外,还有一个额外的可选PDU集,用于支持一个可选的MMBox。对于MMS客户端和MMS代理中继来说,对MMBox的支持是可选的。MMBox可用于存储针对特定MMS客户端到达的所有或部分MM,具体取决于用户分析或MMS客户端操作。与分析有关的问题不在本规范的范围内。以下四种类型的PDUs支持与MMBox有关的直接操作:
• 将当前在MMS代理中继中的MM存储或更新到MMBox中(M-Mbox-Store.req,M-Mbox-Store.conf)
• 查看MMBox的内容(M-Mbox-View.req,M-Mbox-View.conf)
• 上传当前在MMS客户端上的MM并将其存储到MMBox中(M-Mbox-Upload.req,M-Mbox-Upload.conf)
• 从MMBox中删除MM(M-Mbox-Delete.req,M-Mbox-Delete.conf)
此外,还可以使用基本PDUs的可选参数来:
• 并行发送到目的地时在MMBox中保存MM的副本。
• 从MMBox中检索MM
本规范的第6章包含了MMS PDU类型的定义。包括的头字段和值都有详细的描述。
本规范的第7章定义了通过WSP/HTTP传输的MMS PDUs的二进制编码。为头字段名称以及头字段值分配了二进制代码。
MMS PDUs的文本编码不在本规范的范围内。
图上即显示了一组M-Send.req,M-Send.conf的PDU
我这里关注M-Send.req和M-Retrieve.conf,因为这两个PDU类型包含真实的消息内容。
接下来我们可以从规范中找到如何解析PDU。在标准的第7章有详细解释了PDU的二进制编码规范。可以借助这篇博客食用https://blog.csdn.net/cml2030/article/details/6145523
可以看到PDU主要分为header和body两个部分,header描述元信息,body描述消息的实际内容,这和解析email非常像。
解析header:从标准中可以看出各个header都是硬编码的,因此需要结合文档对每一个header单独编写常量。
解析body:从这个标准中我没有找到太多描述body解析方式的解释。可以结合这篇博客来看https://blog.csdn.net/qq_35427437/article/details/89205534
multipart类型的body分为多个部分,由pdu header的Content-Type指定从哪个part作为根开始展示,而作为根的部分是一个smil格式的文件,它与html看起来非常相似。在smil中描述了彩信的布局信息,就像html描述了网页的布局信息一样。
只要对html稍有了解就能看出这两种描述语言非常接近,用标签和属性来表示具体内容的位置。
通过上述的文档阅读和实际用例我们可以认知到:解析MMS协议的报文,需要按照文档规范解析PDU的header和body,并将body的各个部分依次解出后,按照指定的根文件(smil彩信的布局文件)渲染。
实现
遗憾(庆幸?)的是,本次需求并非是建立在完成整个MMS协议通信过程的基础上,而是只需解析其中的两种PDU,完全按照标准开发是很不划算的;另外,预估的工期也很短。所以这里优先找轮子,先看是否有前人封装了相关的java库。
在github上找轮子
在github上以mms作为关键字进行搜索
看上去好像和想象中的不太一样,mmseg与mms协议的关系就像java与javascript的关系一样;而标注sms mms的库又大多带着android的关键字。我本来期待能找一个简单直观的解析pdu的库,但是翻了两三页没有发现。
其实这里我已经蠢到自己了,其实多看几页就能发现最终要用的库了,但是业务背景中交代了没有开源库+自己有点懒就没有细看。
通过GPT和google也没有找到太多有效信息,似乎并没有这样一个轮子。
这怎么可能呢?android手机总能解析彩信吧?于是我开始了本次工作绕的最大的一个圈子:通过AOSP查找解析mms相关的代码。
在AOSP中找轮子
AOSP是Android Open Source Project android开源计划,从这里我们可以查看Android系统源码。从常识判断Android系统一定支持MMS的解析,因此它的源码里也一定会有相关逻辑。但是我对于Android基本上是一窍不通的状态,只能从代码层面而不是架构层面来找线索。
在AOSP的代码浏览页面中,先用mms做关键词找一下是否有相关的服务或接口。
https://cs.android.com/
很幸运,在第二页就找到了一个看起来很可疑的MmsService
在这里看到一个方法
/**
* Try parsing a PDU without knowing the carrier. This is useful for importing
* MMS or storing draft when carrier info is not available
*
* @param data The PDU data
* @return Parsed PDU, null if failed to parse
*/
private static GenericPdu parsePduForAnyCarrier(final byte[] data) {
GenericPdu pdu = null;
try {
pdu = (new PduParser(data, true/*parseContentDisposition*/)).parse();
} catch (RuntimeException e) {
LogUtil.w("parsePduForAnyCarrier: Failed to parse PDU with content disposition", e);
}
if (pdu == null) {
try {
pdu = (new PduParser(data, false/*parseContentDisposition*/)).parse();
} catch (RuntimeException e) {
LogUtil.w("parsePduForAnyCarrier: Failed to parse PDU without content disposition",
e);
}
}
return pdu;
}
从doc中看这是导入未知负载的彩信数据的方法,方法入参也是byte[].这和我们的目标非常接近了。
看这个方法的核心代码在
pdu = (new PduParser(data, true/*parseContentDisposition*/)).parse();
从这里推断PduParser就是我们想找的东西。于是跟踪PduParser的路径来到com.google.android.mms.pdu
https://cs.android.com/android/platform/superproject/main/+/m...
这里有大量解析pdu相关的类。
简单翻阅一下,pdu相关的类基本分为pdu的实体类(GenericPdu及其子类),pdu的属性类(PduHeader PduBody PduPart),pdu的控制类(PduParser)以及常量和工具类(Base64 EncodedStringValue等)组成。从MmsService中可以看出,流程是从byte[]数组构造PduParser,通过.parse()方法转换得到GenericPdu的具体实现。从实现类中我们发现了SendReq.java和RetrieveConf.java,这正是我想要实现的两个类。
获取轮子
接下来如果我们能在项目中直接使用这些代码就好了。一种朴素可行的想法是,通过手动将pdu包内的类导入到项目中。但是翻阅一下就可以看到有些类引用了该包外的类,也就是说这个子包是不封闭的。引用了包外的类也就意味着包外的类也还可以继续扩大引用范围,我担心这会使得手动导入这些类会变得非常困难,于是做了一个非常错误的决定——下载AOSP的源码然后手动整理。
这里可以说是暴露了我的知识盲区,我根本不知道整个下载AOSP源码是什么概念。
AOSP源码是基于git管理的,但是在此基础上又封装了一个"repo"工具,必须要通过repo工具才可以下载源码。经过一顿折腾,我在WSL上安装了repo并配置好git,开始下载时才发现整个项目下载过于缓慢。
由于AOSP源服务器在境外,慢也算正常的。因此马上想到通过国内镜像站来下载。于是,查看清华镜像站的帮助
https://mirrors.tuna.tsinghua.edu.cn/help/AOSP/
这里看到首次同步需要下载80G,有点绷不住了,这么大的资源肯定弄不下来。
再一次从github上找轮子
马上想到再探github,可能会有从AOSP中提取的部分mms相关代码。这次用aosp mms pdu作为关键字搜索,一下子找到了想要的库,而且有很多。
看来之前的检索太狭隘了,只要专门找android关键字就能发现还有前辈专门提取了pdu包作为独立依赖,并且去除了不封闭的元素。
https://github.com/mikaelhg/nondroid-mms
从github添加maven依赖
接下来只需要将该代码引导项目里了。仍然有朴素的方式——从github下载,然后将类添加到项目里。但是有没有更方便的办法?
当然有,我们可以使用jitpack引入github上的项目。
在pom.xml中添加repositories标签
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
添加后即可从jitpack下载依赖。我们引入的依赖是
<dependency>
<groupId>com.github.mikaelhg</groupId>
<artifactId>nondroid-mms</artifactId>
<version>master-0ac06a5e0e-1</version>
</dependency>
对maven依赖进行更新后就正常可以在项目中使用PduParser了。
使用
通过单元测试发现PduParser可以正常将byte[]数组解析成RetrieveConf对象或SendReq对象。这两个类中都封装了其类型对应的各种header的get方法。然后,通过MultimediaMessagePdu的getBody()方法取得的PduBody对象中封装了getPartsNum() getPart(int index)方法,从名字上也很容易理解可以取得multipart的数量和按下标取值。用数量和下标遍历part,取得的每个PduPart对象中封装了每个媒体内容的各项属性,getData()更是可以直接取得媒体内容的byte[]数组。至此,我们可以解析到PDU里的各项header和body的每个part。
下面进行技术总结:
1.解析MMS协议的关键是解析PDU数据。
2.解析PDU数据的逻辑,在AOSP中有具体实现。
3.AOSP的pdu解析相关逻辑,在github上有前辈提取和封装。
4.通过jitpack可以将github上的项目通过maven引入。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。