In this recipe we are going to take a look at the very basics of implementing a feature that allows us to submit scores to a server that processes and stores the data we are sending. Additionally, we will be able to view the list of submitted scores using a web browser.
The server side of this project will be implemented using the Twisted framework (http://twistedmatrix.com). The libraries contained in Twisted make it very easy to implement servers and clients for all kinds of common and custom network protocols. For our purpose, we are going to implement a little custom web server that will accept POST requests for submitting data and will serve the static scoreboard page.
It is important to note that there are several other Python frameworks like Twisted available. Tornado (http://www.tornadoweb.org) and Diesel (http://dieselweb.org) are just two examples of network programming frameworks similar to Twisted. All of them have their upsides and downsides, but in the end Twisted was chosen here out of pure preference as well as for its ease of use. Not to mention that it is implemented in Python, which makes it a good fit among all the other Python code in this recipe and this book.
Before we can begin working on the following steps, we need to install the Twisted framework first using a collection of Python scripts called "setuptools". This is a set of command line tools that handle installing and managing additional third-party libraries from the Python Package Index hosted on http://pypi.python.org/pypi. This is a great source for Python programming libraries and frameworks, definitely worth some time to browse and explore!
To get set for the following tasks, first follow these steps:
setuptools-0.6c11.win32-py2.6.exe
. The exact filename and version may vary slightly because of newer releases but it is important to watch for the string win32-py2.6
in the filename to match the version of Python used in Panda3D. C:Panda3D-1.7.0pythonScripts
to the system search path. easy_install Twisted
. This will download and set up all components needed for using the Twisted framework.Now that the dependencies are installed and ready, it is time to finish the preparation steps by creating two projects. For the first one, follow Setting up the game structure found in Chapter 1 and name the project PostScore. This will be our client application.
When creating the second project, follow the same recipe again, but only up to step 3, naming the project ScoreServer. This is where we will implement the server side of this sample.
This recipe consists of the following tasks:
main.py
file that's part of the ScoreServer project and replace its content with the following code:from twisted.web.server import Site from twisted.web.resource import Resource from twisted.internet import reactor import sqlite3 class ScorePage(Resource): def __init__(self): Resource.__init__(self) self.db = sqlite3.connect("scores.db") cursor = self.db.cursor() args = ("scores",) cursor.execute("select name from sqlite_master where name=?", args) if len(cursor.fetchall()) == 0: cursor.execute("create table scores (player text, score integer)") self.db.commit() cursor.close()
def render_POST(self, request): cursor = self.db.cursor() args = (request.args["player"][0],) cursor.execute("select * from scores where player=?", args) if len(cursor.fetchall()) > 0: args = (request.args["score"][0], request.args["player"][0]) cursor.execute("update scores set score=? where player=?", args) else: args = (request.args["player"][0], request.args["score"][0]) cursor.execute("insert into scores values (?,?)", args) self.db.commit() cursor.close() return "OK"
def render_GET(self, request): networking, Panda3Dhigh scores, sending to servercursor = self.db.cursor() cursor.execute("select * from scores order by score desc") data = cursor.fetchall() cursor.close() result = str(" ".join(["%s %s" % (p, s) for p, s in data])) request.setHeader("Content-Type", "text/plain; charset=utf-8") return result
if __name__ == "__main__": root = Resource() root.putChild("score", ScorePage()) factory = Site(root) reactor.listenTCP(80, factory) reactor.run()
Application.py
and fill in the code below:from direct.showbase.ShowBase import ShowBase from panda3d.core import * class Application(ShowBase): def __init__(self): ShowBase.__init__(self) self.http = HTTPClient.getGlobalPtr() self.channel = self.http.makeChannel(False) self.channel.beginPostForm(DocumentSpec("http://localhost/score"), "player=Foo&score=1337") self.ram = Ramfile() self.channel.downloadToRam(self.ram) taskMgr.add(self.updateChannel, "updateChannel") def updateChannel(self, task): if self.channel.run(): return task.cont elif self.channel.isDownloadComplete(): print self.ram.getData() return task.done else: print "Error posting score."
First, let's take a look at how our server works. For this we need to understand the programming model of the Twisted framework: At the core of our application we find the reactor
object that is responsible for opening ports and polling them in its internal event loop. In our case, port 80 (the standard for a HTTP server) is opened to listen for TCP connections, which leads us directly to the next layer.
When opening the server port, we pass a new object of the class Site
that is responsible for processing the data that is received and sent over our newly opened port as HTTP requests. This means that with these steps, we have already created a web server. The only problem is that as our web server currently is, it would not provide any interesting data.
To add resources that can be queried and retrieved, we first add an empty Resource at the site's root folder. We do not want our server to host anything there. The only thing a client will receive if they request at the document root of our server is an error code, specifically 503. This error means that it is forbidden to access the requested resource. Instead, we add a new child to our virtual directory tree called score
. This causes our service to be reachable via the URL http://localhost/score
.
To be able to accept new scores being submitted by players and present the score list, we add our custom ScorePage
subclass of Resource
to the aforementioned virtual server directory.
In the constructor of ScorePage
, we connect to an SQLite
database, check if a table called scores
exists, and create it if necessary. SQLite
is a very lightweight SQL database implementation that stores its data in a specially formatted local file. It is mainly intended for small, single user applications, which means that for a serious attempt at implementing a server that stores scores, we should think about using a database system that is aimed at bigger scale use cases.
Querying the database requires us to use a cursor object. After executing the query, the cursor holds the results, which we then can retrieve using the fetchall()
method. If we made changes to the database's layout or data, we need to commit()
these changes, or they will be dropped. Also, after we are done with our queries, we should not forget to close()
the cursor to free any resources or handles we might still be holding.
This leaves the render_POST()
and render_GET()
methods to be discussed on the server side of our project. The render_POST()
method is called whenever a client sends data to our server. We then check if a player with the given name has already submitted a score that we need to update or if we need to create a new record in our database. After the data is processed and stored in the database, we're done receiving the request and return the string"OK"
to the client to signal that no error occurred.
An HTTP GET
request asks the server to return the data it hosts at a given address. The render_GET()
method builds this data on the fly as new requests for retrieving the resource located at the score directory of the server are received by the server. Our code queries the database to return all submitted scores, ordered by score in descending order. We build a plain text list of strings, where each line contains a player name and a score, hence the text/plain
MIME type is set in the header of the reply that will be sent back to the client. Of course, we could also return a string that contains HTML and omit setting the MIME type so the client (that is most likely going to be a web browser) will interpret it as a web page.
Before going on to discussing the client side, we should stop and think about an important issue that we have not addressed so far in the server code: security. First, our little server does not perform any sanity checks on the data submitted to it. In a production system, make sure to define which ranges and data types are allowed and add checking routines to prevent possible attacks based on submitting malicious data.
The second point we did not address is client authentication and authorization. So far, any program that is able to send a POST
request could possibly submit data to our server. Surely, we would only want our game to be able to submit data to prevent cheaters from submitting arbitrarily crafted scores, so some mechanisms for verifying clients and encrypting the submitted data will have to be put into place.
Finally, we can take a look at the client, where we use the HTTPClient
to send our request and retrieve the server's reply, which has already been discussed in the recipe Downloading a file from a server found earlier in this chapter. Instead of requesting a document, we use the beginPostForm()
method to send data to the server. What's particularly interesting about this call is the second parameter it accepts, which is the data to send.
The data is sent using a key-value form. Each of these key-value pairs takes the following form key=value
. We can send multiples of these pairs in one request, as shown in the sample code, using the ampersand (&) sign as a delimiter between each key-value pair.