Overview of the XMPP Framework
The xmpp framework started out in 2008 as nothing more than a simple implementation of RFC 3920. It provided a minimal delegate system for receiving the 3 xmpp stanza types (presence, message, iq). The code was compiled into an objective-c framework for easy inclusion in other projects. However, since the framework provided such minimal functionality, it required 3rd party developers to provide a lot of extra code to get something useful. After all, most applications making use of xmpp require a handful of additional features such as rosters, capabilities, or any of the hundreds of XEP's.
It became clear that the project needed to provide implementations of some of the more common xmpp features. Roster support was added, as well as a few other XEP's. During this early period all these additional features were being developed on top of each other in a big monolithic design. In addition, 3rd party developers began to offer additional XEP implementations. But integrating their changes into the monolithic architecture quickly became a nightmare. Additionally the extra features were often unwanted by other developers. Given any XEP, there is only a certain percentage of applications that actually need it. 3rd party developers understandably wanted to keep the monolithic design slim and trim with only those additional features they needed.
And so version 2 of the project was born which introduced a module architecture. The project separated into a core system, and a bunch of modules that add additional features. Developers could simply plug in those features they needed. It was now much easier for 3rd party developers to contribute to the project. And developers could keep their code base slim by including only those modules they needed.
It was at this point that the concept of developing the code into a common objective-c framework began to fade. The monolithic architecture was gone, but a monolithic framework remained. So most 3rd party developers began simply including the files they needed, or customizing the compiled framework to include the subset of required files. Additionally, many new developers were using the project in iPhone apps which don't support non-apple frameworks. Eventually the monolithic framework disappeared, replaced by documentation of how to pick-n-choose your customized xmpp stack. However the project name stuck, so it is still called the "xmpp framework". (Although one could argue that it is still a "framework" in the more generic sense of the term.)
Version 3 continued to build upon the modularization of the code base by making the core thread-safe, and making it easy to parallelize the various modules. The handful of 3rd party developers has grown into a larger community of developers offering not only additional modules, but also bug fixes, support, testing, documentation, suggestions, and encouragement.
Quick Note: This document serves to explain the architecture of the XMPPFramework. It assumes a minimal working knowledge of the xmpp protocol.
The framework is divided into 2 parts:
The xmpp core is the implementation of the xmpp specification (RFC 3920).
Please do not confuse xmpp with chat. XMPP stands for "eXtensible Messaging and Presence Protocol". It is a generic protocol that can be used for many purposes. In fact, there are companies currently using this framework for things such as home automation and delivering code blue alarms to nurses in a hospital.
The extensions include things such as roster support, automatic reconnect, and various implementations of xmpp extensions (XEP's).
The core files of the XMPP Framework are located in the folder named "Core". The files include:
The heart of the framework is the XMPPStream class. This is the primary class you will be interacting with, and it is the class that all extensions and custom code will plug into. It has a number of interesting features designed to make the framework flexible, extensible, and easy to develop on top of. These will be discussed in more depth later in this document.
The XMPPParser is an internal class used by XMPPStream. You can probably take a wild guess as to what it does. You will not need to interact with the parser in any way, shape, or form.
XMPPJID provides an immutable JID (Jabber Identifier) implementation. It supports parsing of JID's, and extracting various parts of the JID in various forms. It conforms to the NSCopying protocol so that JID's may be used as keys in NSDictionary. It even conforms to the NSCoding protocol.
XMPPElement is the base class for the 3 primary XMPP elements: XMPPIQ, XMPPMessage & XMPPPresence. XMPPElement extends NSXMLElement, so you have the entire NSXML foundation with which to inspect any xml element. This is discussed in more detail in the section Elements: IQ, Message, & Presence.
XMPPModule provides the foundation class for optional pluggable extensions. If you are writing your own application specific (proprietary) code, you will likely just create your own class and register to receive delegate invocations. However, if you are implementing a standard XEP, or you want your application specific extensions to be pluggable, then you'll be building atop XMPPModule. Modules are discussed in more detail below.
XMPPLogging provides a very fast, powerful and flexible logging framework. It is discussed in the XMPP Logging section.
XMPPInternal is just internal stuff related to the core and various advanced low-level extensions.
XMPPElement extends NSXMLElement, so you have the entire NSXML foundation with which to inspect any xml element.
In addition to the NSXML foundation, there is a NSXMLElement+XMPP category provided by the framework. This category provides various convenient methods to make your code more concise and readable. For example:
[element attributeIntValueForName:@"age"];
For more information, please see the Working With Elements page.
The configuration of an xmpp stream instance can be divided into multiple parts:
For most people, this involves only a single step - set the myJID property of the stream. For example:
xmppStream.myJID = [XMPPJID jidWithString:@"[email protected]"];
The xmpp stream will figure out the rest by following the XMPP RFC. This involves doing an SRV lookup for _xmpp-client._tcp.domain. In the example above, using gmail, the google servers will likely return something like "talk.google.com", and the xmpp stream will then connect to that server. If the SRV lookup fails, then the xmpp stream will simply connect to the JID's domain.
If you know you are connecting to an xmpp server that doesn't have xmpp SRV records, you can tell the xmpp stream to skip the SRV lookup by specifying the hostName. For example:
xmppStream.myJID = [XMPPJID jidWithString:@"[email protected]"];
xmppStream.hostName = @"myCompany.com";
The hostname also comes in handy when you're using a development xmpp server. Perhaps the server is only available on the local network, or doesn't have a DNS address, etc. For example:
xmppStream.myJID = [XMPPJID jidWithString:@"[email protected]"];
xmppStream.hostName = @"192.168.2.27";
Another optional property is the hostPort. By default, and as per the xmpp specifications, almost all servers run on port 5222. However, if your server is running on a different port, you can set the hostPort property.
XMPPStream has a number of interesting features designed to make the framework flexible, extensible, and easy to develop on top of. One of which is the use of a MulticastDelegate.
What is a MulticastDelegate?
The xmpp framework needs to support an unlimited number of extensions. This includes the official extensions that ship with the framework, as well as any number of extensions or custom code you may want to plug into the framework. So the traditional delegate pattern simply won't work. XMPP modules and extensions need to be separated into their own separate classes, yet each of these classes needs to receive delegate methods. And the standard NSNotification architecture won't work either because some of these delegates require a return variable. (Plus it's really annoying to extract parameters from a notification's userInfo dictionary.)
So a MulticastDelegate allows you to plug into the framework using the standard delegate paradigm, but it allows multiple classes to receive the same delegate notifications. The beauty of this is that you don't have to put all your xmpp handling code in a single class. You can separate your handling into multiple classes, or however you see fit.
You can add / remove yourself as a delegate of the XMPPStream at any time:
[xmppStream addDelegate:self delegateQueue:dispatch_get_main_queue()];
...
[xmppStream removeDelegate:self];
A more detailed discusion of MulticastDelegate can be found here. A more detailed discussion of threading and queues can be found here.
There are a number of extensions that ship with the framework, and of course, you can write as many extensions as you wish. We won't review all available extensions, but we'll list a few here for example purposes.
As an example, we'll plug-in the XMPPReconnect module to our stream:
xmppReconnect = [[XMPPReconnect alloc] init];
// Optional configuration of xmppReconnect could go here.
// The defaults are fine for our purposes.
[xmppReconnect activate:xmppStream];
// You can also optionally add delegates to the module.
[xmppReconnect addDelegate:self delegateQueue:dispatch_get_main_queue()];
// And that's all that is needed.
// The module will receive any delegate methods it needs automatically
// from the xmpp stream, and will continue to do its thing unless you deactivate it.
When you're ready, you can start the connection process:
NSError *error = nil;
if (![xmppStream connect:&error])
{
NSLog(@"Oops, I probably forgot something: %@", error);
}
If you forgot to set a required property, such as myJID, then the connect method will return NO, and the error message will inform you of the problem.
During the connection process, the client and server go through a xmpp handshake. During this time, the server informs the client of various protocols that it supports as well as any that it requires. Some servers may require the connection be secured via SSL/TLS (startTLS). If this is the case, the xmpp stream will automatically secure the connection. If you're connecting to a server with an improper X509 certificate, you may need to implement the xmppStream:willSecureWithSettings: delegate method to alter the default security settings.
After all the connection handshaking has completed, the xmppStreamDidConnect: delegate method is invoked. This is generally where most clients should start the authentication process. This is as simple as:
- (void)xmppStreamDidConnect:(XMPPStream *)sender
{
[xmppStream authenticateWithPassword:password error:NULL];
}
There were several goals for logging throughout the xmpp framework:
It must support several log levels.
Not all log messages have the same priority. Some are about errors, while others are just informational. Levels help developers keep their log messages intact, with the ability to turn them on and off without any difficulty.
It must be configurable on a per-file basis.
A global log level doesn't cut it when the framework consists of so many files. Plus debugging an issue often means developers only want to see log statements from a few files.
It must be configurable to the end user.
Users of the xmpp framework need full control over what ultimately happens concerning the log statements. And users have very different needs. Some want log statements to go to a file. Others may want log statements to go to a database. Or maybe they need to direct log statements to different places depending on whether the log statement is coming from their app or the xmpp framework.
I have worked with many clients over the years, and I see the same problem concerning 3rd party frameworks occurring over and over again. The 3rd party library comes scattered with NSLog statements, which ultimately require the user to go through the library commenting out the NSLog statements, or converting them to some primitive custom macro version.
So rather than whip up yet another primitive macro that ultimately uses the same stupid NSLog, the xmpp framework uses a professional logging framework: CocoaLumberjack. This logging framework is actually faster than NSLog, even when doing the exact same thing. In addition it supports a ton of different configurations, and allows end users to even add their own custom loggers and/or filters and/or formatters. For more information there is a massive amount of documentation available via the Lumberjack Wiki.
Here's what you need to know concerning how logging is setup for XMPPFramework:
Towards the top of most files within the framework you'll find the following
// Log levels: off, error, warn, info, verbose
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
As you can see, there are 4 log levels (plus XMPP_LOG_LEVEL_NONE):
You can change the log level of any file to have it spit out more information.
In addition to this, there is a Trace flag that can be enabled. When tracing is enabled, it spits out the methods that are being called.
Please note that tracing is separate from the log levels. For example, one could set the log level to warning, and enable tracing like this
// Log levels: off, error, warn, info, verbose
static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN | XMPP_LOG_FLAG_TRACE;
In terms of code, this means
XMPPLogTrace(); // Enabled - Will spit out ": "
XMPPLogError(@"I will get logged");
XMPPLogWarn(@"I will get logged");
XMPPLogInfo(@"I will NOT get logged");
XMPPLogVerbose(@"I will NOT get logged");
In addition to this, XMPPStream has an option which enables you to see the raw XML that is being sent / received. You can turn it on in XMPPStream.m like so:
// Log levels: off, error, warn, info, verbose
static const int xmppLogLevel = XMPP_LOG_LEVEL_INFO | XMPP_LOG_FLAG_SEND_RECV;
Recall that the goal of all this logging is to put YOU in control of what gets logged and where those log statements go. This means that you'll need to configure the lumberjack framework when your application starts up. For starters, you can do something as simple as this in your AppDelegate:
#import "DDLog.h"
#import "DDTTYLogger.h"
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
[DDLog addLogger:[DDTTYLogger sharedInstance]];
// All your other code...
}
For more information about Lumberjack take a look at its project page. In fact, I encourage you to start using a professional logging framework within your own application. Once you get the hang of it you'll wonder how you ever lived without it.