标签 RCS 下的文章

关于xml包在Unmarshal时将\r\n重写为\n的问题

今年4月份,中国移动、中国电信、中国联通三大运营商联合举行线上发布会,发布了《5G消息白皮书》。所谓5G消息,即传统短信消息(仅能进行文本展示)的升级版,是由GSMA组织制定的RCS(Rich Communication Suite)消息规范所定义。2019年RCS UP(unified profile)更新到2.4版本,并成为了5G终端标准的一部分,该版本也是第一个具备商用能力的版本,为5G消息商用奠定了基础。中国移动计划2020.6月末正式实现5G消息的商用,目前已经在浙江和广东建立了两个5G消息的支撑节点(分别由中兴和华为承建)。作为电信移动增值领域的厂商,我方也参与了与浙江节点进行行业5G消息平台(MaaP)联调与应用开发。

这引子有些长,本文重点不在5G消息,而在于与行业5G消息平台对接时遇到的一个Go xml包的问题,这是记录一下,以供自己备忘,同时也供广大gopher们参考。

1. 问题现象

行业5G消息使用的通信协议本质上就是xml over http(s)。在http Body的xml中,有一个字段bodyText承载了真正到达5G智能终端上的有效信息载荷,且这个字段是一个CDATA包裹的字段。在我们系统的某个转发流程中,我们解析了从Chatbot(5G行业消息机器人)下发的行业5G消息,但我们发现解析后的bodyText字段中的“\r\n”都被转换为“\n”了。我们用一个例子来直观描述一下该问题:

// xml-rewrite-carriage-return/test2.go

package main

import (
    "encoding/hex"
    "encoding/xml"
    "fmt"
)

type DescCDATA struct {
    Desc string `xml:",cdata"`
}

type Person struct {
    Name string    `xml:"name"`
    Age  int       `xml:"age"`
    Desc DescCDATA `xml:"desc"`
}

var profileFmt = `<person>
<name>"tony bai"</name>
<age>33</age>
<desc><![CDATA[%s]]></desc>
</person>`

func main() {
    c := fmt.Sprintf(profileFmt, "hello\r\nxml")
    var p Person
    err := xml.Unmarshal([]byte(c), &p)
    if err != nil {
        fmt.Println("unmarshal error:", err)
        return
    }
    fmt.Println("unmarshal ok")

    fmt.Println(hex.Dump([]byte("hello\r\nxml")))
    fmt.Println(hex.Dump([]byte(p.Desc.Desc)))
}

运行该例子:

$go run test2.go
unmarshal ok
00000000  68 65 6c 6c 6f 0d 0a 78  6d 6c                    |hello..xml|

00000000  68 65 6c 6c 6f 0a 78 6d  6c                       |hello.xml|

这是一个非常简单的xml unmarshal(反序列化)的例子。我们看到反序列化后,结构体desc字段中的内容相比于原始的xml中desc的内容少了一个字符:0x0d,即“\r”(carriage-return)。我们一直以为针对原xml中CDATA包裹的数据内容,xml包在unmarshal时会原封不动的拷贝下来。为什么”\r”字符会被删除掉呢?我们接下来找找原因。

2. 问题原因

Go是开源的编程语言,它最大的优势就是遇到问题后可以直接看Go标准库源码,当然也可以通过调试工具跟踪到标准库源码中。xml包并不复杂,我选择了直接看xml unmarshal代码的方式。在$GOROOT/src/encoding/xml/xml.go(go 1.14版本)中,我们在Decoder的text方法中找到如下几行代码:

// $GOROOT/src/encoding/xml/xml.go

... ...

func (d *Decoder) text(quote int, cdata bool) []byte {

... ...

                // We must rewrite unescaped \r and \r\n into \n.
                if b == '\r' {
                        d.buf.WriteByte('\n')
                } else if b1 == '\r' && b == '\n' {
                        // Skip \r\n--we already wrote \n.
                } else {
                        d.buf.WriteByte(b)
                }
... ...

}

Decoder的text方法是xml unmarshal在解析如下面name字段的值(xxxx)时被调用的:

<name>xxxx</name>

这段代码的逻辑是:将xxxx中的\r重写为\n,如果存在\r\n,则将其重写为\n。并且无论是否是CDATA字段,这块的逻辑均是生效的。比如我们将上面例子中的desc字段改为非CDATA类型:

// xml-rewrite-carriage-return/test1.go

type Person struct {
    Name string `xml:"name"`
    Age  int    `xml:"age"`
    Desc string `xml:"desc"`
}

var profileFmt = `<person>
<name>"tony bai"</name>
<age>33</age>
<desc>%s</desc>
</person>`

func main() {
    c := fmt.Sprintf(profileFmt, "hello\r\nxml")
    var p Person
    err := xml.Unmarshal([]byte(c), &p)
    if err != nil {
        fmt.Println("unmarshal error:", err)
        return
    }
    fmt.Println("unmarshal ok")

    fmt.Println(hex.Dump([]byte("hello\r\nxml")))
    fmt.Println(hex.Dump([]byte(p.Desc)))
}

该例子的输出:

$go run test1.go
unmarshal ok
00000000  68 65 6c 6c 6f 0d 0a 78  6d 6c                    |hello..xml|

00000000  68 65 6c 6c 6f 0a 78 6d  6c                       |hello.xml|

我们看到:非CDATA包裹的数据,其中的”\r\n”也被重写为“\n”了。

关于这个问题,在Go项目issue中也有人提及:https://github.com/golang/go/issues/24426 。从该issue的讨论中看,Go标准库xml包的实现应该还是参考了xml规范中关于line end的描述的:

XML parsed entities are often stored in computer files which, for editing convenience, are organized into lines. These lines are typically separated by some combination of the characters CARRIAGE RETURN (#xD) and LINE FEED (#xA).

To simplify the tasks of applications, the XML processor must behave as if it normalized all line breaks in external parsed entities (including the document entity) on input, before parsing, by translating both the two-character sequence #xD #xA and any #xD that is not followed by #xA to a single #xA character.

上面的英文规范翻译过来大致是:

XML解析的实体通常存储在计算机文件中,为了便于编辑,这些文件被组织成多行。 这些行通常由字符回车(#xD)和换行(#xA)的某种组合分隔。

为了简化应用程序的任务(解析回车和换行的组合),在解析之前,XML处理器必须对输入的外部解析实体(包括文档实体)进行转换使其规范化,转换规则是:将两字符序列#xD #xA以及后面未紧跟#xA字符的#xD字符转换为单个的#xA字符。

3. 解决方法

我们的述求就是对CDATA包裹的文本数据中的”\r\n”不做“重写”处理。我们采用了下面的方案:clone一份标准库中的xml包,将clone版本放入我们自己的项目路径下,然后在clone版本基础上修改Decoder的text方法的实现

// xml-rewrite-carriage-return/xml/xml.go

... ...

func (d *Decoder) text(quote int, cdata bool) []byte {

... ...

                // We must rewrite unescaped \r and \r\n into \n.
                //
                // tonybai change: only rewrite when text is not in CDATA section
                // (https://github.com/golang/go/issues/24426)
                if !cdata && b == '\r' {
                        d.buf.WriteByte('\n')
                } else if !cdata && b1 == '\r' && b == '\n' {
                        // Skip \r\n--we already wrote \n.
                } else {
                        d.buf.WriteByte(b)
                }

....

}

改造后的代码仅对非CDATA数据进行\r\n的重写,而对于CDATA类型数据,则原封不动的解析出来。我们将test2.go改造成使用我们的clone版本的xml包的示例代码:test3.go

// xml-rewrite-carriage-return/test3.go

package main

import (
    "encoding/hex"

    "github.com/bigwhite/xmltest/xml"

    "fmt"
)

type DescCDATA struct {
    Desc string `xml:",cdata"`
}

type Person struct {
    Name string    `xml:"name"`
    Age  int       `xml:"age"`
    Desc DescCDATA `xml:"desc"`
}

var profileFmt = `<person>
<name>"tony bai"</name>
<age>33</age>
<desc><![CDATA[%s]]></desc>
</person>`

func main() {
    c := fmt.Sprintf(profileFmt, "hello\r\nxml")
    var p Person
    err := xml.Unmarshal([]byte(c), &p)
    if err != nil {
        fmt.Println("unmarshal error:", err)
        return
    }
    fmt.Println("unmarshal ok")

    fmt.Println(hex.Dump([]byte("hello\r\nxml")))
    fmt.Println(hex.Dump([]byte(p.Desc.Desc)))
}

运行该示例:

$go run test3.go
unmarshal ok
00000000  68 65 6c 6c 6f 0d 0a 78  6d 6c                    |hello..xml|

00000000  68 65 6c 6c 6f 0d 0a 78  6d 6c                    |hello..xml|

我们看到这次包裹在CDATA中的\r\n没有被重写,我们对xml包的修改是有效的。

4. 小结

XML作为上一代被设计用来传输和存储数据的标记语言格式,在Go中的支持并不完善,关于标准库xml包的issue还有好多处于open状态。在标准库xml包更新较慢的情况下,clone一份xml包并进行定制不失为一种好的折中方法。

本文所涉及源码在这里可以下载。


我的网课“Kubernetes实战:高可用集群搭建、配置、运维与应用”在慕课网上线了,感谢小伙伴们学习支持!

我爱发短信:企业级短信平台定制开发专家 https://tonybai.com/
smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。

2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

微博:https://weibo.com/bigwhite20xx
微信公众号:iamtonybai
博客:tonybai.com
github: https://github.com/bigwhite

微信赞赏:
img{512x368}

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

2014小结

2014年的最后一个工作日,这里写下有关2014年的一份小结。

年终总结本无固定格式,但写了若干年后,便有了自己的格式。但今年不打算遵循这个格式了,跳出自己的舒适区,随意写写。

2014年12月底,随着亚航QZ8501航班的最后一掉,航空史上都为数不多的灾难年终于画上了句号,留给人们的是久久的惊恐不安,留给遇难者 家属们的是无法释怀的悲伤。2014年12月31日15点,随着A股上证指数最后一个交易日收涨68.86点,稳稳站上3200点,让广大股民们 看到了2015年牛市持续赚钱的希望。不知为何,这个世界几乎总是同时上演着冰与火两种剧本。

短信与微信(包括其他X信)的博弈亦是如此。

短信,这一红极一时的让移动运营商赚得盆满钵满的廉价沟通工具如今却早已成明日黄花。不妨打开手机,翻看一下你的手机通信录,短信列表中是不是除 了验证码(登录、支付业务),就是各种营销垃圾广告,或者是移动运营商自有的客服信息呢。我相信我的情况应该可以代表广大群众了。

随着微信今年推出“企业号”,微信几乎完成了对短信的业务合围:

点对点短信 vs. 联系人、朋友圈、群
SP短信    vs. 订阅号、服务号
行业短信  vs. 服务号、企业号 (营销、售后、内部OA、CRM等)

今年年初招商银行信用卡将300以内的消费提醒短信取消,改为微信提醒,其实就是一个看高微信,看空短信的行为。只是考虑到到达率(用户未开网络时),没 有将大额消费全部转到微信上,而是短信和微信都做提醒。一旦无线网络接入、资费门槛下降、网络速度提升、终端实时在线不再是问题,达到率也将 不是问题时,微信会对短信发起最后的总攻。

这么对比其实也不公平,因为短信和微信本不是一个重量级的对手。从出生的那天起,微信就被赋予了崇高的使命,非短信可比。微信试图连接一切,做统 一入口,建立庞大生态圈;而短信仅仅是一个通道工具罢了。

面对移动短信市场的衰败,移动运营商也在挣扎,也在试图翻盘,或至少平起平坐,但就我了解到的移动运营商产品开发与运营的风格,想和互联网巨头T 掰手腕,下场必输无疑。中国移动年初也蛮拼的,喊出了"RCS(融合通信)"与微信抢手机社交入口,但这都到了2014最后一天了,RCS依旧不见踪 影。

短信免费或退出历史舞台就像周鸿祎在其书《周鸿祎自述:我的互联网方法论》中说的那样是“趋势”,不可违!

我们就是为中国移动短信业务提供服务端软件和方案的。短信若是没了(或变成鸡肋),我们干啥?冰冷的现实摆在大家面前,领导跟我 们说:“转型”。

2014年,至少我们依旧在转型中。老板们把“转型”依旧约束在“移动运营商”这棵大树下面,这让我们转的不那么纯粹,有些拖泥带水,可持续盈利 的业务方向并不明显。从目前来看,今年收入依旧靠传统业务渠道获得。

虽说要“转型”,但领导今年给我的任务却是做好守门员,守住现有市场份额,保证产品线上无事。这并非如我所愿,在一个业务线耕耘多年,业务和技术 能力均到达了天花板,对我个人来说,这不是一个很好的发展规划。但考虑到下面的技术负责人、员工在技术和业务火候儿还欠缺那么一些,我答应了留 守,但会投入部分精力做个人技术转型储备。

业务的转型需要技术做支撑,局限于传统后台服务系统的我们需要张开怀抱,拥抱那些“流行”的新玩意儿。我首先试水!从2014年我的博客中你也许 可以看得出来我试水过的技术,我在尝试跳出自己的各种舒适区,向一些近两年兴起的、将来比较有前途的技术方向靠拢,学习移动互联网的思维和潮流。

上半年曾尝试过终端产品开发的技术,还为此购入若干数码装备,但试过后才发现这仍然不是我的主菜,就和10年前Windows GUI程序开发不是我的菜一样。但这个过程并非没有收获,未来任何业务不与终端开发打交道是不可能的,这个接触过程让我了解到了终端开发的重点和难点,于 是总结经验,整理教训。

正当准备调整方向、重新上路之际,家里出现重大变故,耗费了我整整1个多月的时间,一切几乎都停滞了,直到10月份我才渐渐重新进入状态。

在公司内部技术社区看到公司CTO的一篇文章,讲述移动互联网正在由消费者驱动向企业驱动转变(来自麦肯锡报告),结合微信推出企业号、用友软件的转型来 看(今天听说用友软件更名为“用友网络”了,决心向互联网转型),这个趋势也是我比较认同的,这个方向以及相关技术也是我在正在涉猎以及即将涉猎的。不过 关于企业互联网服务以及平台,自己的相关业务经验、技术和积累还是甚少,征途必然坎坷,自己还需“拼”一下!关于微信这个平台,这个入口,它是腾讯未来战 略的核心,靠着腾讯这棵大树,至少未来几年发展应该还是不错的。

公司的大BOSS这两年一直提倡“创业者的精神",学会在逆境中成长,在困境中成功。但作为在短信这个行业内浸淫了十多年的部门,我们不免产生一些惰性, 更愿意躺在现有的温床上“享受生活”,立足于现有的平台做舒服的事情。经历过2014年的严峻形势,现在的我们应该清醒的认识到这样的舒服生活,温床和平 台都可能将远离我们。如果我们再不主动站起来,我们将再无力站起了。

2014年在个人发展方面做出了“妥协”,2015我打算轻装前行,这对我、对团队成员的成长都是有好处的。年底给领导发总结时,已经和领导书面提出退出 当前业务线的想法。虽然目前还没有收到回复,不过无论怎样,我都坚定了决心,自己作为这个产品线的负责人,已经起不到领路的作用了,是时候退出了。

2015,给自己的关键字是“创业”。《精益创业》一书中作者似乎有这样一句话:“你不一定非要在车库里折腾才算是创业”,在企业内部也可以“创业”,为创造某种新产品或新服务为目的而组建的一个团队或组织内的人都是“创业者”。

以往年份的小结,我总会总结一些数据,比如blog文章、读过多少本书等等。但今年这些数据就不统计了,自己对自己的考核指标"KPI"有所调整,以前哪些指标已经不算数了,列出也就无意义了。

2014这一年,LP给了我很大压力!我能理解,她期望我能取得更大的成功。这让我“亚历山大”啊,这回可是真的。

要说新年的愿望是什么?希望2015年年末时能为自己2015年的所作所为,所取得的进步和成果点个赞

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats