客户端登陆openfire,大概总共需要9个来回才完成登录。
在2G情况下,就表现为客户端登录特别慢,所以,为解决这个问题,对openfire进行了如下优化
openfire的连接、登陆过程分为几个步骤,完整报文如下,总共分为9个round trip:
===================================================================================================================
1 STREAM
RECV:<stream:stream to="jacklin-pc" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
SENT:<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="jacklin-pc" id="96508a6d" xml:lang="en" version="1.0">
SENT:<stream:features><starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"></starttls><mechanisms xmlns="urn:ietf:p
arams:xml:ns:xmpp-sasl"><mechanism>DIGEST-MD5</mechanism><mechanism>JIVE-SHAREDSECRET</mechanism><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>CRAM-MD5</mechanism></mechanisms><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><auth xmlns="http://jabber.org/features/iq-auth"/><register xmlns="http://jabber.org/features/iq-register"/></stream:features>
2 TLS
RECV:<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
SENT:<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
3 STREAM
RECV:<stream:stream to="jacklin-pc" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
SENT:<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="jacklin-pc" id="96508a6d" xml:lang="en" version="1.0"><stream:features><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>DIGEST-MD5</mechanism><mechanism>JIVE-SHAREDSECRET</mechanism><mechanism>PLAIN</mechanism><mechanism>ANONYMOUS</mechanism><mechanism>CRAM-MD5</mechanism></mechanisms><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><auth xmlns="http://jabber.org/features/iq-auth"/><register xmlns="http://jabber.org/features/iq-register"/></stream:features>
4 SASL
RECV:<auth mechanism="PLAIN" xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cmVhbG09ImphY2tsaW4tcGMiLG5vbmNlPSJMamw2RGt4Y3hGSDZxb2dTRE55Nmw2VkYreVQ2YjROMFlTa1BBSlZqIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNz</auth>
SENT:<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></success>
5 STREAM
RECV:<stream:stream to="jacklin-pc" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
SENT:<?xml version='1.0' encoding='UTF-8'?><stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" from="jacklin-pc" id="96508a6d" xml:lang="en" version="1.0"><stream:features><compression xmlns="http://jabber.org/features/compress"><method>zlib</method></compression><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></stream:features>
6 BIND
RECV:<iq id="SfW08-0" type="set"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>Spark 2.6.3</resource></bind></iq>
SENT:<iq type="result" id="SfW08-0" to="jacklin-pc/96508a6d"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>test001@jacklin-pc/Spark 2.6.3</jid></bind></iq>
7 SESSION
RECV:<iq id="SfW08-1" type="set"><session xmlns="urn:ietf:params:xml:ns:xmpp-session"/></iq>
SENT:<iq type="result" id="SfW08-1" to="test001@jacklin-pc/Spark 2.6.3"/>
8 PRESENCE
RECV:<presence id="SfW08-6"><status>在线</status><priority>1</priority></presence>
9 ZLIB
RECV:<compress xmlns='http://jabber.org/protocol/compress'><method>zlib</method></compress>
SENT:<compressed xmlns='http://jabber.org/protocol/compress'/>
===================================================================================================================
1 STREAM优化
其中STREAM类似查询服务器功能,服务器会把服务器的特性返回给客户端,例如SASL策略,iq-auth,zlib压缩,xmpp-bind等等,其实,如果是内部定制的系统,这些特性服务器与客户端都是共知的,所以不需要查询,完全可以省略这些步骤。但是,我发现,在客户端第一次发送stream时,是需要初始化一些内容的,所以,需要再如下地方,加入如下代码:
org.jivesoftware.openfire.nio.ConnectionHandler
- @Override
- public void sessionOpened(IoSession session) throws Exception {
- // Create a new XML parser for the new connection. The parser will be
- // used by the XMPPDecoder filter.
- final XMLLightweightParser parser = new XMLLightweightParser(CHARSET);
- session.setAttribute(XML_PARSER, parser);
- // Create a new NIOConnection for the new session
- final NIOConnection connection = createNIOConnection(session);
- session.setAttribute(CONNECTION, connection);
- session.setAttribute(HANDLER, createStanzaHandler(connection));
- // Set the max time a connection can be idle before closing it. This
- // amount of seconds
- // is divided in two, as Openfire will ping idle clients first (at 50%
- // of the max idle time)
- // before disconnecting them (at 100% of the max idle time). This
- // prevents Openfire from
- // removing connections without warning.
- final int idleTime = getMaxIdleTime() / 2;
- if (idleTime > 0) {
- session.setIdleTime(IdleStatus.READER_IDLE, idleTime);
- }
- // ADD LOCALSESSION START===========================================================
- Log.info("[DO LOCALSESSION]");
- int hashCode = Thread.currentThread().hashCode();
- XMPPPacketReader parser1 = parsers.get(hashCode);
- if (parser1 == null) {
- parser1 = new XMPPPacketReader();
- parser1.setXPPFactory(factory);
- parsers.put(hashCode, parser1);
- }
- String msg = "<stream:stream to='jacklin-pc' xmlns='jabber:client' xmlns:stream='http://etherx.jabber.org/streams' version='1.0'>";
- StanzaHandler handler = (StanzaHandler) session.getAttribute(HANDLER);
- try {
- handler.process(msg, parser1);
- } catch (Exception e) {
- Log.error(
- "Closing connection due to error while processing message: "
- + msg, e);
- connection.close();
- }
- // ADD LOCALSESSION END==============================================================
- }
此目的是让服务器在客户端建立连接阶段就初始化客户端的资源。
2 SESSION
session其实从服务器端看,只是回复了一下客户端,并没有起什么作用,所以客户端可以不发这段报文,服务器端不需要改动。服务器端处理过程如下。
org.jivesoftware.openfire.handler.IQSessionEstablishmentHandler
- public class IQSessionEstablishmentHandler extends IQHandler {
- private IQHandlerInfo info;
- public IQSessionEstablishmentHandler() {
- super("Session Establishment handler");
- info = new IQHandlerInfo("session", "urn:ietf:params:xml:ns:xmpp-session");
- }
- @Override
- public IQ handleIQ(IQ packet) throws UnauthorizedException {
- // Just answer that the session has been activated
- IQ reply = IQ.createResultIQ(packet);
- return reply;
- }
- @Override
- public IQHandlerInfo getInfo() {
- return info;
- }
- }
3 BIND,PRESENCE,ZLIB
其实PRESENCE和ZLIB可以在客户端BIND操作之后,服务器端直接进行,不需要客户端再次协商。所以,我在以下代码,进行了以下改动:
org.jivesoftware.openfire.handler.IQBindHandler
- @Override
- public IQ handleIQ(IQ packet) throws UnauthorizedException {
- LocalClientSession session = (LocalClientSession) sessionManager
- .getSession(packet.getFrom());
- // If no session was found then answer an error (if possible)
- if (session == null) {
- Log.error("Error during resource binding. Session not found in "
- + sessionManager.getPreAuthenticatedKeys() + " for key "
- + packet.getFrom());
- // This error packet will probably won't make it through
- IQ reply = IQ.createResultIQ(packet);
- reply.setChildElement(packet.getChildElement().createCopy());
- reply.setError(PacketError.Condition.internal_server_error);
- return reply;
- }
- IQ reply = IQ.createResultIQ(packet);
- Element child = reply.setChildElement("bind",
- "urn:ietf:params:xml:ns:xmpp-bind");
- // Check if the client specified a desired resource
- String resource = packet.getChildElement().elementTextTrim("resource");
- if (resource == null || resource.length() == 0) {
- // None was defined so use the random generated resource
- resource = session.getAddress().getResource();
- } else {
- // Check that the desired resource is valid
- try {
- resource = JID.resourceprep(resource);
- } catch (StringprepException e) {
- reply.setChildElement(packet.getChildElement().createCopy());
- reply.setError(PacketError.Condition.jid_malformed);
- // Send the error directly since a route does not exist at this
- // point.
- session.process(reply);
- return null;
- }
- }
- // Get the token that was generated during the SASL authentication
- AuthToken authToken = session.getAuthToken();
- if (authToken == null) {
- // User must be authenticated before binding a resource
- reply.setChildElement(packet.getChildElement().createCopy());
- reply.setError(PacketError.Condition.not_authorized);
- // Send the error directly since a route does not exist at this
- // point.
- session.process(reply);
- return reply;
- }
- if (authToken.isAnonymous()) {
- // User used ANONYMOUS SASL so initialize the session as an
- // anonymous login
- session.setAnonymousAuth();
- } else {
- String username = authToken.getUsername().toLowerCase();
- // If a session already exists with the requested JID, then check to
- // see
- // if we should kick it off or refuse the new connection
- ClientSession oldSession = routingTable.getClientRoute(new JID(
- username, serverName, resource, true));
- if (oldSession != null) {
- try {
- int conflictLimit = sessionManager.getConflictKickLimit();
- if (conflictLimit == SessionManager.NEVER_KICK) {
- reply.setChildElement(packet.getChildElement()
- .createCopy());
- reply.setError(PacketError.Condition.conflict);
- // Send the error directly since a route does not exist
- // at this point.
- session.process(reply);
- return null;
- }
- int conflictCount = oldSession.incrementConflictCount();
- if (conflictCount > conflictLimit) {
- // Kick out the old connection that is conflicting with
- // the new one
- StreamError error = new StreamError(
- StreamError.Condition.conflict);
- oldSession.deliverRawText(error.toXML());
- oldSession.close();
- } else {
- reply.setChildElement(packet.getChildElement()
- .createCopy());
- reply.setError(PacketError.Condition.conflict);
- // Send the error directly since a route does not exist
- // at this point.
- session.process(reply);
- return null;
- }
- } catch (Exception e) {
- Log.error("Error during login", e);
- }
- }
- // If the connection was not refused due to conflict, log the user
- // in
- session.setAuthToken(authToken, resource);
- }
- child.addElement("jid").setText(session.getAddress().toString());
- // Send the response directly since a route does not exist at this
- // point.
- session.process(reply);
- // After the client has been informed, inform all listeners as well.
- SessionEventDispatcher.dispatchEvent(session,
- SessionEventDispatcher.EventType.resource_bound);
- // ADD COMPRESSION START==================================================
- session.getConnection().addCompression();
- session.getConnection().startCompression();
- Log.info("[DO COMPRESSION]");
- // ADD COMPRESSION END====================================================
- // ADD PRESENCE START====================================================
- String domain = XMPPServer.getInstance().getServerInfo()
- .getXMPPDomain();
- Presence pp = new Presence();
- pp.setFrom(session.getAddress().toString());
- // pp.setTo(domain);
- XMPPServer.getInstance().getPacketRouter().route(pp);
- Log.info("[DO PRESENCE]:"+ pp.toXML());
- // ADD PRESENCE END====================================================
- return null;
- }
经过以上优化之后,服务器与客户端的协商只剩下3个round trip,过程如下:
2 TLS
RECV:<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
SENT:<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
4 SASL
RECV:<auth mechanism="PLAIN" xmlns="urn:ietf:params:xml:ns:xmpp-sasl">cmVhbG09ImphY2tsaW4tcGMiLG5vbmNlPSJMamw2RGt4Y3hGSDZxb2dTRE55Nmw2VkYreVQ2YjROMFlTa1BBSlZqIixxb3A9ImF1dGgiLGNoYXJzZXQ9dXRmLTgsYWxnb3JpdGhtPW1kNS1zZXNz</auth>
SENT:<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"></success>
6 BIND
RECV:<iq id="SfW08-0" type="set"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><resource>Spark 2.6.3</resource></bind></iq>
SENT:<iq type="result" id="SfW08-0" to="jacklin-pc/96508a6d"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>test001@jacklin-pc/Spark 2.6.3</jid></bind></iq>
客户端按照如下节奏发送报文,并进行操作就可以了,客户端修改代码,在这里不做描述,请修改者自行尝试