`
datoplay
  • 浏览: 1616532 次
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多

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:

<ddjadvertisement inline><ul> <li>Message switching. </li> <li>P2P, P2S, and S2S communication backplane. </li> <li>Service discovery and location. </li> <li>User registry and management. </li> </ul> <p>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. </p> <p>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:</p> <blockquote> <p>{username}@{servername}/{resource}</p> </blockquote> <p><i>username</i> and <i>servername</i> are as you would expect in an e-mail address. The <i>resource</i> 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 <i>resource</i> is specified, the Jabber server routes the message to whichever session the user has given the highest priority.</p> <p>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:</p> <ul> <li> <i><message></message></i>, which contains regular messages from one JID to another. </li> <li> <i><presence></presence></i>, which contains information about the online status of an entity. </li> <li> <i><iq></iq></i>, short for "info query," is used for bookkeeping tasks like logging into servers, searching databases, and so on. </li> </ul> <h3>Elements of a Jabber Message</h3> <p>A Jabber message element can contain several XML attributes that describe the message and how it should be handled. These include:</p> <ul> <li> <i>to</i>. Its value specifies the JID of the intended recipient. </li> <li> <i>from</i>. Its value is the JID of the message sender. </li> <li> <i>id</i>. Is a string identifier for the message; often a random string. </li> <li> <i>type</i>. Indicates how the recipient should treat this message. If present, its value is one of: <i>chat</i> (a message in a one-on-one conversation); <i>groupchat</i> (a message in a multiparty conversation); <i>headline</i> (a "news" item; some IM clients raise a separate window to notify users of headline messages); and <i>error</i> (indicates an error response from the Jabber server). </li> </ul> <p>The message element also contains sub-elements that contain the actual content of the message, including:</p> <ul> <li> <i></i>, which contains the text content of the message. </li> <li> <i><subject></subject></i>, the message's subject line. </li> <li> <i><thread></thread></i>, an identifier for the discussion thread. IM clients use this value to display a message in the appropriate chat window. </li> <li> <i><error></error></i>. If an error occurs, this element contains a description of the error. </li> </ul> <p><a name="re1"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403ge1.htm">Example 1</a> is a typical message that might be part of a one-on-one chat session. You can tell by the <i><message></message></i> element attributes that this message is to <i>dana@localhost</i> from <i>bill@localhost/work</i> and is a <i>chat</i> type message. The <i>to</i> JID does not contain any resource, telling the Jabber server that if <i>dana@localhost</i> has more than one session, deliver the message to the one with the highest priority. The <i>from</i> JID does include the resource (<i>work</i>, in this case) so any reply is delivered to the right Jabber session for <i>bill@localhost</i>. This message happens to have no <i><subject></subject></i> element—that's allowed.</p> <p>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.</p> <h3>Jabber in Ruby</h3> <p>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 <i>uptime</i> command that prints several system status readings in a format like this:</p> <blockquote> <p>6:34pm up 83 days, 7:54, 2 users, load average: 0.00, 0.02, 0.00</p> </blockquote> <p>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.</p> <p><a name="rl1"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#l1">Listing One</a> is a Jabber client that connects to the server, then waits for messages. If the body of the message is, say, <i>start </i>1.0, this client starts a new Ruby thread that repeatedly runs the <i>uptime</i> command and uses the number after the <i>start</i> as the threshold for sending notifications. If the one-minute load average goes above that number, notification is sent.</p> <p>The first thing this script needs to do is log into the Jabber server:</p> <blockquote> <p>session = Jabber::Session.bind_digest(jid, passwd)</p> <p>session.announce_initial_presence</p> </blockquote> <p>The first line uses the Jabber ID (<i>jid</i>) 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.</p> <p>The next line in the script sets up a block of code that runs when a message is received. The <i>Jabber::Protocol::Message</i> object (<i>msg</i>) is passed to that block: </p> <blockquote> <p>session.add_message_listener { |msg|</p> <p># Handle the message</p> <p>}</p> </blockquote> <p>The <i>Message</i> object has accessors for each of the parts of a Jabber message. To get the text of the body of the message, use the <i>msg.body</i> accessor. Here, we use the Ruby <i>split</i> function to separate the start from the threshold number:</p> <blockquote> <p>value = msg.body.split[1]</p> </blockquote> <p>We also use the <i>split</i> function to parse the output of the <i>uptime</i> command. If the load exceeds the threshold, a Jabber message is generated; see <a name="re2"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403ge2.htm">Example 2</a>. 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.</p> <p>Since the script polls the <i>uptime</i> 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 <i>stop</i> 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. </p> <h3>Jabber in Perl</h3> <p>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 <i>make</i> or most any other command-line build tool. </p> <p>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.</p> <p>The first thing <a name="rl2"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#l2">Listing Two</a> does is connect and authenticate to the Jabber server. The four lines in <a name="re3"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403ge3.htm">Example 3</a> initialize the Jabber client connection, define the Perl functions called when the <i><message></message></i>and <i><presence></presence></i> packets are received, connect to the Jabber server, and authenticate to the server using the script's username, password, and resource.</p> <p>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 <i><presence></presence></i> packet so other clients know that the script is online:</p> <blockquote> <p>$connection-&gt;PresenceSend();</p> </blockquote> <p>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:</p> <blockquote> <p>while (1) {my $res = $connection-&gt;Process();}</p> </blockquote> <p>This reads and processes Jabber packets by calling the functions specified in the <i>SetCallBacks</i> method until the script is killed. </p> <p>Our arguments to <i>SetCallBacks</i> specified that when a Jabber <i><presence></presence></i> packet is received, the <i>presenceCB</i> method is called. The script doesn't really make use of the presence information, but <a name="re4"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403ge4.htm">Example 4</a> is a function that shows how it's structured. The first argument to <i>presenceCB</i> is used by NET::Jabber for bookkeeping and not of interest. The second argument is the <i>presence</i> object. It contains the <i>from</i> field (the JID of the client sending the packet); <i>type</i> field (controls how this packet should be interpreted); and <i>show</i> and <i>status</i> fields (indicates whether the client is temporarily away, interested in chatting, and so on).</p> <p>Here, we are interested in <i><message></message></i> packets. When a message packet is received, NET::Jabber calls our <i>messageCB </i>function, which needs to read the function arguments and extract the message body as follows:</p> <blockquote> <p>my $sid = shift;</p> <p>my $msg = shift;</p> <p>my $msgtxt = $msg-&gt;GetBody();</p> </blockquote> <p>Next, the script uses the message body to build and run an Ant command using Perl's <i>system</i> command:</p> <blockquote> <p>system "ant -logfile antout.tmp $msgtxt";</p> <p>my $buildOutput = 'cat antout.tmp';</p> </blockquote> <p>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 <i>$buildOutput</i> 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:</p> <blockquote> <p>$connection-&gt;MessageSend(to=&gt;$msg-&gt; GetFrom(), </p> <p>subject=&gt;"Build of $msgtxt", </p> <p>thread=&gt;"$msg-&gt;GetThread()",</p> <p>type=&gt;$msg-&gt;GetType(),</p> <p>body=&gt;$buildOutput);</p> </blockquote> <p>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 (<a name="rl3"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#l3">Listing Three</a>), the response message is like that in <a name="rf1"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403gf1.htm">Figure 1</a>. If we send a similar "echo" message as a chat message, you see something like <a name="rf2"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403gf2.htm">Figure 2</a>. Because our script uses the <i>type</i> 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.</p> <h3>Jabber in Python</h3> <p>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.</p> <p>First, look at a Jabber XML-RPC client in <a name="rl4"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#l4">Listing Four</a>. 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 <i><iq></iq></i> (info query) packet. </p> <p>To get access to the Jabber core library functions and pack/unpack XML-RPC requests, you must import both the Jabber and xmlrpclib packages:</p> <blockquote> <p>import xmlrpclib</p> <p>import jabber</p> </blockquote> <p>Next, use the <i>dumps()</i> 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:</p> <blockquote> <p>request = xmlrpclib.dumps((text,), method name=Method)</p> </blockquote> <p>Next, log users into the Jabber switch using the client <i>connect()</i> method and log in using the <i>auth()</i> method. Assuming you created a user called <i>peer-b</i> to act as the responding XML-RPC server (<a name="rl5"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#l5">Listing Five</a>) and assigned a resource <i>rpc</i> to the username, you next create an info query packet addressed to <i>peer-b@localhost/rpc</i>, set the query type to a remote procedure call, and set the payload to the XML-RPC request just created:</p> <blockquote> <p>iq = jabber.Iq(to=Endpoint, type='set')</p> <p>iq.setQuery('jabber:iq:rpc')</p> <p>iq.setQueryPayload(request)</p> </blockquote> <p>Finally, you make the remote call and wait for the response.</p> <blockquote> <p>result = con.SendAndWaitForResponse(iq)</p> </blockquote> <p>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 <i>loads()</i>. The <i>parms</i> structure is an array, but normally only the zero-th slot is filled.</p> <blockquote> <p>if result.getType() == 'result':</p> <p>response = str(result.getQueryPayload())</p> <p>parms, func = xmlrpclib.loads(response)</p> <p>print parms[0]</p> </blockquote> <p>Having set up the client, in <a name="rl5"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#l5">Listing Five</a> we define an XML-RPC service that is also a Jabber client. The remote method (<i>Rot13()</i>) 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 <i><iq></iq></i> messages because the XML-RPC request is delivered that way.</p> <p>We extract the query's namespace and get the query payload from the info query passed in as the second argument; see <a name="re5"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403ge5.htm">Example 5</a>(a). Uncomment the print lines if you want to see what the payload looks like as encoded XML, <a name="re5"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403ge5.htm">Example 5</a>(b), and then as a Python structure:</p> <blockquote> <p>((' There was a young lady of Nantes',), u'Rot13')</p> </blockquote> <p>We use the Python structure directly later on to invoke the <i>Rot13() </i>method.</p> <p>Since the Jabber protocol defines several namespaces for IQ packets, we check the query type to make sure it's appropriate (<i>jabber.NS_RPC</i>) and start constructing a reply. We get the sender's Jabber address using <i>iq.getFrom()</i> and set the rest of the fields in the response; see <a name="re6"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403ge6.htm">Example 6</a>(a). Finally, we unpack the XML and use the string in an <i>eval()</i> to invoke the <i>Rot13()</i> method; see <a name="re6"></a><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403ge6.htm">Example 6</a>(b).</p> <p>Once again, note that using the <i>dumps() </i>method to convert data to an XML-RPC <i><param></i> 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:</p> <blockquote> <p><params></params></p> <p><param></p> <p><value></value></p> <p><string>Gurer jnf n lbhat ynql bs Anagrf</string></p> <p></p> <p></p> <p></p> </blockquote> <h3>Conclusion</h3> <p>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. </p> <p></p> <p><b>DDJ</b></p> <h4><a name="l1">Listing One</a></h4> <pre>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 &gt;= 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 </pre> <p><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#rl1">Back to Article</a> </p> <h4><a name="l2">Listing Two</a></h4> <pre>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-&gt;GetFrom("jid")-&gt;GetUserID(); my $msgtxt = $msg-&gt;GetBody(); # run ant my $buildOutput = `ant -logfile antout.tmp $msgtxt`; $buildOutput = `cat antout.tmp`; $connection-&gt;MessageSend(to=&gt;$msg-&gt;GetFrom(), subject=&gt;"Build of $msgtxt", thread=&gt;"$msg-&gt;GetThread()",type=&gt;$msg-&gt;GetType(), body=&gt;$buildOutput); `rm antout.tmp`; } sub presenceCB { my $sid = shift; my $presence = shift; my $from = $presence-&gt;GetFrom(); my $type = $presence-&gt;GetType(); my $show = $presence-&gt;GetShow(); my $status = $presence-&gt;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-&gt;SetCallBacks(message=&gt;/&amp;messageCB, presence=&gt;/&amp;presenceCB); my $status = $connection-&gt;Connect(hostname=&gt;$server); my @result = $connection-&gt;AuthSend(username=&gt;$uname, password=&gt;$pass, resource=&gt;$resource); if ($result[0] ne "ok") { print "ERROR: Authorization failed: $result[0] - $result[1]/n"; exit(0); } $connection-&gt;PresenceSend(); while (1) {my $res = $connection-&gt;Process();} } sub sendMsg { my $otherJid = shift; my $msgText = shift; $connection-&gt;MessageSend(to=&gt;$otherJid, subject=&gt;"chat_demo_subject", thread=&gt;"chat_demo_thread",type=&gt;"chat", body=&gt;$msgText); } connectToJabber(); </pre> <p><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#rl2">Back to Article</a> </p> <h4><a name="l3">Listing Three</a></h4> <pre><project name="ddj" default="echo"><target name="echo" depends=""><echo> This ant script doesn't do too much. </echo></target></project></pre> <p><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#rl3">Back to Article</a> </p> <h4><a name="l4">Listing Four</a></h4> <pre>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() </pre> <p><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#rl4">Back to Article</a> </p> <h4><a name="l5">Listing Five</a></h4> <pre>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 &amp; 32) byte = (byte &amp; (~cap)) if (byte &gt;= ord('A')) and (byte <p><a href="http://www.ddj.com/documents/s=9045/ddj0403g/0403g.htm#rl5">Back to Article</a> </p> </pre></ddjadvertisement>
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics