对openfire的session与路由机制的概念熟悉与理解请参考前文:《openfire的session与路由机制(一)问题域分析》
2.1 Connection接口
Connection接口代表了服务端的一个链接
权限修饰符 | 返回类型 | 重要属性或方法名 | 功能描述 |
public | boolean | validate() | 验证链接是否存活。通常通过发送一个空格符来验证。 |
public | void | init(LocalSession session) | 初始化session的Connection类。允许通过Session的信息来配置Connection类(eg. Stream ID) |
public | byte[] | getAddress() throws UnknownHostException | 获取网络链接方的原生(字节流)的的ip地址 //todo |
String | getHostAddress() throws UnknownHostException | 获取网络链接方用文本表示的ip地址 | |
public | String | getHostName() throws UnknownHostException | 获取网络链接方的主机名称 |
public | Certificate[] | getLocalCertificates() | 获取本地证书链 |
public | Certificate[] | getPeerCertificates() | //todo |
public | void | setUsingSelfSignedCertificate(boolean isSelfSigned) | 设置自指定证书 |
public | boolean | isUsingSelfSignedCertificate | 是否使用自指定证书 |
public | void | close() | 关闭链接 |
public | void | systemShutdown() | 服务器通知给连接方,表明服务器正在关闭,实现的方法在关闭链接之前应当发送一个condition为system-shutdown的stream error |
public | boolean | isClosed() | 链接是否关闭 |
public | boolean | isSecure() | 链接是否安全(eg. 是否使用了SSL/TLS) |
public | void | registerCloseListener(ConnectionCloseListener listener,Object handbackMessage) | 注册监听链接关闭的通知事件 |
public | void | deliver(Packet packet) throws UnauthorizedException | 发送packet报文给链接方,并不检查接收人。 此方法本质上调用了socket.send(packet.getWriteBuffer()) |
public | void | deliverRawText(String text) | 发送原生文本消息给链接方,这是一个非常底层次的发送xml节给客户端的方法。这个方法不应当被调用,除非你有非常充分的理由来调用它。 |
public | void | isFlashClient() | //todo |
| boolean | isCompressed() | 是否使用压缩传输数据 |
| CompressionPolicy | getCompressionPolicy() | 获取压缩策略(optional,disabled) |
| TLSPolicy | getTlsPolicy() | 获取TLS安全策略(required:必须;optional:可选; disabled:失效; legacyMode:传统模式) |
| PacketDeliverer | getPacketDeliverer() | 返回报文发送器(当通过socket发送失败时)。Deliverer将会重试其它的链接发送报文,为了后续的检索将会存储离线的报文 |
| void | startTLS(boolean clientMode) throws Exception | 开启STL安全机制。对于server-2-server链接中,在STL协商过程中,请求TLS协商的server将成为client,其它的server将会成为server。因此,请求TLS的server必须传递“true”给clientMode参数,同时,接收TLS的请求的server必须传递“false”给clientMode参数。 在client-2-server的链接模式中,clientMode的值应传递“false”。因为它将充当TLS协商的server端。 |
| void | addCompression() | 在这个链接中添加压缩filter,但只过滤进入的流量,不过滤出去的流量,因为我们仍然需要发送一个未压缩的stanza给客户端,使得客户端能够启动压缩机制。在我们发送未压缩的stanza后,我们也能够开启出去的流量的压缩。 |
| void | startCompression() | 开启出去的流量的压缩。压缩将只在TLS已经协商之后有效。(然而,也可使用没有TLS的压缩) |
| ConnectionConfiguration | getConfiguration() | 返回本链接所期望的状态表示。它不同于当前的链接的状态。举个例子:TLS能被Configuration所需要,但是这个链接还没被完全地初始化,当前的状态可能不会被TLS加密。 |
从方法列表中可以看出Connection的基本功能:1.服务器发送报文给链接方;2.在链接中启用STL加密通道;3.链接的数据传输使用压缩提高性能;4获取地址连接方的信息(ip,主机名等);5.链接状态操作及监听链接等。
2.2 Session接口
Session接口继承了RouteChannelHandler接口,其父接口为ChannelHandler接口。
Session代表了一个在server和client(c2s)或者和其它的server(s2s)的链接,也代表了一个带有component的链接。认证和用户账号是和c2s模式的链接有关联的,而s2s模式有可选的认证关联,但是没有用户账号相关的部分。我们能从session获取对象管理器,来访问服务器资源。
权限修饰符 | 返回类型 | 重要属性或方法名 | 功能描述 |
public | JID | getAddress() | 获取用户的(JID)地址 |
public | int | getStatus() | 获取当前session的状态(-1:已关闭,1:已链接,3:已认证) |
public | StreamID | getStreamID() | 获取和当前session有关联的Stream id。Stream id由服务端生成,且应是唯一、随机的。 |
public | String | getServerName() | 获取当前session的所属的服务端名称 |
public | Date | getCreationDate() | 获取session的创建时间 |
public | Date | getLastActiveDate() | 获取session上次活动的时间 |
public | long | getNumClientPackets() | 获取客户端发给服务端的报文数 |
public | long | getNumServerPackets() | 获取服务端发给客户端的报文数 |
public | void | close() | 关闭session |
public | boolean | isClosed() | session是否关闭 |
public | boolean | isSecure() | 会话是否安全(eg. 是否使用了SSL/TLS) |
public | String | getHostAddress() throws UnknownHostException | 获取网络链接方用文本表示的ip地址 |
public | String | getHostName() throws UnknownHostException | 获取网络链接方的主机名称 |
public | void | process(Packet packet) | session最核心方法,调用此方法来处理xmpp报文,Packet为Message则进行消息的转发操作等 |
public | void | deliverRawText(String text) | 发送原生文本消息给链接方,这是一个非常底层次的发送xml节给客户端的方法。这个方法不应当被调用,除非你有非常充分的理由来调用它。 这个方法避免获取当前链接的的writer,且避免了直接弄乱writer。因此,这个方法确保了一个正确的stanza的发送。同时,能保证发送的线程安全。 |
| boolean | validate() | 同Connection |
| String | getCipherSuiteName() | 获取TLS加密套件名称 |
| Locale | getLanguage() | 返回当前session使用的本地化(eg. Locale#ENGLISH) |
从session的方法列表可以看出,session的部分方法的用途和Connection的方法一致,同时Session的抽象方法的具体实现中调用了许多Connection的方法,因此可以把Session当成是Connection的上层处理者,而Connection更多是底层的、较为单一操作的处理;
Session类的基本功能主要有:1.对报文及原生报文的处理、转发功能;2.和加密相关的操作;
3.获取和会话相关的信息等;
Session接口的实现层次:
LocalSession抽象类是Session单机的默认实现,提供了接口的骨架实现;ClientSession接口是服务端和客户端会话的抽象;ServerSession是服务端和远程服务端的会话抽象(允许不同服务器的用户进行聊天),它的两个子接口IncomingServerSession和OutgoingServerSession是对发送和接收会话的抽象;ComponentSession是对服务端和Component会话的抽象;ConnectionMultiplexerSession接口是服务端和多路复用链接管理器会话的抽象。
下面来详细认识下Session的抽象类和子接口:
2.2.1 骨架类LocalSession
什么是骨架类?(以下内容摘录自《effictivejava》)
Java的接口不允许包含方法的实现,但是,使用接口来定义类型并不妨碍你为调用者提供实现上的帮助。通过对你导出的每个重要接口都提供一个抽象的骨架实现类,把接口和抽象类的优点结合起来。接口的作用仍然是定义类型,定义规约,但是骨架实现类接管了所有与接口实现相关的的工作。
骨架实现的美妙之处在于,它们为抽象类提供了实现上的帮助,但又不强加“抽象类被用作类型定义”时所特有的严格限制。对于接口的大多数实现来讲,扩展骨架实现类是个很显然的选择,但并不是必需的。如果预置的类无法扩展骨架实现类,这个类始终可以手工实现这个接口。此外骨架实现类仍然能够有助于接口的实现。实现了这个接口的类可以把对于接口方法的调用转发到一个内部私有类的实例上,这个内部私有类扩展了骨架实现类。这种方法被称作模拟多重继承。这项技术具有多重继承的绝大多数优点,同时又避免了相应的缺陷。
首先看下LocalSession的属性列表:
需要关注的主要属性有:
属性定义 | 描述说明 |
private JID address | 一个会话拥有一个jid地址 |
private StreamID streamID | Stream id,随机且唯一 |
protected final Connection conn | 一个会话包含了一个链接 |
protected SessionManager sessionManager | 会话管理对象 |
private final Map<String, Object> sessionData | session临时数据,在会话结束时,所有存储在map中的数据都会消失 |
protected final StreamManager streamManager | XEP-0198 Stream Manager |
类比http的session,会话一旦建立,就能够在session的生命周期中保存、传递自定义属性值,LocalSession的属性sessionData实现了这个机制,使得业务层可以在会话中保存自定义属性值。值得特别注意的是,LocalSession有Connection类型的属性conn,可以说明Session在高层次上封装了Connection。
接下来,特别关注下LocalSession最为重要的方法process,此方法是对ChannelHandler接口的抽象方法的实现。
2.2.2 ClientSession接口
ClientSession接口代表了一次服务端和客户端的会话。可类比http的客户端和服务端的 session机制。
权限修饰符 | 返回类型 | 重要属性或方法名 | 功能描述 |
public | PrivacyList | getActiveList() | 返回客户端用户的私有列表。此列表只对应于特定的session,且在特定session生存期有效。 |
public | void | setActiveList(PrivacyList activeList) | 设置私有列表,覆盖了默认的私有列表。 |
public | String | getDefaultList() | 设置默认的私有列表 |
public | void | setDefaultList(PrivacyList defaultList) | 获取默认的私有列表 |
public | String | getUsername() throws UserNotFoundException | 获取会话客户端的用户名。 |
| boolean | isAnonymousUser() | 是否是匿名用户 |
public | boolean | isInitialized() | 会话是否已初始化 |
public | void | setInitialized(boolean isInit) | 设置会话的初始化状态 |
public | boolean | canFloodOfflineMessages() | 如果在用户上线时,用户的离线消息将被发给用户,此方法将会返回true。 |
public | boolean | isOfflineFloodStopped | 当发送可用的出席信息,如果用户不接收离线消息,则返回true。 |
public | Presence | getPresence() | 获取会话对应的出席信息 |
public | void | setPresence(Presence presence) | 设置出席信息 |
public | int | incrementConflictCount() | 自增conflict的计数,并返回自增后的计数 |
| boolean | isMessageCarbonsEnabled() | 消息carbon是否启动 |
ClientSession除了有Session接口的方法之外,还主要拥有属于自己的方法:1.私有信息、出席信息的存取;2.会话客户端用户的状态(会话初始化状态,是否匿名,carbon状态)
2.2.3 ServerSession接口
ServerSession接口除了继承Session接口的方法外,只有一个抽象方法booleanisUsingServerDialback()。Openfire 中S2S 之间的链接有TLS 和 Dialback 两种加密验证方式。Dialback(远程拨号回送)提供一种弱身份验证的方式,要使用这种方式可以将 ofproperty表中“xmpp.server.tls.enabled”设置为false,并将“xmpp.server.dialback.enabled”设置为true。
Server Dialback机制背后的思想是:接收服务器不从一个发送服务器接受XMPP流量,直到接收服务器有(一个)对已被发送服务器域断言的授权服务器的回调,并且已查实发送服务器真正被授权生成了那个域的XMPP流量。协议也保证了接收服务器正在接受目标域名的xml节。
更多关于Dialback信息请参考:http://xmpp.org/extensions/xep-0220.html
2.2.4 OutgoingServerSession接口
为了能够达到server2server通信, 在服务器之间应使用的两个tcp链接。其中一个链接用来发送报文,而另一个链接用来接收报文。OutgoingServerSession代表到达一个远程server的链接,此链接仅用于发送报文。
一旦和远程server的链接被建立,且至少一个域名被认证,一个新的路由将会被添加到路由表中。出于优化的考虑,相同outgoing链接将被使用,即便远程server有若干主机名。然而,对于每个远程server的主机名,不同的路由在路由表中将被建立。
权限修饰符 | 返回类型 | 重要属性或方法名 | 功能描述 |
| Collection<String> | getAuthenticatedDomains() | 返回带有已认证过的所有域名、子域名、和虚拟主机的列表。远程server将会接受从任何这些域名、子域名、虚拟主机中发出的 报文 |
| void | addAuthenticatedDomains(String domain) | 添加一个新的远程server的已认证域名、子域名、虚拟主机到认证域名列表中 |
| Collection<String> | getHostnames() | 返回远程server关联的主机名列表。主机列表表明了远程server的主机踪迹,用于重用相同session相同远程server,即使server有多个名称。 |
| void | addHostname(String hostname) | 添加一个新的主机名到远程server的已知主机名列表中。 |
| boolean | authenticateSubdomain(String domain, String hostname) | 在一个已存在的outgoing链接上 认证此server的子域。如果已存在的session使用了server的回叫,结果将被发送到远程的server |
OutgoingServerSession接口
2.2.5 IncomingServerSession接口
为了能够达到server2server通信, 在服务器之间应使用的两个tcp链接。其中一个链接用来发送报文,而另一个链接用来接收报文。IncomingServerSession代表到达一个远程server的链接,此链接仅用于接收报文。
返回类型 | 重要属性或方法名 | 功能描述 | |
public | Collection<String> | getValidatedDomains() | 返回带有已经被验证的域名、子域名和虚拟主机的列表 |
public | String | getLocalDomain() | 返回本地server的域名或子域名。这个信息仅被用于阻止从同个远程server到本地server的同个域名或子域名的多个链接。 |
2.3 PacketRouter接口
Router是用来处理进入的报文的。报文将被路由到它们相匹配的处理器。
权限修饰符 | 返回类型 | 重要属性或方法名 | 功能描述 |
| void | route(Packet packet) | 基于报文的类型路由报文 |
| void | route(IQ packet) | 路由给定的IQ报文 |
| void | route(Message packet) | 路由给定的Message报文 |
| void | route(Presence packet) | 路由给定的Presence报文 |
Router的类层次设计如下所示:
Openfire默认对PacketRouter接口的实现类是PacketRouterImpl,PacketRouterImpl类拥有messag、IQ、presence三种类型报文Router的属性,它们的类型分别是MessageRouter、IQRouter、PresenceRouter。而三种类型报文Router都包含了RoutingTable的属性,在报文的路由处理时,会借助路由表进行深一层的路由处理。
2.4 RoutingTable接口
在问题域的定义中,可以看出存会话信息是我们对路由表最为简单、先入为主的认知,但是在openfire实现中,map的value为RoutableChannelHandler,是会话类的基类,这是因为路由表的value还可存其它两类对象:Chatbot和Transport。
源码中的注释描述
RoutingTable维持服务端的路由信息,路由表的实质是一个Map的数据结构,key是jid地址,value是报文的处理器(即RoutableChannelHandler对象)。报文的处理器(典型的是以下三种类型):
l Session 一个属于服务器的域的本地或远程会话。远程会话可能在集群服务器。
l Chatbot 聊天机器人(了解更多chatbot,点击https://zhuanlan.zhihu.com/p/20940374),将有各种各样的Chatbot数据包路由给它
l Transport 外部的服务器域,它可能驻留在同一服务器JVM(比如虚拟主机服务器、groupchat 服务器等)
在绝大部分情况下,调用者不应该干预与给定节点有关联的处理器。简单地获取报文处理器,并且发送报文到节点,把细节留给处理器处理。路由要匹配在XMPP规范中定义的字符串预处理规则。特别的name或resource的通配符路由被null指定,eg. 路由到任何server.com的地址,应当设置name为null,server.com的主机并且resource也是null。
对于user@server.com路由地址,应该指明它的XMPP地址的resource部分为null。Session管理器应当添加一个路由,这个路由既可能是通用地址user@server.com,也可能是full jid地址:user@server.com/resource。
为了能够包含广播功能,你需要通过查询所有特殊节点的“child”节点做部分的匹配。路由表包含了一系列的节点树。节点树是安排在以下等级秩序:
forest | 在路由表中的所有节点。带有host、name和设置为null的resource的XMPP地址,将会匹配所有存在路由表中的节点。使用的时候应极其谨慎,路由表可能包含成千上万条目和为了迭代遍历的安全考虑使用路由表的拷贝副本而产生的迭代器。 |
domain root | 这些节点树的根都是服务器的一个域。一个只包含hotst,而name和resource为null的XMPP地址将匹配(等于)域根,所有孩子节点将包含根条目(如果有的话)和所有条目相同的主机名。 |
user branches | 根的直接子元素是用户分支。一个包含host和name条目且resource为null的XMPP地址将匹配一个特定用户的分支。子元素包含了用户分支(如果有的话)和相同的host和name但忽略了resource的所有条目。用户分支对于用户的广播的执行时非常有用的。注意,如果用户分支位于外部服务器, 将返回server-2-server的传输的唯一路由。
|
resource leaves | 每个用户分支可以有零个或多个资源叶子。部分匹配在XMPP地址和值在主机、名称和资源领域将相当于精确匹配调用因为只有一个路线可以被注册为一个特定的。如果你想搜索的资源叶路线,以及有效的用户部门对该节点如果没有叶的存在,可以看这个getBestRoute()方法的描述
|
note:这点挺重要:以上任何部分或动作会影响到路由对路由表的立即更新。
权限修饰符 | 返回类型 | 重要属性或方法名 | 功能描述 |
| void | addServerRoute(JID route, LocalOutgoingServerSession destination) | 添加指定的outgoing server session的路由记录到路由表 |
| void | addComponentRoute(JID route, RoutableChannelHandler destination) | 添加指定的内部或外部的component的路由记录到路由表 |
| boolean | addClientRoute(JID route, LocalClientSession destination) | 添加指定的client session的路由记录到路由表。一旦用户完成了和服务端的验证后,client session将会被添加。而且,当用户的状态变成可用或不可用的时候,路由表也会再次更新。在一个集群环境下, 实际上拥有client session的集群节点必须将消息发送出来。 |
| void | routePacket(JID jid, Packet packet, boolean fromServer) throws PacketException | 将报文路由到指定的地址。报文的接收者可以是一个本地服务器、component、外部服务器的用户。 当把报文路由到远程server时,如果找不到outgoing链接,将会创建一个与远程server的新的outgoing链接,且报文将被发送。如果一个outgoing链接已经存在,将使用此链接来发送报文。 此外,在一个集群运行环境中,拥有实际的outgoing链接的节点将会被要求发送被请求的报文。 如果内部或外部的component连接到服务器,被路由到component的报文将被发送。此外,在一个集群运行环境中,托管component的节点将被要求发送被请求的报文。首先,将会检查component是在这个jvm是否是可用的,接着,托管component的第一个集群节点将会被使用。 如果用户连接到服务器,路由给用户的报文将会被发送。依赖于报文类型,报文发送者的可用状态或者所有用户session会被考虑。 例如,Message和Presence只能被发送到可用的client session,同时由服务端发起的IQ能够被发送到可用或不可用的session。 |
| boolean | hasClientRoute(JID jid) | 如果一个最近被记录的,且有指定full jid的注册用户或者匿名用户,将会返回true。 |
| boolean | isAnonymousRoute(JID jid) | 如果一个带有指定full jid的匿名的用户有被记录,则返回true |
| boolean | isLocalRoute(JID jid) | 如果指定的JID地址属于一个被当前JVM托管的路由则返回true。 |
| boolean | hasServerRoute(JID jid) | 如果一次outgoing server session存在于指定的远程server则会返回true。参数jid既能是全JID也可以是纯JID,因为只有指定地址的域名将会被用于路由的查阅 |
| boolean | hasComponentRoute(JID jid) | 如果一个内部或外部的Component托管了给定的JID地址则返回true。参数jid既能是全JID也可以是纯JID,因为只有指定地址的域名将会被用于路由的查阅 |
| ClientSession | getClientRoute(JID jid) | 返回与给定XMPP地址关联的client session。如果找不到则返回null。 |
| Collection<ClientSession> | getClientsRoutes(boolean onlyLocal) | 返回已被server认证的的client session列表。 |
| OutgoingServerSession | getServerRoute(JID jid) | 返回与给定XMPP地址关联的outgoing server session。如果找不到则返回null。 |
| Collection<String> | getServerHostnames() | 返回能接收从本服务器发出的报文的远程server的主机名称列表。 |
| int | getServerSessionsCount() | 返回被本地JVN托管的outgoing server session 数目。 |
| Collection<String> | getComponentsDomains() | 返回被server托管的Component的域名。在集群环境中,运行在任何节点的Component的域名将被返回。 |
| List<JID> | getRoutes(JID route, JID requester) | 返回与给定路由地址关联的路由JID列表。当要求路由到一个远程server,被请求的JID将作为包含在被返回列表的唯一值。一次给定远程server的outgoing session是否存在并不重要。 |
| boolean | removeClientRoute(JID route) | 如果一个client session的路由成功地被删除则将返回true。在集群环境中,这条消息必须从实际托管client session的集群节点发出。 |
| boolean | removeServerRoute(JID route) | 如果一个outgoing server的路由成功地被删除则会返回true。在集群环境中,这条消息必须从实际托管client session的集群节点发出。 |
| boolean | removeComponentRoute(JID route) | 如果一个Component的路由成功地被删除则会返回true。内部和外部Component在路由表中都有路由记录。在集群环境中,这条消息必须从实际托管client session的集群节点发出。 |
| void | setRemotePacketRouter(RemotePacketRouter remotePacketRouter) | 设置RemotePacketRouter对象,用于发送报文到集群中的远程节点的实体 |
| RemotePacketRouter | getRemotePacketRouter() | |
| void | broadcastPacket(Message packet, boolean onlyLocal) | 广播已经链接的client session的消息到本地节点或者集群。可用和不可用的client session都能接收到消息 |
从方法列表可以看出:RoutingTable接口的操作主要围绕路由表进行(添加,删除,查询)。
路由表的类层次设计如下所示:
RoutingTableImpl有三种类型报文的Router属性,目前版本,这些router属性主要用于路由失败的处理。RoutingTableImpl类底层路由表功能是借助LocalRoutingTable类实现的。