Python应用02 Python服务器进化

作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明。谢谢!

** 注意,在 Python 3.x 中,BaseHTTPServer, SimpleHTTPServer, CGIHTTPServer 整合到 http.server 包,SocketServer 改名为 socketserver,请注意查阅官方文档。

在上一篇文章中 (用 socket 写一个 Python 服务器),我使用 socket 接口,制作了一个处理 HTTP 请求的 Python 服务器。任何一台装有操作系统和 Python 解释器的计算机,都可以作为 HTTP 服务器使用。我将在这里不断改写上一篇文章中的程序,引入更高级的 Python 包,以写出更成熟的 Python 服务器。

 

支持 POST

我首先增加该服务器的功能。这里增添了表格,以及处理表格提交数据的 "POST" 方法。如果你已经读过用 socket 写一个 Python 服务器,会发现这里只是增加很少的一点内容。

原始程序:

# Written by Vamei
# A messy HTTP server based on TCP socket 

import socket

#
Address
HOST
= ''
PORT
= 8000

text_content = '''
HTTP/1.x 200 OK
Content-Type: text/html

<head>
<title>WOW</title>
</head>
<html>
<p>Wow, Python Server</p>
<IMG src="test.jpg"/>
<form name="input" action="/" method="post">
First name:<input type="text" name="firstname"><br>
<input type="submit" value="Submit">
</form>
</html>
'''

f = open('test.jpg','rb')
pic_content
= '''
HTTP/1.x 200 OK
Content-Type: image/jpg

'''
pic_content
= pic_content + f.read()

# Configure socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
# Serve forever
while True:
s.listen(
3)
conn, addr
= s.accept()
request
= conn.recv(1024) # 1024 is the receiving buffer size
method = request.split(' ')[0]
src
= request.split(' ')[1]

</span><span style="color: rgba(0, 0, 255, 1)">print</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">Connected by</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, addr
</span><span style="color: rgba(0, 0, 255, 1)">print</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">Request is:</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, request


# if GET method request
if method == 'GET':
# if ULR is /test.jpg
if src == '/test.jpg':
content
= pic_content
else: content = text_content
# send message
conn.sendall(content)
# if POST method request
if method == 'POST':
form
= request.split('\r\n')
idx
= form.index('') # Find the empty line
entry = form[idx:] # Main content of the request

value
= entry[-1].split('=')[-1]
conn.sendall(text_content
+ '\n <p>' + value + '</p>')
######
# More operations, such as put the form into database
# ...
######
# close connection

conn.close()

服务器进行的操作很简单,即从 POST 请求中提取数据,再显示在屏幕上。

运行上面 Python 服务器,像上一篇文章那样,使用一个浏览器打开。

 

 

页面新增了表格和提交 (submit) 按钮。在表格中输入 aa 并提交,页面显示出 aa。

 

我下一步要用一些高级包,来简化之前的代码。

 

 

使用 SocketServer

首先使用 SocketServer 包来方便的架设服务器。在上面使用 socket 的过程中,我们先设置了 socket 的类型,然后依次调用 bind(),listen(),accept(),最后使用 while 循环来让服务器不断的接受请求。上面的这些步骤可以通过 SocketServer 包来简化。

SocketServer:

# Written by Vamei
# use TCPServer

import SocketServer

HOST = ''
PORT
= 8000

text_content = '''
HTTP/1.x 200 OK
Content-Type: text/html

<head>
<title>WOW</title>
</head>
<html>
<p>Wow, Python Server</p>
<IMG src="test.jpg"/>
<form name="input" action="/" method="post">
First name:<input type="text" name="firstname"><br>
<input type="submit" value="Submit">
</form>
</html>
'''

f = open('test.jpg','rb')
pic_content
= '''
HTTP/1.x 200 OK
Content-Type: image/jpg

'''
pic_content
= pic_content + f.read()

# This class defines response to each request
class MyTCPHandler(SocketServer.BaseRequestHandler):
def handle(self):
# self.request is the TCP socket connected to the client
request = self.request.recv(1024)

    </span><span style="color: rgba(0, 0, 255, 1)">print</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">Connected by</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">,self.client_address[0]
    </span><span style="color: rgba(0, 0, 255, 1)">print</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">Request is</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">, request

    method     </span>= request.split(<span style="color: rgba(128, 0, 0, 1)">'</span> <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)[0]
    src        </span>= request.split(<span style="color: rgba(128, 0, 0, 1)">'</span> <span style="color: rgba(128, 0, 0, 1)">'</span>)[1<span style="color: rgba(0, 0, 0, 1)">]

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> method == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">GET</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">:
        </span><span style="color: rgba(0, 0, 255, 1)">if</span> src == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">/test.jpg</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">:
            content </span>=<span style="color: rgba(0, 0, 0, 1)"> pic_content
        </span><span style="color: rgba(0, 0, 255, 1)">else</span>: content =<span style="color: rgba(0, 0, 0, 1)"> text_content
        self.request.sendall(content)

    </span><span style="color: rgba(0, 0, 255, 1)">if</span> method == <span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">POST</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">:
        form </span>= request.split(<span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(128, 0, 0, 1)">\r\n</span><span style="color: rgba(128, 0, 0, 1)">'</span><span style="color: rgba(0, 0, 0, 1)">)
        idx </span>= form.index(<span style="color: rgba(128, 0, 0, 1)">''</span>)             <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> Find the empty line</span>
        entry = form[idx:]               <span style="color: rgba(0, 128, 0, 1)">#</span><span style="color: rgba(0, 128, 0, 1)"> Main content of the request</span>
value = entry[-1].split('=')[-1] self.request.sendall(text_content + '\n <p>' + value + '</p>') ###### # More operations, such as put the form into database # ... ######

# Create the server
server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler)
# Start the server, and work forever
server.serve_forever()

 

我建立了一个TCPServer对象,即一个使用 TCP socket 的服务器。在建立 TCPServe 的同时,设置该服务器的 IP 地址和端口。使用server_forever()方法来让服务器不断工作 (就像原始程序中的 while 循环一样)。

我们传递给 TCPServer 一个 MyTCPHandler 类。这个类定义了如何操作 socket。MyTCPHandler 继承自 BaseRequestHandler。改写handler()方法,来具体规定不同情况下服务器的操作。

在 handler() 中,通过self.request来查询通过 socket 进入服务器的请求 (正如我们在 handler() 中对 socket 进行recv()sendall()操作 ),还使用self.address来引用 socket 的客户端地址。

 

经过 SocketServer 的改造之后,代码还是不够简单。 我们上面的通信基于 TCP 协议,而不是 HTTP 协议。因此,我们必须手动的解析 HTTP 协议。我们将建立基于 HTTP 协议的服务器。

 

SimpleHTTPServer: 使用静态文件来回应请求

HTTP 协议基于 TCP 协议,但增加了更多的规范。这些规范,虽然限制了 TCP 协议的功能,但大大提高了信息封装和提取的方便程度。

对于一个 HTTP 请求 (request) 来说,它包含有两个重要信息:请求方法和 URL。

请求方法 (request method)       URL                操作

GET                           /                  发送 text_content

GET                           /text.jpg          发送 pic_content

POST                          /                  分析 request 主体中包含的 value(实际上是我们填入表格的内容); 发送 text_content 和 value

 

根据请求方法和 URL 的不同,一个大型的 HTTP 服务器可以应付成千上万种不同的请求。在 Python 中,我们可以使用 SimpleHTTPServer 包和 CGIHTTPServer 包来规定针对不同请求的操作。其中,SimpleHTTPServer 可以用于处理GET方法和HEAD方法的请求。它读取 request 中的 URL 地址,找到对应的静态文件,分析文件类型,用 HTTP 协议将文件发送给客户。

 

SimpleHTTPServer

 

我们将 text_content 放置在 index.html 中,并单独存储 text.jpg 文件。如果 URL 指向 index_html 的母文件夹时,SimpleHTTPServer 会读取该文件夹下的 index.html 文件。

 

我在当前目录下生成 index.html 文件:

<head>
<title>WOW</title>
</head>
<html>
<p>Wow, Python Server</p>
<IMG src="test.jpg"/>
<form name="input" action="/" method="post">
First name:<input type="text" name="firstname"><br>
<input type="submit" value="Submit">
</form>
</html>

 

改写 Python 服务器程序。使用SimpleHTTPServer包中唯一的类SimpleHTTPRequestHandler:

# Written by Vamei
# Simple HTTPsERVER

import SocketServer
import SimpleHTTPServer

HOST = ''
PORT
= 8000

# Create the server, SimpleHTTPRequestHander is pre-defined handler in SimpleHTTPServer package
server = SocketServer.TCPServer((HOST, PORT), SimpleHTTPServer.SimpleHTTPRequestHandler)
# Start the server
server.serve_forever()

 

这里的程序不能处理 POST 请求我会在后面使用 CGI 来弥补这个缺陷。值得注意的是,Python 服务器程序变得非常简单。将内容存放于静态文件,并根据 URL 为客户端提供内容,这让内容和服务器逻辑分离。每次更新内容时,我可以只修改静态文件,而不用停止整个 Python 服务器。

这些改进也付出代价。在原始程序中,request 中的 URL 只具有指导意义,我可以规定任意的操作。在 SimpleHTTPServer 中,操作与 URL 的指向密切相关。我用自由度,换来了更加简洁的程序。

 

CGIHTTPServer:使用静态文件或者 CGI 来回应请求

CGIHTTPServer 包中的 CGIHTTPRequestHandler 类继承自 SimpleHTTPRequestHandler 类,所以可以用来代替上面的例子,来提供静态文件的服务。此外,CGIHTTPRequestHandler 类还可以用来运行CGI 脚本

CGIHTTPServer

 

先看看什么是CGI (Common Gateway Interface)。CGI 是服务器和应用脚本之间的一套接口标准。它的功能是让服务器程序运行脚本程序,将程序的输出作为 response 发送给客户。总体的效果,是允许服务器动态的生成回复内容,而不必局限于静态文件。

支持 CGI 的服务器程接收到客户的请求,根据请求中的 URL,运行对应的脚本文件。服务器会将 HTTP 请求的信息和 socket 信息传递给脚本文件,并等待脚本的输出。脚本的输出封装成合法的 HTTP 回复,发送给客户。CGI 可以充分发挥服务器的可编程性,让服务器变得“更聪明”。

服务器和 CGI 脚本之间的通信要符合 CGI 标准。CGI 的实现方式有很多,比如说使用 Apache 服务器与 Perl 写的 CGI 脚本,或者 Python 服务器与 shell 写的 CGI 脚本。

 

为了使用 CGI,我们需要使用 BaseHTTPServer 包中的HTTPServer类来构建服务器。Python 服务器的改动很简单。

CGIHTTPServer:

# Written by Vamei
# A messy HTTP server based on TCP socket 

import BaseHTTPServer
import CGIHTTPServer

HOST = ''
PORT
= 8000

# Create the server, CGIHTTPRequestHandler is pre-defined handler
server = BaseHTTPServer.HTTPServer((HOST, PORT), CGIHTTPServer.CGIHTTPRequestHandler)
# Start the server
server.serve_forever()

 

CGIHTTPRequestHandler默认当前目录下的cgi-binht-bin文件夹中的文件为 CGI 脚本,而存放于其他地方的文件被认为是静态文件。因此,我们需要修改一下 index.html,将其中 form 元素指向的 action 改为 cgi-bin/post.py。

<head>
<title>WOW</title>
</head>
<html>
<p>Wow, Python Server</p>
<IMG src="test.jpg"/>
<form name="input" action="cgi-bin/post.py" method="post">
First name:<input type="text" name="firstname"><br>
<input type="submit" value="Submit">
</form>
</html>

 

我创建一个 cgi-bin 的文件夹,并在 cgi-bin 中放入如下 post.py 文件,也就是我们的CGI 脚本

#!/usr/bin/env python
# Written by Vamei
import cgi
form = cgi.FieldStorage()

# Output to stdout, CGIHttpServer will take this as response to the client
print "Content-Type: text/html" # HTML is following
print # blank line, end of headers
print "<p>Hello world!</p>" # Start of content
print "<p>" + repr(form['firstname']) + "</p>"

(post.py 需要有执行权限,见评论区)

第一行说明了脚本所使用的语言,即 Python。 cgi 包用于提取请求中包含的表格信息。脚本只负责将所有的结果输出到标准输出 (使用 print)。CGIHTTPRequestHandler 会收集这些输出,封装成 HTTP 回复,传送给客户端。

对于POST方法的请求,它的 URL 需要指向一个 CGI 脚本 (也就是在 cgi-bin 或者 ht-bin 中的文件)。CGIHTTPRequestHandler 继承自 SimpleHTTPRequestHandler,所以也可以处理GET方法和HEAD方法的请求。此时,如果 URL 指向 CGI 脚本时,服务器将脚本的运行结果传送到客户端;当此时 URL 指向静态文件时,服务器将文件的内容传送到客户端。

更进一步,我可以让 CGI 脚本执行数据库操作,比如将接收到的数据放入到数据库中,以及更丰富的程序操作。相关内容从略。

 

总结

我使用了 Python 标准库中的一些高级包简化了 Python 服务器。最终的效果分离静态内容、CGI 应用和服务器,降低三者之间的耦合,让代码变得简单而容易维护。

希望你享受在自己的电脑上架设服务器的过程。

 

欢迎继续阅读“Python 快速教程