AS for the functions, there aren't t00 many required for this example. The first function, wait for messages, is supplied with a callback function and an optional cursor. The callback function is the function that we should call when new data is available, whether it'S available now or in a couple Of minutes. The cursor iS used tO let the server know the last tweet received by the client. Using this cursor, we can search through the cache Of tweets and serve anything that's arrived since the last time the client made a request. If there's no cursor, we add the callback to the list of waiters and call the function as soon as data becomes available. The other function defined here is new tweets, which is called immediately when new tweets arrive from Twitter. This is fairly simple; it 100PS through the waiters list, calling each callback function with the messages it receives as a parameter. ThiS iS realtime, so that's the first thing it does. The nextthing it does is update the cache. This will allOW any client that is currently reconnecting tO get the tweets that it missed while disconnected. The next thing [ 0 do is handle the actual requests from the web browsers. We'II be adding one new class and modifying the array that we pass when creating the applica- tion Object. lnside your てリ nne て . py file, insert this COde right after the Tweet class: class UpdateHand1er(tornado. web. RequestHand1er, Tweet) : @tornado.web.asynchronous def post(self) : self. get argument("cursor", None) cursor self. wait for messages(self. async callback(self. on new tweets), cursor=cursor) def on new tweets(self, tweets) : if not self. request. connection. stream. closed() : self. finish(dict(tweets=tweets)) This class is much simpler. This is the code that is actually called when we get an Ajax request from the browser. The first thing you'll notice is the @tornado.web.asynchro nous decorator. Adding this bit 0f code tells Tornad0 that we want the HTTP connec- tion tO remain open even after returning from our request handler method. lnside that handler method, we call our wait for messages function supplied with on new tweets, our callback function. Decorators, while outside the scope Of this bOOk, are a really nice feature of Python. They enable you [ 0 "decorate" functions and classes to give 4 、 them extra COde or [ 0 make them act differently. You're actually able tO inject new COde or modify the existing functions themselves. The method on new tweets is what finally gets called when we have new data. There isn't much tO dO in this method Other than printing out the tweets [ 0 the client. After checking [ 0 ensure that the client is still connected, we need [ 0 "finish" the connection. Because 、 in the middle Of an asynchronous request, Tornado relies on us tO close 86 ー Chapter 5 : Taming the Firehose with Tornad0
The first thing this method does is create a copy of the listeners list called tmp listen ers and then clear the original list. We clear out the list because every listener whose callback we actually run no longer needs to be in the listeners list. If the callback is called, we'll end up closing the HTTP connection to the client, and they'll have to reconnect and add themselves tO the listeners list again. SO instead Of looping through listeners, we clear it out and IOOP through tmp listeners instead. If no user id is supplied [ 0 this method, we simply run the callback on every single listener and never add any back [ 0 the original listeners list. However, if a user id has been supplied, we need tO check each listener and send it only tO the correct user. AS we loop through the list and find users that are not matches, we add them back to the main listeners array and continue on with the IOOP. Each time we actually run the callback, we supply the message as the parameter, catching any errors and logging them. Several of our methods thus far have dealt with callbacks and responding [ 0 them, but this script hasn't actually added the ability to requestthem. So that's the next thing to add. AIthough requesting callbacks isn't needed to actually log in, it is needed to find out when Other users have logged in. SO let's add that before we move on tO the Java- Script portion. ln your 取ト夜・ . 段 file, add the following code, but be sure to add it after the BaseHand1er definition. class UpdateHand1er(BaseHand1er) : @tornado.web.asynchronous def post(self) : self. get argument(' user id') user id # add a listener, specifying the handle updates callback self. chat. add listener(self. async callback(self. handle updates), user id=user id) # when we get a callback, send it to the client def handle updates(self, update) : if not self. request. connection. stream. closed() : self. finish(update) According t0 the URL mapping that we specified, this UpdateHand1er class handles all the requests that come into the /updates URL. This class has two main jobs, which are handled by the two methods defined here. When a web client requests updates from the server, whether the updates are login notifications or chat messages, it does SO by making a long polling request [ 0 the /updates URL. That request is handled by the post method. The only work t0 be done is t0 add the client as a listener 0f the chat object. The callto add listener tells the chat objectto call the handle updates method when new data arrives for the supplied user id. Because this iS an asynchronous method, that callback method is wrapped in Tornado's a sync callback functionality. Once the chat class has data available for a specific client, itwill call that client's callback function. That function is handle updates, which has one j0b: send the data [ 0 the client. This method handles that by checking to ensure the client is still connected and Logging ⅲー 111
$info = curl getinfo($ch); ( u て 1 close($ch); if ($info[ 'http code' ] return true; return false; = 204 ) { This really is the bulk of the class that we'll be using, and there really isn'tthat much to it. This defines a class called PubSubHubbub and requires the hub URL when con- structing it. Other than that, it has a simple function called post to POST the content to the hub URL and a param function to add parameters [ 0 the POST request. As it stands, this class could be used by PHP scripts to handle the work of subscribing to feeds and publishing to hubs. However, it'll be a 10t easier for the end users of this class if we add some convenience functions [ 0 speed the process along. TO subscribe t0 a feed, let's append a subscribe function [ 0 the end of the PubSubHubbub class in the p 励 5 励わ励わ励 . php file. public function subscribe($topic url, $callback url, $token=false, $lease=false) { if ( !isset($topic url)) return; / / nothing to do if ( ! preg_match(" lAhttps?://は " , $topic_url)) throw new Exception(' lnvalid URL: . $topic url); $this->param(' hub. topic' , $topic_url); $this->param(' hub. mode' ' subscribe ・ ) ; $this->param(' hub. callback' , $callback url); $this->param(' hub. verify' 'sync'); if($token) $this->param("hub. verify_token", $token); if($lease) $this- 〉 param("hub. lease seconds", $lease); return $this->_post(); This function accepts enough parameters to handle all of the options needed when subscribing tO a hub. The tWO important parameters in this function are the $topic url and the $callback url. The topic URL specifies the actual Atom feed URL that we want to subscribe to. The callback URL is the URL that the hub will ping when it has new content. ln the subscribe function, we check the existence and validity 0f the $topic_url and add it tO the parameter list. We alSO want [ 0 tell the hub which action we're doing; we do this with the hub. mode parameter, which we've set t0 subscribe. Depending on how your system works outside 0f this class, you may want t0 change the hub. verify method or change the token and lease parameters, but these default values should be fine for many implementations. PubSubHubbub ー引
The JavaScript Parts ・々 ' ve created our classes in Python and coded our templates in HTML, so now it's time for a bit ofJavaScript. The template file that we created included a JavaScript file called ール〃れ that was supposed tO be served from the static directory. Let's create that file now. Create a new directory called static and create a file called ル″れ inside it. Set up the basics of that file with the following code: google. load("jquery' , google ・ set0nLoadCa11back( function() { $(document) ・ ready(function() { T. init(); T. p 。 11 ( ) ; }); var T = { cursor: false, types: [ ・ hashtags ・ , ー retweets ー T. init = function() { t 、 ・ links ・ ] $(' input[type=checkbox] つ . c1ick(). click); T. click = function(ev) { if(ev. target. id = ・ a11 ' ) { for(i in T. types) $('#' + T. types[i]) . attr( ・ checked ・ , $('#all ・ ). attr( ・ checked')); else $('#all ・ ). attr( ・ checked ・ , false); = function ( ) { T. P011 var args if(). cursor) args. cursor = T. cursor; $ ・ ajax({ "/updates" url: "POST" type : dataType: json" data : $. param(args), success: T. new tweets 96 ー Chapter 5 : Taming the Firehose with Tornad0 an object called T. alization functions. Once that callback arrives, we run the init and P011 methods on the jQuery library for us and wait for a callback from GoogIe before we run any initi- This JavaScript sets up our application. Once again, we ask Google's Ajax APIs to load
FinaIIy, that function calls PubSubHubbub : : post(), which assembles the POST request fror れ the parameters and contacts the hub server.\'Vhile most Of the response from the hub is ignored, post returns true or false tO signify whether the post has been suc- cessful. However, when you re using the sync mode for hub. verify, the callback URL will actually get pinged before this functions returns. realtime experience. immediately and provide the users with a much smoother and more 4 、 best [ 0 run this function from queue, which will enable you tO return you should avoid forcing users tO wait for this function [ 0 return. lt's Although most hubs should return from the subscribe request quickly, 32 ー Chapter 2 : ReaItime Syndication die($_REQUEST[ ・ hub challenge' ]); / / echo the hub challenge back to the hub. ・ subscribe ・ ) { else if($_REQUEST[ ・ hub mode'] die("\n\nSomething went wrong when subscribing ・ " ) ; else { . we should be a11 subscribed. Try sharing something. " ) ; . $ SERVER[ ・ PHP SELF']; die("Done. $r = $p->subscribe($topic_url, $callback); / / POST to the server and request a subscription . $_SERVER[ ・ HTTP HOST' ] $callback = ' http://・ / / This is the URL the hub will ping (this file). $topic_url = "http://www ・ google.com/reader/public/. / / enter your "Shared ltems" Atom URL = new PubSubHubBub($hub); / / initialize the class $hub = "http://pubsubhubbub.appspot.com/" ・ / / define the hub if($ REQUEST[ 'request subscription' ] ) { include once("pubsubhubbub. php"); く ?php the following code: and can handle the whole subscription process. Open a file called ⅲ x. 〃わ〃 and add that actually uses it. we'll start with a simple PHP script that can run on our web server NOW that we have enough Of the class built tO subscribe tO a hub, let's write some code
く body on10ad="ipGeo. initialize('map')" onunload="GUnload()"> く div id="header"> く h1>iPandemic く /hl> く ul id="options"> く li 〉く a id="button-cough" href="javascript://" onc1ick="ipGeo ・ cough();"> [ COUGH ! ] く / a 〉く / li 〉 く / ul > く /diV> ThiS new option simply gives the user an interface element tO CliCk on and initiate a cough action. Let's keep going and add the client cough functionality. ln the appengine/ static/geo.js file, add the following method: = function() { ipGeo. cough ipGeo. getLocation(function(position) { / / pull out the lat/long coords var lat = position. coords. latitude; position. coords. longitude; var 10n / / recenter the map ipGeo. map. setCenter(new GLatLng(lat, 10n ) , 17 ) ; / / post the actual cough request $. post(' な ough/' { ・ lat ・ : lat, ・ 10n ' : 10n } , ipGeo. receivedGermData, json ・ ); } , function() {}); This method keeps all of its functionality wrapped up inside a successful callback from the ipGeo ・ get Location method. When that method returns successfully, we pull out the latitude and longitude coordinates, recenter the map, and post the results tO the /cough/ URL. Once we've made the request t0 that URL, our cough should be added to the datastore, but we're still notshowing anything on the map. TO handle that, we request a callback when jQuery's post method completes. Let's add that callback method to 叩〃ビ〃 g ⅲ c / ビ 0 now: ipGeo. receivedGermData = function(data) { / / pull out the data sent from the cough callback data. message; var message = data. germs; var germs / / if there is a message, display it t0 the user if(message) { alert(message) ; 246 ー Chapter 10 : Putting lt 則 Together
Tornad0 applications are built using a very basic Model-View-Controller (MVC) framework. Each application provides a simple mapping 0f URLs or URL patterns to specific classes. These classes can define methods t0 handle both GET and POST re- quests [ 0 the URL. As requests hitthe server, the web framework 100kS atthe requested URL and determines which class should respond [ 0 the request. lt then calls either the get ( ) or post ( ) function 0f the class, depending on the method of the HTTP request. See Figure 5-1 for an example of how requests are handled in Tornado. POST request t0 /contract-info/save ↓ Class to handle /contact-info/profile CIass t0 handle /contact-i nfo/delete 0a55 to handle /contact-info/save ロ回ロ回ロロ Figure 5-7. HTTP 尾 q レ e 立 routing ⅲ TO 川 d0 Tornado also has built-in support for long polling, or as it is called in Tornado parlance, "hanging requests. Just like in Bayeux, when the browser makes a request tO the server, the server waits tO respond until data is actually available. However, when writing a Tornado application, the developer makes a request tO an asynchronous function and provides the framework with a callback function. When the data is ready, the server executes the developer's callback function which finishes the final request. Figure 5-2 shows the callback process ⅲ Tornad0. Tornado ー 81
The X and y coordinates are set dynamically [ 0 ensure that this window doesn't com- pletely cover any 0f the other windows that have already been displayed. Next, we set the title Of the Panel window and create the HTML elements within it. The first element created is the div that will actually be used as the content area Of the chat. When a user sends a message, it will be rendered in the content div. Since the chat Object will add the content tO this div from Other functions, we set the id attribute Of the div [ 0 a value that iS unique tO the user it represents. The textarea iS then rendered in the same fashion. This textarea will be the input field for sending new messages in the chat window. NOW, we've built the contents Of the chat window, we want tO give the user some feedback about what just happened. So, in the footer of the newly created window, the next line shOWS that the user is "Online. '' Rather than keep that as a permanent status message for the user, we set up a function tO clear the message after half a second. lt may seem like a superfluous action, but it's these types Of small details that create a well-rounded experience for the user. At this point, we're done building the internal representation Of the user, SO we add it [ 0 the chat. users array. This object is used in 0ther methods [ 0 help keep track 0f users as they perform different actions. After that, we simply need tO render the chat window [ 0 the screen. The final bit of code needed to tie the whole login process together is the ability to poll the server tO receive notifications. AS shown earlier, the login_success method looped through all of the connected users and added them to the screen. Once it finished displaying the users, ⅱ ran a method called chat. P011 t0 start polling the server for new updates. lt's a simple little method t0 add to the 訪 file: chat. P011 = function() { { success: chat. handle updates } ; var callback YAHOO. util. Connect. asyncRequest(' POST ・ '/updates ' callback, user id=' + chat. user id); After seeing how the Ajax request worked in the login method, this code should 100k pretty familiar. All this does is make a request t0 the /updates URL and specifya callback for when it successfully returns. ln the Python code, the class UpdateHandler responds to the /updates URL, it's designed [ 0 be general enough [ 0 handle responses for all types 0f events. SO we've specified a general handle updates as the callback method for those requests. we'll be expanding this method a bit, butto handle login requests, it's fairly straightforward: chat. handle updates = function(ev) { / / parse the JSON var message = YAHOO. lang. JSON. parse(ev. responseText) ; Logging ⅲー 115
this method will grow along with the functionality of this application, atthe moment it has only one call. The Home. init method makes a call t0 Home. appendJS [ 0 a Twitter search ( : / / 覊 a 尾 / ル襯夜託 0 〃 URL. This URL will return a list 0f the currently trending topics across Twitter, which will be used in the first widget t0 get a feed of tweets about those topics. ln addition, the standard Twitter search URL, we add a callback parameter [ 0 the URL. When this request returns from the server, the data will be passed [ 0 this callback method. The Home. appendJS method appends a new script elementto the DOM. lt takes in a URL parameter and adds a random number t0 the end. Simply adding a random num- ber [ 0 the end ofthe URL won't have any effect on mostJavaScript includes, but it does ensure that every request has a different URL. This will ensure that the browser doesn't automatically return cached content for the request. This URL is then used as the src for the script. This method doesn't do anything with the output ofthe included script; it's assumed that each included file can execute any COde that iS needed. ln this case, all of the scripts we've included are JSONP requests that execute callback functions once the content is fully loaded. A word about JSONP The widgets that we are building will not use any external JavaScript libraries or even Ajax-style XMLHttpRequest (XHR) calls. TO load data from the external APIs, this example uses JSONP or JSON with padding to make our requests. JSONP is a super simple method 0f fetching JSON data from remote servers. lnitially introduced by Bob lppolito ( 印 : 〃わ 0 わ . 0 〃川 . g / の℃わⅳぉ / 2005 月 2 / 05 / 尾川 0 0 〃 0 〃 , it has been implemented by manyAPIs when makingJSON-based requests. UsingJSONP allows you to essentially make API requests t0 remote servers by simply adding new JavaScript includes [ 0 the DOM, which can be done programmatically. For example, if you were making an API request for a list 0f videos from Vimeo ( 印 : / / ⅵ川 eo. co 川 ) ⅲ JSON format, the URL for the JSON format would be http : / / vimeo.com/api/v2/ted/videos.json and the output would 100k something like this: "url" : "http:\/\/vimeo.(。mV7466620"}, "title" : "Speaking", "url" : "http:\/\/vimeo.(。mV6955666"} : " 7466620 " , "title" : "Party Time" : " 6955666 " That gives you the data you requested, but there is no simple way [ 0 use the data if you are requesting the URL via a く script> tag. WithJSONP, most APls allowyou t0 specify a callback URL along with the API request, so that when you include the URL, you're able [ 0 specify a JavaScript callback function and the whole response will be passed as a parameter tO that function. OnVimeo, that same data could be accessed by requesting http ://vimeo.com/api/v2/ted/videos.json?callback=parse\/ideos, which would give you a response similar tO the following: 42 ー Chapter3: The Dynamic Homepage (Widgets in Pseudorealtime)
contain the map Object, SO that it can be easily referenced in the different member functlons. The first of those member functions is getLocation, which is a wrapper call for the browsers, own navigator. geolocation. getCurrentPosition meth0d. This method is available on most recent 1 れ Obile browsers and iS even available desktop browsers with increasing frequency. Firefox on the desktop already includes this functionality natively within the browser. Rather than checking tO see whether each browser sup- ports this function every time we want tO call it, we simply call this ipGeo. get Loca tion method, which wraps up that logic and makes the proper success and failure callbacks depending on what happens. Because locating the exact GPS coordinates 0f the browser can take some time, navigator. geolocation. getCurrentPosition runs asynchronously and executes the proper callback when data is available. Aside from the success and fail callback functions, we also pass along a small object with a single key/value pair for maximumAge. Making a getCurrentLocation request is expensive, bOth in terms Of the amount Of time it takes tO return and the drain on batteries it takes tO use a GPS sensor in a mobile device. Setting the maximumAge param- eter tO 3()0 OOO milliseconds allOWS us [ 0 cache the results for five minutes. This amount Of cache time will ensure that any geolocation-based action that takes place in a stand- ard session will require only one actual external request. This alSO stops the result from being cached indefinitely, which is actually the default behavior on some devices. The next method is ipGeo. initialize, which takes in a parameter that is just the ID Of the DIV we're going to use to display the map. This method then checks t0 see whether Google thinks this browser is compatible with their maps API and creates the map object. There are a number of different options ( 印 : / ん 0 . goog . し 0 川ん〃な / a 内 / doc 川 e 〃 0 〃 / 尾を〃記 . 川 / # GMa 〃〇〃行 0 ) that can be used when working with these maps, but we're just going t0 use the default options and tellthe map t0 use the default user interface. Next, we simply call the get Location methOd and, upon success, we center the map on the coordinates it returned and add a marker at the current location. NOW that we've built S01 れ e basic JavaScript functionality intO this application, we need to add it t0 the HTML views. 从々 don't need t0 add this t0 the logged-out page,Just the page where the user has already been authenticated. Open up 川〃 s / 〃ⅲ . 川 / and make the following adjustments: く tit1e>iPandemic く /title> - f0 て jQuery - く script type="text/javascript" src="http://www ・ google ・ com/jsapi"> く /script> - for the maps API - く script src="http://maps ・ google ・ com/maps?file=api&v=2&sensor=true&key= YOUR-GOOGLE-MAPS-API-KEY" type="text/javascript"> く /script> 242 ー ChapterIO: Putting lt 則 Together