第 4 章 HTTP协议网络编程

本章攻略:

  • 从HTTP服务器下载数据
  • 在你的设备中伺服HTTP请求
  • 访问网站后提取cookie信息
  • 提交网页表单
  • 通过代理服务器发送Web请求
  • 使用HEAD请求检查网页是否存在
  • 把客户端伪装成Mozilla Firefox
  • 使用HTTP压缩节省Web请求消耗的带宽
  • 编写一个支持断点续传功能的HTTP容错客户端
  • 使用Python和OpenSSL编写一个简单的HTTPS服务器

4.1 简介

本章介绍Python HTTP网络库和一些第三方库的功能。例如,以一种更友好、更简洁的方式处理HTTP请求的requests库。其中有一个攻略用到了OpenSSL库,创建支持SSL的Web服务器。

多个攻略都介绍了HTTP协议的很多常规特性,例如使用POST请求提交网页表单、处理首部信息和使用压缩等。

4.2 从HTTP服务器下载数据

你可能想要编写一个简单的HTTP客户端,通过原生的HTTP协议从任意的Web服务器上下载一些数据。这是自己开发HTTP浏览器的第一步。

4.2.1 实战演练

我们要使用Python编写的微型浏览器访问www.python.org。这个浏览器使用Python中的httplib模块编写。

代码清单4-1说明了如何编写一个简单的HTTP客户端,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import argparse

  5. import httplib

  6. REMOTE_SERVER_HOST = 'www.python.org'

  7. REMOTE_SERVER_PATH = '/'

  8. class HTTPClient:

  9. def __init__(self, host):

  10. self.host = host

  11. def fetch(self, path):

  12. http = httplib.HTTP(self.host)

  13. # Prepare header

  14. http.putrequest("GET", path)

  15. http.putheader("User-Agent", __file__)

  16. http.putheader("Host", self.host)

  17. http.putheader("Accept", "/") http.endheaders()

  18. try:

  19. errcode, errmsg, headers = http.getreply()

  20. except Exception, e:

  21. print "Client failed error code: %s message:%s headers:%s" %(errcode, errmsg, headers) else:

  22. print "Got homepage from %s" %self.host

  23. file = http.getfile()

  24. return file.read()

  25. if __name__ == "__main__":

  26. parser = argparse.ArgumentParser(description='HTTP Client Example') parser.add_argument('--host', action="store", dest="host", default=REMOTE_SERVER_HOST) parser.add_argument('--path', action="store", dest="path", default=REMOTE_SERVER_PATH) given_args = parser.parse_args()

  27. host, path = given_args.host, given_args.path

  28. client = HTTPClient(host)

  29. print client.fetch(path)

这个攻略默认从www.python.org中获取一个网页。运行这个脚本时可以指定主机和路径参数,也可以不指定。运行脚本后会看到如下输出:

  1. $ python 4_1_download_data.py --host=www.python.org Got homepage from www.python.org

  2. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://

  3. www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

  4. <html xmlns="http://www.w3.og/1999/xhtml" xml:lang="en" lang="en"> <head>

如果运行脚本时指定的路径不存在,会显示如下的服务器响应:

  1. $ python 4_1_download_data.py --host='www.python.org' --path='not-exist'

  2. Got homepage from www.python.org

  3. <!DOCTYPE html PUBLIC "-/W3C//DTD XHTML 1.0 Transitional//EN" "http://

  4. www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

  5. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head>

  6. <meta http-equiv="content-type" content="text/html; charset=utf-8" > <title>Page Not Found<title>

4.2.2 原理分析

这个攻略使用Python的内置库httplib,定义了一个HTTPClient类,从远程主机上获取数据。在fetch()方法中使用HTTP()函数及其他辅助函数(例如putrequest()putheader())创建了一个虚拟的HTTP客户端,首先指定一个GET/path字符串,然后设定用户代理,其值为当前脚本(__file__)。

发起请求的getreply()方法放在一个try-except块中。响应通过getfile()方法获取,然后读取数据流中的内容。

4.3 在你的设备中伺服HTTP请求

你可能想编写一个自己的Web服务器,处理客户端请求,返回一个简单的欢迎消息。

4.3.1 实战演练

Python集成了一个非常简单的Web服务器,可以在命令行中启动,如下所示:

  1. $ python -m SimpleHTTPServer 8080

执行这个命令后会在端口8080上启动一个HTTP Web服务器。通过在浏览器中输入http://localhost:8080,可以访问这个服务器。你将看到的是运行上述命令时所在文件夹里的内容。如果这个文件夹中有能被Web服务器识别的索引文件,例如index.html,在浏览器中就会显示这个文件的内容。如果你想完全掌控Web服务器,就得启动自己定制的HTTP服务器。

代码清单4-2是这个定制的HTTP Web服务器,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import argparse

  5. import sys

  6. from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer

  7. DEFAULT_HOST = '127.0.0.1'

  8. DEFAULT_PORT = 8800

  9. class RequestHandler(BaseHTTPRequestHandler):

  10. """ Custom request handler"""

  11. def do_GET(self):

  12. """ Handler for the GET requests """

  13. self.send_response(200)

  14. self.send_header('Content-type','text/html')

  15. self.end_headers()

  16. # Send the message to browser

  17. self.wfile.write("Hello from server!")

  18. return

  19. class CustomHTTPServer(HTTPServer):

  20. "A custom HTTP server"

  21. def __init__(self, host, port):

  22. server_address = (host, port)

  23. HTTPServer.__init__(self, server_address, RequestHandler)

  24. def run_server(port):

  25. try:

  26. server= CustomHTTPServer(DEFAULT_HOST, port)

  27. print "Custom HTTP server started on port: %s" % port server.serve_forever()

  28. except Exception, err:

  29. print "Error:%s" %err

  30. except KeyboardInterrupt:

  31. print "Server interrupted and is shutting down..."

  32. server.socket.close()

  33. if __name__ == "__main__":

  34. parser = argparse.ArgumentParser(description='Simple HTTP Server Example') parser.add_argument('--port', action="store", dest="port", type=int, default=DEFAULT_PORT) given_args = parser.parse_args()

  35. port = given_args.port

  36. run\_server(port)

下面的截图是一个简单的HTTP服务器:

图4-1

运行这个Web服务器,然后在浏览器中访问,会看到浏览器中显示了一行文本“Hello from server!”,如下所示:

  1. $ python 4_2_simple_http_server.py --port=8800

  2. Custom HTTP server started on port: 8800

  3. localhost - - [18/Apr/2013 13:39:33] "GET HTTP1.1" 200 -

  4. localhost \- \- [18/Apr/2013 13:39:33] "GET favicon.ico HTTP1.1" 200

4.3.2 原理分析

在这个攻略中,我们定义了CustomHTTPServer类,它继承自HTTPServer类。在CustomHTTPServer类的构造方法中,设定了服务器地址和用户输入的端口号,还用到了RequestHandler类。客户端连到服务器上时,服务器就通过RequestHandler类处理请求。

RequestHandler类定义了处理客户端GET请求的方法。这个方法向客户端发送一个HTTP首部(状态码200),然后使用write()方法返回一个成功消息“Hello from server!”。

4.4 访问网站后提取cookie信息

很多网站使用cookie在你的本地硬盘中存储各种信息。你可能想要查看cookie中保存的信息,或者使用cookie自动登录网站。

4.4.1 实战演练

假设我们要登录流行的代码分享网站www.bitbucket.org,我们要在登录页面(https://bitbucket.org/ account/signin/?next=/)提交登录信息。登录页面的截图如下所示:

图4-2

我们要记下表单中几个字段的ID,然后决定提交哪些虚拟值。我们首先要访问登录页面,再访问首页,查看在cookie中保存了什么。

代码清单4-3说明了如何提取cookie信息,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import cookielib

  5. import urllib

  6. import urllib2

  7. ID_USERNAME = 'id_username'

  8. ID_PASSWORD = 'id_password'

  9. USERNAME = 'you@email.com'

  10. PASSWORD = 'mypassword'

  11. LOGIN_URL = 'https://bitbucket.org/account/signin/?next=/'

  12. NORMAL_URL = 'https://bitbucket.org/'

  13. def extract_cookie_info():

  14. """ Fake login to a site with cookie"""

  15. # setup cookie jar

  16. cj = cookielib.CookieJar()

  17. login_data = urllib.urlencode({ID_USERNAME : USERNAME, ID_PASSWORD : PASSWORD}) # create url opener

  18. opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) resp = opener.open(LOGIN_URL, login_data)

  19. # send login info

  20. for cookie in cj:

  21. print "----First time cookie: %s --> %s" %(cookie.name, cookie.value)

  22. print "Headers: %s" %resp.headers

  23. # now access without any login info

  24. resp = opener.open(NORMAL_URL)

  25. for cookie in cj:

  26. print "++++Second time cookie: %s --> %s" %(cookie.name, cookie.value)

  27. print "Headers: %s" %resp.headers

  28. if __name__ == '__main__':

  29. extract\_cookie\_info()

运行这个脚本后得到的输出如下:

  1. $ python 4_3_extract_cookie_information.py ----First time cookie: bb_session --> aed58dde1228571bf60466581790566d Headers: Server: nginx/1.2.4

  2. Date: Sun, 05 May 2013 15:13:56 GMT

  3. Content-Type: text/html; charset=utf-8

  4. Content-Length: 21167

  5. Connection: close

  6. X-Served-By: bitbucket04

  7. Content-Language: en

  8. X-Static-Version: c67fb01467cf

  9. Expires: Sun, 05 May 2013 15:13:56 GMT

  10. Vary: Accept-Language, Cookie

  11. Last-Modified: Sun, 05 May 2013 15:13:56 GMT

  12. X-Version: 14f9c66ad9db

  13. ETag: "3ba81d9eb350c295a453b5ab6e88935e"

  14. X-Request-Count: 310

  15. Cache-Control: max-age=0

  16. Set-Cookie: bb_session=aed58dde1228571bf60466581790566d; expires=Sun, 19-May-2013 15:13:56 GMT; httponly; Max-Age=1209600; Path=/; secure

  17. Strict-Transport-Security: max-age=2592000

  18. X-Content-Type-Options: nosniff

  19. ++++Second time cookie: bb_session --> aed58dde1228571bf60466581790566d Headers: Server: nginx/1.2.4

  20. Date: Sun, 05 May 2013 15:13:57 GMT

  21. Content-Type: text/html; charset=utf-8

  22. Content-Length: 36787

  23. Connection: close

  24. X-Served-By: bitbucket02

  25. Content-Language: en

  26. X-Static-Version: c67fb01467cf

  27. Vary: Accept-Language, Cookie

  28. X-Version: 14f9c66ad9db

  29. X-Request-Count: 97

  30. Strict-Transport-Security: max-age=2592000

  31. X\-Content\-Type\-Options: nosniff

4.4.2 原理分析

我们使用Python中的cookielib模块创建了一个cookie容器cj。登录数据使用urllib.urlencode()方法编码。urllib2模块中有个build_opener()方法,其参数是一个HTTPCookieProcessor类实例。我们要把之前创建的cookie容器传给HTTPCookieProcessor类的构造方法。urllib2.build_opener()方法的返回值是一个URL打开器。我们要调用这个打开器两次:一次访问登录页面,一次访问网站的首页。从响应的首部可以看出,在Set-Cookie首部中只设定了一个cookie,即bb_sessioncookielib模块的更多信息可以在Python官方文档中查看,网址是http://docs.python.org/2/library/cookielib.html

4.5 提交网页表单

浏览网络时,一天之中我们要提交好多次网页表单。现在,我们要使用Python代码提交表单。

4.5.1 准备工作

这个攻略用到了一个Python第三方模块,叫作requests。这个模块的安装方法参见安装指南:http://docs.python-requests.org/en/latest/user/install/。例如,可以在命令行中使用pip安装requests模块,如下所示:

  1. $ pip install requests

4.5.2 实战演练

让我们来提交一些虚拟数据,注册Twitter账户。提交表单可以使用两种请求方法:GETPOST。不太敏感的数据,例如搜索查询,一般使用GET请求提交。敏感的数据则通过POST请求发送。我们来试一下使用这两种方法提交数据。

代码清单4-4说明了如何提交网页表单,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import requests

  5. import urllib

  6. import urllib2

  7. ID_USERNAME = 'signup-user-name'

  8. ID_EMAIL = 'signup-user-email'

  9. ID_PASSWORD = 'signup-user-password'

  10. USERNAME = 'username'

  11. EMAIL = 'you@email.com'

  12. PASSWORD = 'yourpassword'

  13. SIGNUP_URL = 'https://twitter.com/account/create'

  14. def submit_form():

  15. """Submit a form"""

  16. payload = {ID_USERNAME : USERNAME,

  17. ID_EMAIL : EMAIL,

  18. ID_PASSWORD : PASSWORD,}

  19. # make a get request

  20. resp = requests.get(SIGNUP_URL)

  21. print "Response to GET request: %s" %resp.content

  22. # send POST request

  23. resp = requests.post(SIGNUP_URL, payload)

  24. print "Headers from a POST request response: %s" %resp.headers #print "HTML Response: %s" %resp.read()

  25. if __name__ == '__main__':

  26. submit\_form()

运行这个脚本后,会看到如下输出:

  1. $ python 4_4_submit_web_form.py Response to GET request: <?xml version="1.0" encoding="UTF-8"?> <hash>

  2. <error>This method requires a POST.</error>

  3. <request>/account/create</request>

  4. </hash>

  5. Headers from a POST request response: {'status': '200 OK', 'content-length': '21064', 'set-cookie': 'twittersess=BAh7CD--

  6. d2865d40d1365eeb2175559dc5e6b99f64ea39ff; domain=.twitter.com; path=/; HttpOnly', 'expires': 'Tue, 31 Mar 1981 05:00:00 GMT', 'vary': 'Accept-Encoding', 'last-modified': 'Sun, 05 May 2013

  7. 15:59:27 GMT', 'pragma': 'no-cache', 'date': 'Sun, 05 May 2013

  8. 15:59:27 GMT', 'x\-xss\-protection': '1; mode=block', 'x\-transaction': 'a4b425eda23b5312', 'content\-encoding': 'gzip', 'strict\-transport\-security': 'max\-age=631138519', 'server': 'tfe', 'x\-mid': 'f7cde9a3f3d111310427116adc90bf3e8c95e868', 'x\-runtime': '0.09969', 'etag': '"7af6f92a7f7b4d37a6454caa6094071d"', 'cache\-control': 'no\-cache, no\-store, must\-revalidate, pre\-check=0, post\-check=0', 'x\-frame\-options': 'SAMEORIGIN', 'content\-type': 'text/html; charset=utf\-8'}

4.5.3 原理分析

这个攻略使用了第三方模块requests。这个模块提供了便利的包装方法get()post(),能正确编码URL中的数据并提交表单。

在这个攻略中,我们创建了一个数据字典,包含用户名、密码和电子邮件地址,用于注册Twitter账户。我们首先使用GET方法提交表单,但Twitter返回一个错误,说页面只支持POST方法。然后我们使用POST方法提交数据,结果Twitter接受了注册请求,这一点可以由首部数据证实。

4.6 通过代理服务器发送Web请求

你可能想通过代理访问网页。如果你为浏览器配置了一个代理服务器,而且代理可用,就可以运行这个攻略。否则,可以使用网上其他可用的公共代理服务器。

4.6.1 准备工作

你需要一个可使用的代理服务器。你可以使用谷歌或其他搜索引擎找到一个免费的代理服务器。这里,为了演示,我们使用的代理服务器是165.24.10.8

4.6.2 实战演练

我们来通过一个公共代理服务器发送HTTP请求。

代码清单4-5说明了如何通过代理服务器发送Web请求,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import urllib

  5. URL = 'https://www.github.com'

  6. PROXY_ADDRESS = "165.24.10.8:8080" # By Googling free proxy server

  7. if __name__ == '__main__':

  8. resp = urllib.urlopen(URL, proxies = {"http" : PROXY\_ADDRESS}) print "Proxy server returns response headers: %s " %resp.headers

运行这个脚本后,会看到如下输出:

  1. $ python 4_5_proxy_web_request.py Proxy server returns response headers: Server: GitHub.com

  2. Date: Sun, 05 May 2013 16:16:04 GMT

  3. Content-Type: text/html; charset=utf-8

  4. Connection: close

  5. Status: 200 OK

  6. Cache-Control: private, max-age=0, must-revalidate

  7. Strict-Transport-Security: max-age=2592000

  8. X-Frame-Options: deny

  9. Set-Cookie: logged_in=no; domain=.github.com; path=/; expires=Thu, 05-May-2033 16:16:04 GMT; HttpOnly Set-Cookie: ghsess=BAh7...; path=/; expires=Sun, 01-Jan-2023 00:00:00

  10. GMT; secure; HttpOnly

  11. X-Runtime: 8

  12. ETag: "66fcc37865eb05c19b2d15fbb44cd7a9"

  13. Content-Length: 10643

  14. Vary: Accept\-Encoding

4.6.3 原理分析

这个攻略很简短,使用在谷歌中找到的一个公共代理服务器访问社会化代码分享网站www.github.com。代理服务器的地址传给urllib模块的urlopen()方法。我们把响应的HTTP首部打印出来,以证明代理设置起到了作用。

4.7 使用HEAD请求检查网页是否存在

你可能想在不下载HTML内容的前提下检查网页是否存在。此时我们要使用浏览器客户端发送get HEAD请求。根据维基百科中的定义,HEAD请求和GET请求的响应一样,只是前者没有响应主体。使用HEAD请求可以获取响应首部中的元信息,而不用传输整个网页的内容。

4.7.1 实战演练

我们要向www.python.org发送一个HEAD请求。这个请求不会下载首页的内容,而是检查服务器是否返回正确的响应,例如OKFOUNDMOVED PERMANENTLY等。

代码清单4-6说明了如何使用HEAD请求检查网页,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import argparse

  5. import httplib

  6. import urlparse

  7. import re

  8. import urllib

  9. DEFAULT_URL = 'http://www.python.org'

  10. HTTP_GOOD_CODES = [httplib.OK, httplib.FOUND, httplib.MOVED_PERMANENTLY]

  11. def get_server_status_code(url):

  12. """

  13. Download just the header of a URL and

  14. return the server's status code.

  15. """

  16. host, path = urlparse.urlparse(url)[1:3]

  17. try:

  18. conn = httplib.HTTPConnection(host)

  19. conn.request('HEAD', path)

  20. return conn.getresponse().status

  21. except StandardError:

  22. return None

  23. if __name__ == '__main__':

  24. parser = argparse.ArgumentParser(description='Example HEAD Request') parser.add_argument('--url', action="store", dest="url", default=DEFAULT_URL) given_args = parser.parse_args()

  25. url = given_args.url

  26. if get_server_status_code(url) in HTTP_GOOD_CODES:

  27. print "Server: %s status is OK: " %url

  28. else:

  29. print "Server: %s status is NOT OK!" %url

运行这个脚本后,会根据HEAD请求的响应显示成功消息或错误消息,如下所示:

  1. $ python 4_6_checking_webpage_with_HEAD_request.py Server: http://www.python.org status is OK!

  2. $ python 4\_6\_checking\_webpage\_with\_HEAD\_request.py \-\-url=http://www.zytho.org Server: http://www.zytho.org status is NOT OK!

4.7.2 原理分析

我们使用httplib模块中的HTTPConnection()方法向服务器发起HEAD请求。如果需要,可以指定要访问的路径。在这个攻略中,HTTPConnection()方法检查的是www.python.org首页。如果URL不正确,在返回码的可接受列表中就无法找到返回的响应。

4.8 把客户端伪装成Mozilla Firefox

在Python代码中,你可能想假装成在使用Mozilla Firefox访问Web服务器。

4.8.1 实战演练

你可以在HTTP请求首部中发送自己定制的用户代理值。

代码清单4-7说明了如何把客户端伪装成Mozilla Firefox浏览器,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import urllib2

  5. BROWSER = 'Mozilla/5.0 (Windows NT 5.1; rv:20.0) Gecko/20100101 Firefox/20.0'

  6. URL = 'http://www.python.org'

  7. def spoof_firefox():

  8. opener = urllib2.build_opener()

  9. opener.addheaders = [('User-agent', BROWSER)]

  10. result = opener.open(URL)

  11. print "Response headers:"

  12. for header in result.headers.headers:

  13. print "\t",header

  14. if __name__ == '__main__':

  15. spoof\_firefox()

运行这个脚本后,会看到如下输出:

  1. $ python 4_7_spoof_mozilla_firefox_in_client_code.py Response headers:

  2. Date: Sun, 05 May 2013 16:56:36 GMT

  3. Server: Apache/2.2.16 (Debian)

  4. Last-Modified: Sun, 05 May 2013 00:51:40 GMT

  5. ETag: "105800d-5280-4dbedfcb07f00"

  6. Accept-Ranges: bytes

  7. Content-Length: 21120

  8. Vary: Accept-Encoding

  9. Connection: close

  10. Content\-Type: text/html

4.8.2 原理分析

我们使用urllib2模块中的build_opener()方法创建自定义浏览器,把用户代理字符串设为Mozilla/5.0 (Windows NT 5.1; rv:20.0) Gecko/20100101 Firefox/20.0

4.9 使用HTTP压缩节省Web请求消耗的带宽

你可能想让Web服务器在下载网页时有更好的性能表现。压缩HTTP数据能提升伺服网页内容的速度。

4.9.1 实战演练

我们来编写一个Web服务器,把内容压缩成gzip格式后再提供给访问者。

代码清单4-8说明了如何压缩HTTP数据,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import argparse

  5. import string

  6. import os

  7. import sys

  8. import gzip

  9. import cStringIO

  10. from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer

  11. DEFAULT_HOST = '127.0.0.1'

  12. DEFAULT_PORT = 8800

  13. HTML_CONTENT = """<html><body><h1>Compressed Hello World!</h1></body></html>"""

  14. class RequestHandler(BaseHTTPRequestHandler):

  15. """ Custom request handler"""

  16. def do_GET(self):

  17. """ Handler for the GET requests """

  18. self.send_response(200)

  19. self.send_header('Content-type','text/html')

  20. self.send_header('Content-Encoding','gzip')

  21. zbuf = self.compress_buffer(HTML_CONTENT)

  22. sys.stdout.write("Content-Encoding: gzip\r\n")

  23. self.send_header('Content-Length',len(zbuf))

  24. self.end_headers()

  25. # Send the message to browser

  26. zbuf = self.compress_buffer(HTML_CONTENT)

  27. sys.stdout.write("Content-Encoding: gzip\r\n")

  28. sys.stdout.write("Content-Length: %d\r\n" % (len(zbuf))) sys.stdout.write("\r\n")

  29. self.wfile.write(zbuf)

  30. return

  31. def compress_buffer(self, buf):

  32. zbuf = cStringIO.StringIO()

  33. zfile = gzip.GzipFile(mode = 'wb', fileobj = zbuf, compresslevel = 6) zfile.write(buf)

  34. zfile.close()

  35. return zbuf.getvalue()

  36. if __name__ == '__main__':

  37. parser = argparse.ArgumentParser(description='Simple HTTP Server Example') parser.add_argument('--port', action="store", dest="port", type=int, default=DEFAULT_PORT) given_args = parser.parse_args()

  38. port = given_args.port

  39. server_address = (DEFAULT_HOST, port)

  40. server = HTTPServer(server_address, RequestHandler)

  41. server.serve\_forever()

运行这个脚本后,在浏览器中访问http://localhost:8800,会看到浏览器中显示了文本“Compressed Hello World!”(HTTP压缩后得到的结果),如下所示:

  1. $ python 4_8_http_compression.py localhost - - [22/Feb/2014 12:01:26] "GET HTTP1.1" 200 -

  2. Content-Encoding: gzip

  3. Content-Encoding: gzip

  4. Content-Length: 71

  5. localhost - - [22/Feb/2014 12:01:26] "GET favicon.ico HTTP1.1" 200 -

  6. Content-Encoding: gzip

  7. Content-Encoding: gzip

  8. Content\-Length: 71

下面的截图展示了Web服务器伺服压缩内容的过程:

图4-3

4.9.2 原理分析

我们实例化BaseHTTPServer模块中的HTTPServer类,创建了一个Web服务器。然后为这个服务器实例定义了一个请求处理方法,它使用compress_buffer()方法压缩发给客户端的每个响应,再把事先定义好的HTML内容发送给客户端。

4.10 编写一个支持断点续传功能的HTTP容错客户端

你可能想编写一个容错的客户端,它在因某种原因初次下载失败后能继续下载文件。

4.10.1 实战演练

让我们从www.python.org上下载Python 2.7的源码。使用resume_download()函数下载的文件在中止后能继续下载尚未下载的内容。

代码清单4-9说明了如何继续下载,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. import urllib

  5. import os

  6. TARGET_URL = 'http://python.org/ftp/python/2.7.4/'

  7. TARGET_FILE = 'Python2.7.4.tgz'

  8. class CustomURLOpener(urllib.FancyURLopener):

  9. """Override FancyURLopener to skip error 206 (when a

  10. partial file is being sent)

  11. """

  12. def http_error_206(self, url, fp, errcode, errmsg, headers, data=None): pass

  13. def resume_download():

  14. file_exists = False

  15. CustomURLClass = CustomURLOpener()

  16. if os.path.exists(TARGET_FILE):

  17. out_file = open(TARGET_FILE,"ab")

  18. file_exists = os.path.getsize(TARGET_FILE)

  19. #If the file exists, then only download the unfinished part CustomURLClass.addheader("range","bytes=%s-" % (file_exists)) else:

  20. out_file = open(TARGET_FILE,"wb")

  21. web_page = CustomURLClass.open(TARGET_URL + TARGET_FILE)

  22. #Check if last download was OK

  23. if int(web_page.headers['Content-Length']) == file_exists: loop = 0

  24. print "File already downloaded!"

  25. byte_count = 0

  26. while True:

  27. data = web_page.read(8192)

  28. if not data:

  29. break

  30. out_file.write(data)

  31. byte_count = byte_count + len(data)

  32. web_page.close()

  33. out_file.close()

  34. for k,v in web_page.headers.items():

  35. print k, "=",v

  36. print "File copied", byte_count, "bytes from", web_page.url

  37. if __name__ == '__main__':

  38. resume\_download()

运行这个脚本后,会看到如下输出结果:

  1. $ python 4_9_http_fail_over_client.py content-length = 14489063

  2. content-encoding = x-gzip

  3. accept-ranges = bytes

  4. connection = close

  5. server = Apache/2.2.16 (Debian)

  6. last-modified = Sat, 06 Apr 2013 14:16:10 GMT

  7. content-range = bytes 0-14489062/14489063

  8. etag = "1748016-dd15e7-4d9b1d8685e80"

  9. date = Tue, 07 May 2013 12:51:31 GMT

  10. content-type = application/x-tar

  11. File copied 14489063 bytes from http://python.org/ftp/python/2.7.4/Python2.7.4.tgz

4.10.2 原理分析

在这个攻略中,我们定义了一个URL打开器类,继承自urllib模块中的FancyURLopener类,不过重定义了用于分段下载内容的http_error_206()方法。resume_download()函数首先检查目标文件是否存在,如果不存在就尝试使用自定义的URL打开器类下载。

4.11 使用Python和OpenSSL编写一个简单的HTTPS服务器

你需要使用Python编写一个安全的Web服务器,而且已经有了SSL密钥和证书文件。

4.11.1 准备工作

你需要安装第三方Python模块pyOpenSSL。这个模块可从PyPI上下载,地址为https://pypi.python.org/pypi/pyOpenSSL。在Windows和Linux主机上都要安装一些其他的包,在http://pythonhosted.org/pyOpenSSL/中有说明。

4.11.2 实战演练

把证书文件放在当前工作目录后,我们就可以创建一个Web服务器,利用这个证书向客户端发送加密后的内容。

代码清单4-10是安全HTTP服务器的代码,如下所示:

  1. #!usrbin/env python # Python Network Programming Cookbook -- Chapter - 4

  2. # This program is optimized for Python 2.7.

  3. # It may run on any other version with/without modifications.

  4. # Requires pyOpenSSL and SSL packages installed

  5. import socket, os

  6. from SocketServer import BaseServer

  7. from BaseHTTPServer import HTTPServer

  8. from SimpleHTTPServer import SimpleHTTPRequestHandler

  9. from OpenSSL import SSL

  10. class SecureHTTPServer(HTTPServer):

  11. def __init__(self, server_address, HandlerClass):

  12. BaseServer.__init__(self, server_address, HandlerClass) ctx = SSL.Context(SSL.SSLv23_METHOD)

  13. fpem = 'server.pem' # location of the server private key and the server certificate ctx.use_privatekey_file (fpem)

  14. ctx.use_certificate_file(fpem)

  15. self.socket = SSL.Connection(ctx, socket.socket(self.address_family, self.socket_type)) self.server_bind()

  16. self.server_activate()

  17. class SecureHTTPRequestHandler(SimpleHTTPRequestHandler):

  18. def setup(self):

  19. self.connection = self.request

  20. self.rfile = socket._fileobject(self.request, "rb", self.rbufsize) self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)

  21. def run_server(HandlerClass = SecureHTTPRequestHandler,

  22. ServerClass = SecureHTTPServer):

  23. server_address = ('', 4443) # port needs to be accessible by user server = ServerClass(server_address, HandlerClass)

  24. running_address = server.socket.getsockname()

  25. print "Serving HTTPS Server on %s:%s ..." %(running_address[0], running_address[1]) server.serve_forever()

  26. if __name__ == '__main__':

  27. run\_server()

运行这个脚本后会看到如下输出:

  1. $ python 4_10_https_server.py Serving HTTPS Server on 0.0.0.0:4443 ...

4.11.3 原理分析

如果你仔细观察前面编写Web服务器的攻略会发现,它和这个攻略没有太多的区别,基本流程都是一样的。最重要的一个不同点是,这个脚本调用了SSL.Context()方法,并将其参数设为SSL.SSLv23_METHOD。我们使用Python OpenSSL第三方模块提供的Connection类创建了一个SSL套接字。Connection类构造方法的参数是前面创建的上下文对象,以及地址族和套接字类型。

服务器的证书文件保存在当前目录中。证书文件提供给上下文对象使用。最后,调用server_activate()方法激活服务器。