Simple HTTP Server with IronPythonServing HTTP with HttpListener
![]()
IntroductionThe Python standard library includes several simple classes, like BaseHTTPServer for serving over HTTP. These can be very useful for simple proof-of-concept implementations that present information via a web-browser. This article implements a (very) simple server in IronPython, which serves over HTTP. It doesn't serve files from a directory structure, but is easy to extend to perform whatever task you want. As well as using the HttpListener class, this article explores text encoding, asynchronous callbacks, URI and XHTML escaping, the system message box, and creating a simple Windows Forms dialog. If you are new to .NET, this is a valuable tour of parts of the .NET 'standard library' [1]. HttpListenerThe basic class for listening and responding to HTTP requests, is HttpListener. Note This class is available only on computers running the Windows XP SP2 or Windows Server 2003 operating systems. If you attempt to create an HttpListener object on a computer that is running an earlier operating system, the constructor throws a PlatformNotSupportedException exception. This class probably won't scale to writing a production webserver or application server (at least, why would you want to?). It is fine for simple servers though, and can handle client authentication and HTTPS. There are two ways of using this class, synchronous and asynchronous. In synchronous mode, the listener class blocks whilst handling each request (like SimpleHTTPServer and friends). In asynchronous mode, each request is handled in its own thread (with all the associated complexity of threads if you are accessing shared data structures). This article will use HttpListener in asynchronous mode. The basic use pattern is very simple. We need to use the AsyncCallback delegate to create the callback which will be launched to handle each request. from System import AsyncCallback from System.Net import HttpListener, HttpListenerException listener = HttpListener() prefix = 'http://*:8080/' listener.Prefixes.Add(prefix) try: listener.Start() except HttpListenerException: raise Exception('Starting server failed') result = listener.BeginGetContext(AsyncCallback(handleRequest), listener) result.AsyncWaitHandle.WaitOne() listener.Close() You specify the port to listen on (as well as the domain to handle requests for), using prefixes. The 'Prefixes' property of the listener is a collection, so one listener can listen to as many of these as you want. A prefix string is a scheme (http or https), a host, an optional port, and an optional path. An example of a complete prefix string is "http://localhost:8080/customerData/". When you specify an explicit port, you can replace the domain name with a "*" to handle requests to all domains. The listener is started by calling Start(), which can raise a System.Net.HttpListenerException if the port is already in use (or there is some other problem). We start the handling of requests, by calling BeginGetContext, this requires an asynchronous callback - so we use the AsyncCallback delegate which can wrap an IronPython function. The code above then waits for a request to arrive and then closes the listener. To serve continuously, we can use: while True: result = listener.BeginGetContext(AsyncCallback(handleRequest), listener) result.AsyncWaitHandle.WaitOne() Here result is an instance of the AsyncResult Class, which "Encapsulates the results of an asynchronous operation on an asynchronous delegate". Handling RequestsThe actual request handling is done in the function that we passed to the AsyncCallback delegate. from System.Text import Encoding def handleRequest(result): listener = result.AsyncState context = listener.EndGetContext(result) request = context.Request response = context.Response text = getTextFromRequest(request) buffer = Encoding.UTF8.GetBytes(text) response.ContentLength64 = buffer.Length output = response.OutputStream output.Write(buffer, 0, buffer.Length) output.Close() This should be a function that takes one argument, the AsyncResult object that we saw earlier. This allows us to get back to the listener instance and call EndGetContext, which releases the listener to receive an other request. From the context (an HttpListenerContext) we have access to objects representing the request, and the response. On the response we set the content length (ContentLength64) and have access to a stream (OutputStream) to write the output to (which must be closed when we have finished writing to it). If we want to send text from a string, we'll have to convert the string into bytes first. We can do this with Encoding.UTF8.GetBytes [2]. The Encoding class is a very useful one for converting between text and bytes on .NET. GetBytes returns a bytes buffer, which is exactly what is needed by the Write method of the output stream. SimpleServerUsing all of this, we can put together a simple server class: from System import AsyncCallback from System.Net import HttpListener, HttpListenerException from System.Text import Encoding class SimpleServer(object): def __init__(self): self.text = """ <HTML> <HEAD><TITLE>Welcome to the Simple Server</TITLE></HEAD> <BODY><STRONG><H1>Welcome to the Simple Server</H1>%s</STRONG></BODY> </HTML> """ self.pagesServed = 0 def serveforever(self, port): self.failed = False listener = HttpListener() prefix = 'http://*:%s/' % str(port) listener.Prefixes.Add(prefix) try: listener.Start() except HttpListenerException: self.failed = True return while True: result = listener.BeginGetContext(AsyncCallback(self.handleRequest), listener) result.AsyncWaitHandle.WaitOne() def handleRequest(self, result): listener = result.AsyncState try: context = listener.EndGetContext(result) except: # Catch the exception when the thread has been aborted return request = context.Request response = context.Response text = self.getText(request) buffer = Encoding.UTF8.GetBytes(text) response.ContentLength64 = buffer.Length output = response.OutputStream output.Write(buffer, 0, buffer.Length) output.Close() def getText(self, request): self.pagesServed += 1 url = '<P><STRONG>URL Requested: %s</STRONG></P>' % request.RawUrl pagesServed = '<P><STRONG>Number of Pages Served: %s</STRONG></P>' % self.pagesServed return self.text % (url + pagesServed) This serves a simple HTML page, which reports the URL requested, and the number of pages served so far. In order to illustrate it, the next section will create a Windows Forms dialog which launches the server on a separate thread. It closes the server by aborting the thread (naughty), which means we need to wrap the call to EndGetContext in a try... except block, because the last call could happen after the listener has been closed when the main thread exits. We get the URL that has been requested from the request object, using the request.RawUrl. To build more complex behaviour, look at the properties and methods available on the request and response objects. For handling URLs, you may find the Uri class useful. It "provides an object representation of a uniform resource identifier (URI) and easy access to the parts of the URI" (including escaping and unescaping [3]). Having created our server, lets build a simple way of accessing it - a dialog. Server DialogThis dialog presents you with a textbox to enter a port number, and a button to launch the server. The server is launched on its own thread, which is aborted when you close the form. If the port number is invalid, or the server fails to start (port in use or some other problem) then a message box appears warning you about the problem. import clr clr.AddReference('System.Drawing') clr.AddReference('System.Windows.Forms') from SimpleServer import SimpleServer from System.Drawing import Point, Size from System.Threading import Thread, ThreadStart from System.Windows.Forms import ( Application, Button, Form, FormBorderStyle, Label, MessageBox, MessageBoxButtons, MessageBoxIcon, TextBox ) class ServerDialog(Form): def __init__(self): self.layout() self.server = SimpleServer() self.serverThread = None self.serveButton.Click += lambda _, __: self.serve() self.Closing += lambda _, __: self.onClose() def layout(self): self.portLabel = Label() self.portTextBox = TextBox() self.serveButton = Button() self.portLabel.AutoSize = True self.portLabel.Location = Point(13, 22) self.portLabel.Size = Size(29, 13) self.portLabel.Text = 'Port:' self.portTextBox.Location = Point(49, 22) self.portTextBox.Size = Size(42, 20) self.portTextBox.Text = '8080' self.serveButton.Location = Point(49, 60) self.serveButton.Size = Size(75, 23) self.serveButton.Text = 'Serve' self.ClientSize = Size(190, 108) self.Controls.Add(self.serveButton) self.Controls.Add(self.portTextBox) self.Controls.Add(self.portLabel) self.FormBorderStyle = FormBorderStyle.FixedDialog self.Text = 'Simple Server' def onClose(self): if self.serverThread is not None: self.serverThread.Abort() def serve(self): port = self.portTextBox.Text.strip() if not port.isdigit() or int(port) < 80 or int(port) > 65535: MessageBox.Show( 'The Port Number Must Be an Integer > 79 and < 65536', "Error Starting Server", MessageBoxButtons.OK, MessageBoxIcon.Error ) return print 'Starting server on port: %s' % port self.serverThread = Thread(ThreadStart(lambda : self.server.serveforever(port))) self.serverThread.Start() Thread.Sleep(50) if self.server.failed: MessageBox.Show( 'Failed to Start Server: Please Check that the Port is Not in Use', "Error Starting Server", MessageBoxButtons.OK, MessageBoxIcon.Error ) else: self.serveButton.Enabled = False self.portTextBox.Enabled = False Application.Run(ServerDialog()) When you launch this application, it will look like this: ![]() As soon as you press 'Serve' (assuming the server starts successfully of course), the text box and serve button will be disabled and you should be able to browse to http://localhost:8080/ (or whatever port you chose) and see the results: ![]() Have fun.
For buying techie books, science fiction, computer hardware or the latest gadgets: visit The Voidspace Amazon Store. If you're looking for a new techie job, try the Voidspace Tech Job Board. This is part of the Hidden Network of technology and programming jobs.
Last edited Fri Feb 15 13:42:11 2008. Counter... |
|||||||
|
Blogads
Follow me on: Tech Jobs |