Με το Twisted framework μπορούμε πολύ εύκολα να κατασκευάσουμε εφαρμογές web server. Προσέξτε όμως ότι η χρήση του Twisted δεν είναι η περισσότερο ενδεδειγμένη όταν:
Αν και τα παραπάνω μπορούν να υλοποιηθούν μέσω του Twisted, είναι προτιμότερο να χρησιμοποιήσουμε π.χ. στην πρώτη περίπτωση έναν κλασσικό web server όπως ο Apache, ενώ στη δεύτερη να χρησιμοποιήσουμε κάποιο templating engine σε PHP, Ruby κ.ο.κ.
Ένας web server στο Twisted εξυπηρετεί καλύτερα εξειδικευμένες εφαρμογές που ανταλλάσσουν δεδομένα μέσω του πρωτοκόλλου HTTP.
Στο παράδειγμα που ακολουθεί, ο web server που κατασκευάζεται επιστρέφει μια δοκιμαστική ιστοσελίδα στο path “/page”:
from twisted.application import internet,service
from twisted.web import server,resource
# definitions of resources
class Root(resource.Resource): # the root resource class
isLeaf = False
class Page(resource.Resource): # the sample page resource class
isLeaf = True
def render_GET(self,request):
""" called to handle a GET request.
'request' is an object of type twisted.web.http.Request.
The returned string will be sent as reply to client. """
return '<html><head><title>A Sample Page</title></head><body>Sample content</body><html>'
# end of reply
# main program part
# build the resources tree
root = Root() # the "/" resource
root.putChild("page",Page()) # the "/data" resource
# here we connect a "Site" (subclass of "twistewd.web.http.HTTPFactory") to a "resource"
site = server.Site(root)
# create an object that represents our application
# object must be called exactly 'application' to use the twistd launcher!
application = service.Application("simple-web-server")
# create the echo service that our application will offer
web_echo_service = internet.TCPServer(50007,site)
# add the web echo service to our application object
web_echo_service.setServiceParent(application)
Μπορούμε να δοκιμάσουμε τον server με έναν απλό web client σε Python (θεωρώντας ότι ο client και ο server τρέχουν στον ίδιο υπολογιστή):
import urllib
page = urllib.urlopen("http://localhost:50007/page")
text = page.read()
page.close()
print text
Μπορούμε επίσης να δούμε τα επιστρεφόμενα δεδομένα σε έναν web browser, πλοηγούμενοι στη διεύθυνση URL http://localhost:50007/page ή να χρησιμοποιήσουμε τα command-line εργαλεία wget ή curl:
wget -q -O - http://localhost:50007/page
curl http://localhost:50007/page
Συνήθως χρειάζεται να εξυπηρετούμε και στατικό περιεχόμενο μέσω του web server μας. Αυτό στο Twisted γίνεται ως εξής:
from twisted.application import internet,service
from twisted.web import server,resource
from twisted.web.static import File
# definitions of resources
class Root(resource.Resource): # the root resource class
isLeaf = False
class Page(resource.Resource): # the sample page resource class
isLeaf = True
def render_GET(self,request):
""" called to handle a GET request.
'request' is an object of type twisted.web.http.Request.
The returned string will be sent as reply to client. """
return '<html><head><title>A Sample Page</title></head><body>Sample content</body><html>'
# end of reply
# main program part
# build the resources tree
root = Root() # the "/" resource
root.putChild("page",Page()) # the "/data" resource
root.putChild("content",File("staticfolder")) # the "/content" static files resource
# here we connect a "Site" (subclass of "twistewd.web.http.HTTPFactory") to a "resource"
site = server.Site(root)
# create an object that represents our application
# object must be called exactly 'application' to use the twistd launcher!
application = service.Application("simple-web-server")
# create the echo service that our application will offer
web_echo_service = internet.TCPServer(50007,site)
# add the web echo service to our application object
web_echo_service.setServiceParent(application)
Στο προηγούμενο παράδειγμα υποθέτουμε ότι στον φάκελο εκτέλεσης του server υπάρχει υποφάκελος staticfolder, ο οποίος περιέχει ιστοσελίδες, εικόνες ή αλλο στατικό περιεχόμενο. Αυτά θα είναι διαθέσιμα στο path /content/όνομα-αρχείου.
Προφανώς, η χρησιμότητα ενός προγράμματος web server είναι πολύ μικρή αν δεν μπορεί να δεχτεί παραμέτρους κατά την αίτηση. Πριν δούμε όμως πώς χειριζόμαστε τέτοιες αιτήσεις, ας δούμε πώς στέλνονται οι παράμετροι από την πλευρά του client.
Ένα παράδειγμα σε Python:
import urllib
# params sent to server
params = { 'a':23,'b':'testαβγδ','c':'+&=' }
# create appropriate params string
paramstr = urllib.urlencode(params)
page = urllib.urlopen("http://localhost:50007/page?"+paramstr)
text = page.read()
page.close()
print text
Μπορούμε να επιτύχουμε το ίδιο μέσω του κατάλληλου URL σε έναν browser, εφόσον οι παράμετροι κωδικοποιούνται στο URL. Προσέξτε ότι στην περίπτωση αυτή θα πρέπει εσείς να φροντίσετε για το σωστό url-encoding.
Το αντίστοιχο παράδειγμα σε Python:
import urllib
# params sent to server
params = { 'a':23,'b':'testαβγδ','c':'+&=' }
# create appropriate params string
paramstr = urllib.urlencode(params)
page = urllib.urlopen("http://localhost:50007/page",paramstr)
text = page.read()
page.close()
print text
Μπορούμε να στείλουμε POST παραμέτρους με το wget ή το curl. Προσέξτε όμως ότι το wget δεν φροντίζει το url-encoding:
wget -q -O - --post-data "a=1&b=3" http://localhost:50007/page
curl --data "a=1&b=3" http://localhost:50007/page
curl --data-urlencode 'a=&' --data b=3 http://localhost:50007/page
Στον server η αντιμετώπιση είναι η ίδια, τόσο στην περίπτωση του GET όσο στην περίπτωση του POST. Απλά δημιουργούμε τις αντίστοιχες μεθόδους render_GET και render_POST:
def render_GET(self,request):
""" called to handle a GET request. """
# specify a non-html reply content type
request.setHeader('Content-type','text/plain')
# request.args is a dict of the form { 'key1':['value1'],'key2':['value2','value2a']..}
retstr = 'You have sent me these GET params:\n'
for k,vl in request.args.items():
retstr += k+':\t\t'
for v in vl:
retstr += v+' '
retstr += '\n'
return retstr
def render_POST(self,request):
""" called to handle a POST request. """
# specify a non-html reply content type
request.setHeader('Content-type','text/plain')
# request.args is a dict of the form { 'key1':['value1'],'key2':['value2','value2a']..}
retstr = 'You have sent me these POST params:\n'
for k,vl in request.args.items():
retstr += k+':\t\t'
for v in vl:
retstr += v+' '
retstr += '\n'
return retstr
Παρατηρήστε ότι και στις δύο περιπτώσεις οι παράμετροι που στάλθηκαν κατά την αίτηση περιλαμβάνονται σε ένα dict-like αντικείμενο της αίτησης, το request.args. Αυτό έχει τη μορφή:
{ 'key1':['value1'],'key2':['value2','value2a']..}
επιτρέποντας την εμφάνιση του ίδιου key στην αίτηση περισσότερο από μία φορά.
Note
Στο προηγούμενο παράδειγμα αλλάζουμε τον τύπο των επιστρεφόμενων δεδομένων σε text/plain για να μπορούμε να δούμε το αποτέλεσμα και μέσα από έναν web browser, χωρίς να νοιαζόμαστε για τη μορφοποίηση σε HTML των δεδομένων (όταν δεν ορίζεται ρητά ο τύπος των επιστρεφόμενων δεδομένων, το Twisted δηλώνει τη μορφή HTML). Η αλλαγή επιστρεφόμενου τύπου γίνεται με το:
# specify a non-html reply content type
request.setHeader('Content-type','text/plain')
Συχνά ο application web server επιστρέφει διαφορετικό τύπο δεδομένων, ανάλογα με την αίτηση του client. Ο τελευταίος μπορεί να ζητήσει έναν ειδικό τύπο για τα δεδομένα που θα επιστραφούν -εφόσον το υποστηρίζει ο server (content negotiation). Για την αίτηση αυτή υπάρχει η εξής επικεφαλίδα στο HTTP (π.χ. όταν ζητείται επιστροφή περιεχομένου XML):
Accept:text/xml
Ας δούμε αρχικά πώς μπορούμε να στείλουμε αίτηση για εναλλακτικό περιεχόμενο από την πλευρά του client, χρησιμοποιώντας την πιο εξελιγμένη βιβλιοθήκη urllib2:
import urllib2
req = urllib2.Request("http://localhost:50007/page")
req.add_header('Accept','text/plain')
page = urllib2.urlopen(req)
text = page.read()
page.close()
print text
Επίσης μπορούμε να πετύχουμε το ίδιο αποτέλεσμα ως εξής:
wget -q -O - --header="Accept:text/plain" http://localhost:50007/page
curl -H "Accept:text/plain" http://localhost:50007/page
Στην πλευρά του server, χρησιμοποιούμε το αντικείμενο request.received_headers για να εξετάσουμε αν υπάρχει αίτηση εναλλακτικού περιεχομένου. Παρατηρήστε ότι πρέπει να απαντάμε σε κάθε περίπτωση, ακόμα κι όταν δεν παρέχουμε τη μορφή που ζητείται ή όταν δεν ζητείται κάποια μορφή περιεχομένου!
def render_GET(self,request):
""" called to handle a GET request """
# request.received_headers is a dict with headers of request.
if 'accept' in request.received_headers: # NOTE: header name gets lowercased!
reqtype = request.received_headers['accept']
if reqtype=='text/plain':
return 'A Sample Page.\nSample content\n'
else: # if not supported, return html
return '<html><head><title>A Sample Page</title></head><body>Sample content</body><html>'
else: # if no content type requested, return html
return '<html><head><title>A Sample Page</title></head><body>Sample content</body><html>'
Note
Σε νεώτερες εκδόσεις του Twisted το request.received_headers αντικαθίσταται από το αντικείμενο request.requestHeaders, το οποίο διαθέτει μεθόδους όπως getRawHeaders('Accept') και hasHeader('Accept'). Πάντως, διατηρείται η συμβατότητα και με τον κώδικα που χρησιμοποιεί το request.received_headers.