13.5. A Simple Python File Server
Time for something more realistic; let's conclude this chapter by putting some of these socket ideas to work doing something a bit more useful than echoing text back and forth. Example 13-10 implements both the server-side and the client-side logic needed to ship a requested file from server to client machines over a raw socket.
In effect, this script implements a simple file download system. One instance of the script is run on the machine where downloadable files live (the server), and another on the machines you wish to copy files to (the clients). Command-line arguments tell the script which flavor to run and optionally name the server machine and port number over which conversations are to occur. A server instance can respond to any number of client file requests at the port on which it listens, because it serves each in a thread.
Example 13-10. PP3E\Internet\Sockets\getfile.py
This script isn't much different from the examples we saw earlier. Depending on the command-line arguments passed, it invokes one of two functions:
The most novel feature here is the protocol between client and server: the client starts the conversation by shipping a filename string up to the server, terminated with an end-of-line character, and including the file's directory path in the server. At the server, a spawned thread extracts the requested file's name by reading the client socket, and opens and transfers the requested file back to the client, one chunk of bytes at a time.
13.5.1. Running the File Server and Clients
Since the server uses threads to process clients, we can test both client and server on the same Windows machine. First, let's start a server instance and execute two client instances on the same machine while the server runs:
[server window, localhost] C:\...\PP3E\Internet\Sockets>python getfile.py -mode server Server connected by ('127.0.0.1', 1089) at Thu Mar 16 11:54:21 2000 Server connected by ('127.0.0.1', 1090) at Thu Mar 16 11:54:37 2000 [client window, localhost] C:\...\Internet\Sockets>ls class-server.py echo.out.txt testdir thread-server.py echo-client.py fork-server.py testecho.py echo-server.py getfile.py testechowait.py C:\...\Internet\Sockets>python getfile.py -file testdir\python15.lib -port 50001 Client got testdir\python15.lib at Thu Mar 16 11:54:21 2000 C:\...\Internet\Sockets>python getfile.py -file testdir\textfile Client got testdir\textfile at Thu Mar 16 11:54:37 2000
Clients run in the directory where you want the downloaded file to appearthe client instance code strips the server directory path when making the local file's name. Here the "download" simply copies the requested files up to the local parent directory (the DOS fc command compares file contents):
C:\...\Internet\Sockets>ls class-server.py echo.out.txt python15.lib testechowait.py echo-client.py fork-server.py testdir textfile echo-server.py getfile.py testecho.py thread-server.py C:\...\Internet\Sockets>fc /B python1.lib testdir\python15.lib Comparing files python15.lib and testdir\python15.lib FC: no differences encountered C:\...\Internet\Sockets>fc /B textfile testdir\textfile Comparing files textfile and testdir\textfile FC: no differences encountered
As usual, we can run server and clients on different machines as well. Here the script is being used to run a remote server on a Linux machine and a few clients on a local Windows PC (I added line breaks to some of the command lines to make them fit). Notice that client and server machine times are different nowthey are fetched from different machines' clocks and so may be arbitrarily skewed:
[server Telnet window: first message is the python15.lib request in client window1] [lutz@starship lutz]$ python getfile.py -mode server Server connected by ('18.104.22.168', 1185) at Thu Mar 16 16:02:07 2000 Server connected by ('22.214.171.124', 1187) at Thu Mar 16 16:03:24 2000 Server connected by ('126.96.36.199', 1189) at Thu Mar 16 16:03:52 2000 Server connected by ('188.8.131.52', 1191) at Thu Mar 16 16:04:09 2000 Server connected by ('184.108.40.206', 1193) at Thu Mar 16 16:04:38 2000 [client window 1: started first, runs in thread while other client requests are made in client window 2, and processed by other threads] C:\...\Internet\Sockets>python getfile.py -mode client -host starship.python.net -port 50001 -file python15.lib Client got python15.lib at Thu Mar 16 14:07:37 2000 C:\...\Internet\Sockets>fc /B python15.lib testdir\python15.lib Comparing files python15.lib and testdir\python15.lib FC: no differences encountered [client window 2: requests made while client window 1 request downloading] C:\...\Internet\Sockets>python getfile.py -host starship.python.net -file textfile Client got textfile at Thu Mar 16 14:02:29 2000 C:\...\Internet\Sockets>python getfile.py -host starship.python.net -file textfile Client got textfile at Thu Mar 16 14:04:11 2000 C:\...\Internet\Sockets>python getfile.py -host starship.python.net -file textfile Client got textfile at Thu Mar 16 14:04:21 2000 C:\...\Internet\Sockets>python getfile.py -host starship.python.net -file index.html Client got index.html at Thu Mar 16 14:06:22 2000 C:\...\Internet\Sockets>fc textfile testdir\textfile Comparing files textfile and testdir\textfile FC: no differences encountered
One subtle security point here: the server instance code is happy to send any server-side file whose pathname is sent from a client, as long as the server is run with a username that has read access to the requested file. If you care about keeping some of your server-side files private, you should add logic to suppress downloads of restricted files. I'll leave this as a suggested exercise here, but I will implement such filename checks in a different getfile download tool in Chapter 16.[*]
13.5.2. Adding a User-Interface Frontend
You might have noticed that we have been living in the realm of the command line for this entire chapterour socket clients and servers have been started from simple DOS or Linux shells. Nothing is stopping us from adding a nice point-and-click user interface to some of these scripts, though; GUI and network scripting are not mutually exclusive techniques. In fact, they can be arguably sexy when used together well.
For instance, it would be easy to implement a simple Tkinter GUI frontend to the client-side portion of the getfile script we just met. Such a tool, run on the client machine, may simply pop up a window with Entry widgets for typing the desired filename, server, and so on. Once download parameters have been input, the user interface could either import and call the getfile.client function with appropriate option arguments, or build and run the implied getfile.py command line using tools such as os.system, os.fork, tHRead, and so on.
220.127.116.11. Using Frames and command lines
To help make all of this more concrete, let's very quickly explore a few simple scripts that add a Tkinter frontend to the getfile client-side program. All of these examples assume that you are running a server instance of getfile; they merely add a GUI for the client side of the conversation, to fetch a file from the server. The first, in Example 13-11, creates a dialog for inputting server, port, and filename information, and simply constructs the corresponding getfile command line and runs it with os.system.
Example 13-11. PP3E\Internet\Sockets\getfilegui-1.py
When run, this script creates the input form shown in Figure 13-1. Pressing the Enter key (<Return>) runs a client-side instance of the getfile program; when the generated getfile command line is finished, we get the verification pop up displayed in Figure 13-2.
Figure 13-1. getfilegui-1 in action
Figure 13-2. getfilegui-1 verification pop up
18.104.22.168. Using grids and function calls
The first user-interface script (Example 13-11) uses the pack geometry manager and Frames to lay out the input form, and runs the getfile client as a standalone program. It's just as easy to use the grid manager for layout, and to import and call the client-side logic function instead of running a program. The script in Example 13-12 shows how.
Example 13-12. PP3E\Internet\Sockets\getfilegui-2.py
This version makes a similar window (Figure 13-3), but adds a button at the bottom that does the same thing as an Enter key pressit runs the getfile client procedure. Generally speaking, importing and calling functions (as done here) is faster than running command lines, especially if done more than once. The getfile script is set up to work either wayas program or function library.
Figure 13-3. getfilegui-2 in action
22.214.171.124. Using a reusable form-layout class
If you're like me, though, writing all the GUI form layout code in those two scripts can seem a bit tedious, whether you use packing or grids. In fact, it became so tedious to me that I decided to write a general-purpose form-layout class, shown in Example 13-13, which handles most of the GUI layout grunt work.
Example 13-13. PP3E\Internet\Sockets\form.py
Running this module standalone triggers its self-test code at the bottom. Without arguments (and when double-clicked in a Windows file explorer), the self-test generates a form with canned input fields captured in Figure 13-4, and displays the fields' values on Enter key presses or Submit button clicks:
C:\...\PP3E\Internet\Sockets>python form.py Job => Educator, Entertainer Age => 38 Name => Bob
Figure 13-4. Form test, canned fields
With a command-line argument, the form class module's self-test code prompts for an arbitrary set of field names for the form; fields can be constructed as dynamically as we like. Figure 13-5 shows the input form constructed in response to the following console interaction. Field names could be accepted on the command line too, but raw_input works just as well for simple tests like this. In this mode, the GUI goes away after the first submit, because DynamicForm.onSubmit says so.
Figure 13-5. Form test, dynamic fields
C:\...\PP3E\Internet\Sockets>python form.py - Enter field names: Name Email Web Locale Field values... Email => email@example.com Locale => Colorado Web => http://rmi.net/~lutz Name => mel
And last but not least, Example 13-14 shows the getfile user interface again, this time constructed with the reusable form layout class. We need to fill in only the form labels list, and provide an onSubmit callback method of our own. All of the work needed to construct the form comes "for free," from the imported and widely reusable Form superclass.
Example 13-14. PP3E\Internet\Sockets\getfilegui.py
The form layout class imported here can be used by any program that needs to input form-like data; when used in this script, we get a user interface like that shown in Figure 13-6 under Windows (and similar on other platforms).
Figure 13-6. getfilegui in action
Pressing this form's Submit button or the Enter key makes the getfilegui script call the imported getfile.client client-side function as before. This time, though, we also first change to the local directory typed into the form so that the fetched file is stored there (getfile stores in the current working directory, whatever that may be when it is called). As usual, we can use this interface to connect to servers running locally on the same machine, or remotely. Here is some of the interaction we get for each mode:
[talking to a local server] C:\...\PP3E\Internet\Sockets>python getfilegui.py Port Number => 50001 Local Dir? => temp Server Name => localhost File Name => testdir\python15.lib Client got testdir\python15.lib at Tue Aug 15 22:32:34 2000 [talking to a remote server] [lutz@starship lutz]$ /usr/bin/python getfile.py -mode server -port 51234 Server connected by ('126.96.36.199', 1111) at Tue Aug 15 21:48:13 2000 C:\...\PP3E\Internet\Sockets>python getfilegui.py Port Number => 51234 Local Dir? => temp Server Name => starship.python.net File Name => public_html/index.html Client got public_html/index.html at Tue Aug 15 22:42:06 2000
One caveat worth pointing out here: the GUI is essentially dead while the download is in progress (even screen redraws aren't handledtry covering and uncovering the window and you'll see what I mean). We could make this better by running the download in a thread, but since we'll see how to do that in the next chapter when we explore the FTP protocol, you should consider this problem a preview.
In closing, a few final notes: first, I should point out that the scripts in this chapter use Tkinter techniques we've seen before and won't go into here in the interest of space; be sure to see the GUI chapters in this book for implementation hints.
Keep in mind, too, that these interfaces just add a GUI on top of the existing script to reuse its code; any command-line tool can be easily GUI-ified in this way to make it more appealing and user friendly. In Chapter 15, for example, we'll meet a more useful client-side Tkinter user interface for reading and sending email over sockets (PyMailGUI), which largely just adds a GUI to mail-processing tools. Generally speaking, GUIs can often be added as almost an afterthought to a program. Although the degree of user-interface and core logic separation varies per program, keeping the two distinct makes it easier to focus on each.
And finally, now that I've shown you how to build user interfaces on top of this chapter's getfile, I should also say that they aren't really as useful as they might seem. In particular, getfile clients can talk only to machines that are running a getfile server. In the next chapter, we'll discover another way to download filesFTPwhich also runs on sockets but provides a higher-level interface and is available as a standard service on many machines on the Net. We don't generally need to start up a custom server to transfer files over FTP, the way we do with getfile. In fact, the user-interface scripts in this chapter could be easily changed to fetch the desired file with Python's FTP tools, instead of the getfile module. But instead of spilling all the beans here, I'll just say, "Read on."