Jade Dungeon

联系人与状态

JID类的实现

在第二章中已经描述过:XMPP协议中JID所代表的是一个登录端点。在本应用中,JID可以 被抽象为一个Jid类:

Jid类

在图\ref{fig:ch07.jid.class.png}中所展示的结构中local、domain、resource这三个 成员是每个Jid实例都包含的,是典型的成员变量。另外两个是静态成员:

  • fromString()是静态方法,功能是把字符串转为Jid实例。
  • jidPattern是用来匹配Jid格式的正则表达式。

通过对正则表达式的匹配,可以很方便地从字符串构建出Jid实例。

val jidPattern = ("""(((\w+([-_\.]\w+)*)@)?)""" +
	"""(\w+([-_\.]\w+)*)((/(\w+([-_\.]\w+)*))?)""").r

/* create instance from String */
def fromString(str: String): Option[Jid] = str match {
	case jidPattern(a,b,local,c,domain,d,e,f,resource,g) => 
		Some(Jid(lo,dom,rec))
	case _ => None
}

伴生对象

Java中可以通过关键字static声明成员为静态成员,Scala中不支持static语法,而是 采用单例对象来实现静态成员的。通过object关键字就可以生成一个单例对象,而且 如果单例对象与某个类名相同的话,就自动成为这个类的「伴生对象」。Scala中的类可以 自由访问伴生对象中的私有成员:

/* class */
class Jid(val local: String, val domain: String, 
		val resource: String) { }

/* object */
object Jid {
	val jidPattern = /* ... */
	
	def fromString(str: String): Option[Jid] = /* ... */
}

工厂方法

在Scala语言中,伴生对象的apply()方法被视为该类的静态工厂方法。工厂方法以后, 可以通过「类名()」的形式得到类的实例,而不用通过关键字new调用构造函数。

object Jid {
	def apply(val local: String, val domain: String, 
		val resource: String) = { new Jid(local, domain, resource) }
}

Option类型

方法fromString()的作用是从字符串创建一个Jid实例:

def fromString(str: String): Option[Jid] = /* ... */

注意这个方法的返回类型不是Jid而是Option[Jid]。在Scala语言中Option类型相当 于一个容器,表示该值的内容可能为空。Option类型存在的意义就是在调用该方法时 提醒程序员,返回值有可能为空。在操作这个返回值前强制程序员先对返回值为空的情况 作出处理,这样就可以避免发生NullPointerException异常抛出引发程序中断。 比较常见的处理方法就是使用getOrElse()来定义没有返回值时的默认行为。

Option[Jid]表示期待返回的值是Jid实例,但不保证一定会返回Jid实例:因为 参数str是字符串类型,并一定都是格式正确的JID,所以方法的返回值有可能为空。

Scala的模式匹配与样本类

Jid类型作为用户连接的抽象,在本应用中会非常频繁地被用到。比如从Jid实例中提取 出local、domain、resource的值。

在Java或其他面向对象的语言中一般都是通过成员变量名来访问:

local = jid.getLocal();
domain = jid.getDomain();
resource = jid.getResource();

而在Scala中,由于语言本身提供的模式匹配语法,可以很方便地把Jid实例的成员匹配 到变量上去。首先要加上关键字case把一个Scala类声明为样本类:

case class Jid(val local: String, val domain: String, 
		val resource: String) { }

声明为样本类后就可以作为模式匹配的条件:

obj match {
	case Jid(local, domain, resource) => { /* case block */ }
	case _ => None
}

case Jid(l, d, r)语言起到了两个作用:

  1. 首先进行类型匹配,如果obj为Jid类的实例,则匹配成功。
  2. 如果匹配成功,那么按照工厂方法apply(local,domain,resource)方法的参数把顺序, 把localdomainresource三个工厂方法的参数分别绑定到变量ldr ,这三个变量的作用域为=>后的语句块。

对于第二个匹配条件case _,表示匹配所有的情况。如果前一个条件不符合,那么就 自动匹配这个条件。

除了模式匹配,在样本类还有以下这些作用:

  • 样本类有自动产生的工厂方法apply()
  • 自动包含copy()方法可以得到一个副本。
  • 编译器为样本类添加了可读性更强的toString()方法。
  • 自动提供的hashCode()equals()方法会树型嵌套作用于成员变量。

虽然样本类有很多方便的特性,但并不是所有的类都适合定义为样本类。如果一个样本类 是从其他样本类继承过来的,那么不会自动实现默认的toString()hashCode()equals()copy()方法。而且编译器会提示警告。在以后的Scala版本里可能会禁止 样本类扩展子类。所以,推荐只有最末端的子类是样本类。

抽取器

之前介绍的工厂方法可以根据指定参数来创建实例,与之对应的有抽取器unapply()方法 根据一个实例来提取出特定的属性值。以Jid为例:

  • apply()方法根据localdomainresource返回Jid实例。
  • unapply()方法根据一个Jid实例返回该实例的localdomainresource

Jid类的unapply()方法中,参数是任意类型的Any。因为模式匹配可以非常方便 地匹配参数的真正类型:

	def unapply(obj: Any): Option[(String, String, String)] = {
		obj match {
			case Jid(l, d, r) if (isBlank(d)) => None
			case Jid(l, d, r) if (isBlank(l)) => Some((null, d, null))
			case Jid(l, d, r) if (isBlank(r)) => Some((l, d, null))
			case Jid(l, d, r) => Some((l, d, r))
			case _ => None
		}
	}

联系人列表

用Jid类实现了一个JID的抽象以后,还需要一个类作为存放联系人列表的容器。所以在 这里抽象为Roster类:

联系人列表类

  • 用户的在线状态可以抽象为内部类Presense,其成员jid为JID实例,priority为 该登录端点的权重,status为该登录端点的在线状态。
  • 枚举类型Subscription抽象了unauth(对方未同意)和both(双向订阅)两种 状态。
  • 内部类Member代表了每一个独立的联系人。
  • Roster类为联系人的抽象。主要成员是一个HashMap存放联系人与JID字符串的映射。

向服务器申请联系人列表与联系人的状态

在登录成功以后服务器后会出iq报文:

<iq id="xqHCu-1" type="result" from="jabber.org"/>

当客户端收到上面例子中这样没有内部元素的iq报文,就说明可以请求联系人和联系人 状态了。在IQHandlerprocess()方法中,会同时调用requireRoster()requirePresence()。申请的过程如图\ref{fig:ch07.iqhdl.png}:

处理iq消息

requireRoster()方法会发出如下的报文请求联系人列表:

<iq type="get" id="xqHCu-2">
	<query xmlns="jabber:iq:roster"></query>
</iq>

requirePresence()方法如下的报文请求联系人状态:

<presence id="29D92-32"></presence>

解析服务器发送的联系人名单

服务器收到iq/query请求以后返回联系人格式是一个有query子元素的iq节点:

<iq id="xqHCu-2" type="result" to="sorr@jabber.org/jadexmpp">
	<query ver="12" xmlns="jabber:iq:roster">
		<item subscription="both" name="Jade Shan" jid="ao23@gmail.com">
			<group>Buddies</group>
		</item>
		<item subscription="both" jid="coru@gmail.com">
			<group>Buddies</group>
		</item>
	</query>
</iq>

如图\ref{fig:ch07.rosteritem.class.png}内部类Item代表一个服务器发来的消息:

服务器发出的联系人信息

因为返回的联系人名单也是在iq报文中的,所以只要在处理iq的处理器中判断是否有 query/item子元素就可以实现解析联系人名单的功能。如图\ref{fig:ch07.iqphdl.png} :

处理联系人信息

process()方法从XML报文中取出jid属性、name属性与group子元素创建Item 实例,并作为消息发送给Roster实例。 Roster类作为一个Actor实例,如果收到Item类型的消息就会调用updateMember() 方法更新联系人名单。

解析服务器发送的联系人状态

前一节中已经介绍过如何解析联系人名单,接下来再讨论如何更新联系人状态。服务器在 收到更新联系人状态的请求后返回记录为多个presence记录。格式为:

<presence id="99jn5-513" to="sorr@jabber.org" 
	from="ao23@gmail.com/androidcHg66345792">
	<status>online</status>
	<priority>0</priority>
</presence>
<presence id="99jn5-514" to="sorr@jabber.org" 
	from="kod92@gmail.com/androidcHg66345792">
	<status>online</status>
	<priority>0</priority>
</presence>

同样可以很简单从服务器的响应建立Presence实例,和之前的其他服务器响应的处理 方式一样,通过创建一个Presence标签来处理:

服务器发出的联系人状态变化信息

Roster作为一个Actor实例,如果收到Presence类型的消息就会调用 updatePresence()方法更新联系人的在线状态:

更新联系人状态