因业务需求,需要在不对ejabberd进行修改的情况下,对用户发送的消息进行过滤和统计,实现禁言用户等。
我们的实现方案是:在用户和ejabberd集群中间插入一个代理服务,这个代理对用户来说是一个xmpp服务器,对ejabberd来说是一个xmpp客户端,它需要对用户发送给ejabberd的xmpp包进行解析,截取出消息包,将消息包经过业务立即进行处理,再传给ejabberd;对于ejabberd发送给用户的消息,不会进行任何处理,原样转发。
在了解这个代理服务的工作原理之前,首先需要了解xmpp协议。
XMPP是一种基于标准通用标记语言的子集XML的协议,工作在TCP之上,被设计用于实时通讯。TCP建立完成后,起始包大概样子如下:
以文档的角度来看,客户端或服务器发送的所有XML文本连缀在一起,从<stream>到</stream>构成了一个完整的XML文档。其中的stream标签就是所谓的XML Stream。在<stream>与</stream>中间的那些<message>...</message>这样的XML元素就是所谓的XML Stanza(XML节)。XMPP核心协议通信的基本模式是先建立一个stream,然后协商一堆安全之类的东西,中间通信过程就是客户端发送XML Stanza,一个接一个的。服务器根据客户端发送的信息以及程序的逻辑,发送XML Stanza给客户端。但是这个过程并不是一问一答的,任何时候都有可能从一方发信给另外一方。通信的最后阶段是</stream>关闭流,关闭TCP连接。
注意:从客户端到服务器和从服务器到客户端,分别有一份这样的“文档”,并不是写同一份“文档”。
回到代理的工作方式本身,代理需要做的就是正确的解析xmpp格式,<message></message>之外的东西原样转发,<message></message>内所有的东西需要正确的从xmpp流中解析并投递到应用层。在第一版设计中,使用字符串匹配的方式来找出message的头和尾,进行分离,但是这样的方式存在问题,并不是按照协议规范在玩,一旦标签中嵌套标签就会出现问题,解析方式不够优雅。在golang的标准库中,没有提供针对xmpp的解析库,我们需要利用标准库中的xml相关的api和栈结构,将message包分离出来。
golang官方xml库只提供将xml的token(词元)从io.Reader流中解析出来的方法,我们需要对token的类型进行判断。如果是StartElement,则压栈,并写缓存;如果是其他类型,则写缓存;如果是EndElement,则需要和栈顶的Element判断,相同则出栈,不同则流存在问题,关闭连接。
伪码如下:
以这样的方式,可以从xmpp流中分离出单个的完整数据包,再使用xml.Unmarshal将数据包转化为内存结构,方便进行业务处理。