CSDN博客

img jiangtao

DDJ文章:Instant Messaging A Programmer's Tool?

发表于2004/4/8 9:05:00  2718人阅读

Jabber and lightweight languages do the trick

By William Wright and Dana Moore

William and Dana are the authors of Jabber Developer's Handbook (SAMS, 2003) and engineers with BBN. They can be contracted at wwright@bbn.com and dana.moore@bbn.com, respectively.

When it comes to instant messaging (IM), we almost always think of it as a mechanism for humans to speak with one another textually in semireal time. Perhaps because of its most common use in person-to-person (P2P) textual conversation, software developers have not considered IM as a delivery platform for person-to-systems (P2S) or distributed system-to-system (S2S) interactions. Nonetheless, it is in P2S and S2S that open instant messaging platforms such as Jabber may have the most to offer.

Consider that when developers design and deliver P2S systems like web clients and servers, we almost never think of the interactions between user and system as "conversations." This is not to say that building such systems is particularly hard, especially with an IM protocol such as Jabber performing four essential services:

  • Message switching.
  • P2P, P2S, and S2S communication backplane.
  • Service discovery and location.
  • User registry and management.

In this article, we examine the Jabber client-side protocol. Jabber is extremely language-friendly, with mature libraries for Java, C/C++, and .NET, among others. Our examples focus on Python, Perl, and Ruby.

The Jabber client protocol (http://www.jabber.org/) is remarkably straightforward. Every participant in a Jabber session has a unique Jabber identifier (JID) that looks a lot like an e-mail address:

{username}@{servername}/{resource}

username and servername are as you would expect in an e-mail address. The resource part of the JID is only important when a particular user has more than one session active at once and it is used to disambiguate the message destination. If no resource is specified, the Jabber server routes the message to whichever session the user has given the highest priority.

The Jabber protocol is a bidirectional XML stream exchanged between the IM client and Jabber server. Over the course of a session, the client sends one whole XML document to the server and the server sends one whole XML document to the client. As a programmer using Jabber interface libraries, you don't need to be concerned about the root element of the XML stream—you only need to handle sub-elements. There are three types of subelements in the Jabber client protocol:

  • , which contains regular messages from one JID to another.
  • , which contains information about the online status of an entity.
  • , short for "info query," is used for bookkeeping tasks like logging into servers, searching databases, and so on.

Elements of a Jabber Message

A Jabber message element can contain several XML attributes that describe the message and how it should be handled. These include:

  • to. Its value specifies the JID of the intended recipient.
  • from. Its value is the JID of the message sender.
  • id. Is a string identifier for the message; often a random string.
  • type. Indicates how the recipient should treat this message. If present, its value is one of: chat (a message in a one-on-one conversation); groupchat (a message in a multiparty conversation); headline (a "news" item; some IM clients raise a separate window to notify users of headline messages); and error (indicates an error response from the Jabber server).

The message element also contains sub-elements that contain the actual content of the message, including:

  • , which contains the text content of the message.
  • , the message's subject line.
  • , an identifier for the discussion thread. IM clients use this value to display a message in the appropriate chat window.
  • . If an error occurs, this element contains a description of the error.

Example 1 is a typical message that might be part of a one-on-one chat session. You can tell by the element attributes that this message is to dana@localhost from bill@localhost/work and is a chat type message. The to JID does not contain any resource, telling the Jabber server that if dana@localhost has more than one session, deliver the message to the one with the highest priority. The from JID does include the resource (work, in this case) so any reply is delivered to the right Jabber session for bill@localhost. This message happens to have no element—that's allowed.

Now that we've reviewed the basic parts of a Jabber message, we'll show how to send and receive Jabber messages from scripting languages in the context of some common software development and system-administration tasks.

Jabber in Ruby

Say you want to check on a computer to make sure that it's not overloaded or crashed, and have it send you a message when the load reaches a certain level. To see this data on a Linux machine, you might use the uptime command that prints several system status readings in a format like this:

6:34pm up 83 days, 7:54, 2 users, load average: 0.00, 0.02, 0.00

This line includes the current system time, how long the system has been running, how many users are logged in, and the load on the system, averaged over the last 1, 5, and 15 minutes. Since a runaway process causes load average numbers to increase, we should keep an eye on these numbers. Rather than monitor them manually, we'll write a Ruby script using the jabber4r library (http://jabber4r.rubyforge .org/) that sends a Jabber message if something is awry.

Listing One is a Jabber client that connects to the server, then waits for messages. If the body of the message is, say, start 1.0, this client starts a new Ruby thread that repeatedly runs the uptime command and uses the number after the start as the threshold for sending notifications. If the one-minute load average goes above that number, notification is sent.

The first thing this script needs to do is log into the Jabber server:

session = Jabber::Session.bind_digest(jid, passwd)

session.announce_initial_presence

The first line uses the Jabber ID (jid) and password for that JID to initialize the connection and authenticate to the server. The second line lets other clients know that this client is online. That way, if someone has added this JID to their Jabber roster (other IM systems call this a "Buddy List"), they could see that the script was up and running and that, by implication, the computer was up and running. If the computer crashes, the Jabber server updates rosters to indicate that the script is offline.

The next line in the script sets up a block of code that runs when a message is received. The Jabber::Protocol::Message object (msg) is passed to that block:

session.add_message_listener { |msg|

# Handle the message

}

The Message object has accessors for each of the parts of a Jabber message. To get the text of the body of the message, use the msg.body accessor. Here, we use the Ruby split function to separate the start from the threshold number:

value = msg.body.split[1]

We also use the split function to parse the output of the uptime command. If the load exceeds the threshold, a Jabber message is generated; see Example 2. Filling in the reply message using fields from the original message ensures that the response goes back to the right client session and that the Jabber client knows how to associate the response with the original request.

Since the script polls the uptime command every five seconds in an infinite loop, you need a way to stop it. Control-C works, but it's more elegant to have the script respond to a stop command in a Jabber message. The last few lines of the message handler look for the word "stop" in a message, and set a flag to indicate that any polling thread associated with the sender should exit. It would be easy to extend this example to run any program and send the output as a Jabber message to anyone who is interested.

Jabber in Perl

Next, we initiate a software build using the NET::Jabber library (http://www .jabberstudio.org/projects/netjabber/) for Perl and Jabber. This approach uses the Ant Java build tool, but could be modified to handle make or most any other command-line build tool.

As in the Ruby example, this script logs into the Jabber server and waits for messages. When it receives a message, it interprets the body of the message as the name of the Ant target to build, executes Ant, and returns the text output of the Ant command to the sender. This lets developers request a full build at any time without requiring that each developer maintain a full build environment.

The first thing Listing Two does is connect and authenticate to the Jabber server. The four lines in Example 3 initialize the Jabber client connection, define the Perl functions called when the and packets are received, connect to the Jabber server, and authenticate to the server using the script's username, password, and resource.

Once these four lines are complete, the script connects to the server and can send and/or receive messages. One handy thing to do is to send a packet so other clients know that the script is online:

$connection->PresenceSend();

If you have the build script in your Jabber roster, you can see at a glance whether the build system is available. The only thing left is to process Jabber messages as they come in on the connection:

while (1) {my $res = $connection->Process();}

This reads and processes Jabber packets by calling the functions specified in the SetCallBacks method until the script is killed.

Our arguments to SetCallBacks specified that when a Jabber packet is received, the presenceCB method is called. The script doesn't really make use of the presence information, but Example 4 is a function that shows how it's structured. The first argument to presenceCB is used by NET::Jabber for bookkeeping and not of interest. The second argument is the presence object. It contains the from field (the JID of the client sending the packet); type field (controls how this packet should be interpreted); and show and status fields (indicates whether the client is temporarily away, interested in chatting, and so on).

Here, we are interested in packets. When a message packet is received, NET::Jabber calls our messageCB function, which needs to read the function arguments and extract the message body as follows:

my $sid = shift;

my $msg = shift;

my $msgtxt = $msg->GetBody();

Next, the script uses the message body to build and run an Ant command using Perl's system command:

system "ant -logfile antout.tmp $msgtxt";

my $buildOutput = 'cat antout.tmp';

In an open environment, you want to scrub the message body for shell-special characters; otherwise, someone could run any command they chose. The output of the Ant process is stored in a temporary file and read into the $buildOutput variable, which we use to construct the response message. Sending a Jabber message from Perl is a one-line command, but that one line can have several parameters:

$connection->MessageSend(to=>$msg-> GetFrom(),

subject=>"Build of $msgtxt",

thread=>"$msg->GetThread()",

type=>$msg->GetType(),

body=>$buildOutput);

As before, we use source-message attributes in the response message to ensure that the IM client knows how to handle this message. The Ant output is included as the body of the message. If we send a regular message with the content "echo" to an example Ant script (Listing Three), the response message is like that in Figure 1. If we send a similar "echo" message as a chat message, you see something like Figure 2. Because our script uses the type attribute of the incoming message, the IM client knows to route regular messages to the main message window and the chat message to the chat window.

Jabber in Python

The final example creates web services using Jabber and Python via the JabberPy library (http://jabberpy.sourceforge.net/). The XML-RPC specification defines an encoding of RPC requests and responses—a method call, the arguments it consumes, and the result it returns and mandates HTTP as a transport. However, if a server is behind a firewall, incoming HTTP requests are blocked. An easy way around this is to replace the HTTP transport with Jabber.

First, look at a Jabber XML-RPC client in Listing Four. What's different from the ordinary Jabber conversation is that neither of the participants in the dialogue exchange the usual Jabber messages. Rather, they are going to exchange only XML-RPC requests and responses wrapped in an (info query) packet.

To get access to the Jabber core library functions and pack/unpack XML-RPC requests, you must import both the Jabber and xmlrpclib packages:

import xmlrpclib

import jabber

Next, use the dumps() method to convert the argument that (in the Python JabberPy library) must be converted from a Python tuple to a properly encoded XML-RPC request:

request = xmlrpclib.dumps((text,), method name=Method)

Next, log users into the Jabber switch using the client connect() method and log in using the auth() method. Assuming you created a user called peer-b to act as the responding XML-RPC server (Listing Five) and assigned a resource rpc to the username, you next create an info query packet addressed to peer-b@localhost/rpc, set the query type to a remote procedure call, and set the payload to the XML-RPC request just created:

iq = jabber.Iq(to=Endpoint, type='set')

iq.setQuery('jabber:iq:rpc')

iq.setQueryPayload(request)

Finally, you make the remote call and wait for the response.

result = con.SendAndWaitForResponse(iq)

The returned response object contains the return type and an encoded payload. We test the result type to determine whether we got a good response or "fault" (error), get the payload, then parse the returned XML into a Python structure using loads(). The parms structure is an array, but normally only the zero-th slot is filled.

if result.getType() == 'result':

response = str(result.getQueryPayload())

parms, func = xmlrpclib.loads(response)

print parms[0]

Having set up the client, in Listing Five we define an XML-RPC service that is also a Jabber client. The remote method (Rot13()) is a global method. The JabberPy API provides support for all three types of message types; in this case, we simply stub out the listeners for presence and normal message, concentrating only on messages because the XML-RPC request is delivered that way.

We extract the query's namespace and get the query payload from the info query passed in as the second argument; see Example 5(a). Uncomment the print lines if you want to see what the payload looks like as encoded XML, Example 5(b), and then as a Python structure:

((' There was a young lady of Nantes',), u'Rot13')

We use the Python structure directly later on to invoke the Rot13() method.

Since the Jabber protocol defines several namespaces for IQ packets, we check the query type to make sure it's appropriate (jabber.NS_RPC) and start constructing a reply. We get the sender's Jabber address using iq.getFrom() and set the rest of the fields in the response; see Example 6(a). Finally, we unpack the XML and use the string in an eval() to invoke the Rot13() method; see Example 6(b).

Once again, note that using the dumps() method to convert data to an XML-RPC stanza requires the data in the form of a tuple. Uncomment the aforementioned print statement to see the XML stanza of data returned to the invoking client:

Gurer jnf n lbhat ynql bs Anagrf

Conclusion

We hope we've given you a taste of programming instant messages with Jabber. It's so easy to add this functionality that we find ourselves coming up with new ideas for it every day.

DDJ

Listing One

require 'jabber4r/jabber4r'
jid = "uptime@localhost/uptime"
passwd = "uptime"
$status = {}

session = Jabber::Session.bind_digest(jid, passwd)
session.announce_initial_presence
session.add_message_listener { |msg|
  if (msg.body.include? "start")
    value = msg.body.split[1]
    $status[msg.from.to_s] = "running"
    t = Thread.new {
      while $status[msg.from.to_s] == "running"
        data = `uptime`.split[7]
        if (data.to_f >= value.to_f) 
          reply = Jabber::Protocol::Message.new(nil)
          reply.to = msg.from
          reply.thread = msg.thread
          reply.type = msg.type
          reply.set_body(`uptime`)
          reply.set_subject("Your uptime request")
          session.connection.send(reply)
        end
        sleep 5
      end
    }
  elsif (msg.body.include? "stop")
    $status[msg.from.to_s] = "stop"
  end
}
Thread.stop

Back to Article

Listing Two

use strict;
use Net::Jabber 'Client';
my $jid = 'build@localhost/work';
my $pass = 'build';
my $connection;

sub messageCB {
  my $sid = shift;
  my $msg = shift;
  my $src = $msg->GetFrom("jid")->GetUserID();
  my $msgtxt = $msg->GetBody();
  # run ant
  my $buildOutput = `ant -logfile antout.tmp $msgtxt`;
  $buildOutput = `cat antout.tmp`;
  $connection->MessageSend(to=>$msg->GetFrom(), subject=>"Build of $msgtxt",
                           thread=>"$msg->GetThread()",type=>$msg->GetType(),
                           body=>$buildOutput);
  `rm antout.tmp`;
}
sub presenceCB {
  my $sid = shift;
  my $presence = shift;
  my $from = $presence->GetFrom();
  my $type = $presence->GetType();
  my $show = $presence->GetShow();
  my $status = $presence->GetStatus();
  print "$from is now $show/$status/n";
}
sub connectToJabber {
  my $uname;
  my $server;
  my $resource;
  ($uname, $server, $resource) = ($jid =~/([^@]*)@([^//]*)//(.*)/);
  $connection = new Net::Jabber::Client();
  $connection->SetCallBacks(message=>/&messageCB, presence=>/&presenceCB);
  my $status = $connection->Connect(hostname=>$server);
  my @result = $connection->AuthSend(username=>$uname, password=>$pass,
                                     resource=>$resource);
  if ($result[0] ne "ok")
  {
    print "ERROR: Authorization failed: $result[0] - $result[1]/n";
    exit(0);
  }
  $connection->PresenceSend();
  while (1) {my $res = $connection->Process();}
}
sub sendMsg {
  my $otherJid = shift;
  my $msgText = shift;
  $connection->MessageSend(to=>$otherJid, subject=>"chat_demo_subject",
                           thread=>"chat_demo_thread",type=>"chat",
                           body=>$msgText);
}
connectToJabber();

Back to Article

Listing Three


  
    
      This ant script doesn't do too much.
    
  

Back to Article

Listing Four

import jabber
import xmlrpclib
import string
import sys
Server = 'localhost'
Username = 'peer-a'
Password = 'peer-a'
Resource = 'rpc'

Endpoint = 'peer-b@localhost/rpc'
Method = 'Rot13'
text = "There was a young lady of Nantes"

request = xmlrpclib.dumps((text,), methodname=Method)
con = jabber.Client(host=Server)
try:
    con.connect()
except IOError, e:
    print "Unable to connect: %s" % e
    sys.exit(0)
con.auth(Username, Password, Resource)
iq = jabber.Iq(to=Endpoint, type='set')
iq.setQuery('jabber:iq:rpc')
iq.setQueryPayload(request)
result = con.SendAndWaitForResponse(iq)
if result.getType() == 'result':
    response = str(result.getQueryPayload())
    parms, func = xmlrpclib.loads(response)
    print parms[0]
else:
    print "Error"
con.disconnect()

Back to Article

Listing Five

import xmlrpclib, jabber
import sys, re, os, string
Server = 'localhost'
Username = 'peer-b'
Password = 'peer-b'
Resource = 'rpc'
def Rot13(text):
    rot= ""
    for x in range(len(text)):
        byte = ord(text[x])
        cap = (byte & 32)
        byte = (byte & (~cap))
        if (byte >= ord('A')) and (byte <= ord('Z')):
            byte = ((byte - ord('A') + 13) % 26 + ord('A'))
        byte = (byte | cap)
        rot = rot + chr(byte)
    return rot

def iqCB(con, iq):
    myFromID = iq.getTo()
    type = iq.getType()
    payload = xmlrpclib.loads(str(iq.getQueryPayload()))
    if iq.getQuery() ==  jabber.NS_RPC:
      resultIq = jabber.Iq(to=iq.getFrom(), type='result')
      resultIq.setID(iq.getID())
      resultIq.setFrom(iq.getTo())
      resultIq.setQuery(iq.getQuery()) 
      evalString = payload[1]+"('"+payload[0][0]+"')"
      # Actually call the requested method
      returnParams = xmlrpclib.dumps(tuple([eval(evalString)]))
      resultIq.setQueryPayload(returnParams)
      con.send(resultIq)
#--------- "MAIN" -----------------------------
con = jabber.Client(host=Server)
try:
    con.connect()
except IOError, e:
    print "Couldn't connect: %s" % e
    sys.exit(0)
else:
    print "Connected"
con.process(1)
if con.auth(Username, Password, Resource):
    print "Authorized"
else:
    print "problems with handshake: ", con.lastErr, con.lastErrCode
    sys.exit(1)
con.setIqHandler(iqCB)
try:
    while(1):
        con.process(300)
except KeyboardInterrupt:
    con.disconnect()


Back to Article

0 0

相关博文

我的热门文章

img
取 消
img