实现登录功能
与服务器建立Stream连接
XMPPConnection
是一个扩展了Connection
的抽象类,建立连接的过程在方法
connect()
中。该方法的主要工作主要分成四步:
-
新建
IOStream
对象并建立Socket连接。 - 根据Socket的IO分别建立Reader和Writer。
- 启动Reader和Writer作为并行运行的作业。
- 通过Writer向服务器发送stream:stream标签建立XMPP连接。
详细的连接过程如图\ref{fig:ch06.conn.login.png}所示:
在建立了输入流与输出流以后,再调用 initConnection()
方法初始化连接,它内部调用
initReaderAndWriter()
方法从Socket
实例中取得了输入流与输出流,分别包装成了
BufferReader
与BufferWriter
实例。
初始化packetReader
和packetWriter
实例的状态需要调用它们的init()
方法。由于
packetReader
和packetWriter
都是Actor
的子类,所以还要调用start()
方法来
启动它们。
在packetWriter
启动了以后,就可以调用openStream()
方法发出XMPP报文开始建立
一个XMPP协议连接。
第五章中已经介绍过PacketWriter
类了,它作为一个Actor实例在一个独立的
线程中等待接收消息。在这里openStream()
方法把一个字符串文本作为消息传递给
packetWriter
实例,它就会把这个消息写入输出流。
实现消息处理
服务器在收到客户端所发的<stream:stream>
报文以后,也会马上打开一个新的请求,
内容为:
<stream:stream version="1.0" id="50e3f803c52d6859" from="jabber.org" xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client"> <stream:features> <starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/> <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> <mechanism>DIGEST-MD5</mechanism> </mechanisms> </stream:features>
注意这一段XML文本并没有闭合<stream:stream>
标签,因为只有一个XMPP会话终止时
才会发送</stream:stream>
关闭标签。所以之前在ReaderStatusHealper
的实现过程中
会忽略所收到的stream
标签闭合完整。以上的文本在本系统中被视为两段独立的XML
文本stream:stream
和stream:features
被分别处理。
对于收到的每一段XMPP报文,都要实现对应的逻辑来处理。所以抽象出MsgHandler
类来
处理每一种不同的XMPP报文。
MsgHandler
是一个带有三个成员方法的特质:
-
canProcess()
方法是一个抽象方法,子类通过实现该方法来判断收到的报文是否可以 由这个类来处理。 -
process()
方法也是一个抽象方法,子类通过实现该方法来执行具体的业务逻辑。 -
handle()
方法是一个已经实现的方法,当canProcess()
方法确定收到的报文是可以 由这个类处理的话,就调用process()
方法执行具体的操作。
由于XMPP报文有多种类型,所以需要不同的MsgHandler
类实现。如图\ref{fig:ch06.msghdl.class.png}所示:
为了统一管理所有的MsgHandler
实例,还需要定义一个MessageProcesser
类。各种
不同类型的MsgHandler
实例都作为MessageProcesser
类的内部成员以列表的形式组织
起来,结构如图\ref{fig:ch06.psghdl.class.png}所示:
MessageProcesser
有两个成员:
-
msgHandlers
是由多个MsgHandler
组成的列表,每种不同的MsgHandler
都用来处理 特定类型的消息。 -
foreachHandler()
会在每次有新消息收到时被调用,它的主要工作就是调用每一个MsgHandler
的handle()
方法来处理当前收到的消息。
整个处理消息的过程的时序图如\ref{fig:ch06.msghandler.png}所示:
处理stream报文
先来处理stream:stream
标签,需要扩展MsgHander
生成一个专门的处理器
StreamHandler
。canProcess()
方法判断一段XML是否应该被这个处理器处理的条件有
三个:
- 命名空间为:http://etherx.jabber.org/streams
- 前缀为:stream
- 标签名为:stream
而StreamHandler
的process()
方法也很简单,就是提取出from
属性并存储到连接配置
类ConnCfg
的serviceName
属性中:
处理features报文
接下来处理features
标签:
<stream:features> <starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/> <mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> <mechanism>DIGEST-MD5</mechanism> </mechanisms> </stream:features>
对应的处理器为StreamFeatureHandler
:
canProcess()
方法只需要通过前缀与标签名来判断就可以确认收到的文本是否应该
由自己处理。
process()
方法需要判断是否有starttls
和mechanism
这两个子元素,如果有的话
分别交由成员方法startTLS()
和processMechanisms()
完成:
processMechanisms()
会遍历mechansisms
的子元素,把服务器所支持的实现机制
保存到SaslAuthentication.serverMechNameList
里。
startTLS()
向服务器发出报文表示准备开始TLS连接:
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls" />
处理proceed标签
当服务器收到starttls
消息以后,作出回应:
<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
对于这个process
标签,交由ProceedTLSHandler
处理器进行处理:
首先还是要由canProcess()
方法来检查收到的报文是否应该由当前类来处理:
- 命名空间为:http://etherx.jabber.org/streams
- 不能有标签前缀。
- 标签名为:proceed
主要逻辑在proceedTLSReceived()
方法中,
该方法首先通过SSLContext
类的静态工厂方法取得对应TLS
安全连接的上下文环境
实例context
,然后按connCfg
的配置生成TrustManager
(connCfg
中配置有本地
证书文件的位置等信息),接下来以TrustManager
为参数初始化context
。
当context
初始化完成以后,就可以用它的成员方法getSocketFactory
取得带TLS
安全传输的Socket工厂,建立新的Socket连接,并取得输入输出流。
从本方法最后一行的conn.ioStream.openStrem
可以看到:在得到了基于安全连接的
Socket与输入输出流以后还需要重新建立一个XMPP连接。
重新建立连接
重新建立连接的过程和前一次建立连接的过程基本相同。唯一的区别是在这次服务器
返回的响应中,stream:features
节点不会包含starttls
子节点。造成这一差异原因
是因为本次的连接已经是基于TLS安全连接的,所以服务器不会再次要求建立TLS安全
连接。
通过SASL安全登录
在成功建立了安全连接以后,就可以通过login()
方法进行SASL认证方式登录了。
login()
方法的最后一行会调用在前一章介绍过的SASLMechanism
类的
authenticate()
方法启动SASL登录操作:
随后便开始验证操作,首先服务器会返回一个challenge
:
<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> bm9uY2U9ImJlOFQxOWZiam1GY0VaN1Fpc1dMNnBLYWhoQWtTZ3U0MExZY0 Q5UkQ5SWM9IixyZWFsbT0iamFiYmVyLm9yZyIscW9wPSJhdXRoIixtYXhi dWY9MTYzODQsY2hhcnNldD11dGYtOCxhbGdvcml0aG09bWQ1LXNlc3M= </challenge>
处理这个challenge
的类是SASLChallengerHander
:
它会调用SASLMechanism
的challengeReceived()
方法。
在该方法中首先会对收到的文本通过Base64算法进行解码,然后调用SASLMechanism
类的
Response()
生成正确的响应,在通过Base64算法对响应编码后作为的响应回传给服务器。
服务器会检查客户端给出的响应是否能与之前给出的challenge内容匹配,如果匹配的话
服务器就会返回sussess
报文确认登录成功。
登录成功建立新连接
处理登录成功的success
报文:
<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"> cnNwYXV0aD05NWIxZDAyY2Y1MWQ4NWM1NzQ3ZDAxYzE2Y2UwYjU3NA== </success>
由SASLSuccessHandler
来处理的:
这个处理器的工作就非常简单了,只要发送stream:stream
重新打开一个连接即可。
绑定会话与连接
成功登录以后重新打开新stream:stream
的过程也是和之前的过程类似,这里就不再重复
解释了。有区别的还是stream:features
报文,登录成功后里面包含有bind
子元素:
<stream:features> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><required/></bind> <session xmlns="urn:ietf:params:xml:ns:xmpp-session"> <optional/> </session> </stream:features>
所以在之前的StreamFeatureHandler
处理器中,要加上对bind
和session
元素的处理
:
处理的方式就是通过新建两个IQ
类的实例,它们的成员方法toXML()
可以把实例
序列化为XML文本。所以以下两段XML文本会被作为响应发送给服务器:
回应绑定的resource
的格式为:
<iq type="get" id="xqHCu-0"> <bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"> <resource>jadexmpp</resource> </bind> </iq>
回应建立session
的格式为:
<iq type="get" id="xqHCu-1"> <session xmlns="urn:ietf:params:xml:ns:xmpp-session"/> </iq>
到这里为止,已经完成了对XMPP登录过程的关键逻辑介绍。