Read Programming Python Online

Authors: Mark Lutz

Tags: #COMPUTERS / Programming Languages / Python

Programming Python (108 page)

BOOK: Programming Python
5.11Mb size Format: txt, pdf, ePub
ads
Summary: Choosing a Server Scheme

So when should you use
select
to build a server, instead of threads or forks? Needs vary per
application, of course, but as mentioned, servers based on the
select
call generally perform very well when
client transactions are relatively short and are not CPU-bound. If they
are not short, threads or forks may be a better way to split processing
among multiple clients. Threads and forks are especially useful if
clients require long-running processing above and beyond the socket
calls used to pass data. However, combinations are possible too—nothing
is stopping a select-based polling loop from using threads, too.

It’s important to remember that schemes based on
select
(and nonblocking sockets) are not
completely immune to blocking. In
Example 12-9
, for instance, the
send
call that echoes text back to a
client might block, too, and hence stall the entire server. We could
work around that blocking potential by using
select
to make sure that the output operation
is ready before we attempt it (e.g., use the
writesocks
list and add another loop to send
replies to ready output sockets), albeit at a noticeable cost in program
clarity.

In general, though, if we cannot split up the processing of a
client’s request in such a way that it can be multiplexed with other
requests and not block the server’s main loop,
select
may not be the best way to construct a
server by itself. While some network servers can satisfy this
constraint, many cannot.

Moreover,
select
also seems
more complex than spawning either processes or threads, because we need
to manually transfer control among all tasks (for instance, compare the
threaded and
select
versions of our
echo server, even without write selects). As usual, though, the degree
of that complexity varies per application. The
asyncore
standard library module mentioned earlier
simplifies some of the tasks of implementing a
select
-based event-loop socket server, and
Twisted offers additional hybrid
solutions
.

[
46
]
Confusingly,
select
-based
servers are often called
asynchronous
, to
describe their multiplexing of short-lived transactions. Really,
though, the classic forking and threading servers we met earlier are
asynchronous, too, as they do not wait for completion of a given
client’s request. There is a clearer distinction between serial and
parallel servers—the former process one transaction at a time and
the latter do not—and “synchronous” and “asynchronous” are
essentially synonyms for “serial” and “parallel.” By this
definition, forking, threading, and
select
loops are three alternative ways to
implement parallel, asynchronous servers.

Making Sockets Look Like Files and Streams

So far in this chapter,
we’ve focused on the role of sockets in the classic
client/server networking model. That’s one of their primary roles, but
they have other common use cases as well.

In
Chapter 5
, for instance, we saw
sockets as a basic IPC device between processes and threads on a single
machine. And in
Chapter 10
’s exploration of
linking non-GUI scripts to GUIs, we wrote a utility module (
Example 10-23
) which connected a
caller’s standard output stream to a socket, on which a GUI could listen
for output to be displayed. There, I promised that we’d flesh out that
module with additional transfer modes once we had a chance to explore
sockets in more depth. Now that we have, this section takes a brief detour
from the world of remote network servers to tell the rest of this
story.

Although some programs can be written or rewritten to converse over
sockets explicitly, this isn’t always an option; it may be too expensive
an effort for existing scripts, and might preclude desirable nonsocket
usage modes for others. In some cases, it’s better to allow a script to
use standard stream tools such as the
print
and
input
built-in functions and
sys
module file calls (e.g.,
sys.stdout.write
), and connect them to sockets
only when needed.

Because such stream tools are designed to operate on text-mode
files, though, probably the biggest trick here is fooling them into
operating on the inherently binary mode and very different method
interface of sockets. Luckily, sockets come with a method that achieves
all the forgery we need.

The
socket object
makefile
method comes in handy anytime you wish to process a socket with normal
file object methods or need to pass a socket to an existing interface or
program that expects a file. The socket wrapper object returned allows
your scripts to transfer data over the underlying socket with
read
and
write
calls, rather than
recv
and
send
. Since
input
and
print
built-in functions use the former methods
set, they will happily interact with sockets wrapped by this call,
too.

The
makefile
method also allows
us to treat normally binary socket data as text instead of byte strings,
and has additional arguments such as
encoding
that let us specify nondefault Unicode
encodings for the transferred text—much like the built-in
open
and
os.fdopen
calls we met in
Chapter 4
do for file descriptors. Although
text can always be encoded and decoded with manual calls after binary mode
socket transfers,
makefile
shifts the
burden of text encodings from your code to the file wrapper object.

This equivalence to files comes in handy any time we want to use
software that supports file interfaces. For example, the Python
pickle
module’s
load
and
dump
methods expect an object with a file-like interface (e.g.,
read
and
write
methods), but they don’t require a
physical file. Passing a TCP/IP socket wrapped with the
makefile
call to the pickler allows us to ship
serialized Python objects over the Internet, without having to pickle to
byte strings ourselves and call raw socket methods manually. This is an
alternative to using the
pickle
module’s string-based calls (
dumps
,
loads
) with socket
send
and
recv
calls, and might offer more flexibility for software that must support a
variety of transport mechanisms. See
Chapter 17
for more details on object
serialization
interfaces
.

More generally, any component that expects a file-like method
protocol will gladly accept a socket wrapped with a socket object
makefile
call. Such interfaces will also accept
strings wrapped with the built-in
io.StringIO
class, and any other sort of object
that supports the same kinds of method calls as built-in file objects. As
always in Python, we code to protocols—object interfaces—not to specific
datatypes.

A Stream Redirection Utility

To illustrate the
makefile
method’s
operation,
Example 12-10
implements a variety of
redirection schemes, which redirect the caller’s streams to a socket
that can be used by another process for communication. The first of its
functions connects output, and is what we used in
Chapter 10
; the others connect input, and both
input and output in three different modes.

Naturally, the wrapper object returned by
socket.makefile
can also be used with direct
file interface
read
and
write
method calls and independently of
standard streams. This example uses those methods, too, albeit in most
cases indirectly and implicitly through the
print
and
input
stream access built-ins, and reflects a
common use case for the tool.

Example 12-10. PP4E\Internet\Sockets\socket_stream_redirect.py

"""
###############################################################################
Tools for connecting standard streams of non-GUI programs to sockets that
a GUI (or other) program can use to interact with the non-GUI program.
###############################################################################
"""
import sys
from socket import *
port = 50008 # pass in different port if multiple dialogs on machine
host = 'localhost' # pass in different host to connect to remote listeners
def initListenerSocket(port=port):
"""
initialize connected socket for callers that listen in server mode
"""
sock = socket(AF_INET, SOCK_STREAM)
sock.bind(('', port)) # listen on this port number
sock.listen(5) # set pending queue length
conn, addr = sock.accept() # wait for client to connect
return conn # return connected socket
def redirectOut(port=port, host=host):
"""
connect caller's standard output stream to a socket for GUI to listen
start caller after listener started, else connect fails before accept
"""
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((host, port)) # caller operates in client mode
file = sock.makefile('w') # file interface: text, buffered
sys.stdout = file # make prints go to sock.send
return sock # if caller needs to access it raw
def redirectIn(port=port, host=host):
"""
connect caller's standard input stream to a socket for GUI to provide
"""
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((host, port))
file = sock.makefile('r') # file interface wrapper
sys.stdin = file # make input come from sock.recv
return sock # return value can be ignored
def redirectBothAsClient(port=port, host=host):
"""
connect caller's standard input and output stream to same socket
in this mode, caller is client to a server: sends msg, receives reply
"""
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((host, port)) # or open in 'rw' mode
ofile = sock.makefile('w') # file interface: text, buffered
ifile = sock.makefile('r') # two file objects wrap same socket
sys.stdout = ofile # make prints go to sock.send
sys.stdin = ifile # make input come from sock.recv
return sock
def redirectBothAsServer(port=port, host=host):
"""
connect caller's standard input and output stream to same socket
in this mode, caller is server to a client: receives msg, send reply
"""
sock = socket(AF_INET, SOCK_STREAM)
sock.bind((host, port)) # caller is listener here
sock.listen(5)
conn, addr = sock.accept()
ofile = conn.makefile('w') # file interface wrapper
ifile = conn.makefile('r') # two file objects wrap same socket
sys.stdout = ofile # make prints go to sock.send
sys.stdin = ifile # make input come from sock.recv
return conn

To test, the script in
Example 12-11
defines five sets of
client/server functions. It runs the client’s code in process, but
deploys the Python
multiprocessing
module we met in
Chapter 5
to portably
spawn the server function’s side of the dialog in a separate process. In
the end, the client and server test functions run in different
processes, but converse over a socket that is connected to standard
streams within the test script’s process.

Example 12-11. PP4E\Internet\Sockets\test-socket_stream_redirect.py

"""
###############################################################################
test the socket_stream_redirection.py modes
###############################################################################
"""
import sys, os, multiprocessing
from socket_stream_redirect import *
###############################################################################
# redirected client output
###############################################################################
def server1():
mypid = os.getpid()
conn = initListenerSocket() # block till client connect
file = conn.makefile('r')
for i in range(3): # read/recv client's prints
data = file.readline().rstrip() # block till data ready
print('server %s got [%s]' % (mypid, data)) # print normally to terminal
def client1():
mypid = os.getpid()
redirectOut()
for i in range(3):
print('client %s: %s' % (mypid, i)) # print to socket
sys.stdout.flush() # else buffered till exits!
###############################################################################
# redirected client input
###############################################################################
def server2():
mypid = os.getpid() # raw socket not buffered
conn = initListenerSocket() # send to client's input
for i in range(3):
conn.send(('server %s: %s\n' % (mypid, i)).encode())
def client2():
mypid = os.getpid()
redirectIn()
for i in range(3):
data = input() # input from socket
print('client %s got [%s]' % (mypid, data)) # print normally to terminal
###############################################################################
# redirect client input + output, client is socket client
###############################################################################
def server3():
mypid = os.getpid()
conn = initListenerSocket() # wait for client connect
file = conn.makefile('r') # recv print(), send input()
for i in range(3): # readline blocks till data
data = file.readline().rstrip()
conn.send(('server %s got [%s]\n' % (mypid, data)).encode())
def client3():
mypid = os.getpid()
redirectBothAsClient()
for i in range(3):
print('client %s: %s' % (mypid, i)) # print to socket
data = input() # input from socket: flushes!
sys.stderr.write('client %s got [%s]\n' % (mypid, data)) # not redirected
###############################################################################
# redirect client input + output, client is socket server
###############################################################################
def server4():
mypid = os.getpid()
sock = socket(AF_INET, SOCK_STREAM)
sock.connect((host, port))
file = sock.makefile('r')
for i in range(3):
sock.send(('server %s: %s\n' % (mypid, i)).encode()) # send to input()
data = file.readline().rstrip() # recv from print()
print('server %s got [%s]' % (mypid, data)) # result to terminal
def client4():
mypid = os.getpid()
redirectBothAsServer() # I'm actually the socket server in this mode
for i in range(3):
data = input() # input from socket: flushes!
print('client %s got [%s]' % (mypid, data)) # print to socket
sys.stdout.flush() # else last buffered till exit!
###############################################################################
# redirect client input + output, client is socket client, server xfers first
###############################################################################
def server5():
mypid = os.getpid() # test 4, but server accepts
conn = initListenerSocket() # wait for client connect
file = conn.makefile('r') # send input(), recv print()
for i in range(3):
conn.send(('server %s: %s\n' % (mypid, i)).encode())
data = file.readline().rstrip()
print('server %s got [%s]' % (mypid, data))
def client5():
mypid = os.getpid()
s = redirectBothAsClient() # I'm the socket client in this mode
for i in range(3):
data = input() # input from socket: flushes!
print('client %s got [%s]' % (mypid, data)) # print to socket
sys.stdout.flush() # else last buffered till exit!
###############################################################################
# test by number on command-line
###############################################################################
if __name__ == '__main__':
server = eval('server' + sys.argv[1])
client = eval('client' + sys.argv[1]) # client in this process
multiprocessing.Process(target=server).start() # server in new process
client() # reset streams in client
#import time; time.sleep(5) # test effect of exit flush

Run the test script with a client and server number on the command
line to test the module’s tools; messages display process ID numbers,
and those within square brackets reflect a transfer across streams
connected to sockets (twice, when nested):

C:\...\PP4E\Internet\Sockets>
test-socket_stream_redirect.py 1
server 3844 got [client 1112: 0]
server 3844 got [client 1112: 1]
server 3844 got [client 1112: 2]
C:\...\PP4E\Internet\Sockets>
test-socket_stream_redirect.py 2
client 5188 got [server 2020: 0]
client 5188 got [server 2020: 1]
client 5188 got [server 2020: 2]
C:\...\PP4E\Internet\Sockets>
test-socket_stream_redirect.py 3
client 7796 got [server 2780 got [client 7796: 0]]
client 7796 got [server 2780 got [client 7796: 1]]
client 7796 got [server 2780 got [client 7796: 2]]
C:\...\PP4E\Internet\Sockets>
test-socket_stream_redirect.py 4
server 4288 got [client 3852 got [server 4288: 0]]
server 4288 got [client 3852 got [server 4288: 1]]
server 4288 got [client 3852 got [server 4288: 2]]
C:\...\PP4E\Internet\Sockets>
test-socket_stream_redirect.py 5
server 6040 got [client 7728 got [server 6040: 0]]
server 6040 got [client 7728 got [server 6040: 1]]
server 6040 got [client 7728 got [server 6040: 2]]

If you correlate this script’s output with its code to see how
messages are passed between client and server, you’ll find that
print
and
input
calls in client functions are ultimately
routed over sockets to another process. To the client functions, the
socket linkage is largely invisible.

Text-mode files and buffered output streams

Before we
move on, there are two remarkably subtle aspects of the
example’s code worth highlighting:

Binary to text translations

Raw sockets transfer binary byte strings, but by opening
the wrapper files in text mode, their content is automatically
translated to text strings on input and output. Text-mode file
wrappers are required if accessed through standard stream tools
such as the
print
built-in
that writes text strings (as we’ve learned, binary mode files
require byte strings instead). When dealing with the raw socket
directly, though, text must still be manually encoded to byte
strings, as shown in most of
Example 12-11
’s tests.

Buffered streams, program output, and deadlock

As we learned in Chapters
5
and
10
, standard streams are
normally buffered, and printed text may need to be flushed so
that it appears on a socket connected to a process’s output
stream. Indeed, some of this example’s tests require explicit or
implicit flush calls to work properly at all; otherwise their
output is either incomplete or absent altogether until program
exit. In pathological cases, this can lead to deadlock, with a
process waiting for output from another that never appears. In
other configurations, we may also get socket errors in a reader
if a writer exits too soon, especially in two-way
dialogs.

For example, if
client1
and
client4
did not flush
periodically as they do, the only reason that they would work is
because output streams are automatically flushed when their
process exits. Without manual flushes,
client1
transfers no data until
process exit (at which point all its output is sent at once in a
single message), and
client4
’s data is incomplete till exit
(its last printed message is delayed).

Even more subtly, both
client3
and
client4
rely on the fact that the
input
built-in first
automatically flushes
sys.stdout
internally for its prompt
option, thereby sending data from preceding
print
calls. Without this implicit
flush (or the addition of manual flushes),
client3
would experience
deadlock
immediately, as would
client4
if its manual flush call was
removed (even with
input
’s
flush, removing
client4
’s
manual flush causes its final
print
message to not be transferred
until process exit).
client5
has this same behavior as
client4
, because it simply swaps which
process binds and accepts and which connects.

In the general case, if we wish to read a program’s output
as it is produced, instead of all at once when it exits or as
its buffers fill, the program must either call
sys.stdout.flush
periodically, or be
run with unbuffered streams by using
Python’s
-u
command-line argument of
Chapter 5
if applicable.

Although we can open socket wrapper files in
unbuffered mode
with a second
makefile
argument of zero (like normal
open
), this does not allow
the wrapper to run in the text mode required for
print
and desired for
input
. In fact, attempting to make a
socket wrapper file both text mode and unbuffered this way fails
with an exception, because Python 3.X no longer supports
unbuffered mode for text files (it is allowed for binary mode
only today). In other words, because
print
requires text mode, buffered
mode is also implied for output stream files. Moreover,
attempting to open a socket file wrapper in
line-buffered
mode appears to not be
supported in Python 3.X (more on this ahead).

While some buffering behavior may be library and platform
dependent, manual flush calls or direct socket access might
sometimes still be required. Note that sockets can also be made
nonblocking with the
setblocking(0)
method, but this only
avoids wait states for transfer calls and does not address the
data producer’s failure to send buffered output.

BOOK: Programming Python
5.11Mb size Format: txt, pdf, ePub
ads

Other books

Ala de dragón by Margaret Weis, Tracy Hickman
Confessions of a Yakuza by Saga, Junichi
Play Me by Diane Alberts
The Fruit of the Tree by Jacquelynn Luben
Killer Crust by Chris Cavender
Accidental Voyeur by Jennifer Kacey