Jade Dungeon

实现登录功能

与服务器建立Stream连接

XMPPConnection是一个扩展了Connection的抽象类,建立连接的过程在方法 connect()中。该方法的主要工作主要分成四步:

  1. 新建IOStream对象并建立Socket连接。
  2. 根据Socket的IO分别建立Reader和Writer。
  3. 启动Reader和Writer作为并行运行的作业。
  4. 通过Writer向服务器发送stream:stream标签建立XMPP连接。

详细的连接过程如图\ref{fig:ch06.conn.login.png}所示:

建立连接并登录

在建立了输入流与输出流以后,再调用 initConnection()方法初始化连接,它内部调用 initReaderAndWriter()方法从Socket实例中取得了输入流与输出流,分别包装成了 BufferReaderBufferWriter实例。

初始化packetReaderpacketWriter实例的状态需要调用它们的init()方法。由于 packetReaderpacketWriter都是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:streamstream: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包含多个处理器

MessageProcesser有两个成员:

  • msgHandlers是由多个MsgHandler组成的列表,每种不同的MsgHandler都用来处理 特定类型的消息。
  • foreachHandler()会在每次有新消息收到时被调用,它的主要工作就是调用每一个 MsgHandlerhandle()方法来处理当前收到的消息。

整个处理消息的过程的时序图如\ref{fig:ch06.msghandler.png}所示:

调用对应的消息处理器

处理stream报文

先来处理stream:stream标签,需要扩展MsgHander生成一个专门的处理器 StreamHandlercanProcess()方法判断一段XML是否应该被这个处理器处理的条件有 三个:

StreamHandlerprocess()方法也很简单,就是提取出from属性并存储到连接配置 类ConnCfgserviceName属性中:

处理stream消息

处理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

处理feature消息

canProcess()方法只需要通过前缀与标签名来判断就可以确认收到的文本是否应该 由自己处理。

process()方法需要判断是否有starttlsmechanism这两个子元素,如果有的话 分别交由成员方法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处理器进行处理:

处理process消息

首先还是要由canProcess()方法来检查收到的报文是否应该由当前类来处理:

主要逻辑在proceedTLSReceived()方法中, 该方法首先通过SSLContext类的静态工厂方法取得对应TLS安全连接的上下文环境 实例context,然后按connCfg的配置生成TrustManagerconnCfg中配置有本地 证书文件的位置等信息),接下来以TrustManager为参数初始化context

context初始化完成以后,就可以用它的成员方法getSocketFactory取得带TLS 安全传输的Socket工厂,建立新的Socket连接,并取得输入输出流。

从本方法最后一行的conn.ioStream.openStrem可以看到:在得到了基于安全连接的 Socket与输入输出流以后还需要重新建立一个XMPP连接。

重新建立连接

重新建立连接的过程和前一次建立连接的过程基本相同。唯一的区别是在这次服务器 返回的响应中,stream:features节点不会包含starttls子节点。造成这一差异原因 是因为本次的连接已经是基于TLS安全连接的,所以服务器不会再次要求建立TLS安全 连接。

通过SASL安全登录

在成功建立了安全连接以后,就可以通过login()方法进行SASL认证方式登录了。 login()方法的最后一行会调用在前一章介绍过的SASLMechanism类的 authenticate()方法启动SASL登录操作:

申请SASL登录

随后便开始验证操作,首先服务器会返回一个challenge

<challenge xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
	bm9uY2U9ImJlOFQxOWZiam1GY0VaN1Fpc1dMNnBLYWhoQWtTZ3U0MExZY0
	Q5UkQ5SWM9IixyZWFsbT0iamFiYmVyLm9yZyIscW9wPSJhdXRoIixtYXhi
	dWY9MTYzODQsY2hhcnNldD11dGYtOCxhbGdvcml0aG09bWQ1LXNlc3M=
</challenge>

处理这个challenge的类是SASLChallengerHander

处理challenge

它会调用SASLMechanismchallengeReceived()方法。 在该方法中首先会对收到的文本通过Base64算法进行解码,然后调用SASLMechanism类的 Response()生成正确的响应,在通过Base64算法对响应编码后作为的响应回传给服务器。

服务器会检查客户端给出的响应是否能与之前给出的challenge内容匹配,如果匹配的话 服务器就会返回sussess报文确认登录成功。

登录成功建立新连接

处理登录成功的success报文:

<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
	cnNwYXV0aD05NWIxZDAyY2Y1MWQ4NWM1NzQ3ZDAxYzE2Y2UwYjU3NA==
</success>

SASLSuccessHandler来处理的:

登录成功后再次新建stream连接

这个处理器的工作就非常简单了,只要发送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处理器中,要加上对bindsession元素的处理 :

处理的方式就是通过新建两个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登录过程的关键逻辑介绍。