Python-网络编程秘籍-全-

Python 网络编程秘籍(全)

原文:zh.annas-archive.org/md5/be2c3b37614dba23c0b929ca251cb0f0

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

全部赞美归于上帝!我很高兴这本书现在已经出版,并且我想感谢所有为这本书的出版做出贡献的人。本书是 Python 网络编程的探索性指南。它触及了广泛的网络协议,如 TCP/UDP、HTTP/HTTPS、FTP、SMTP、POP3、IMAP、CGI 等。凭借 Python 的力量和交互性,它为网络和系统管理、Web 应用程序开发、与本地和远程网络交互、低级网络数据包捕获和分析等现实世界任务编写各种脚本带来了乐趣和快乐。本书的主要重点是让您在所涵盖的主题上获得实践经验。因此,本书涉及的理论较少,但内容丰富,实用性强。

本书以“DevOps”思维模式编写,在这种模式下,开发者也多少负责运营工作,即部署应用程序并管理其各个方面,例如远程服务器管理、监控、扩展和优化以获得更好的性能。本书向您介绍了一系列开源的第三方 Python 库,它们在各种用例中都非常易于使用。我每天都在使用这些库中的许多来享受自动化我的 DevOps 任务。例如,我使用 Fabric 来自动化软件部署任务,以及其他库用于其他目的,例如在互联网上搜索事物、屏幕抓取或从 Python 脚本中发送电子邮件。

我希望您会喜欢本书中提供的食谱,并将它们扩展以使它们更加强大和有趣。

本书涵盖的内容

第一章,套接字、IPv4 和简单的客户端/服务器编程,通过一系列小任务向您介绍 Python 的核心网络库,并使您能够创建您的第一个客户端/服务器应用程序。

第二章,多路复用套接字 I/O 以获得更好的性能,讨论了使用默认和第三方库扩展您的客户端/服务器应用程序的各种有用技术。

第三章,IPv6、Unix 域套接字和网络接口,更多地关注管理您的本地机器和照顾您的本地局域网。

第四章,使用 HTTP 进行互联网编程,使您能够创建具有各种功能的小型命令行浏览器,例如提交 Web 表单、处理 Cookies、管理部分下载、压缩数据以及通过 HTTPS 提供安全内容。

第五章,电子邮件协议、FTP 和 CGI 编程,为您带来自动化 FTP 和电子邮件任务(如操作您的 Gmail 账户、从脚本中读取或发送电子邮件或为您的 Web 应用程序创建留言簿)的乐趣。

第六章,屏幕抓取和其他实用应用,介绍了各种第三方 Python 库,它们执行一些实用任务,例如在谷歌地图上定位公司、从维基百科抓取信息、在 GitHub 上搜索代码库或从 BBC 读取新闻。

第七章,跨机器编程,让您体验通过 SSH 自动化系统管理和部署任务。您可以从笔记本电脑远程运行命令、安装软件包或设置新的网站。

第八章,与 Web 服务协作 – XML-RPC、SOAP 和 REST,介绍了各种 API 协议,如 XML-RPC、SOAP 和 REST。您可以通过编程方式向任何网站或 Web 服务请求信息并与它们交互。例如,您可以在亚马逊或谷歌上搜索产品。

第九章,网络监控和安全,介绍了捕获、存储、分析和操作网络数据包的各种技术。这鼓励您进一步使用简洁的 Python 脚本来调查您的网络安全问题。

您需要为这本书准备的东西

您需要一个运行良好的 PC 或笔记本电脑,最好使用任何现代 Linux 操作系统,如 Ubuntu、Debian、CentOS 等。本书中的大多数食谱在其他平台(如 Windows 和 Mac OS)上也能运行。

您还需要一个有效的互联网连接来安装文中提到的第三方软件库。如果您没有互联网连接,您可以下载这些第三方库并一次性安装。

以下是一个包含其下载 URL 的第三方库列表:

运行某些食谱所需的非 Python 软件如下:

本书面向对象

如果你是一名网络程序员、系统/网络管理员或 Web 应用开发者,这本书非常适合你。你应该对 Python 编程语言和 TCP/IP 网络概念有基本的了解。然而,如果你是新手,你将在阅读本书的过程中逐渐理解这些概念。本书可作为任何网络编程学术课程中开发动手技能的补充材料。

习惯用法

在这本书中,你会发现许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码词汇、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:

如果你需要知道远程机器的 IP 地址,你可以使用内置库函数gethostbyname()

代码块设置如下:

def test_socket_timeout():
   s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  print "Default socket timeout: %s" %s.gettimeout()
  s.settimeout(100)
  print "Current socket timeout: %s" %s.gettimeout()

任何命令行输入或输出都如下所示:

$ python 2_5_echo_server_with_diesel.py --port=8800
[2013/04/08 11:48:32] {diesel} WARNING:Starting diesel <hand-rolledselect.epoll>

新术语重要词汇以粗体显示。

注意

警告或重要注意事项以如下框的形式出现。

小贴士

小贴士和技巧看起来像这样。

读者反馈

我们欢迎读者的反馈。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。

要发送一般反馈,只需将电子邮件发送到<feedback@packtpub.com>,并在邮件主题中提及书名。

如果你在某个领域有专业知识,并且对撰写或参与一本书感兴趣,请参阅我们的作者指南 www.packtpub.com/authors

客户支持

现在你已经是 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助你从购买中获得最大收益。

下载示例代码

您可以从 www.packtpub.com 的账户下载您购买的 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。

错误清单

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书籍,点击错误提交表单链接,并输入您的错误详细信息来报告它们。一旦您的错误得到验证,您的提交将被接受,错误将被上传到我们的网站,或添加到该标题的错误清单中。任何现有的错误清单都可以通过从 www.packtpub.com/support 选择您的标题来查看。

盗版

互联网上版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。

请通过 <copyright@packtpub.com> 与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者和提供有价值内容方面的帮助。

询问

如果你在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 联系我们,我们将尽力解决。

第一章:套接字、IPv4 和简单的客户端/服务器编程

在本章中,我们将涵盖以下菜谱:

  • 打印你的机器名称和 IPv4 地址

  • 获取远程机器的 IP 地址

  • 将 IPv4 地址转换为不同的格式

  • 根据端口号和协议查找服务名

  • 将整数从主机字节序转换为网络字节序,反之亦然

  • 设置和获取默认套接字超时

  • 优雅地处理套接字错误

  • 修改套接字的发送/接收缓冲区大小

  • 将套接字改为阻塞/非阻塞模式

  • 重新使用套接字地址

  • 从互联网时间服务器打印当前时间

  • 编写 SNTP 客户端

  • 编写简单的回声客户端/服务器应用程序

简介

本章通过一些简单的菜谱介绍了 Python 的核心网络库。Python 的socket模块既有基于类的工具,也有基于实例的工具。基于类的和基于实例的方法之间的区别在于前者不需要套接字对象的实例。这是一个非常直观的方法。例如,为了打印你的机器的 IP 地址,你不需要套接字对象。相反,你只需调用套接字的基于类的方法。另一方面,如果你需要向服务器应用程序发送一些数据,创建一个套接字对象来执行该显式操作会更直观。本章中提供的菜谱可以分为以下三个组:

  • 在前几个菜谱中,已经使用了基于类的工具来提取有关主机、网络和任何目标服务的有用信息。

  • 之后,使用基于实例的工具提供了一些更多的菜谱。演示了一些常见的套接字任务,包括操作套接字超时、缓冲区大小、阻塞模式等等。

  • 最后,使用基于类的和基于实例的工具构建了一些客户端,它们执行一些实际的任务,例如将机器时间与互联网服务器同步或编写通用的客户端/服务器脚本。

你可以使用这些演示的方法来编写你自己的客户端/服务器应用程序。

打印你的机器名称和 IPv4 地址

有时候,你需要快速查找有关你的机器的一些信息,例如主机名、IP 地址、网络接口数量等等。使用 Python 脚本实现这一点非常简单。

准备工作

在开始编码之前,你需要在你的机器上安装 Python。Python 在大多数 Linux 发行版中都是预安装的。对于 Microsoft Windows 操作系统,你可以从 Python 网站下载二进制文件:www.python.org/download/

你可以查阅你操作系统的文档,检查和审查你的 Python 设置。在你机器上安装 Python 之后,你可以通过在命令行中键入python来尝试打开 Python 解释器。这将显示解释器提示符>>>,应该类似于以下输出:

~$ python 
Python 2.7.1+ (r271:86832, Apr 11 2011, 18:05:24) 
[GCC 4.5.2] on linux2 
Type "help", "copyright", "credits" or "license" for more information. >>> 

如何操作...

由于这个菜谱非常简短,您可以在 Python 解释器中交互式地尝试它。

首先,我们需要使用以下命令导入 Python 的socket库:

>>> import socket

然后,我们调用socket库中的gethostname()方法,并将结果存储在变量中,如下所示:

>>> host_name = socket.gethostname()
>>> print "Host name: %s" %host_name
Host name: debian6
>>> print "IP address: %s" %socket.gethostbyname(host_name)
IP address: 127.0.1.1

整个活动可以封装在一个独立的函数print_machine_info()中,它使用内置的 socket 类方法。

我们从通常的 Python __main__块调用我们的函数。在运行时,Python 将值分配给一些内部变量,例如__name__。在这种情况下,__name__指的是调用进程的名称。当从命令行运行此脚本时,如以下命令所示,名称将是__main__,但如果模块是从另一个脚本导入的,则名称将不同。这意味着当模块从命令行调用时,它将自动运行我们的print_machine_info函数;然而,当单独导入时,用户需要显式调用该函数。

列表 1.1 显示了如何获取我们的机器信息,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter -1 
# This program is optimized for Python 2.7\. It may run on any
# other Python version with/without modifications.

import socket

def print_machine_info():
    host_name = socket.gethostname()
    ip_address = socket.gethostbyname(host_name)
    print "Host name: %s" % host_name
    print "IP address: %s" % ip_address

if __name__ == '__main__':
    print_machine_info()

为了运行这个菜谱,您可以使用提供的源文件从命令行如下操作:

$ python 1_1_local_machine_info.py

在我的机器上,以下输出如下:

Host name: debian6
IP address: 127.0.0.1

这个输出将取决于您机器的系统主机配置而有所不同。

它是如何工作的...

import socket语句导入 Python 的核心网络库之一。然后,我们使用两个实用函数,gethostname()gethostbyname(host_name)。您可以在命令行中输入help(socket.gethostname)来查看在线帮助信息。或者,您可以在您的网络浏览器中输入以下地址:docs.python.org/3/library/socket.html。您可以参考以下命令:

gethostname(...)
 gethostname() -> string 
 Return the current host name. 

gethostbyname(...) 
 gethostbyname(host) -> address 
 Return the IP address (a string of the form '255.255.255.255') for a host.

第一个函数不接受任何参数,并返回当前或本地主机名。第二个函数接受一个hostname参数,并返回其 IP 地址。

获取远程机器的 IP 地址

有时候,您需要将一台机器的主机名转换为其对应的 IP 地址,例如,进行快速域名查找。这个菜谱介绍了一个简单的函数来完成这个任务。

如何操作...

如果您需要知道远程机器的 IP 地址,您可以使用内置库函数gethostbyname()。在这种情况下,您需要将远程主机名作为其参数传递。

在这种情况下,我们需要调用gethostbyname()类函数。让我们看看这个简短代码片段的内部。

列表 1.2 显示了如何获取远程机器的 IP 地址,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 1
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import socket

def get_remote_machine_info():
    remote_host = 'www.python.org'
    try:
        print "IP address: %s" %socket.gethostbyname(remote_host)
    except socket.error, err_msg:
        print "%s: %s" %(remote_host, err_msg)

if __name__ == '__main__':
    get_remote_machine_info()

如果您运行前面的代码,它将给出以下输出:

$ python 1_2_remote_machine_info.py 
IP address of www.python.org: 82.94.164.162

它是如何工作的...

这个配方将gethostbyname()方法封装在一个用户定义的函数get_remote_machine_info()中。在这个配方中,我们介绍了异常处理的概念。正如你所看到的,我们将主要函数调用封装在一个try-except块中。这意味着如果在执行此函数期间发生某些错误,此错误将由这个try-except块处理。

例如,让我们更改remote_host值,并将www.python.org替换为不存在的某个内容,例如www.pytgo.org。现在运行以下命令:

$ python 1_2_remote_machine_info.py 
www.pytgo.org: [Errno -5] No address associated with hostname

try-except块捕获错误并向用户显示错误消息,指出没有与主机名www.pytgo.org关联的 IP 地址。

将 IPv4 地址转换为不同的格式

当你想要处理低级网络功能时,有时,IP 地址的常规字符串表示法并不很有用。它们需要转换为 32 位的打包二进制格式。

如何操作...

Python 套接字库有处理各种 IP 地址格式的实用工具。在这里,我们将使用其中的两个:inet_aton()inet_ntoa()

让我们创建一个convert_ip4_address()函数,其中将使用inet_aton()inet_ntoa()进行 IP 地址转换。我们将使用两个示例 IP 地址,127.0.0.1192.168.0.1

列表 1.3 显示了ip4_address_conversion如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 1
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import socket
from binascii import hexlify

def convert_ip4_address():
    for ip_addr in ['127.0.0.1', '192.168.0.1']:
        packed_ip_addr = socket.inet_aton(ip_addr)
        unpacked_ip_addr = socket.inet_ntoa(packed_ip_addr)
        print "IP Address: %s => Packed: %s, Unpacked: %s"\
	 %(ip_addr, hexlify(packed_ip_addr), unpacked_ip_addr)

if __name__ == '__main__':
    convert_ip4_address()

现在,如果你运行这个配方,你会看到以下输出:

$ python 1_3_ip4_address_conversion.py 

IP Address: 127.0.0.1 => Packed: 7f000001, Unpacked: 127.0.0.1
IP Address: 192.168.0.1 => Packed: c0a80001, Unpacked: 192.168.0.1

它是如何工作的...

在这个配方中,两个 IP 地址已经使用for-in语句从字符串转换为 32 位打包格式。此外,还调用了来自binascii模块的 Python hexlify函数。这有助于以十六进制格式表示二进制数据。

根据端口和协议查找服务名称

如果你想要发现网络服务,确定使用 TCP 或 UDP 协议在哪些端口上运行哪些网络服务可能会有所帮助。

准备工作

如果你知道网络服务的端口号,你可以使用套接字库中的getservbyport()套接字类函数来查找服务名称。在调用此函数时,你可以选择性地提供协议名称。

如何操作...

让我们定义一个find_service_name()函数,其中将使用getservbyport()套接字类函数调用几个端口,例如80, 25。我们可以使用 Python 的for-in循环结构。

列表 1.4 显示了finding_service_name如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter -  1
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import socket

def find_service_name():
    protocolname = 'tcp'
    for port in [80, 25]:
        print "Port: %s => service name: %s" %(port, socket.getservbyport(port, protocolname))
    print "Port: %s => service name: %s" %(53, socket.getservbyport(53, 'udp'))

if __name__ == '__main__':
    find_service_name()

如果你运行此脚本,你会看到以下输出:

$ python 1_4_finding_service_name.py 

Port: 80 => service name: http
Port: 25 => service name: smtp
Port: 53 => service name: domain

它是如何工作的...

在这个配方中,使用for-in语句遍历一系列变量。因此,对于每次迭代,我们使用一个 IP 地址,以打包和未打包的格式转换它们。

将整数从主机字节序转换为网络字节序以及反向转换

如果你需要编写一个低级网络应用程序,可能需要处理两个机器之间通过线缆的低级数据传输。这种操作需要将数据从本地主机操作系统转换为网络格式,反之亦然。这是因为每个都有自己的数据特定表示。

如何操作...

Python 的 socket 库提供了从网络字节序转换为主机字节序以及相反方向的工具。你可能需要熟悉它们,例如,ntohl()/htonl()

让我们定义一个convert_integer()函数,其中使用ntohl()/htonl() socket 类函数来转换 IP 地址格式。

列表 1.5 显示了integer_conversion如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
import socket
def convert_integer():
    data = 1234
    # 32-bit
    print "Original: %s => Long  host byte order: %s, Network byte order: %s"\
    %(data, socket.ntohl(data), socket.htonl(data))
    # 16-bit
    print "Original: %s => Short  host byte order: %s, Network byte order: %s"\
    %(data, socket.ntohs(data), socket.htons(data))
if __name__ == '__main__':
    convert_integer()

如果你运行这个菜谱,你会看到以下输出:

$ python 1_5_integer_conversion.py 
Original: 1234 => Long  host byte order: 3523477504, Network byte order: 3523477504
Original: 1234 => Short  host byte order: 53764, Network byte order: 53764

它是如何工作的...

在这里,我们取一个整数并展示如何将其在网络字节序和主机字节序之间转换。ntohl() socket 类函数将网络字节序从长格式转换为主机字节序。在这里,n代表网络,h代表主机;l代表长,s代表短,即 16 位。

设置和获取默认 socket 超时

有时,你需要操作 socket 库某些属性的默认值,例如,socket 超时。

如何操作...

你可以创建一个 socket 对象实例,并调用gettimeout()方法来获取默认超时值,以及调用settimeout()方法来设置特定的超时值。这在开发自定义服务器应用程序时非常有用。

我们首先在test_socket_timeout()函数内部创建一个 socket 对象。然后,我们可以使用 getter/setter 实例方法来操作超时值。

列表 1.6 显示了socket_timeout如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 1
# This program is optimized for Python 2.7\. It may run on any   
# other Python version with/without modifications

import socket

def test_socket_timeout():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    print "Default socket timeout: %s" %s.gettimeout()
    s.settimeout(100)
    print "Current socket timeout: %s" %s.gettimeout()    

if __name__ == '__main__':
    test_socket_timeout()

运行前面的脚本后,你可以看到以下修改默认 socket 超时的方式:

$ python 1_6_socket_timeout.py 
Default socket timeout: None
Current socket timeout: 100.0

它是如何工作的...

在这个代码片段中,我们首先通过将 socket 家族和 socket 类型作为 socket 构造函数的第一个和第二个参数传递来创建一个 socket 对象。然后,你可以通过调用gettimeout()来获取 socket 超时值,并通过调用settimeout()方法来更改该值。传递给settimeout()方法的超时值可以是秒(非负浮点数)或None。此方法用于操作阻塞-socket 操作。将超时设置为None将禁用 socket 操作的超时。

优雅地处理 socket 错误

在任何网络应用程序中,一端尝试连接,而另一端由于网络媒体故障或其他原因没有响应是非常常见的情况。Python 的 socket 库通过socket.error异常提供了一个优雅的方法来处理这些错误。在这个菜谱中,展示了几个示例。

如何操作...

让我们创建一些 try-except 代码块,并在每个块中放入一个潜在的错误类型。为了获取用户输入,可以使用 argparse 模块。此模块比仅使用 sys.argv 解析命令行参数更强大。在 try-except 块中,可以放置典型的套接字操作,例如创建套接字对象、连接到服务器、发送数据和等待回复。

以下示例代码展示了如何用几行代码说明这些概念。

列表 1.7 展示了 socket_errors 如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 1
# This program is optimized for Python 2.7\. It may run on any   
# other Python version with/without modifications.

import sys
import socket
import argparse 

def main():
    # setup argument parsing
    parser = argparse.ArgumentParser(description='Socket Error Examples')
    parser.add_argument('--host', action="store", dest="host", required=False)
    parser.add_argument('--port', action="store", dest="port", type=int, required=False)
    parser.add_argument('--file', action="store", dest="file", required=False)
    given_args = parser.parse_args()
    host = given_args.host
    port = given_args.port
    filename = given_args.file

    # First try-except block -- create socket 
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    except socket.error, e:
        print "Error creating socket: %s" % e
        sys.exit(1)

    # Second try-except block -- connect to given host/port
    try:
        s.connect((host, port))
    except socket.gaierror, e:
        print "Address-related error connecting to server: %s" % e
        sys.exit(1)
    except socket.error, e:
        print "Connection error: %s" % e
        sys.exit(1)

    # Third try-except block -- sending data
    try:
        s.sendall("GET %s HTTP/1.0\r\n\r\n" % filename)
    except socket.error, e:
        print "Error sending data: %s" % e
        sys.exit(1)

    while 1:
        # Fourth tr-except block -- waiting to receive data from remote host
        try:
            buf = s.recv(2048)
        except socket.error, e:
            print "Error receiving data: %s" % e
            sys.exit(1)
        if not len(buf):
            break
        # write the received data
        sys.stdout.write(buf) 

if __name__ == '__main__':
    main()

它是如何工作的...

在 Python 中,可以使用 argparse 模块将命令行参数传递给脚本并在脚本中解析它们。这个模块在 Python 2.7 中可用。对于更早的 Python 版本,此模块可以在 Python 包索引PyPI)中单独安装。你可以通过 easy_installpip 来安装它。

在这个示例中,设置了三个参数:主机名、端口号和文件名。此脚本的用法如下:

$ python 1_7_socket_errors.py –host=<HOST> --port=<PORT> --file=<FILE>

如果你尝试使用一个不存在的宿主,此脚本将打印出如下地址错误:

$ python 1_7_socket_errors.py --host=www.pytgo.org --port=8080 --file=1_7_socket_errors.py 
Address-related error connecting to server: [Errno -5] No address associated with hostname

如果特定端口没有服务,并且你尝试连接到该端口,那么这将引发连接超时错误,如下所示:

$ python 1_7_socket_errors.py --host=www.python.org --port=8080 --file=1_7_socket_errors.py 

由于主机 www.python.org 没有监听 8080 端口,这将返回以下错误:

Connection error: [Errno 110] Connection timed out

然而,如果你向正确的端口发送一个任意请求,错误可能不会被应用程序级别捕获。例如,运行以下脚本不会返回错误,但 HTML 输出告诉我们这个脚本有什么问题:

$ python 1_7_socket_errors.py --host=www.python.org --port=80 --file=1_7_socket_errors.py

HTTP/1.1 404 Not found
Server: Varnish
Retry-After: 0
content-type: text/html
Content-Length: 77
Accept-Ranges: bytes
Date: Thu, 20 Feb 2014 12:14:01 GMT
Via: 1.1 varnish
Age: 0
Connection: close

<html>
<head>
<title> </title>
</head>
<body>
unknown domain: </body></html>

在前面的示例中,使用了四个 try-except 块。除了第二个块使用 socket.gaierror 外,所有块都使用 socket.errorsocket.gaierror 用于地址相关错误。还有两种其他类型的异常:socket.herror 用于旧版 C API,如果你在套接字上使用 settimeout() 方法,当该套接字发生超时时,将引发 socket.timeout

修改套接字的发送/接收缓冲区大小

默认套接字缓冲区大小在很多情况下可能不合适。在这种情况下,可以将默认套接字缓冲区大小更改为更合适的值。

如何操作...

让我们使用套接字对象的 setsockopt() 方法来调整默认套接字缓冲区大小。

首先,定义两个常量:SEND_BUF_SIZE/RECV_BUF_SIZE,然后在函数中包装套接字实例对 setsockopt() 方法的调用。在修改之前检查缓冲区大小也是一个好主意。请注意,我们需要分别设置发送和接收缓冲区大小。

列表 1.8 展示了如何修改套接字的发送/接收缓冲区大小如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 1
# This program is optimized for Python 2.7\. It may run on any
# other Python version with/without modifications

import socket

SEND_BUF_SIZE = 4096
RECV_BUF_SIZE = 4096

def modify_buff_size():
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM )

    # Get the size of the socket's send buffer
    bufsize = sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
    print "Buffer size [Before]:%d" %bufsize

    sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
    sock.setsockopt(
            socket.SOL_SOCKET,
            socket.SO_SNDBUF,
            SEND_BUF_SIZE)
    sock.setsockopt(
            socket.SOL_SOCKET,
            socket.SO_RCVBUF,
            RECV_BUF_SIZE)
    bufsize = sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
    print "Buffer size [After]:%d" %bufsize

if __name__ == '__main__':
    modify_buff_size()

如果你运行前面的脚本,它将显示套接字缓冲区大小的变化。以下输出可能因操作系统的本地设置而异:

$ python 1_8_modify_buff_size.py 
Buffer size [Before]:16384
Buffer size [After]:8192

它是如何工作的...

你可以在套接字对象上调用getsockopt()setsockopt()方法来分别检索和修改套接字对象的属性。setsockopt()方法接受三个参数:leveloptnamevalue。在这里,optname接受选项名称,value是相应选项的值。对于第一个参数,所需的符号常量可以在套接字模块(SO_*etc.)中找到。

将套接字转换为阻塞/非阻塞模式

默认情况下,TCP 套接字被置于阻塞模式。这意味着控制权不会返回到你的程序,直到某个特定操作完成。例如,如果你调用connect() API,连接会阻塞你的程序直到操作完成。在许多情况下,你不想让程序永远等待,无论是等待服务器的响应还是等待任何错误来停止操作。例如,当你编写一个连接到 Web 服务器的 Web 浏览器客户端时,你应该考虑一个可以在操作过程中取消连接过程的功能。这可以通过将套接字置于非阻塞模式来实现。

如何操作...

让我们看看 Python 中可用的选项。在 Python 中,套接字可以被置于阻塞或非阻塞模式。在非阻塞模式下,如果任何 API 调用,例如send()recv(),遇到任何问题,将会引发错误。然而,在阻塞模式下,这不会停止操作。我们可以创建一个普通的 TCP 套接字,并实验阻塞和非阻塞操作。

为了操纵套接字的阻塞特性,我们首先需要创建一个套接字对象。

然后,我们可以调用setblocking(1)来设置阻塞或setblocking(0)来取消阻塞。最后,我们将套接字绑定到特定端口并监听传入的连接。

列表 1.9 显示了套接字如何转换为阻塞或非阻塞模式,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 1
# This program is optimized for Python 2.7\. It may run on any
# other Python version with/without modifications

import socket

def test_socket_modes():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setblocking(1)
    s.settimeout(0.5)
    s.bind(("127.0.0.1", 0))

    socket_address = s.getsockname()
    print "Trivial Server launched on socket: %s" %str(socket_address)
    while(1):
        s.listen(1)

if __name__ == '__main__':
    test_socket_modes()

如果你运行这个菜谱,它将启动一个具有阻塞模式启用的简单服务器,如下面的命令所示:

$ python 1_9_socket_modes.py 
Trivial Server launched on socket: ('127.0.0.1', 51410)

它是如何工作的...

在这个菜谱中,我们通过在setblocking()方法中将值设置为1来在套接字上启用阻塞。同样,你可以在该方法中将值0取消设置,使其变为非阻塞。

这个特性将在一些后续的菜谱中重用,其真正目的将在那里详细阐述。

重复使用套接字地址

你希望套接字服务器始终在特定的端口上运行,即使它在有意或意外关闭后也是如此。在某些情况下,如果你的客户端程序始终连接到该特定服务器端口,这很有用。因此,你不需要更改服务器端口。

如何操作...

如果你在一个特定的端口上运行 Python 套接字服务器,并在关闭后尝试重新运行它,你将无法使用相同的端口。它通常会抛出如下错误:

Traceback (most recent call last):
 File "1_10_reuse_socket_address.py", line 40, in <module>
 reuse_socket_addr()
 File "1_10_reuse_socket_address.py", line 25, in reuse_socket_addr
 srv.bind( ('', local_port) )
 File "<string>", line 1, in bind
socket.error: [Errno 98] Address already in use

解决这个问题的方法是为套接字启用重用选项SO_REUSEADDR

在创建套接字对象后,我们可以查询地址重用的状态,比如说一个旧状态。然后,我们调用setsockopt()方法来改变其地址重用状态。然后,我们遵循绑定到地址和监听传入客户端连接的常规步骤。在这个例子中,我们捕获KeyboardInterrupt异常,这样如果您按下Ctrl + C,Python 脚本将不会显示任何异常消息而终止。

列表 1.10 显示了如何如下重用套接字地址:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 1
# This program is optimized for Python 2.7\. It may run on any
# other Python version with/without modifications

import socket
import sys

def reuse_socket_addr():
    sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

    # Get the old state of the SO_REUSEADDR option
    old_state = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR )
    print "Old sock state: %s" %old_state

    # Enable the SO_REUSEADDR option
    sock.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
    new_state = sock.getsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR )
    print "New sock state: %s" %new_state

    local_port = 8282

    srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind( ('', local_port) )
    srv.listen(1)
    print ("Listening on port: %s " %local_port)
    while True:
        try:
            connection, addr = srv.accept()
            print 'Connected by %s:%s' % (addr[0], addr[1])
        except KeyboardInterrupt:
            break
        except socket.error, msg:
            print '%s' % (msg,)

if __name__ == '__main__':
    reuse_socket_e addr()

此菜谱的输出将类似于以下命令:

$ python 1_10_reuse_socket_address.py 
Old sock state: 0
New sock state: 1
Listening on port: 8282 

它是如何工作的...

您可以从一个控制台窗口运行此脚本,并尝试从另一个控制台窗口通过键入telnet localhost 8282连接到该服务器。在关闭服务器程序后,您可以在同一端口上再次运行它。然而,如果您注释掉设置SO_REUSEADDR的行,服务器将无法再次运行。

从互联网时间服务器打印当前时间

许多程序依赖于准确的机器时间,例如 UNIX 中的make命令。您的机器时间可能不同,需要与网络中的另一个时间服务器同步。

准备工作

为了将您的机器时间与互联网上的一个时间服务器同步,您可以编写一个 Python 客户端。为此,将使用ntplib。在这里,客户端/服务器对话将使用网络时间协议NTP)进行。如果您的机器上未安装ntplib,您可以使用以下命令通过pipeasy_installPyPI获取它:

$ pip install ntplib

如何做...

我们创建了一个NTPClient实例,然后通过传递 NTP 服务器地址来调用其上的request()方法。

列表 1.11 显示了如何如下从互联网时间服务器打印当前时间:

 #!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 1
# This program is optimized for Python 2.7\. It may run on any
# other Python version with/without modifications

import ntplib
from time import ctime

def print_time():
    ntp_client = ntplib.NTPClient()
    response = ntp_client.request('pool.ntp.org')
    print ctime(response.tx_time)

if __name__ == '__main__':
    print_time()

在我的机器上,此菜谱显示了以下输出:

$ python 1_11_print_machine_time.py 
Thu Mar 5 14:02:58 2012

它是如何工作的...

在这里,已经创建了一个 NTP 客户端,并向互联网 NTP 服务器之一pool.ntp.org发送了一个 NTP 请求。使用ctime()函数来打印响应。

编写 SNTP 客户端

与前面的菜谱不同,有时您不需要从 NTP 服务器获取精确的时间。您可以使用 NTP 的一个更简单的版本,称为简单网络时间协议。

如何做...

让我们创建一个不使用任何第三方库的纯 SNTP 客户端。

让我们先定义两个常量:NTP_SERVERTIME1970NTP_SERVER是我们客户端将要连接的服务器地址,而TIME1970是 1970 年 1 月 1 日的参考时间(也称为纪元)。您可以在www.epochconverter.com/找到纪元时间的值或将其转换为纪元时间。实际的客户端创建一个 UDP 套接字(SOCK_DGRAM)来遵循 UDP 协议连接到服务器。然后,客户端需要发送 SNTP 协议数据('\x1b' + 47 * '\0')在一个数据包中。我们的 UDP 客户端使用sendto()recvfrom()方法发送和接收数据。

当服务器以打包数组的形式返回时间信息时,客户端需要一个专门的struct模块来解包数据。有趣的数据位于数组的第 11 个元素。最后,我们需要从解包值中减去参考值TIME1970以获取实际当前时间。

列表 1.11 展示了如何编写一个 SNTP 客户端,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 1
# This program is optimized for Python 2.7\. It may run on any
# other Python version with/without modifications
import socket
import struct
import sys
import time

NTP_SERVER = "0.uk.pool.ntp.org"
TIME1970 = 2208988800L

def sntp_client():
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    data = '\x1b' + 47 * '\0'
    client.sendto(data, (NTP_SERVER, 123))
    data, address = client.recvfrom( 1024 )
    if data:
        print 'Response received from:', address
    t = struct.unpack( '!12I', data )[10]
    t -= TIME1970
    print '\tTime=%s' % time.ctime(t)

if __name__ == '__main__':
    sntp_client()

此配方打印从互联网时间服务器通过 SNTP 协议接收的当前时间,如下所示:

$ python 1_12_sntp_client.py 
Response received from: ('87.117.251.2', 123) 
 Time=Tue Feb 25 14:49:38 2014 

它是如何工作的...

此 SNTP 客户端创建一个套接字连接并发送协议数据。在接收到 NTP 服务器(在这种情况下,0.uk.pool.ntp.org)的响应后,它使用struct解包数据。最后,它减去参考时间,即 1970 年 1 月 1 日,并使用 Python 时间模块的内置方法ctime()打印时间。

编写一个简单的回显客户端/服务器应用程序

在使用 Python 的基本套接字 API 进行测试后,我们现在创建一个套接字服务器和客户端。在这里,你将有机会利用你在前面的配方中获得的基本知识。

如何操作...

在这个例子中,服务器将回显从客户端接收到的任何内容。我们将使用 Python 的argparse模块从命令行指定 TCP 端口。服务器和客户端脚本都将接受此参数。

首先,我们创建服务器。我们首先创建一个 TCP 套接字对象。然后,我们设置地址重用,以便我们可以根据需要多次运行服务器。我们将套接字绑定到我们本地机器上的指定端口。在监听阶段,我们确保使用listen()方法的 backlog 参数来监听队列中的多个客户端。最后,我们等待客户端连接并发送一些数据到服务器。当数据被接收时,服务器将数据回显给客户端。

列表 1.13a 展示了如何编写一个简单的回显客户端/服务器应用程序,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 1
# This program is optimized for Python 2.7\. It may run on any
# other Python version with/without modifications.

import socket
import sys
import argparse

host = 'localhost'
data_payload = 2048
backlog = 5 

def echo_server(port):
    """ A simple echo server """
    # Create a TCP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Enable reuse address/port 
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    # Bind the socket to the port
    server_address = (host, port)
    print "Starting up echo server  on %s port %s" % server_address
    sock.bind(server_address)
    # Listen to clients, backlog argument specifies the max no. of queued connections
    sock.listen(backlog) 
    while True: 
        print "Waiting to receive message from client"
        client, address = sock.accept() 
        data = client.recv(data_payload) 
        if data:
            print "Data: %s" %data
            client.send(data)
            print "sent %s bytes back to %s" % (data, address)
        # end connection
        client.close() 

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Socket Server Example')
    parser.add_argument('--port', action="store", dest="port", type=int, required=True)
    given_args = parser.parse_args() 
    port = given_args.port
    echo_server(port)

在客户端代码中,我们使用端口号创建一个客户端套接字并连接到服务器。然后,客户端向服务器发送消息Test message. This will be echoed,并且客户端立即以几个段的形式接收到消息。在这里,构建了两个 try-except 块来捕获此交互会话中的任何异常。

列表 1-13b 展示了回显客户端,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 1
# This program is optimized for Python 2.7\. It may run on any
# other Python version with/without modifications.

import socket
import sys

import argparse

host = 'localhost'

def echo_client(port):
    """ A simple echo client """
    # Create a TCP/IP socket
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    # Connect the socket to the server
    server_address = (host, port)
    print "Connecting to %s port %s" % server_address
    sock.connect(server_address)

    # Send data
    try:
        # Send data
        message = "Test message. This will be echoed"
        print "Sending %s" % message
        sock.sendall(message)
        # Look for the response
        amount_received = 0
        amount_expected = len(message)
        while amount_received < amount_expected:
            data = sock.recv(16)
            amount_received += len(data)
            print "Received: %s" % data
    except socket.errno, e:
        print "Socket error: %s" %str(e)
    except Exception, e:
        print "Other exception: %s" %str(e)
    finally:
        print "Closing connection to the server"
        sock.close()

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Socket Server Example')
    parser.add_argument('--port', action="store", dest="port", type=int, required=True)
    given_args = parser.parse_args() 
    port = given_args.port
    echo_client(port)

它是如何工作的...

为了查看客户端/服务器交互,在一个控制台中启动以下服务器脚本:

$ python 1_13a_echo_server.py --port=9900 
Starting up echo server  on localhost port 9900 

Waiting to receive message from client 

现在,从另一个终端运行客户端,如下所示:

$ python 1_13b_echo_client.py --port=9900 
Connecting to localhost port 9900 
Sending Test message. This will be echoed 
Received: Test message. Th 
Received: is will be echoe 
Received: d 
Closing connection to the server

当客户端连接到 localhost 时,服务器也会打印以下消息:

Data: Test message. This will be echoed 
sent Test message. This will be echoed bytes back to ('127.0.0.1', 42961) 
Waiting to receive message from client

第二章. 为提高性能而进行多路复用套接字 I/O

在本章中,我们将介绍以下食谱:

  • 在你的套接字服务器应用程序中使用 ForkingMixIn

  • 在你的套接字服务器应用程序中使用 ThreadingMixIn

  • 使用 select.select 编写聊天服务器

  • 使用 select.epoll 多路复用 Web 服务器

  • 使用 Diesel 并发库多路复用 echo 服务器

简介

本章重点介绍使用一些有用的技术来提高套接字服务器的性能。与上一章不同,这里我们考虑多个客户端将连接到服务器,并且通信可以是异步的。服务器不需要以阻塞方式处理来自客户端的请求,这可以独立完成。如果一个客户端接收或处理数据花费了更多时间,服务器不需要等待。它可以使用单独的线程或进程与其他客户端交谈。

在本章中,我们还将探讨select模块,该模块提供了特定平台的 I/O 监控功能。此模块建立在底层操作系统的内核的 select 系统调用之上。对于 Linux,手册页位于man7.org/linux/man-pages/man2/select.2.html,可以检查以查看此系统调用的可用功能。由于我们的套接字服务器希望与许多客户端交互,select可以非常有助于监控非阻塞套接字。还有一些第三方 Python 库也可以帮助我们同时处理多个客户端。我们包含了一个使用 Diesel 并发库的示例食谱。

尽管为了简洁起见,我们将使用两个或少数几个客户端,但读者可以自由扩展本章的食谱,并使用它们与成百上千的客户端一起使用。

在你的套接字服务器应用程序中使用 ForkingMixIn

你已经决定编写一个异步 Python 套接字服务器应用程序。服务器在处理客户端请求时不会阻塞。因此,服务器需要一个机制来独立处理每个客户端。

Python 2.7 版本的SocketServer类包含两个实用类:ForkingMixInThreadingMixInForkingMixin类将为每个客户端请求生成一个新的进程。本节将讨论此类。ThreadingMixIn类将在下一节中讨论。有关更多信息,您可以参考 Python 文档docs.python.org/2/library/socketserver.html

如何做到...

让我们重写之前在 第一章 中描述的回显服务器,即 套接字、IPv4 和简单的客户端/服务器编程。我们可以利用 SocketServer 类家族的子类。它提供了现成的 TCP、UDP 和其他协议服务器。我们可以创建一个从 TCPServerForkingMixin 继承而来的 ForkingServer 类。前者父类将使我们的 ForkingServer 类能够执行我们之前手动执行的所有必要服务器操作,例如创建套接字、绑定到地址和监听传入的连接。我们的服务器还需要从 ForkingMixin 继承以异步处理客户端。

ForkingServer 类也需要设置一个请求处理器,以规定如何处理客户端请求。在这里,我们的服务器将回显从客户端接收到的文本字符串。我们的请求处理器类 ForkingServerRequestHandler 是从 SocketServer 库提供的 BaseRequestHandler 继承而来的。

我们可以用面向对象的方式编写我们的回显服务器客户端,ForkingClient。在 Python 中,类的构造函数方法被称为 __init__()。按照惯例,它接受一个 self 参数来附加该特定类的属性或属性。ForkingClient 回显服务器将在 __init__() 中初始化,并在 run() 方法中分别向服务器发送消息。

如果你根本不熟悉面向对象编程OOP),在尝试掌握这个食谱的同时,回顾 OOP 的基本概念可能会有所帮助。

为了测试我们的 ForkingServer 类,我们可以启动多个回显客户端,并查看服务器如何响应客户端。

列表 2.1 展示了在套接字服务器应用程序中使用 ForkingMixin 的示例代码如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 2
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
# See more: http://docs.python.org/2/library/socketserver.html

import os
import socket
import threading
import SocketServer

SERVER_HOST = 'localhost'
SERVER_PORT = 0 # tells the kernel to pick up a port dynamically
BUF_SIZE = 1024
ECHO_MSG = 'Hello echo server!'

class ForkedClient():
    """ A client to test forking server"""    
    def __init__(self, ip, port):
        # Create a socket
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        # Connect to the server
	     self.sock.connect((ip, port))

    def run(self):
        """ Client playing with the server"""
        # Send the data to server
        current_process_id = os.getpid()
        print 'PID %s Sending echo message to the server : "%s"' % (current_process_id, ECHO_MSG)
        sent_data_length = self.sock.send(ECHO_MSG)
        print "Sent: %d characters, so far..." %sent_data_length

        # Display server response
        response = self.sock.recv(BUF_SIZE)
        print "PID %s received: %s" % (current_process_id, response[5:])

    def shutdown(self):
        """ Cleanup the client socket """
        self.sock.close()

class ForkingServerRequestHandler(SocketServer.BaseRequestHandler):

    def handle(self):        
        # Send the echo back to the client
        data = self.request.recv(BUF_SIZE)
        current_process_id = os.getpid()
        response = '%s: %s' % (current_process_id, data)
        print "Server sending response [current_process_id: data] = [%s]" %response
        self.request.send(response)
        return

class ForkingServer(SocketServer.ForkingMixIn,
                    SocketServer.TCPServer,
                    ):
    """Nothing to add here, inherited everything necessary from parents"""
    pass

def main():
    # Launch the server
    server = ForkingServer((SERVER_HOST, SERVER_PORT), ForkingServerRequestHandler)
    ip, port = server.server_address # Retrieve the port number
    server_thread = threading.Thread(target=server.serve_forever)
    server_thread.setDaemon(True) # don't hang on exit
    server_thread.start()
    print 'Server loop running PID: %s' %os.getpid()

    # Launch the client(s)
    client1 =  ForkedClient(ip, port)
    client1.run()

    client2 =  ForkedClient(ip, port)
    client2.run()

    # Clean them up
    server.shutdown()
    client1.shutdown()
    client2.shutdown()
    server.socket.close()

if __name__ == '__main__':
    main()

它是如何工作的...

ForkingServer 的一个实例在主线程中启动,该线程已被设置为守护线程以在后台运行。现在,两个客户端已经开始与服务器交互。

如果你运行脚本,它将显示以下输出:

$ python 2_1_forking_mixin_socket_server.py
Server loop running PID: 12608
PID 12608 Sending echo message to the server : "Hello echo server!"
Sent: 18 characters, so far...
Server sending response [current_process_id: data] = [12610: Hello echo server!]
PID 12608 received: : Hello echo server!
PID 12608 Sending echo message to the server : "Hello echo server!"
Sent: 18 characters, so far...
Server sending response [current_process_id: data] = [12611: Hello echo server!]
PID 12608 received: : Hello echo server!

服务器端口号可能因操作系统内核动态选择而不同。

在你的套接字服务器应用程序中使用 ThreadingMixIn

可能由于某些特定原因,例如,在多个线程之间共享该应用程序的状态,避免进程间通信的复杂性,或者其它原因,你更愿意编写一个基于线程的应用程序而不是基于进程的应用程序。在这种情况下,如果你喜欢使用 SocketServer 库编写异步网络服务器,你需要 ThreadingMixin

准备工作

通过对我们之前的食谱进行一些小的修改,你可以得到一个使用 ThreadingMixin 的工作版本的网络服务器。

小贴士

下载示例代码

您可以从www.packtpub.com上的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

如何做...

如前所述,基于ForkingMixIn的 socket 服务器,ThreadingMixIn套接字服务器将遵循与回声服务器相同的编码模式,除了以下几点。首先,我们的ThreadedTCPServer将继承自TCPServerTheadingMixIn。这个多线程版本将在客户端连接时启动一个新线程。更多详细信息可以在docs.python.org/2/library/socketserver.html找到。

我们套接字服务器的请求处理器类ForkingServerRequestHandler会将回声发送回客户端,从新的线程中。您可以在这里检查线程信息。为了简单起见,我们将客户端代码放在一个函数中而不是一个类中。客户端代码创建客户端套接字并向服务器发送消息。

列表 2.2 展示了使用ThreadingMixIn作为以下示例的回声套接字服务器示例代码:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 2
# This program is optimized for Python 2.7
# It may run on any other version with/without modifications.
import os
import socket
import threading
import SocketServer
SERVER_HOST = 'localhost'
SERVER_PORT = 0 # tells the kernel to pick up a port dynamically
BUF_SIZE = 1024

def client(ip, port, message):
    """ A client to test threading mixin server"""    
    # Connect to the server
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((ip, port))
    try:
        sock.sendall(message)
        response = sock.recv(BUF_SIZE)
        print "Client received: %s" %response
    finally:
        sock.close()

class ThreadedTCPRequestHandler(SocketServer.BaseRequestHandler):
    """ An example of threaded TCP request handler """
    def handle(self):
        data = self.request.recv(1024)
        current_thread = threading.current_thread()
        response = "%s: %s" %(current_thread.name, data)
        self.request.sendall(response)

class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer):
    """Nothing to add here, inherited everything necessary from parents"""
    pass
if __name__ == "__main__":
    # Run server
    server = ThreadedTCPServer((SERVER_HOST, SERVER_PORT), ThreadedTCPRequestHandler)
    ip, port = server.server_address # retrieve ip address
    # Start a thread with the server -- one  thread per request
    server_thread = threading.Thread(target=server.serve_forever)
    # Exit the server thread when the main thread exits
    server_thread.daemon = True
    server_thread.start()
    print "Server loop running on thread: %s"  %server_thread.name
    # Run clients
    client(ip, port, "Hello from client 1")
    client(ip, port, "Hello from client 2")
    client(ip, port, "Hello from client 3")
    # Server cleanup
    server.shutdown()

它是如何工作的...

这个菜谱首先创建一个服务器线程,并在后台启动它。然后它启动三个测试客户端向服务器发送消息。作为回应,服务器将消息回显给客户端。在服务器的请求处理器的handle()方法中,您可以看到我们检索当前线程信息并打印它。这应该在每个客户端连接中都是不同的。

在这个客户端/服务器对话中,使用了sendall()方法来确保发送所有数据而不丢失:

$ python 2_2_threading_mixin_socket_server.py
Server loop running on thread: Thread-1
Client received: Thread-2: Hello from client 1
Client received: Thread-3: Hello from client 2
Client received: Thread-4: Hello from client 3

使用 select.select 编写聊天服务器

在任何较大的网络服务器应用程序中,每个客户端都可能是数百或数千个并发连接到服务器,为每个客户端启动一个单独的线程或进程可能不可行。由于可用的内存和主机 CPU 功率有限,我们需要一种更好的技术来处理大量客户端。幸运的是,Python 提供了select模块来克服这个问题。

如何做...

我们需要编写一个高效的聊天服务器,能够处理数百或大量客户端连接。我们将使用select()方法,来自select模块,这将使我们的聊天服务器和客户端能够执行任何任务,而无需始终阻塞发送或接收调用。

让我们设计这个菜谱,使得一个单独的脚本可以通过额外的--name参数启动客户端和服务器。只有当从命令行传递--name=server时,脚本才会启动聊天服务器。传递给--name参数的任何其他值,例如client1client2,将启动一个聊天客户端。我们可以使用--port参数从命令行指定我们的聊天服务器端口号。对于更大的应用程序,可能更倾向于为服务器和客户端编写独立的模块。

列表 2.3 显示了使用select.select作为以下示例的聊天应用程序:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 2
# This program is optimized for Python 2.7
# It may run on any other version with/without modifications
import select
import socket
import sys
import signal
import cPickle
import struct
import argparse

SERVER_HOST = 'localhost'
CHAT_SERVER_NAME = 'server'

# Some utilities
def send(channel, *args):
    buffer = cPickle.dumps(args)
    value = socket.htonl(len(buffer))
    size = struct.pack("L",value)
    channel.send(size)
    channel.send(buffer)

def receive(channel):
    size = struct.calcsize("L")
    size = channel.recv(size)
    try:
        size = socket.ntohl(struct.unpack("L", size)[0])
    except struct.error, e:
        return ''
    buf = ""
    while len(buf) < size:
        buf = channel.recv(size - len(buf))
    return cPickle.loads(buf)[0]

send()方法接受一个名为channel的命名参数和一个位置参数*args。它使用cPickle模块的dumps()方法序列化数据。它使用struct模块确定数据的大小。同样,receive()方法接受一个名为channel的命名参数。

现在我们可以按照以下方式编写ChatServer类:

class ChatServer(object):
    """ An example chat server using select """
 def __init__(self, port, backlog=5):
   self.clients = 0
   self.clientmap = {}
   self.outputs = [] # list output sockets
   self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   # Enable re-using socket address
   self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
   self.server.bind((SERVER_HOST, port))
   print 'Server listening to port: %s ...' %port
   self.server.listen(backlog)
   # Catch keyboard interrupts
   signal.signal(signal.SIGINT, self.sighandler)

    def sighandler(self, signum, frame):
        """ Clean up client outputs"""
        # Close the server
        print 'Shutting down server...'
        # Close existing client sockets
        for output in self.outputs:
            output.close()            
        self.server.close()

    def get_client_name(self, client):
        """ Return the name of the client """
        info = self.clientmap[client]
        host, name = info[0][0], info[1]
        return '@'.join((name, host))

现在的ChatServer类的主要可执行方法应该如下所示:

    def run(self):
        inputs = [self.server, sys.stdin]
        self.outputs = []
        running = True
        while running:
         try:
          readable, writeable, exceptional = \
          select.select(inputs, self.outputs, [])
            except select.error, e:
                break
            for sock in readable:
                if sock == self.server:
                    # handle the server socket
                    client, address = self.server.accept()
                    print "Chat server: got connection %d from %s" %\                     (client.fileno(), address)
                    # Read the login name
                    cname = receive(client).split('NAME: ')[1]
                    # Compute client name and send back
                    self.clients += 1
                    send(client, 'CLIENT: ' + str(address[0]))
                    inputs.append(client)
                    self.clientmap[client] = (address, cname)
                    # Send joining information to other clients
                    msg = "\n(Connected: New client (%d) from %s)" %\                   (self.clients, self.get_client_name(client))
                    for output in self.outputs:
                        send(output, msg)
                    self.outputs.append(client)
                elif sock == sys.stdin:
                    # handle standard input
                    junk = sys.stdin.readline()
                    running = False
                else:
                    # handle all other sockets
                    try:
                        data = receive(sock)
                        if data:
                            # Send as new client's message...
                            msg = '\n#[' + self.get_client_name(sock)\
                                   + ']>>' + data
                            # Send data to all except ourself
                            for output in self.outputs:
                                if output != sock:
                                    send(output, msg)
                        else:
                            print "Chat server: %d hung up" % \
                            sock.fileno()
                            self.clients -= 1
                            sock.close()
                            inputs.remove(sock)
                            self.outputs.remove(sock)
                            # Sending client leaving info to others
                            msg = "\n(Now hung up: Client from %s)" %\                             self.get_client_name(sock)
                            for output in self.outputs:
                                send(output, msg)
                    except socket.error, e:
                        # Remove
                        inputs.remove(sock)
                        self.outputs.remove(sock)
        self.server.close()

聊天服务器初始化时带有一些数据属性。它存储客户端的数量、每个客户端的映射和输出套接字。通常的服务器套接字创建也会设置重用地址的选项,这样就不会在相同的端口上再次启动服务器时出现问题。聊天服务器构造函数的可选backlog参数设置了服务器可以监听的最大排队连接数。

这个聊天服务器的一个有趣方面是使用signal模块捕获用户中断,通常是通过键盘。因此,注册了一个信号处理程序sighandler用于中断信号(SIGINT)。这个信号处理程序捕获键盘中断信号并关闭所有可能等待发送数据的输出套接字。

我们聊天服务器的主执行方法run()在一个while循环中执行其操作。该方法注册了一个选择接口,其中输入参数是聊天服务器套接字stdin。输出参数由服务器的输出套接字列表指定。作为回报,select提供了三个列表:可读、可写和异常套接字。聊天服务器只对可读套接字感兴趣,其中有一些数据准备好被读取。如果该套接字指向自身,那么这意味着已经建立了一个新的客户端连接。因此,服务器检索客户端的名称并将此信息广播给其他客户端。在另一种情况下,如果输入参数有任何内容,聊天服务器将退出。同样,聊天服务器处理其他客户端的套接字输入。它将接收到的任何客户端数据中继给其他客户端,并共享他们的加入/离开信息。

聊天客户端代码类应包含以下代码:

class ChatClient(object):
    """ A command line chat client using select """

    def __init__(self, name, port, host=SERVER_HOST):
        self.name = name
        self.connected = False
        self.host = host
        self.port = port
        # Initial prompt
        self.prompt='[' + '@'.join((name, socket.gethostname().split('.')[0])) + ']> '
        # Connect to server at port
        try:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.sock.connect((host, self.port))
            print "Now connected to chat server@ port %d" % self.port
            self.connected = True
            # Send my name...
            send(self.sock,'NAME: ' + self.name)
            data = receive(self.sock)
            # Contains client address, set it
            addr = data.split('CLIENT: ')[1]
            self.prompt = '[' + '@'.join((self.name, addr)) + ']> '
        except socket.error, e:
            print "Failed to connect to chat server @ port %d" % self.port
            sys.exit(1)

    def run(self):
        """ Chat client main loop """
        while self.connected:
            try:
                sys.stdout.write(self.prompt)
                sys.stdout.flush()
                # Wait for input from stdin and socket
                readable, writeable,exceptional = select.select([0, self.sock], [],[])
                for sock in readable:
                    if sock == 0:
                        data = sys.stdin.readline().strip()
                        if data: send(self.sock, data)
                    elif sock == self.sock:
                        data = receive(self.sock)
                        if not data:
                            print 'Client shutting down.'
                            self.connected = False
                            break
                        else:
                            sys.stdout.write(data + '\n')
                            sys.stdout.flush()

            except KeyboardInterrupt:
                print " Client interrupted. """
                self.sock.close()
                break

聊天客户端使用名称参数初始化,并在连接时将此名称发送到聊天服务器。它还设置了自定义提示[ name@host ]>。此客户端的执行方法run()只要与服务器保持连接就会继续其操作。类似于聊天服务器,聊天客户端也使用select()进行注册。如果任何可读套接字准备好,它将使客户端能够接收数据。如果 sock 值是0并且有可用数据,则可以发送数据。相同的信息也会显示在 stdout 中,或者在我们的情况下,是命令行控制台。现在,我们的主方法应该获取命令行参数,并按照以下方式调用服务器或客户端:

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Socket Server Example with Select')
    parser.add_argument('--name', action="store", dest="name", required=True)
    parser.add_argument('--port', action="store", dest="port", type=int, required=True)
    given_args = parser.parse_args()
    port = given_args.port
    name = given_args.name
    if name == CHAT_SERVER_NAME:
        server = ChatServer(port)
        server.run()
    else:
        client = ChatClient(name=name, port=port)
        client.run()

我们希望运行这个脚本三次:一次用于聊天服务器,两次用于两个聊天客户端。对于服务器,我们传递–name=serverport=8800。对于client1,我们更改名称参数--name=client1,对于client2,我们设置--name=client2。然后从client1的值提示中发送消息"Hello from client 1",该消息将在client2的提示中打印出来。同样,我们从client2的提示中发送"hello from client 2",该消息将在client1的提示中显示。

服务器输出如下:

$ python 2_3_chat_server_with_select.py --name=server --port=8800
Server listening to port: 8800 ...
Chat server: got connection 4 from ('127.0.0.1', 56565)
Chat server: got connection 5 from ('127.0.0.1', 56566)

client1的输出如下:

$ python 2_3_chat_server_with_select.py --name=client1 --port=8800
Now connected to chat server@ port 8800
[client1@127.0.0.1]>
(Connected: New client (2) from client2@127.0.0.1)
[client1@127.0.0.1]> Hello from client 1
[client1@127.0.0.1]>
#[client2@127.0.0.1]>>hello from client 2

client2的输出如下:

$ python 2_3_chat_server_with_select.py --name=client2 --port=8800
Now connected to chat server@ port 8800
[client2@127.0.0.1]>
#[client1@127.0.0.1]>>Hello from client 1
[client2@127.0.0.1]> hello from client 2
[client2@127.0.0.1]

整个交互过程如下截图所示:

如何操作...

它是如何工作的...

在我们模块的顶部,我们定义了两个实用函数:send()receive()

聊天服务器和客户端使用这些实用函数,这些函数之前已经演示过。之前也讨论了聊天服务器和客户端方法的细节。

使用 select.epoll 多路复用 Web 服务器

Python 的select模块有几个平台特定的网络事件管理函数。在 Linux 机器上,epoll可用。这将利用操作系统内核来轮询网络事件,并让我们的脚本知道何时发生某些事情。这听起来比之前提到的select.select方法更高效。

如何操作...

让我们编写一个简单的 Web 服务器,它可以向任何连接的 Web 浏览器返回一行文本。

核心思想是在这个 Web 服务器初始化期间,我们应该调用select.epoll()并注册我们服务器的文件描述符以接收事件通知。在 Web 服务器的执行代码中,监视套接字事件如下:

Listing 2.4 Simple web server using select.epoll
#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 2
# This program is optimized for Python 2.7
# It may run on any other version with/without modifications.
import socket
import select
import argparse
SERVER_HOST = 'localhost'
EOL1 = b'\n\n'
EOL2 = b'\n\r\n'
SERVER_RESPONSE  = b"""HTTP/1.1 200 OK\r\nDate: Mon, 1 Apr 2013 01:01:01 GMT\r\nContent-Type: text/plain\r\nContent-Length: 25\r\n\r\n
Hello from Epoll Server!"""

class EpollServer(object):
    """ A socket server using Epoll"""
    def __init__(self, host=SERVER_HOST, port=0):
      self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
      self.sock.bind((host, port))
      self.sock.listen(1)
      self.sock.setblocking(0)
      self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
      print "Started Epoll Server"
      self.epoll = select.epoll()
      self.epoll.register(self.sock.fileno(), select.EPOLLIN)

 def run(self):
  """Executes epoll server operation"""
  try:
     connections = {}; requests = {}; responses = {}
     while True:
   events = self.epoll.poll(1)
   for fileno, event in events:
     if fileno == self.sock.fileno():
       connection, address = self.sock.accept()
       connection.setblocking(0)
       self.epoll.register(connection.fileno(), select.EPOLLIN)
       connections[connection.fileno()] = connection
       requests[connection.fileno()] = b''
       responses[connection.fileno()] = SERVER_RESPONSE
     elif event & select.EPOLLIN:
       requests[fileno] += connections[fileno].recv(1024)
       if EOL1 in requests[fileno] or EOL2 in requests[fileno]:
             self.epoll.modify(fileno, select.EPOLLOUT)
             print('-'*40 + '\n' + requests[fileno].decode()[:-2])
      elif event & select.EPOLLOUT:
         byteswritten = connections[fileno].send(responses[fileno])
         responses[fileno] = responses[fileno][byteswritten:]
         if len(responses[fileno]) == 0:
             self.epoll.modify(fileno, 0)
             connections[fileno].shutdown(socket.SHUT_RDWR)
         elif event & select.EPOLLHUP:
              self.epoll.unregister(fileno)
              connections[fileno].close()
              del connections[fileno]
 finally:
   self.epoll.unregister(self.sock.fileno())
   self.epoll.close()
   self.sock.close()

if __name__ == '__main__':
 parser = argparse.ArgumentParser(description='Socket Server Example with Epoll')
 parser.add_argument('--port', action="store", dest="port", type=int, required=True)
    given_args = parser.parse_args()
    port = given_args.port
    server = EpollServer(host=SERVER_HOST, port=port)
    server.run()

如果你运行此脚本并通过浏览器(如 Firefox 或 IE)访问 Web 服务器,通过输入http://localhost:8800/,控制台将显示以下输出:

$ python 2_4_simple_web_server_with_epoll.py --port=8800
Started Epoll Server
----------------------------------------
GET / HTTP/1.1
Host: localhost:8800
Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-Agent: Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.43 Safari/537.31
DNT: 1
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-GB,en-US;q=0.8,en;q=0.6
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3
Cookie: MoodleSession=69149dqnvhett7br3qebsrcmh1; MOODLEID1_=%257F%25BA%2B%2540V

----------------------------------------
GET /favicon.ico HTTP/1.1
Host: localhost:8800
Connection: keep-alive
Accept: */*
DNT: 1
User-Agent: Mozilla/5.0 (X11; Linux i686) AppleWebKit/537.31 (KHTML, like Gecko) Chrome/26.0.1410.43 Safari/537.31
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-GB,en-US;q=0.8,en;q=0.6
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.3

你还将在你的浏览器中看到以下行:

Hello from Epoll Server!

以下截图显示了该场景:

如何操作...

如何操作...

在我们的EpollServer Web 服务器的构造函数中,创建了一个套接字服务器并将其绑定到给定端口的 localhost。服务器的套接字设置为非阻塞模式(setblocking(0))。还设置了TCP_NODELAY选项,以便我们的服务器可以不进行缓冲交换数据(如 SSH 连接的情况)。接下来,创建了select.epoll()实例,并将套接字的文件描述符传递给该实例以帮助监控。

在 Web 服务器的run()方法中,它开始接收套接字事件。这些事件如下表示:

  • EPOLLIN:此套接字读取事件

  • EPOLLOUT:此套接字写入事件

对于服务器套接字,它设置了响应SERVER_RESPONSE。当套接字有任何想要写入数据的连接时,它可以在EPOLLOUT事件案例中这样做。EPOLLHUP事件表示由于内部错误条件而意外关闭套接字。

使用 Diesel 并发库的多路复用回显服务器

有时您需要编写一个大型自定义网络应用程序,该应用程序希望避免重复的服务器初始化代码,该代码创建套接字、绑定到地址、监听和处理基本错误。有许多 Python 网络库可以帮助您删除样板代码。在这里,我们可以检查一个名为 Diesel 的库。

准备工作

Diesel 使用带有协程的非阻塞技术来高效地编写网络服务器。正如网站所述,Diesel 的核心是一个紧密的事件循环,使用 epoll 提供接近平坦的性能,支持 10,000 个连接以及更多。在这里,我们通过一个简单的回显服务器介绍 Diesel。您还需要 diesel 库 3.0 或更高版本。您可以使用 pip 命令完成此操作:$ pip install diesel >= 3.0

如何做...

在 Python Diesel 框架中,应用程序使用Application()类的实例初始化,并使用此实例注册事件处理器。让我们看看编写回显服务器有多简单。

列表 2.5 显示了使用 Diesel 作为以下回显服务器示例的代码:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 2
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
# You also need diesel library 3.0 or any later version

import diesel
import argparse

class EchoServer(object):
    """ An echo server using diesel"""

    def handler(self, remote_addr):
        """Runs the echo server"""
        host, port = remote_addr[0], remote_addr[1]
        print "Echo client connected from: %s:%d" %(host, port)

        while True:
            try:
                message = diesel.until_eol()
                your_message = ': '.join(['You said', message])
                diesel.send(your_message)
            except Exception, e:
                print "Exception:",e

def main(server_port):
    app = diesel.Application()
    server = EchoServer()    
    app.add_service(diesel.Service(server.handler, server_port))
    app.run()

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Echo server example with Diesel')
    parser.add_argument('--port', action="store", dest="port", type=int, required=True)
    given_args = parser.parse_args()
    port = given_args.port
    main(port)

如果您运行此脚本,服务器将显示以下输出:

$ python 2_5_echo_server_with_diesel.py --port=8800
[2013/04/08 11:48:32] {diesel} WARNING:Starting diesel <hand-rolled select.epoll>
Echo client connected from: 127.0.0.1:56603

在另一个控制台窗口中,可以启动另一个 Telnet 客户端,并按以下方式测试向我们的服务器发送的回显消息:

$ telnet localhost 8800
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello Diesel server ?
You said: Hello Diesel server ?

以下截图说明了 Diesel 聊天服务器的交互:

如何做...

它是如何工作的...

我们的脚本为--port参数取了一个命令行参数,并将其传递给初始化并运行我们的 Diesel 应用程序的main()函数。

Diesel 有一个服务概念,其中可以使用许多服务构建应用程序。EchoServer有一个handler()方法。这使得服务器能够处理单个客户端连接。Service()方法接受handler方法和端口号来运行该服务。

handler()方法内部,我们确定服务器的行为。在这种情况下,服务器只是返回消息文本。

如果我们将这段代码与第一章中的套接字、IPv4 和简单的客户端/服务器编程部分,在编写简单的回声客户端/服务器应用程序配方(列表 1.13a)中进行比较,那么我们可以非常清楚地看到,我们不需要编写任何样板代码,因此可以很容易地专注于高级应用程序逻辑。

第三章:IPv6、Unix 域套接字和网络接口

在本章中,我们将涵盖以下主题:

  • 将本地端口转发到远程主机

  • 使用 ICMP ping 网络上的主机

  • 等待远程网络服务

  • 列出您的机器上的接口

  • 查找您的机器上特定接口的 IP 地址

  • 检查您的机器上的接口是否已启动

  • 检测您的网络上的不活跃机器

  • 使用连接套接字(socketpair)执行基本 IPC

  • 使用 Unix 域套接字执行 IPC

  • 查看您的 Python 是否支持 IPv6 套接字

  • 从 IPv6 地址中提取 IPv6 前缀

  • 编写 IPv6 echo 客户端/服务器

简介

本章扩展了 Python 的 socket 库的使用,结合了一些第三方库。它还讨论了一些高级技术,例如 Python 标准库中的异步 ayncore 模块。本章还涉及了各种协议,从 ICMP ping 到 IPv6 客户端/服务器。

在本章中,通过一些示例配方介绍了一些有用的 Python 第三方模块。例如,网络数据包捕获库 Scapy 在 Python 网络程序员中广为人知。

一些配方致力于探索 Python 中的 IPv6 实用工具,包括 IPv6 客户端/服务器。其他一些配方涵盖了 Unix 域套接字。

将本地端口转发到远程主机

有时,您可能需要创建一个本地端口转发器,将所有从本地端口到特定远程主机的流量重定向。这可能有助于允许代理用户浏览某些网站,同时防止他们浏览其他网站。

如何做到这一点...

让我们创建一个本地端口转发脚本,该脚本将所有接收到的端口 8800 上的流量重定向到谷歌首页 (www.google.com)。我们可以将本地和远程主机以及端口号传递给此脚本。为了简化,让我们只指定本地端口号,因为我们知道 Web 服务器运行在端口 80 上。

列表 3.1 显示了端口转发示例,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
LOCAL_SERVER_HOST = 'localhost'
REMOTE_SERVER_HOST = 'www.google.com'
BUFSIZE = 4096
import asyncore
import socket

首先,我们定义 端口转发器 类:

class PortForwarder(asyncore.dispatcher):
    def __init__(self, ip, port, remoteip,remoteport,backlog=5):
        asyncore.dispatcher.__init__(self)
        self.remoteip=remoteip
        self.remoteport=remoteport
        self.create_socket(socket.AF_INET,socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind((ip,port))
        self.listen(backlog)
    def handle_accept(self):
        conn, addr = self.accept()
        print "Connected to:",addr
        Sender(Receiver(conn),self.remoteip,self.remoteport)

现在,我们需要指定 接收器发送器 类,如下所示:

class Receiver(asyncore.dispatcher):
    def __init__(self,conn):
        asyncore.dispatcher.__init__(self,conn)
        self.from_remote_buffer=''
        self.to_remote_buffer=''
        self.sender=None
    def handle_connect(self):
        pass
    def handle_read(self):
        read = self.recv(BUFSIZE)
        self.from_remote_buffer += read
    def writable(self):
        return (len(self.to_remote_buffer) > 0)
    def handle_write(self):
        sent = self.send(self.to_remote_buffer)
        self.to_remote_buffer = self.to_remote_buffer[sent:]
    def handle_close(self):
        self.close()
        if self.sender:
            self.sender.close()
class Sender(asyncore.dispatcher):
    def __init__(self, receiver, remoteaddr,remoteport):
        asyncore.dispatcher.__init__(self)
        self.receiver=receiver
        receiver.sender=self
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.connect((remoteaddr, remoteport))
    def handle_connect(self):
        pass
    def handle_read(self):
        read = self.recv(BUFSIZE)
        self.receiver.to_remote_buffer += read
    def writable(self):
        return (len(self.receiver.from_remote_buffer) > 0)
    def handle_write(self):
        sent = self.send(self.receiver.from_remote_buffer)
        self.receiver.from_remote_buffer = self.receiver.from_remote_buffer[sent:]
    def handle_close(self):
        self.close()
        self.receiver.close()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Port forwarding example')
    parser.add_argument('--local-host', action="store", dest="local_host", default=LOCAL_SERVER_HOST)
    parser.add_argument('--local-port', action="store", dest="local_port", type=int, required=True)
    parser.add_argument('--remote-host', action="store", dest="remote_host",  default=REMOTE_SERVER_HOST)
    parser.add_argument('--remote-port', action="store", dest="remote_port", type=int, default=80)
    given_args = parser.parse_args() 
    local_host, remote_host = given_args.local_host, given_args.remote_host
    local_port, remote_port = given_args.local_port, given_args.remote_port
    print "Starting port forwarding local %s:%s => remote %s:%s" % (local_host, local_port, remote_host, remote_port)
    PortForwarder(local_host, local_port, remote_host, remote_port)
    asyncore.loop()

如果您运行此脚本,它将显示以下输出:

$ python 3_1_port_forwarding.py --local-port=8800 
Starting port forwarding local localhost:8800 => remote www.google.com:80 

现在,打开您的浏览器并访问 http://localhost:8800。这将带您进入谷歌首页,脚本将打印类似以下命令的内容:

Connected to: ('127.0.0.1', 38557)

以下截图显示了将本地端口转发到远程主机:

如何做到这一点...

它是如何工作的...

我们创建了一个从 asyncore.dispatcher 继承的端口转发类 PortForwarder subclassed,它围绕套接字对象进行包装。当某些事件发生时,它提供了一些额外的有用功能,例如,当连接成功或客户端连接到服务器套接字时。您可以选择覆盖此类中定义的方法集。在我们的例子中,我们只覆盖了 handle_accept() 方法。

已从 asyncore.dispatcher 派生出两个其他类。Receiver 类处理传入的客户端请求,而 Sender 类接收此 Receiver 实例并处理发送到客户端的数据。如您所见,这两个类覆盖了 handle_read()handle_write()writeable() 方法,以促进远程主机和本地客户端之间的双向通信。

总结来说,PortForwarder 类在本地套接字中接收传入的客户端请求,并将其传递给 Sender 类实例,该实例随后使用 Receiver 类实例在指定的端口上与远程服务器建立双向通信。

使用 ICMP 对网络上的主机进行 ping 操作

ICMP ping 是您曾经遇到的最常见的网络扫描类型。在命令行提示符或终端中打开并输入 ping www.google.com 非常容易。在 Python 程序内部这样做有多难?这个配方为您展示了 Python ping 的示例。

准备工作

您需要在您的机器上具有超级用户或管理员权限才能运行此配方。

如何操作...

您可以懒洋洋地编写一个 Python 脚本,该脚本调用系统 ping 命令行工具,如下所示:

import subprocess
import shlex

command_line = "ping -c 1 www.google.com"
args = shlex.split(command_line)
try:
      subprocess.check_call(args,stdout=subprocess.PIPE,\
stderr=subprocess.PIPE)
    print "Google web server is up!"
except subprocess.CalledProcessError:
    print "Failed to get ping."

然而,在许多情况下,系统的 ping 可执行文件可能不可用或不可访问。在这种情况下,我们需要一个纯 Python 脚本来执行 ping 操作。请注意,此脚本需要以超级用户或管理员身份运行。

列表 3.2 显示了如下 ICMP ping:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import os
import argparse
import socket
import struct
import select
import time

ICMP_ECHO_REQUEST = 8 # Platform specific
DEFAULT_TIMEOUT = 2
DEFAULT_COUNT = 4 

class Pinger(object):
    """ Pings to a host -- the Pythonic way"""
    def __init__(self, target_host, count=DEFAULT_COUNT, timeout=DEFAULT_TIMEOUT):
        self.target_host = target_host
        self.count = count
        self.timeout = timeout
    def do_checksum(self, source_string):
        """  Verify the packet integritity """
        sum = 0
        max_count = (len(source_string)/2)*2
        count = 0
        while count < max_count:
            val = ord(source_string[count + 1])*256 + ord(source_string[count])
            sum = sum + val
            sum = sum & 0xffffffff 
            count = count + 2
        if max_count<len(source_string):
            sum = sum + ord(source_string[len(source_string) - 1])
            sum = sum & 0xffffffff 
        sum = (sum >> 16)  +  (sum & 0xffff)
        sum = sum + (sum >> 16)
        answer = ~sum
        answer = answer & 0xffff
        answer = answer >> 8 | (answer << 8 & 0xff00)
        return answer

    def receive_pong(self, sock, ID, timeout):
        """
        Receive ping from the socket.
        """
        time_remaining = timeout
        while True:
            start_time = time.time()
            readable = select.select([sock], [], [], time_remaining)
            time_spent = (time.time() - start_time)
            if readable[0] == []: # Timeout
                return

            time_received = time.time()
            recv_packet, addr = sock.recvfrom(1024)
            icmp_header = recv_packet[20:28]
            type, code, checksum, packet_ID, sequence = struct.unpack(
                "bbHHh", icmp_header
            )
            if packet_ID == ID:
                bytes_In_double = struct.calcsize("d")
                time_sent = struct.unpack("d", recv_packet[28:28 + bytes_In_double])[0]
                return time_received - time_sent

            time_remaining = time_remaining - time_spent
            if time_remaining <= 0:
                return

我们需要一个 send_ping() 方法,它将 ping 请求的数据发送到目标主机。此外,这将调用 do_checksum() 方法来检查 ping 数据的完整性,如下所示:

    def send_ping(self, sock,  ID):
        """
        Send ping to the target host
        """
        target_addr  =  socket.gethostbyname(self.target_host)
        my_checksum = 0
        # Create a dummy header with a 0 checksum.
        header = struct.pack("bbHHh", ICMP_ECHO_REQUEST, 0, my_checksum, ID, 1)
        bytes_In_double = struct.calcsize("d")
        data = (192 - bytes_In_double) * "Q"
        data = struct.pack("d", time.time()) + data
        # Get the checksum on the data and the dummy header.
        my_checksum = self.do_checksum(header + data)
        header = struct.pack(
            "bbHHh", ICMP_ECHO_REQUEST, 0, socket.htons(my_checksum), ID, 1
        )
        packet = header + data
        sock.sendto(packet, (target_addr, 1))

让我们定义另一个名为 ping_once() 的方法,它对目标主机进行单次 ping 调用。它通过将 ICMP 协议传递给 socket() 创建一个原始的 ICMP 套接字。异常处理代码负责处理脚本不是以超级用户身份运行或发生任何其他套接字错误的情况。让我们看一下以下代码:

    def ping_once(self):
        """
        Returns the delay (in seconds) or none on timeout.
        """
        icmp = socket.getprotobyname("icmp")
        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_RAW, icmp)
        except socket.error, (errno, msg):
            if errno == 1:
                # Not superuser, so operation not permitted
                msg +=  "ICMP messages can only be sent from root user processes"
                raise socket.error(msg)
        except Exception, e:
            print "Exception: %s" %(e)
        my_ID = os.getpid() & 0xFFFF
        self.send_ping(sock, my_ID)
        delay = self.receive_pong(sock, my_ID, self.timeout)
        sock.close()
        return delay

此类的主体执行方法是 ping()。它在一个 for 循环内部调用 ping_once() 方法 count 次,并接收 ping 响应的延迟(以秒为单位)。如果没有返回延迟,则表示 ping 失败。让我们看一下以下代码:

    def ping(self):
        """
        Run the ping process
        """
        for i in xrange(self.count):
            print "Ping to %s..." % self.target_host,
            try:
                delay  =  self.ping_once()
            except socket.gaierror, e:
                print "Ping failed. (socket error: '%s')" % e[1]
                break
            if delay  ==  None:
               print "Ping failed. (timeout within %ssec.)" % \  \
                      self.timeout
            else:
                delay  =  delay * 1000
                print "Get pong in %0.4fms" % delay

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Python ping')
    parser.add_argument('--target-host', action="store", dest="target_host", required=True)
    given_args = parser.parse_args()  
    target_host = given_args.target_host
    pinger = Pinger(target_host=target_host)
    pinger.ping()

此脚本显示了以下输出。此脚本以超级用户权限运行:

$ sudo python 3_2_ping_remote_host.py --target-host=www.google.com 
Ping to www.google.com... Get pong in 7.6921ms 
Ping to www.google.com... Get pong in 7.1061ms 
Ping to www.google.com... Get pong in 8.9211ms 
Ping to www.google.com... Get pong in 7.9899ms 

它是如何工作的...

已经构建了一个名为 Pinger 的类来定义一些有用的方法。该类使用一些用户定义的或默认的输入进行初始化,如下所示:

  • target_host:这是要 ping 的目标主机

  • count:这是进行 ping 的次数

  • timeout:这是确定何时结束未完成的 ping 操作的值

send_ping() 方法获取目标主机的 DNS 主机名并使用 struct 模块创建一个 ICMP_ECHO_REQUEST 数据包。使用 do_checksum() 方法检查方法的数据完整性是必要的。它接受源字符串并对其进行操作以产生正确的校验和。在接收端,receive_pong() 方法等待响应,直到超时或接收到响应。它捕获 ICMP 响应头,然后比较数据包 ID 并计算请求和响应周期中的延迟。

等待远程网络服务

在网络服务恢复期间,有时运行一个脚本来检查服务器何时再次上线可能很有用。

如何做...

我们可以编写一个客户端,使其永久等待或超时等待特定的网络服务。在这个例子中,默认情况下,我们希望检查本地主机上的 Web 服务器是否启动。如果您指定了其他远程主机或端口,则将使用该信息。

列表 3.3 显示了等待远程网络服务,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import socket
import errno
from time import time as now

DEFAULT_TIMEOUT = 120
DEFAULT_SERVER_HOST = 'localhost'
DEFAULT_SERVER_PORT = 80

class NetServiceChecker(object):
    """ Wait for a network service to come online"""
    def __init__(self, host, port, timeout=DEFAULT_TIMEOUT):
        self.host = host
        self.port = port
        self.timeout = timeout
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    def end_wait(self):
        self.sock.close()

    def check(self):
        """ Check the service """
        if self.timeout:
            end_time = now() + self.timeout

        while True:
            try:
                if self.timeout:
                    next_timeout = end_time - now()
                    if next_timeout < 0:
                        return False
                    else:
                        print "setting socket next timeout %ss"\
                       %round(next_timeout)
                        self.sock.settimeout(next_timeout)
                self.sock.connect((self.host, self.port))
            # handle exceptions
            except socket.timeout, err:
                if self.timeout:
                    return False
            except socket.error, err:
                print "Exception: %s" %err
            else: # if all goes well
                self.end_wait()
                return True

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Wait for Network Service')
    parser.add_argument('--host', action="store", dest="host",  default=DEFAULT_SERVER_HOST)
    parser.add_argument('--port', action="store", dest="port", type=int, default=DEFAULT_SERVER_PORT)
    parser.add_argument('--timeout', action="store", dest="timeout", type=int, default=DEFAULT_TIMEOUT)
    given_args = parser.parse_args() 
    host, port, timeout = given_args.host, given_args.port, given_args.timeout
    service_checker = NetServiceChecker(host, port, timeout=timeout)
    print "Checking for network service %s:%s ..." %(host, port)
    if service_checker.check():
        print "Service is available again!"

如果您的机器上运行着像 Apache 这样的 Web 服务器,此脚本将显示以下输出:

$ python 3_3_wait_for_remote_service.py 
Waiting for network service localhost:80 ... 
setting socket next timeout 120.0s 
Service is available again!

现在,停止 Apache 进程,运行此脚本,然后再次启动 Apache。输出模式将不同。在我的机器上,发现了以下输出模式:

Exception: [Errno 103] Software caused connection abort 
setting socket next timeout 104.189137936 
Exception: [Errno 111] Connection refused 
setting socket next timeout 104.186291933 
Exception: [Errno 103] Software caused connection abort 
setting socket next timeout 104.186164856 
Service is available again!

以下截图显示了等待一个活动的 Apache Web 服务器进程:

如何做...

它是如何工作的...

上述脚本使用 argparse 模块来获取用户输入并处理主机名、端口和超时,即我们的脚本将等待所需网络服务的时间。它启动 NetServiceChecker 类的一个实例并调用 check() 方法。此方法计算等待的最终结束时间,并使用套接字的 settimeout() 方法来控制每一轮的结束时间,即 next_timeout。然后它使用套接字的 connect() 方法来测试所需的网络服务是否可用,直到套接字超时。此方法还会捕获套接字超时错误,并将套接字超时与用户给出的超时值进行比较。

列举您的机器上的接口

如果您需要列出机器上存在的网络接口,在 Python 中这并不复杂。有几个第三方库可以在几行内完成这项工作。然而,让我们看看如何使用纯套接字调用完成这项工作。

准备中

您需要在 Linux 服务器上运行此脚本。要获取可用接口的列表,您可以执行以下命令:

$ /sbin/ifconfig

如何做...

列表 3.4 展示了如何列出网络接口,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
import sys
import socket
import fcntl
import struct
import array

SIOCGIFCONF = 0x8912 #from C library sockios.h
STUCT_SIZE_32 = 32
STUCT_SIZE_64 = 40
PLATFORM_32_MAX_NUMBER =  2**32
DEFAULT_INTERFACES = 8

def list_interfaces():
    interfaces = []
    max_interfaces = DEFAULT_INTERFACES
    is_64bits = sys.maxsize > PLATFORM_32_MAX_NUMBER
    struct_size = STUCT_SIZE_64 if is_64bits else STUCT_SIZE_32
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 
    while True:
        bytes = max_interfaces * struct_size
        interface_names = array.array('B', '\0' * bytes)
        sock_info = fcntl.ioctl( 
            sock.fileno(),
            SIOCGIFCONF,
            struct.pack('iL', bytes,interface_names.buffer_info()[0])
        )
        outbytes = struct.unpack('iL', sock_info)[0]
        if outbytes == bytes:
            max_interfaces *= 2  
        else: 
            break
    namestr = interface_names.tostring()
    for i in range(0, outbytes, struct_size):
        interfaces.append((namestr[i:i+16].split('\0', 1)[0]))
    return interfaces

if __name__ == '__main__':
    interfaces = list_interfaces()
    print "This machine has %s network interfaces: %s." %(len(interfaces), interface)

前面的脚本将列出网络接口,如下所示输出:

$ python 3_4_list_network_interfaces.py 
This machine has 2 network interfaces: ['lo', 'eth0'].

它是如何工作的...

此菜谱代码使用低级套接字功能来找出系统上存在的接口。单个list_interfaces()方法创建一个套接字对象,并通过操作此对象来查找网络接口信息。它是通过调用fnctl模块的ioctl()方法来做到这一点的。fnctl模块与一些 Unix 例程接口,例如fnctl()。此接口在底层文件描述符套接字上执行 I/O 控制操作,该套接字是通过调用套接字对象的fileno()方法获得的。

ioctl()方法的附加参数包括在 C 套接字库中定义的SIOCGIFADDR常量和由struct模块的pack()函数产生的数据结构。数据结构指定的内存地址在ioctl()调用后会修改。在这种情况下,interface_names变量包含这些信息。在解包ioctl()调用的sock_info返回值后,如果数据的大小表明需要,网络接口的数量会增加两次。这是通过while循环完成的,以发现所有接口,如果我们的初始接口计数假设不正确的话。

接口名称是从interface_names变量的字符串格式中提取出来的。它读取该变量的特定字段,并将接口列表中的值附加到这些字段上。在list_interfaces()函数的末尾,这些信息被返回。

在您的机器上查找特定接口的 IP 地址

在您的 Python 网络应用程序中,可能需要查找特定网络接口的 IP 地址。

准备工作

此菜谱专门为 Linux 服务器准备。有一些 Python 模块专门设计用于在 Windows 和 Mac 平台上实现类似的功能。例如,请参阅sourceforge.net/projects/pywin32/以获取 Windows 特定的实现。

如何操作...

您可以使用fnctl模块查询您机器上的 IP 地址。

列表 3.5 展示了如何在您的机器上查找特定接口的 IP 地址,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import sys
import socket
import fcntl
import struct
import array

def get_ip_address(ifname):
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    return socket.inet_ntoa(fcntl.ioctl(
        s.fileno(),
        0x8915,  # SIOCGIFADDR
        struct.pack('256s', ifname[:15])
    )[20:24])

if __name__ == '__main__':
    #interfaces =  list_interfaces()
    parser = argparse.ArgumentParser(description='Python networking utils')
    parser.add_argument('--ifname', action="store", dest="ifname", required=True)
    given_args = parser.parse_args() 
    ifname = given_args.ifname    
    print "Interface [%s] --> IP: %s" %(ifname, get_ip_address(ifname)) 

此脚本的输出显示在一行中,如下所示:

$ python 3_5_get_interface_ip_address.py --ifname=eth0 
Interface [eth0] --> IP: 10.0.2.15 

它是如何工作的...

此菜谱与上一个类似。前面的脚本接受一个命令行参数:要查找 IP 地址的网络接口的名称。get_ip_address()函数创建一个套接字对象,并调用fnctl.ioctl()函数来查询该对象的 IP 信息。请注意,socket.inet_ntoa()函数将二进制数据转换为人类可读的以点分隔的字符串,正如我们所熟悉的那样。

查找您的机器上某个接口是否已启动

如果你机器上有多个网络接口,在开始对特定接口进行任何操作之前,你可能会想知道该网络接口的状态,例如,接口是否实际上处于开启状态。这确保了你的命令被路由到活动接口。

准备工作

此食谱是为 Linux 机器编写的。因此,此脚本不会在 Windows 或 Mac 主机上运行。在此食谱中,我们使用nmap,一个著名的网络扫描工具。你可以从其网站nmap.org/了解更多关于nmap的信息。

你还需要python-nmap模块来运行此食谱。这可以通过以下方式使用pip安装:

$ pip install python-nmap

如何做到这一点...

我们可以创建一个套接字对象并获取该接口的 IP 地址。然后,我们可以使用任何扫描技术来探测接口状态。

列表 3.6 展示了检测网络接口状态,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import socket
import struct
import fcntl
import nmap
SAMPLE_PORTS = '21-23'

def get_interface_status(ifname):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    ip_address = socket.inet_ntoa(fcntl.ioctl(
        sock.fileno(),
        0x8915, #SIOCGIFADDR, C socket library sockios.h
        struct.pack('256s', ifname[:15])
    )[20:24])

    nm = nmap.PortScanner()         
    nm.scan(ip_address, SAMPLE_PORTS)      
    return nm[ip_address].state()          

if  __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Python networking utils')
    parser.add_argument('--ifname', action="store", dest="ifname", required=True)
    given_args = parser.parse_args() 
    ifname = given_args.ifname    
    print "Interface [%s] is: %s" %(ifname, get_interface_status(ifname))      

如果你运行此脚本来查询eth0的状态,它将显示类似于以下输出:

$ python 3_6_find_network_interface_status.py --ifname=eth0 
Interface [eth0] is: up

它是如何工作的...

该食谱从命令行获取接口名称,并将其传递给get_interface_status()函数。此函数通过操作 UDP 套接字对象来找到该接口的 IP 地址。

此食谱需要nmap第三方模块。我们可以使用pip install命令安装它。nmap扫描实例nm是通过调用PortScanner()创建的。对本地 IP 地址的初始扫描为我们提供了相关网络接口的状态。

检测网络上的不活跃机器

如果你被赋予了网络上几台机器的 IP 地址列表,并且要求你编写一个脚本来找出哪些主机定期不活跃,你将希望创建一个网络扫描程序,而不需要在目标主机计算机上安装任何东西。

准备工作

此食谱需要安装 Scapy 库(> 2.2),可以从www.secdev.org/projects/scapy/files/scapy-latest.zip获取。

如何做到这一点...

我们可以使用 Scapy,一个成熟的第三方网络分析库,来启动 ICMP 扫描。由于我们希望定期进行,我们需要 Python 的sched模块来安排扫描任务。

列表 3.7 展示了如何检测不活跃机器,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
# This recipe requires scapy-2.2.0 or higher 

import argparse
import time
import sched
from scapy.all import sr, srp, IP, UDP, ICMP, TCP, ARP, Ether
RUN_FREQUENCY = 10
scheduler = sched.scheduler(time.time, time.sleep)

def detect_inactive_hosts(scan_hosts):
    """ 
    Scans the network to find scan_hosts are live or dead
    scan_hosts can be like 10.0.2.2-4 to cover range. 
    See Scapy docs for specifying targets.   
    """
    global scheduler
    scheduler.enter(RUN_FREQUENCY, 1, detect_inactive_hosts, (scan_hosts, ))
    inactive_hosts = []
    try:
        ans, unans = sr(IP(dst=scan_hosts)/ICMP(),retry=0, timeout=1)
        ans.summary(lambda(s,r) : r.sprintf("%IP.src% is alive"))
        for inactive in unans:
            print "%s is inactive" %inactive.dst
            inactive_hosts.append(inactive.dst)
        print "Total %d hosts are inactive" %(len(inactive_hosts))
    except KeyboardInterrupt:
        exit(0)
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Python networking utils')
    parser.add_argument('--scan-hosts', action="store", dest="scan_hosts", required=True)
    given_args = parser.parse_args() 
    scan_hosts = given_args.scan_hosts    
    scheduler.enter(1, 1, detect_inactive_hosts, (scan_hosts, ))
    scheduler.run()

此脚本的输出将类似于以下命令:

$ sudo python 3_7_detect_inactive_machines.py --scan-hosts=10.0.2.2-4
Begin emission:
.*...Finished to send 3 packets.
.
Received 6 packets, got 1 answers, remaining 2 packets
10.0.2.2 is alive
10.0.2.4 is inactive
10.0.2.3 is inactive
Total 2 hosts are inactive
Begin emission:
*.Finished to send 3 packets.
Received 3 packets, got 1 answers, remaining 2 packets
10.0.2.2 is alive
10.0.2.4 is inactive
10.0.2.3 is inactive
Total 2 hosts are inactive

它是如何工作的...

上述脚本首先从命令行获取网络主机列表scan_hosts。然后,它创建一个计划,在延迟一秒后启动detect_inactive_hosts()函数。目标函数接受scan_hosts参数并调用 Scapy 的sr()函数。

此函数通过再次调用schedule.enter()函数来安排自己在每 10 秒后重新运行。这样,我们就定期运行这个扫描任务。

Scapy 的 sr() 扫描函数接受一个 IP、协议和一些扫描控制信息。在这种情况下,IP() 方法将 scan_hosts 作为要扫描的目标主机传递,协议指定为 ICMP。这也可以是 TCP 或 UDP。我们没有指定重试和一秒超时以加快脚本运行速度。然而,你可以尝试适合你的选项。

扫描 sr() 函数返回响应和未响应的主机作为元组。我们检查未响应的主机,构建一个列表,并打印该信息。

使用连接套接字(socketpair)执行基本 IPC

有时,两个脚本需要通过两个进程相互之间传递一些信息。在 Unix/Linux 中,有一个连接套接字的概念,即 socketpair。我们在这里可以实验一下。

准备中

这个配方是为 Unix/Linux 主机设计的。Windows/Mac 不适合运行此配方。

如何实现...

我们使用一个 test_socketpair() 函数来包装一些测试套接字 socketpair() 函数的几行代码。

列表 3.8 展示了 socketpair 的一个示例,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import socket
import os

BUFSIZE = 1024

def test_socketpair():
    """ Test Unix socketpair"""
    parent, child = socket.socketpair()

    pid = os.fork()
    try:
        if pid:
            print "@Parent, sending message..."
            child.close()
            parent.sendall("Hello from parent!")
            response = parent.recv(BUFSIZE)
            print "Response from child:", response
            parent.close()

        else:
            print "@Child, waiting for message from parent"
            parent.close()
            message = child.recv(BUFSIZE)
            print "Message from parent:", message
            child.sendall("Hello from child!!")
            child.close()
    except Exception, err:
        print "Error: %s" %err

if __name__ == '__main__':
    test_socketpair()

上一段脚本的输出如下:

$ python 3_8_ipc_using_socketpairs.py
@Parent, sending message... 
@Child, waiting for message from parent 
Message from parent: Hello from parent! 
Response from child: Hello from child!! 

它是如何工作的...

socket.socketpair() 函数简单地返回两个连接的套接字对象。在我们的例子中,我们可以称其中一个为父进程,另一个为子进程。我们通过 os.fork() 调用创建另一个进程。这会返回父进程的进程 ID。在每个进程中,首先关闭另一个进程的套接字,然后通过进程套接字上的 sendall() 方法调用交换消息。try-except 块在发生任何类型的异常时打印错误。

使用 Unix 域套接字执行 IPC

Unix 域套接字UDS)有时被用作在两个进程之间通信的便捷方式。在 Unix 中,一切概念上都是文件。如果你需要一个此类 IPC 行动的例子,这可能很有用。

如何实现...

我们启动一个 UDS 服务器,将其绑定到文件系统路径,UDS 客户端使用相同的路径与服务器通信。

列表 3.9a 展示了一个 Unix 域套接字服务器,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import socket
import os
import time

SERVER_PATH = "/tmp/python_unix_socket_server"

def run_unix_domain_socket_server():
    if os.path.exists(SERVER_PATH):
        os.remove( SERVER_PATH )

    print "starting unix domain socket server."
    server = socket.socket( socket.AF_UNIX, socket.SOCK_DGRAM )
    server.bind(SERVER_PATH)

    print "Listening on path: %s" %SERVER_PATH
    while True:
        datagram = server.recv( 1024 )
        if not datagram:
            break
        else:
            print "-" * 20
            print datagram
        if "DONE" == datagram:
            break
    print "-" * 20
    print "Server is shutting down now..."
    server.close()
    os.remove(SERVER_PATH)
    print "Server shutdown and path removed."

if __name__ == '__main__':
    run_unix_domain_socket_server()

列表 3.9b 展示了一个 UDS 客户端,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import socket
import sys

SERVER_PATH = "/tmp/python_unix_socket_server"

def run_unix_domain_socket_client():
    """ Run "a Unix domain socket client """
    sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)

    # Connect the socket to the path where the server is listening
    server_address = SERVER_PATH 
    print "connecting to %s" % server_address
    try:
        sock.connect(server_address)
    except socket.error, msg:
        print >>sys.stderr, msg
        sys.exit(1)

    try:
        message = "This is the message.  This will be echoed back!"
        print  "Sending [%s]" %message
        sock.sendall(message)
        amount_received = 0
        amount_expected = len(message)

        while amount_received < amount_expected:
            data = sock.recv(16)
            amount_received += len(data)
            print >>sys.stderr, "Received [%s]" % data

    finally:
        print "Closing client"
        sock.close()

if __name__ == '__main__':
    run_unix_domain_socket_client()

服务器输出如下:

$ python 3_9a_unix_domain_socket_server.py 
starting unix domain socket server. 
Listening on path: /tmp/python_unix_socket_server
-------------------- 
This is the message.  This will be echoed back!

客户端输出如下:

$ python 3_9b_unix_domain_socket_client.py 
connecting to /tmp/python_unix_socket_server 
Sending [This is the message.  This will be echoed back!]

它是如何工作的...

为 UDS 客户端/服务器定义了一个常见的交互路径。客户端和服务器使用相同的路径进行连接和监听。

在服务器代码中,我们删除了之前运行此脚本时存在的路径。然后创建一个 Unix 数据报套接字并将其绑定到指定的路径。然后监听传入的连接。在数据处理循环中,它使用 recv() 方法从客户端获取数据,并在屏幕上打印该信息。

客户端代码简单地打开一个 Unix 数据报套接字,并连接到共享的服务器地址。它使用 sendall() 向服务器发送消息。然后它等待消息被回显给自己,并打印那条消息。

查找你的 Python 是否支持 IPv6 套接字

IP 版本 6 或 IPv6 正越来越被行业采用以构建新的应用程序。如果您想编写 IPv6 应用程序,您首先想知道的是您的机器是否支持 IPv6。这可以通过以下 Linux/Unix 命令行完成:

$ cat /proc/net/if_inet6 
00000000000000000000000000000001 01 80 10 80       lo 
fe800000000000000a0027fffe950d1a 02 40 20 80     eth0 

从您的 Python 脚本中,您还可以检查您的机器上是否存在 IPv6 支持,以及 Python 是否安装了该支持。

准备工作

对于此配方,使用pip安装 Python 第三方库netifaces,如下所示:

$ pip install   netifaces

如何操作...

我们可以使用第三方库netifaces来检查您的机器上是否有 IPv6 支持。我们可以从这个库中调用interfaces()函数来列出系统中存在的所有接口。

列表 3.10 显示了 Python IPv6 支持检查器,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
# This program depends on Python module netifaces => 0.8
import socket
import argparse
import netifaces as ni

def inspect_ipv6_support():
    """ Find the ipv6 address"""
    print "IPV6 support built into Python: %s" %socket.has_ipv6
    ipv6_addr = {}
    for interface in ni.interfaces():
        all_addresses = ni.ifaddresses(interface)
        print "Interface %s:" %interface
        for family,addrs in all_addresses.iteritems():
            fam_name = ni.address_families[family]
            print '  Address family: %s' % fam_name
            for addr in addrs:
                if fam_name == 'AF_INET6':
                    ipv6_addr[interface] = addr['addr']
                print     '    Address  : %s' % addr['addr']
                nmask = addr.get('netmask', None)
                if nmask:
                    print '    Netmask  : %s' % nmask
                bcast = addr.get('broadcast', None)
                if bcast:
                    print '    Broadcast: %s' % bcast
    if ipv6_addr:
        print "Found IPv6 address: %s" %ipv6_addr
    else:
        print "No IPv6 interface found!"  

if __name__ == '__main__':
    inspect_ipv6_support()

此脚本的输出将如下所示:

$ python 3_10_check_ipv6_support.py 
IPV6 support built into Python: True 
Interface lo: 
 Address family: AF_PACKET 
 Address  : 00:00:00:00:00:00 
 Address family: AF_INET 
 Address  : 127.0.0.1 
 Netmask  : 255.0.0.0 
 Address family: AF_INET6 
 Address  : ::1 
 Netmask  : ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff 
Interface eth0: 
 Address family: AF_PACKET 
 Address  : 08:00:27:95:0d:1a 
 Broadcast: ff:ff:ff:ff:ff:ff 
 Address family: AF_INET 
 Address  : 10.0.2.15 
 Netmask  : 255.255.255.0 
 Broadcast: 10.0.2.255 
 Address family: AF_INET6 
 Address  : fe80::a00:27ff:fe95:d1a
 Netmask  : ffff:ffff:ffff:ffff:: 
Found IPv6 address: {'lo': '::1', 'eth0': 'fe80::a00:27ff:fe95:d1a'}

以下截图显示了 IPv6 客户端和服务器之间的交互:

如何操作...

它是如何工作的...

IPv6 支持检查器函数inspect_ipv6_support()首先检查 Python 是否使用socket.has_ipv6构建了 IPv6。接下来,我们调用netifaces模块中的interfaces()函数。这为我们提供了所有接口的列表。如果我们通过传递一个网络接口给ifaddresses()方法,我们可以获取该接口的所有 IP 地址。然后,我们提取各种 IP 相关信息,如协议族、地址、子网掩码和广播地址。然后,如果协议族匹配AF_INET6,则将网络接口的地址添加到IPv6_address字典中。

从 IPv6 地址中提取 IPv6 前缀

在您的 IPv6 应用程序中,您需要挖掘出 IPv6 地址以获取前缀信息。请注意,IPv6 地址的高 64 位由全局路由前缀加上子网 ID 表示,如 RFC 3513 中定义。一个通用前缀(例如,/48)包含一个基于短前缀的较长、更具体的前缀(例如,/64)。Python 脚本在生成前缀信息方面非常有帮助。

如何操作...

我们可以使用netifacesnetaddr第三方库来查找给定 IPv6 地址的 IPv6 前缀信息,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import socket
import netifaces as ni
import netaddr as na

def extract_ipv6_info():
    """ Extracts IPv6 information"""
    print "IPV6 support built into Python: %s" %socket.has_ipv6
    for interface in ni.interfaces():
        all_addresses = ni.ifaddresses(interface)
        print "Interface %s:" %interface
        for family,addrs in all_addresses.iteritems():
            fam_name = ni.address_families[family]
            #print '  Address family: %s' % fam_name
            for addr in addrs:
                if fam_name == 'AF_INET6':
                    addr = addr['addr']
                    has_eth_string = addr.split("%eth")
                    if has_eth_string:
       addr = addr.split("%eth")[0]
       print "    IP Address: %s" %na.IPNetwork(addr)
       print "    IP Version: %s" %na.IPNetwork(addr).version
       print "    IP Prefix length: %s" %na.IPNetwork(addr).prefixlen
       print "    Network: %s" %na.IPNetwork(addr).network
       print "    Broadcast: %s" %na.IPNetwork(addr).broadcast
if __name__ == '__main__':
    extract_ipv6_info()

此脚本的输出如下所示:

$ python 3_11_extract_ipv6_prefix.py 
IPV6 support built into Python: True 
Interface lo: 
 IP Address: ::1/128 
 IP Version: 6 
 IP Prefix length: 128 
 Network: ::1 
 Broadcast: ::1 
Interface eth0: 
 IP Address: fe80::a00:27ff:fe95:d1a/128 
 IP Version: 6 
 IP Prefix length: 128 
 Network: fe80::a00:27ff:fe95:d1a 
 Broadcast: fe80::a00:27ff:fe95:d1a 

它是如何工作的...

Python 的netifaces模块为我们提供了网络接口的 IPv6 地址。它使用interfaces()ifaddresses()函数来完成此操作。netaddr模块特别有助于操作网络地址。它有一个IPNetwork()类,为我们提供了一个地址,IPv4 或 IPv6,并计算前缀、网络和广播地址。在这里,我们找到这个信息类实例的版本、前缀长度和网络和广播属性。

编写 IPv6 echo 客户端/服务器

您需要编写一个 IPv6 兼容的服务器或客户端,并想知道 IPv6 兼容服务器或客户端与其 IPv4 对应版本之间可能存在的差异。

如何实现...

我们使用与使用 IPv6 编写回显客户端/服务器相同的方法。唯一的重大区别是使用 IPv6 信息创建套接字的方式。

列表 12a 显示了 IPv6 回显服务器,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 3
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse 
import socket
import sys

HOST = 'localhost'

def echo_server(port, host=HOST):
    """Echo server using IPv6 """
    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, 				socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
        af, socktype, proto, canonname, sa = res
        try:
            sock = socket.socket(af, socktype, proto)
        except socket.error, err:
            print "Error: %s" %err

        try:
            sock.bind(sa)
            sock.listen(1)
            print "Server listening on %s:%s" %(host, port)
        except socket.error, msg:
            sock.close()
            continue
        break
        sys.exit(1)
    conn, addr = sock.accept()
    print 'Connected to', addr
    while True:
        data = conn.recv(1024)
        print "Received data from the client: [%s]" %data
        if not data: break
        conn.send(data)
        print "Sent data echoed back to the client: [%s]" %data
    conn.close()

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='IPv6 Socket Server Example')
    parser.add_argument('--port', action="store", dest="port", type=int, required=True)
    given_args = parser.parse_args() 
    port = given_args.port
    echo_server(port)

列表 12b 显示了 IPv6 回显客户端,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 3
# This program is optimized for Python 2.7.

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

import argparse
import socket
import sys

HOST = 'localhost'
BUFSIZE = 1024

def ipv6_echo_client(port, host=HOST):
    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
        af, socktype, proto, canonname, sa = res
        try:
            sock = socket.socket(af, socktype, proto)
        except socket.error, err:
            print "Error:%s" %err
        try:
            sock.connect(sa)
        except socket.error, msg:
            sock.close()
            continue
    if sock is None:
        print 'Failed to open socket!'
        sys.exit(1)
    msg = "Hello from ipv6 client"
    print "Send data to server: %s" %msg
    sock.send(msg)
    while True:
        data = sock.recv(BUFSIZE)
        print 'Received from server', repr(data)
        if not data: 
            break
    sock.close()
if __name__ == '__main__': 
    parser = argparse.ArgumentParser(description='IPv6 socket client example')
    parser.add_argument('--port', action="store", dest="port", type=int, required=True)
    given_args = parser.parse_args() 
    port = given_args.port
    ipv6_echo_client(port)

服务器输出如下:

$ python 3_12a_ipv6_echo_server.py --port=8800 
Server lisenting on localhost:8800 
Connected to ('127.0.0.1', 35034) 
Received data from the client: [Hello from ipv6 client] 
Sent data echoed back to the client: [Hello from ipv6 client] 

客户端输出如下:

$ python 3_12b_ipv6_echo_client.py --port=8800 
Send data to server: Hello from ipv6 client 
Received from server 'Hello from ipv6 client' 

它是如何工作的...

IPv6 回显服务器首先通过调用socket.getaddrinfo()确定其 IPv6 信息。请注意,我们传递了AF_UNSPEC协议来创建 TCP 套接字。结果信息是一个包含五个值的元组。我们使用其中的三个值,地址族、套接字类型和协议,来创建服务器套接字。然后,这个套接字与之前元组中的套接字地址绑定。接下来,它监听传入的连接并接受它们。一旦建立连接,它从客户端接收数据并将其回显。

在客户端代码中,我们创建一个符合 IPv6 规范的客户端套接字实例,并使用该实例的send()方法发送数据。当数据被回显回来时,使用recv()方法来获取它。

第四章:使用 HTTP 编程 Internet

在本章中,我们将涵盖以下主题:

  • 从 HTTP 服务器下载数据

  • 从你的机器上服务 HTTP 请求

  • 访问网站后提取 cookie 信息

  • 提交网页表单

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

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

  • 在客户端代码中伪造 Mozilla Firefox

  • 使用 HTTP 压缩在 Web 请求中节省带宽

  • 使用带有恢复和部分下载的 HTTP 失效客户端编写

  • 使用 Python 和 OpenSSL 编写简单的 HTTPS 服务器代码

简介

本章解释了 Python HTTP 网络库函数和一些第三方库。例如,requests 库以一种更优雅、更简洁的方式处理 HTTP 请求。在其中一个菜谱中使用了 OpenSSL 库来创建一个启用 SSL 的 Web 服务器。

在几个菜谱中已经展示了许多常见的 HTTP 协议功能,例如,使用 POST 提交网页表单,操作头部信息,使用压缩等。

从 HTTP 服务器下载数据

你想要编写一个简单的 HTTP 客户端,从任何 Web 服务器使用本机 HTTP 协议获取一些数据。这可能是创建你自己的 HTTP 浏览器的第一步。

如何实现...

让我们使用 Python 的 httplib 创建的 Pythonic 最小浏览器访问 www.python.org

列表 4.1 解释了以下简单 HTTP 客户端的代码:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import httplib

REMOTE_SERVER_HOST = 'www.python.org'
REMOTE_SERVER_PATH = '/'

class HTTPClient:

  def __init__(self, host):
    self.host = host

  def fetch(self, path):
    http = httplib.HTTP(self.host)

    # Prepare header
    http.putrequest("GET", path)
    http.putheader("User-Agent", __file__)
    http.putheader("Host", self.host)
    http.putheader("Accept", "*/*")
    http.endheaders()

    try:
      errcode, errmsg, headers = http.getreply()

    except Exception, e:
      print "Client failed error code: %s message:%s headers:%s" 
%(errcode, errmsg, headers)
    else: 
      print "Got homepage from %s" %self.host 

    file = http.getfile()
    return file.read()

if __name__ == "__main__":
  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() 
  host, path = given_args.host, given_args.path
  client = HTTPClient(host)
  print client.fetch(path)

此菜谱默认会从 www.python.org 获取页面。你可以带或不带主机和路径参数运行此菜谱。如果运行此脚本,它将显示以下输出:

$  python 4_1_download_data.py --host=www.python.org 
Got homepage from www.python.org
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html  xml:lang="en" lang="en">

<head>
 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
 <title>Python Programming Language &ndash; Official Website</title>
....

如果你使用无效的路径运行此菜谱,它将显示以下服务器响应:

$ python 4_1_download_data.py --host='www.python.org' --path='/not-
exist'
Got homepage from www.python.org
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html  xml:lang="en" lang="en">
<head>
 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
 <title>Page Not Found</title>
 <meta name="keywords" content="Page Not Found" />
 <meta name="description" content="Page Not Found" />

它是如何工作的...

此菜谱定义了一个 HTTPClient 类,用于从远程主机获取数据。它是使用 Python 的本机 httplib 库构建的。在 fetch() 方法中,它使用 HTTP() 函数和其他辅助函数创建一个虚拟 HTTP 客户端,例如 putrequest()putheader()。它首先放置 GET/path 字符串,然后设置用户代理,即当前脚本的名称 (__file__)。

主要请求 getreply() 方法被放置在一个 try-except 块中。响应从 getfile() 方法检索,并读取流的内容。

从你的机器上服务 HTTP 请求

你想要创建自己的 Web 服务器。你的 Web 服务器应该处理客户端请求并发送一个简单的 hello 消息。

如何实现...

Python 随带一个非常简单的 Web 服务器,可以从命令行启动,如下所示:

$ python -m SimpleHTTPServer 8080

这将在端口 8080 上启动一个 HTTP 网络服务器。你可以通过在浏览器中输入 http://localhost:8080 来访问这个网络服务器。这将显示运行前面命令的当前目录的内容。如果该目录中包含任何网络服务器索引文件,例如 index.html,则你的浏览器将显示 index.html 的内容。然而,如果你想要完全控制你的网络服务器,你需要启动你的自定义 HTTP 服务器...

列表 4.2 给出了自定义 HTTP 服务器的以下代码:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import sys
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer

DEFAULT_HOST = '127.0.0.1'
DEFAULT_PORT = 8800

class RequestHandler(BaseHTTPRequestHandler):
  """ Custom request handler"""

  def do_GET(self):
    """ Handler for the GET requests """
    self.send_response(200)
    self.send_header('Content-type','text/html')
    self.end_headers()
    # Send the message to browser
    self.wfile.write("Hello from server!")

class CustomHTTPServer(HTTPServer):
  "A custom HTTP server"
  def __init__(self, host, port):
    server_address = (host, port)
    HTTPServer.__init__(self, server_address, RequestHandler)

def run_server(port):
  try:
    server= CustomHTTPServer(DEFAULT_HOST, port)
    print "Custom HTTP server started on port: %s" % port
    server.serve_forever()
  except Exception, err:
    print "Error:%s" %err
  except KeyboardInterrupt:
    print "Server interrupted and is shutting down..."
    server.socket.close()

if __name__ == "__main__":
  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() 
  port = given_args.port
  run_server(port)

下面的截图显示了一个简单的 HTTP 服务器:

如何操作...

如果你运行这个网络服务器并通过浏览器访问 URL,这将向浏览器发送一行文本 Hello from server!,如下所示:

$ python 4_2_simple_http_server.py --port=8800
Custom HTTP server started on port: 8800
localhost - - [18/Apr/2013 13:39:33] "GET / HTTP/1.1" 200 -
localhost - - [18/Apr/2013 13:39:33] "GET /favicon.ico HTTP/1.1" 200 

它是如何工作的...

在这个菜谱中,我们创建了从 HTTPServer 类继承的 CustomHTTPServer 类。在构造方法中,CustomHTTPServer 类设置了从用户输入接收的服务器地址和端口。在构造方法中,我们的网络服务器的 RequestHandler 类已经设置。每当有客户端连接时,服务器都会根据这个类来处理请求。

RequestHandler 定义了处理客户端 GET 请求的动作。它使用 write() 方法发送一个带有成功消息 Hello from server! 的 HTTP 头(代码 200)。

访问网站后提取 cookie 信息

许多网站使用 cookie 在本地磁盘上存储它们的各种信息。你希望看到这些 cookie 信息,也许可以使用 cookie 自动登录到该网站。

如何操作...

让我们假装登录到一个流行的代码共享网站,www.bitbucket.org。我们希望在登录页面 bitbucket.org/account/signin/?next=/ 上提交登录信息。下面的截图显示了登录页面:

如何操作...

因此,我们记录下表单元素的 ID,并决定应该提交哪些假值。我们第一次访问这个页面,然后下次访问主页以观察已经设置了哪些 cookie。

列表 4.3 如下解释了如何提取 cookie 信息:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import cookielib 
import urllib
import urllib2

ID_USERNAME = 'id_username'
ID_PASSWORD = 'id_password'
USERNAME = 'you@email.com'
PASSWORD = 'mypassword'
LOGIN_URL = 'https://bitbucket.org/account/signin/?next=/'
NORMAL_URL = 'https://bitbucket.org/'

def extract_cookie_info():
  """ Fake login to a site with cookie"""
  # setup cookie jar
  cj = cookielib.CookieJar()
  login_data = urllib.urlencode({ID_USERNAME : USERNAME, 
  ID_PASSWORD : PASSWORD})
  # create url opener
  opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj))
  resp = opener.open(LOGIN_URL, login_data)

  # send login info 
  for cookie in cj:
    print "----First time cookie: %s --> %s" %(cookie.name, 
cookie.value)
    print "Headers: %s" %resp.headers

  # now access without any login info
  resp = opener.open(NORMAL_URL)
  for cookie in cj:
    print "++++Second time cookie: %s --> %s" %(cookie.name, 
cookie.value)

  print "Headers: %s" %resp.headers

if __name__ == '__main__':
  extract_cookie_info()

运行这个菜谱会产生以下输出:

$ python 4_3_extract_cookie_information.py 
----First time cookie: bb_session --> aed58dde1228571bf60466581790566d
Headers: Server: nginx/1.2.4
Date: Sun, 05 May 2013 15:13:56 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 21167
Connection: close
X-Served-By: bitbucket04
Content-Language: en
X-Static-Version: c67fb01467cf
Expires: Sun, 05 May 2013 15:13:56 GMT
Vary: Accept-Language, Cookie
Last-Modified: Sun, 05 May 2013 15:13:56 GMT
X-Version: 14f9c66ad9db
ETag: "3ba81d9eb350c295a453b5ab6e88935e"
X-Request-Count: 310
Cache-Control: max-age=0
Set-Cookie: bb_session=aed58dde1228571bf60466581790566d; expires=Sun, 19-May-2013 15:13:56 GMT; httponly; Max-Age=1209600; Path=/; secure

Strict-Transport-Security: max-age=2592000
X-Content-Type-Options: nosniff

++++Second time cookie: bb_session --> aed58dde1228571bf60466581790566d
Headers: Server: nginx/1.2.4
Date: Sun, 05 May 2013 15:13:57 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 36787
Connection: close
X-Served-By: bitbucket02
Content-Language: en
X-Static-Version: c67fb01467cf
Vary: Accept-Language, Cookie
X-Version: 14f9c66ad9db
X-Request-Count: 97
Strict-Transport-Security: max-age=2592000
X-Content-Type-Options: nosniff

它是如何工作的...

我们使用了 Python 的cookielib并设置了一个 cookie 存储,cj。登录数据已经使用urllib.urlencode进行了编码。urllib2有一个build_opener()方法,它接受一个包含HTTPCookieProcessor()实例的预定义 cookie 存储,并返回一个 URL 打开器。我们调用这个打开器两次:一次用于登录页面,一次用于网站的首页。看起来只有bb_session一个 cookie 通过页面头部的 set-cookie 指令被设置。有关cookielib的更多信息可以在官方 Python 文档网站上找到,网址为docs.python.org/2/library/cookielib.html

提交网络表单

在网络浏览过程中,我们每天会多次提交网络表单。现在,你希望通过 Python 代码来完成这项操作。

准备中

此方法使用名为requests的第三方 Python 模块。你可以通过遵循docs.python-requests.org/en/latest/user/install/中的说明来安装此模块的兼容版本。例如,你可以使用pip从命令行安装requests,如下所示:

$ pip install requests

如何操作...

让我们提交一些假数据以在www.twitter.com注册。每个表单提交都有两种方法:GETPOST。不太敏感的数据,例如搜索查询,通常通过GET提交,而更敏感的数据则通过POST方法发送。让我们尝试使用这两种方法提交数据。

列表 4.4 如下解释了提交网络表单:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import requests
import urllib
import urllib2

ID_USERNAME = 'signup-user-name'
ID_EMAIL = 'signup-user-email'
ID_PASSWORD = 'signup-user-password'
USERNAME = 'username'
EMAIL = 'you@email.com'
PASSWORD = 'yourpassword'
SIGNUP_URL = 'https://twitter.com/account/create'

def submit_form():
    """Submit a form"""
    payload = {ID_USERNAME : USERNAME,
               ID_EMAIL    :  EMAIL,
               ID_PASSWORD : PASSWORD,}

    # make a get request
    resp = requests.get(SIGNUP_URL)
    print "Response to GET request: %s" %resp.content

    # send POST request
    resp = requests.post(SIGNUP_URL, payload)
    print "Headers from a POST request response: %s" %resp.headers
    #print "HTML Response: %s" %resp.read()

if __name__ == '__main__':
    submit_form()

如果你运行此脚本,你将看到以下输出:

$ python 4_4_submit_web_form.py 
Response to GET request: <?xml version="1.0" encoding="UTF-8"?>
<hash>
 <error>This method requires a POST.</error>
 <request>/account/create</request>
</hash>

Headers from a POST request response: {'status': '200 OK', 'content-
length': '21064', 'set-cookie': '_twitter_sess=BAh7CD--
d2865d40d1365eeb2175559dc5e6b99f64ea39ff; domain=.twitter.com; 
path=/; HttpOnly', 'expires': 'Tue, 31 Mar 1981 05:00:00 GMT', 
'vary': 'Accept-Encoding', 'last-modified': 'Sun, 05 May 2013 
15:59:27 GMT', 'pragma': 'no-cache', 'date': 'Sun, 05 May 2013 
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'}

它是如何工作的...

此方法使用第三方模块requests。它有方便的包装方法get()post(),这些方法可以对数据进行 URL 编码并正确提交表单。

在这个方法中,我们创建了一个包含用户名、密码和电子邮件以创建 Twitter 账户的数据负载。当我们第一次使用GET方法提交表单时,Twitter 网站返回一个错误,表示该页面只支持POST。在提交数据后,页面会处理它。我们可以从头部数据中确认这一点。

通过代理服务器发送网络请求

你希望通过代理服务器浏览网页。如果你已经配置了浏览器使用代理服务器并且它工作正常,你可以尝试这个方法。否则,你可以使用互联网上可用的任何公共代理服务器。

准备中

你需要访问一个代理服务器。你可以在 Google 或任何其他搜索引擎上搜索以找到免费代理服务器。在这里,为了演示,我们使用了165.24.10.8

如何操作...

让我们通过公共域名代理服务器发送我们的 HTTP 请求。

列表 4.5 如下解释了通过代理服务器代理网络请求:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import urllib

URL = 'https://www.github.com'
PROXY_ADDRESS = "165.24.10.8:8080" 

if __name__ == '__main__':
  resp = urllib.urlopen(URL, proxies = {"http" : PROXY_ADDRESS})
  print "Proxy server returns response headers: %s " 
%resp.headers

如果你运行此脚本,它将显示以下输出:

$ python 4_5_proxy_web_request.py 
Proxy server returns response headers: Server: GitHub.com
Date: Sun, 05 May 2013 16:16:04 GMT
Content-Type: text/html; charset=utf-8
Connection: close
Status: 200 OK
Cache-Control: private, max-age=0, must-revalidate
Strict-Transport-Security: max-age=2592000
X-Frame-Options: deny
Set-Cookie: logged_in=no; domain=.github.com; path=/; expires=Thu, 05-May-2033 16:16:04 GMT; HttpOnly
Set-Cookie: _gh_sess=BAh7...; path=/; expires=Sun, 01-Jan-2023 00:00:00 GMT; secure; HttpOnly
X-Runtime: 8
ETag: "66fcc37865eb05c19b2d15fbb44cd7a9"
Content-Length: 10643
Vary: Accept-Encoding

它是如何工作的...

这是一个简短的食谱,我们在 Google 搜索中找到的公共代理服务器上,使用社交代码共享网站www.github.com进行访问。代理地址参数已传递给urlliburlopen()方法。我们打印出响应的 HTTP 头信息,以显示代理设置在这里是有效的。

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

你希望在不下载 HTML 内容的情况下检查网页的存在。这意味着我们需要使用浏览器客户端发送一个get HEAD请求。根据维基百科,HEAD请求请求的响应与GET请求对应的响应相同,但不包括响应体。这对于检索响应头中编写的元信息很有用,而无需传输整个内容。

如何做...

我们希望向www.python.org发送一个HEAD请求。这不会下载主页的内容,而是检查服务器是否返回了有效的响应之一,例如OKFOUNDMOVED PERMANENTLY等。

列表 4.6 解释了如何使用HEAD请求检查网页,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
import argparse
import httplib
import urlparse
import re
import urllib

DEFAULT_URL = 'http://www.python.org'
HTTP_GOOD_CODES =  [httplib.OK, httplib.FOUND, httplib.MOVED_PERMANENTLY]

def get_server_status_code(url):
  """
  Download just the header of a URL and
  return the server's status code.
  """
  host, path = urlparse.urlparse(url)[1:3] 
  try:
    conn = httplib.HTTPConnection(host)
    conn.request('HEAD', path)
    return conn.getresponse().status
    except StandardError:
  return None

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Example HEAD 
Request')
  parser.add_argument('--url', action="store", dest="url", 
default=DEFAULT_URL)
  given_args = parser.parse_args() 
  url = given_args.url
  if get_server_status_code(url) in HTTP_GOOD_CODES:
    print "Server: %s status is OK: " %url
  else:
    print "Server: %s status is NOT OK!" %url

运行此脚本将显示如果使用HEAD请求找到页面,则会显示成功或错误信息:

$ python 4_6_checking_webpage_with_HEAD_request.py 
Server: http://www.python.org status is OK!
$ python 4_6_checking_webpage_with_HEAD_request.py --url=http://www.zytho.org
Server: http://www.zytho.org status is NOT OK!

它是如何工作的...

我们使用了httplibHTTPConnection()方法,它可以向服务器发送HEAD请求。如果需要,我们可以指定路径。在这里,HTTPConnection()方法检查了www.python.org的主页或路径。然而,如果 URL 不正确,它无法在接受的返回代码列表中找到返回的响应。

在你的客户端代码中欺骗 Mozilla Firefox

从你的 Python 代码中,你希望让网络服务器认为你正在使用 Mozilla Firefox 进行浏览。

如何做...

你可以在 HTTP 请求头中发送自定义的用户代理值。

列表 4.7 解释了如何在客户端代码中欺骗 Mozilla Firefox,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import urllib2

BROWSER = 'Mozilla/5.0 (Windows NT 5.1; rv:20.0) Gecko/20100101 
Firefox/20.0'
URL = 'http://www.python.org'

def spoof_firefox():
  opener = urllib2.build_opener()
  opener.addheaders = [('User-agent', BROWSER)]
  result = opener.open(URL)
  print "Response headers:"
  for header in  result.headers.headers:
    print "\t",header

if __name__ == '__main__':
  spoof_firefox()

如果你运行这个脚本,你将看到以下输出:

$ python 4_7_spoof_mozilla_firefox_in_client_code.py 
Response headers:
 Date: Sun, 05 May 2013 16:56:36 GMT
 Server: Apache/2.2.16 (Debian)
 Last-Modified: Sun, 05 May 2013 00:51:40 GMT
 ETag: "105800d-5280-4dbedfcb07f00"
 Accept-Ranges: bytes
 Content-Length: 21120
 Vary: Accept-Encoding
 Connection: close
 Content-Type: text/html

它是如何工作的...

我们使用了urllib2build_opener()方法来创建我们的自定义浏览器,其用户代理字符串已设置为Mozilla/5.0 (Windows NT 5.1; rv:20.0) Gecko/20100101 Firefox/20.0

在 Web 请求中使用 HTTP 压缩来节省带宽

你希望为你的网络服务器用户在下载网页时提供更好的性能。通过压缩 HTTP 数据,你可以加快 Web 内容的提供速度。

如何做...

让我们创建一个网络服务器,它在将内容压缩为gzip格式后提供服务。

列表 4.8 解释了 HTTP 压缩,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
import argparse
import string
import os
import sys
import gzip
import cStringIO
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer

DEFAULT_HOST = '127.0.0.1'
DEFAULT_PORT = 8800
HTML_CONTENT = """<html><body><h1>Compressed Hello  World!</h1></body></html>"""

class RequestHandler(BaseHTTPRequestHandler):
  """ Custom request handler"""

  def do_GET(self):
    """ Handler for the GET requests """
    self.send_response(200)
    self.send_header('Content-type','text/html')
    self.send_header('Content-Encoding','gzip')

    zbuf = self.compress_buffer(HTML_CONTENT)
    sys.stdout.write("Content-Encoding: gzip\r\n")
    self.send_header('Content-Length',len(zbuf))
    self.end_headers()

  # Send the message to browser
    zbuf = self.compress_buffer(HTML_CONTENT)
    sys.stdout.write("Content-Encoding: gzip\r\n")
    sys.stdout.write("Content-Length: %d\r\n" % (len(zbuf)))
    sys.stdout.write("\r\n")
    self.wfile.write(zbuf)
  return

  def compress_buffer(self, buf):
    zbuf = cStringIO.StringIO()
    zfile = gzip.GzipFile(mode = 'wb',  fileobj = zbuf, 
compresslevel = 6)
    zfile.write(buf)
    zfile.close()
    return zbuf.getvalue()

if __name__ == '__main__':
  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() 
  port = given_args.port
  server_address =  (DEFAULT_HOST, port)
  server = HTTPServer(server_address, RequestHandler)
  server.serve_forever()

你可以运行这个脚本,并在访问http://localhost:8800时在你的浏览器屏幕上看到Compressed Hello World!文本(这是 HTTP 压缩的结果):

$ python 4_8_http_compression.py 
localhost - - [22/Feb/2014 12:01:26] "GET / HTTP/1.1" 200 -
Content-Encoding: gzip
Content-Encoding: gzip
Content-Length: 71
localhost - - [22/Feb/2014 12:01:26] "GET /favicon.ico HTTP/1.1" 200 -
Content-Encoding: gzip
Content-Encoding: gzip
Content-Length: 71

以下截图展示了由网络服务器提供的压缩内容:

如何做...

它是如何工作的...

我们通过从BaseHTTPServer模块实例化HTTPServer类来创建了一个网络服务器。我们向这个服务器实例附加了一个自定义请求处理器,该处理器使用compress_buffer()方法压缩每个客户端响应。已经向客户端提供了预定义的 HTML 内容。

使用 Python 和 OpenSSL 编写具有恢复和部分下载功能的 HTTP 故障转移客户端

你可能想创建一个故障转移客户端,如果第一次尝试下载失败,它将重新下载文件。

如何操作...

让我们从www.python.org下载 Python 2.7 代码。一个resume_download()文件将恢复该文件的任何未完成下载。

列表 4.9 解释了恢复下载如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.# It may run on any other version with/without modifications.

import urllib, os
TARGET_URL = 'http://python.org/ftp/python/2.7.4/'
TARGET_FILE = 'Python-2.7.4.tgz'

class CustomURLOpener(urllib.FancyURLopener):
  """Override FancyURLopener to skip error 206 (when a
    partial file is being sent)
  """
  def http_error_206(self, url, fp, errcode, errmsg, headers, 
data=None):
    pass

  def resume_download():
    file_exists = False
    CustomURLClass = CustomURLOpener()
  if os.path.exists(TARGET_FILE):
    out_file = open(TARGET_FILE,"ab")
    file_exists = os.path.getsize(TARGET_FILE)
    #If the file exists, then only download the unfinished part
    CustomURLClass.addheader("Download range","bytes=%s-" % 
(file_exists))
  else:
    out_file = open(TARGET_FILE,"wb")

  web_page = CustomURLClass.open(TARGET_URL + TARGET_FILE)

  #If the file exists, but we already have the whole thing, don't 
download again
  if int(web_page.headers['Content-Length']) == file_exists:
    loop = 0
    print "File already downloaded!"

  byte_count = 0
  while True:
    data = web_page.read(8192)
    if not data:
      break
    out_file.write(data)
    byte_count = byte_count + len(data)

  web_page.close()
  out_file.close()

  for k,v in web_page.headers.items():
    print k, "=",v
  print "File copied", byte_count, "bytes from", web_page.url

if __name__ == '__main__':
  resume_download()

运行此脚本将产生以下输出:

$   python 4_9_http_fail_over_client.py
content-length = 14489063
content-encoding = x-gzip
accept-ranges = bytes
connection = close
server = Apache/2.2.16 (Debian)
last-modified = Sat, 06 Apr 2013 14:16:10 GMT
content-range = bytes 0-14489062/14489063
etag = "1748016-dd15e7-4d9b1d8685e80"
date = Tue, 07 May 2013 12:51:31 GMT
content-type = application/x-tar
File copied 14489063 bytes from http://python.org/ftp/python/2.7.4/Python-2.7.4.tgz

它是如何工作的...

在这个菜谱中,我们创建了一个继承自urllib模块的FancyURLopener方法的自定义 URL 打开器类,但覆盖了http_error_206(),以便下载部分内容。因此,我们的方法检查目标文件是否存在,如果不存在,它将尝试使用自定义 URL 打开器类下载。

使用 Python 和 OpenSSL 编写简单的 HTTPS 服务器代码

你需要一个用 Python 编写的安全网络服务器代码。你已经准备好了你的 SSL 密钥和证书文件。

准备工作

你需要安装第三方 Python 模块pyOpenSSL。这可以从 PyPI(pypi.python.org/pypi/pyOpenSSL)获取。在 Windows 和 Linux 主机上,你可能需要安装一些额外的包,这些包在pythonhosted.org//pyOpenSSL/上有文档说明。

如何操作...

在当前工作文件夹放置证书文件后,我们可以创建一个使用此证书为客户端提供加密内容的网络服务器。

列表 4.10 解释了以下安全 HTTP 服务器的代码:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 4
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
# Requires pyOpenSSL and SSL packages installed

import socket, os
from SocketServer import BaseServer
from BaseHTTPServer import HTTPServer
from SimpleHTTPServer import SimpleHTTPRequestHandler
from OpenSSL import SSL

class SecureHTTPServer(HTTPServer):
  def __init__(self, server_address, HandlerClass):
    BaseServer.__init__(self, server_address, HandlerClass)
    ctx = SSL.Context(SSL.SSLv23_METHOD)
    fpem = 'server.pem' # location of the server private key and 
the server certificate
    ctx.use_privatekey_file (fpem)
    ctx.use_certificate_file(fpem)
    self.socket = SSL.Connection(ctx, 
socket.socket(self.address_family, self.socket_type))
    self.server_bind()
    self.server_activate()

class SecureHTTPRequestHandler(SimpleHTTPRequestHandler):
  def setup(self):
    self.connection = self.request
    self.rfile = socket._fileobject(self.request, "rb", 
self.rbufsize)
    self.wfile = socket._fileobject(self.request, "wb", 
self.wbufsize)

  def run_server(HandlerClass = SecureHTTPRequestHandler,
    ServerClass = SecureHTTPServer):
    server_address = ('', 4443) # port needs to be accessible by 
user
    server = ServerClass(server_address, HandlerClass)
    running_address = server.socket.getsockname()
    print "Serving HTTPS Server on %s:%s ..." 
%(running_address[0], running_address[1])
    server.serve_forever()

if __name__ == '__main__':
  run_server()

如果你运行此脚本,它将产生以下输出:

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

如何工作...

如果你注意到了创建网络服务器的先前菜谱,在基本程序方面没有太大区别。主要区别在于使用带有SSLv23_METHOD参数的 SSL Context()方法。我们已经使用 Python OpenSSL 第三方模块的Connection()类创建了 SSL 套接字。这个类接受这个上下文对象以及地址族和套接字类型。

服务器证书文件保存在当前目录中,并且已经通过上下文对象应用。最后,服务器通过server_activate()方法被激活。

第五章:电子邮件协议、FTP 和 CGI 编程

在本章中,我们将介绍以下配方:

  • 在远程 FTP 服务器上列出文件

  • 将本地文件上传到远程 FTP 服务器

  • 将当前工作目录作为压缩 ZIP 文件发送电子邮件

  • 使用 POP3 下载您的 Google 电子邮件

  • 使用 IMAP 检查您的远程电子邮件

  • 通过 Gmail SMTP 服务器发送带附件的电子邮件

  • 使用 CGI 为您的(基于 Python 的)Web 服务器编写留言簿

简介

本章通过 Python 配方探讨了 FTP、电子邮件和 CGI 通信协议。Python 是一种非常高效且友好的语言。使用 Python,您可以轻松编写简单的 FTP 操作,如文件下载和上传。

本章中有些有趣的配方,例如使用 Python 脚本操作您的 Google 电子邮件,也称为 Gmail 账户。您可以使用这些配方通过 IMAP、POP3 和 SMTP 协议检查、下载和发送电子邮件。在另一个配方中,带有 CGI 的 Web 服务器还演示了基本 CGI 操作,例如在您的 Web 应用程序中编写访客留言表单。

在远程 FTP 服务器上列出文件

您想列出官方 Linux 内核 FTP 站点 ftp.kernel.org 上可用的文件。您可以选择任何其他 FTP 站点来尝试此配方。

准备工作

如果您在一个具有用户账户的真实 FTP 站点上工作,您需要一个用户名和密码。然而,在这种情况下,您不需要用户名(和密码),因为您可以使用 Linux 内核的 FTP 站点匿名登录。

如何做...

我们可以使用 ftplib 库从我们选择的 FTP 站点获取文件。有关此库的详细文档,请参阅 docs.python.org/2/library/ftplib.html

让我们看看如何使用 ftplib 获取一些文件。

列表 5.1 提供了一个简单的 FTP 连接测试,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 5
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

FTP_SERVER_URL = 'ftp.kernel.org'

import ftplib
def test_ftp_connection(path, username, email):
    #Open ftp connection
    ftp = ftplib.FTP(path, username, email)

   #List the files in the /pub directory
    ftp.cwd("/pub")
    print "File list at %s:" %path
    files = ftp.dir()
    print files

    ftp.quit()
if __name__ == '__main__':
    test_ftp_connection(path=FTP_SERVER_URL, username='anonymous',
                        email='nobody@nourl.com', 
                        )

此配方将列出 FTP 路径 ftp.kernel.org/pub 中存在的文件和文件夹。如果您运行此脚本,您将看到以下输出:

$ python 5_1_list_files_on_ftp_server.py
File list at ftp.kernel.org:
drwxrwxr-x    6 ftp      ftp          4096 Dec 01  2011 dist
drwxr-xr-x   13 ftp      ftp          4096 Nov 16  2011 linux
drwxrwxr-x    3 ftp      ftp          4096 Sep 23  2008 media
drwxr-xr-x   17 ftp      ftp          4096 Jun 06  2012 scm
drwxrwxr-x    2 ftp      ftp          4096 Dec 01  2011 site
drwxr-xr-x   13 ftp      ftp          4096 Nov 27  2011 software
drwxr-xr-x    3 ftp      ftp          4096 Apr 30  2008 tools

它是如何工作的...

此配方使用 ftplib 创建与 ftp.kernel.org 的 FTP 客户端会话。test_ftp_connection() 函数接受 FTP 路径、用户名和电子邮件地址以连接到 FTP 服务器。

可以通过调用 ftplibFTP() 函数并使用前面的连接凭证来创建 FTP 客户端会话。这返回一个客户端句柄,然后可以使用它来运行常用的 ftp 命令,例如更改工作目录的命令或 cwd()dir() 方法返回目录列表。

调用 ftp.quit() 退出 FTP 会话是个好主意。

将本地文件上传到远程 FTP 服务器

您想将文件上传到 FTP 服务器。

准备工作

让我们设置一个本地 FTP 服务器。在 Unix/Linux 中,您可以使用以下命令安装 wu-ftpd 软件包:

$ sudo apt-get install wu-ftpd

在 Windows 机器上,您可以安装 FileZilla FTP 服务器,可以从filezilla-project.org/download.php?type=server下载。

您应该根据 FTP 服务器包的用户手册创建一个 FTP 用户账户。

您还希望将文件上传到 FTP 服务器。您可以将服务器地址、登录凭证和文件名作为脚本输入参数指定。您应该创建一个名为readme.txt的本地文件,并在其中输入任何文本。

如何操作...

使用以下脚本,让我们设置一个本地 FTP 服务器。在 Unix/Linux 系统中,您可以安装 wu-ftpd 包。然后,您可以上传文件到已登录用户的家目录。您可以将服务器地址、登录凭证和文件名作为脚本输入参数指定。

列表 5.2 给出了 FTP 上传示例,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 5
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import os
import argparse
import ftplib

import getpass 
LOCAL_FTP_SERVER = 'localhost'
LOCAL_FILE = 'readme.txt'
def ftp_upload(ftp_server, username, password, file_name):
    print "Connecting to FTP server: %s" %ftp_server
    ftp = ftplib.FTP(ftp_server)
    print "Login to FTP server: user=%s" %username
    ftp.login(username, password)
    ext = os.path.splitext(file_name)[1]
    if ext in (".txt", ".htm", ".html"):
        ftp.storlines("STOR " + file_name, open(file_name))
    else:
        ftp.storbinary("STOR " + file_name, open(file_name, "rb"), 1024)
    print "Uploaded file: %s" %file_name

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='FTP Server Upload Example')
    parser.add_argument('--ftp-server', action="store", dest="ftp_server", default=LOCAL_FTP_SERVER)
    parser.add_argument('--file-name', action="store", dest="file_name", default=LOCAL_FILE)
    parser.add_argument('--username', action="store", dest="username", default=getpass.getuser())
    given_args = parser.parse_args() 
    ftp_server, file_name, username = given_args.ftp_server, given_args.file_name, given_args.username
    password = getpass.getpass(prompt="Enter you FTP password: ")
    ftp_upload(ftp_server, username, password, file_name)

如果您设置了一个本地 FTP 服务器并运行以下脚本,此脚本将登录到 FTP 服务器,然后上传文件。如果没有从命令行默认提供文件名参数,它将上传readme.txt文件。

$ python 5_2_upload_file_to_ftp_server.py 
Enter your FTP password: 
Connecting to FTP server: localhost
Login to FTP server: user=faruq
Uploaded file: readme.txt

$ cat /home/faruq/readme.txt 
This file describes what to do with the .bz2 files you see elsewhere
on this site (ftp.kernel.org).

它是如何工作的...

在这个菜谱中,我们假设本地 FTP 服务器正在运行。或者,您也可以连接到远程 FTP 服务器。ftp_upload()方法使用 Python 的ftplib模块的FTP()函数创建 FTP 连接对象。使用login()方法,它将登录到服务器。

登录成功后,ftp对象使用storlines()storbinary()方法发送 STOR 命令。第一种方法用于发送 ASCII 文本文件,如 HTML 或文本文件。后者方法用于二进制数据,如压缩存档。

将这些 FTP 方法包装在try-catch错误处理块中是一个好主意,这里为了简洁没有展示。

将当前工作目录作为压缩的 ZIP 文件发送电子邮件

将当前工作目录的内容作为压缩的 ZIP 存档发送可能很有趣。您可以使用这个方法快速与您的朋友分享文件。

准备工作

如果您的机器上没有安装任何邮件服务器,您需要安装一个本地邮件服务器,如postfix。在 Debian/Ubuntu 系统中,可以使用apt-get默认设置安装,如下所示:

$ sudo apt-get install postfix

如何操作...

让我们先压缩当前目录,然后创建一个电子邮件消息。我们可以通过外部 SMTP 主机发送电子邮件消息,或者我们可以使用本地电子邮件服务器来完成这个任务。像其他菜谱一样,让我们从解析命令行输入中获取发件人和收件人信息。

列表 5.3 展示了如何将电子邮件文件夹转换为压缩的 ZIP 文件,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 5
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import os
import argparse
import smtplib
import zipfile
import tempfile
from email import encoders
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart    
def email_dir_zipped(sender, recipient):
    zf = tempfile.TemporaryFile(prefix='mail', suffix='.zip')
    zip = zipfile.ZipFile(zf, 'w')
    print "Zipping current dir: %s" %os.getcwd()
    for file_name in os.listdir(os.getcwd()):
        zip.write(file_name)
    zip.close()
    zf.seek(0)
    # Create the message
    print "Creating email message..."
    email_msg = MIMEMultipart()
    email_msg['Subject'] = 'File from path %s' %os.getcwd()
    email_msg['To'] = ', '.join(recipient)
    email_msg['From'] = sender
    email_msg.preamble = 'Testing email from Python.\n'
    msg = MIMEBase('application', 'zip')
    msg.set_payload(zf.read())
    encoders.encode_base64(msg)
    msg.add_header('Content-Disposition', 'attachment', 
                   filename=os.getcwd()[-1] + '.zip')
    email_msg.attach(msg)
    email_msg = email_msg.as_string()

    # send the message
    print "Sending email message..."
    smtp = None
    try:
        smtp = smtplib.SMTP('localhost')
        smtp.set_debuglevel(1)
        smtp.sendmail(sender, recipient, email_msg)
    except Exception, e:
        print "Error: %s" %str(e)
    finally:
        if smtp:
           smtp.close()

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Email Example')
    parser.add_argument('--sender', action="store", dest="sender", default='you@you.com')
    parser.add_argument('--recipient', action="store", dest="recipient")
    given_args = parser.parse_args()
    email_dir_zipped(given_args.sender, given_args.recipient)

运行此菜谱将显示以下输出。额外的输出显示是因为我们启用了电子邮件调试级别。

$ python 5_3_email_current_dir_zipped.py --recipient=faruq@localhost
Zipping current dir: /home/faruq/Dropbox/PacktPub/pynet-cookbook/pynetcookbook_code/chapter5
Creating email message...
Sending email message...
send: 'ehlo [127.0.0.1]\r\n'
reply: '250-debian6.debian2013.com\r\n'
reply: '250-PIPELINING\r\n'
reply: '250-SIZE 10240000\r\n'
reply: '250-VRFY\r\n'
reply: '250-ETRN\r\n'
reply: '250-STARTTLS\r\n'
reply: '250-ENHANCEDSTATUSCODES\r\n'
reply: '250-8BITMIME\r\n'
reply: '250 DSN\r\n'
reply: retcode (250); Msg: debian6.debian2013.com
PIPELINING
SIZE 10240000
VRFY
ETRN
STARTTLS
ENHANCEDSTATUSCODES
8BITMIME
DSN
send: 'mail FROM:<you@you.com> size=9141\r\n'
reply: '250 2.1.0 Ok\r\n'
reply: retcode (250); Msg: 2.1.0 Ok
send: 'rcpt TO:<faruq@localhost>\r\n'
reply: '250 2.1.5 Ok\r\n'
reply: retcode (250); Msg: 2.1.5 Ok
send: 'data\r\n'
reply: '354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: End data with <CR><LF>.<CR><LF>
data: (354, 'End data with <CR><LF>.<CR><LF>')
send: 'Content-Type: multipart/mixed; boundary="===============0388489101==...[TRUNCATED]
reply: '250 2.0.0 Ok: queued as 42D2F34A996\r\n'
reply: retcode (250); Msg: 2.0.0 Ok: queued as 42D2F34A996
data: (250, '2.0.0 Ok: queued as 42D2F34A996')

它是如何工作的...

我们使用了 Python 的zipfilesmtplibemail模块,通过email_dir_zipped()方法实现了将文件夹作为压缩存档发送电子邮件的目标。此方法接受两个参数:发件人和收件人的电子邮件地址以创建电子邮件消息。

要创建 ZIP 存档,我们使用tempfile模块的TemporaryFile()类创建一个临时文件。我们提供一个文件名前缀mail和后缀.zip。然后,我们通过传递临时文件作为参数,使用ZipFile()类初始化 ZIP 存档对象。稍后,我们使用 ZIP 对象的write()方法调用添加当前目录下的文件。

要发送电子邮件,我们使用email.mime.multipart模块中的MIMEMultipart()类创建一个多部分 MIME 消息。像我们通常的电子邮件消息一样,主题、收件人和发件人信息被添加到电子邮件头中。

我们使用MIMEBase()方法创建电子邮件附件。在这里,我们首先指定 application/ZIP 头信息,并在该消息对象上调用set_payload()。然后,为了正确编码消息,使用编码器模块中的encode_base64()方法。使用add_header()方法构建附件头信息也是很有帮助的。现在,我们的附件已经准备好通过attach()方法调用包含在主电子邮件消息中。

发送电子邮件需要您调用smtplibSMTP()类实例。有一个sendmail()方法将利用操作系统提供的例程正确地发送电子邮件消息。其细节隐藏在幕后。然而,您可以通过启用调试选项来查看详细的交互,如本菜谱所示。

参考信息

使用 POP3 下载您的谷歌电子邮件

您希望通过 POP3 协议下载您的谷歌(或几乎任何其他电子邮件提供商的)电子邮件。

准备工作

要运行此菜谱,您应该有一个谷歌或任何其他服务提供商的电子邮件账户。

如何操作...

在这里,我们尝试从用户的谷歌电子邮件账户中下载第一封电子邮件。用户名从命令行提供,但密码是保密的,不会通过命令行传递。而是在脚本运行时输入,并保持从显示中隐藏。

列表 5.4 展示了如何通过POP3下载我们的谷歌电子邮件,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 5
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import getpass
import poplib
GOOGLE_POP3_SERVER = 'pop.googlemail.com'

def download_email(username): 
    mailbox = poplib.POP3_SSL(GOOGLE_POP3_SERVER, '995') 
    mailbox.user(username)
    password = getpass.getpass(prompt="Enter you Google password: ") 
    mailbox.pass_(password) 
    num_messages = len(mailbox.list()[1])
    print "Total emails: %s" %num_messages
    print "Getting last message" 
    for msg in mailbox.retr(num_messages)[1]:
        print msg
    mailbox.quit()

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Email Download Example')
    parser.add_argument('--username', action="store", dest="username", default=getpass.getuser())
    given_args = parser.parse_args() 
    username = given_args.username
    download_email(username)

如果您运行此脚本,您将看到类似以下的一个输出。出于隐私考虑,消息已被截断。

$ python 5_4_download_google_email_via_pop3.py --username=<USERNAME>
Enter your Google password: 
Total emails: 333
Getting last message
...[TRUNCATED]

工作原理...

此配方通过 POP3 下载用户的第一封 Google 邮件。download_email() 方法使用 Python 和 poplibPOP3_SSL() 类创建一个 mailbox 对象。我们将 Google POP3 服务器和端口号传递给类构造函数。然后,mailbox 对象通过调用 user() 方法设置用户账户。密码通过使用 getpass 模块的 getpass() 方法安全地从用户那里收集,然后传递给 mailbox 对象。mailboxlist() 方法以 Python 列表的形式给出电子邮件。

此脚本首先显示邮箱中存储的电子邮件数量,并使用 retr() 方法调用检索第一封消息。最后,在邮箱上调用 quit() 方法来清理连接是安全的。

使用 IMAP 检查您的远程电子邮件

除了使用 POP3,您还可以使用 IMAP 从您的 Google 账户检索电子邮件消息。在这种情况下,检索后消息不会被删除。

准备工作

要运行此配方,您应该有一个 Google 或其他服务提供商的电子邮件账户。

如何做到这一点...

让我们连接到您的 Google 电子邮件账户并读取第一封电子邮件消息。如果您不删除它,第一封电子邮件消息将是 Google 的欢迎消息。

列表 5.5 展示了如何使用 IMAP 检查 Google 邮件的方法:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 5
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import getpass
import imaplib
GOOGLE_IMAP_SERVER = 'imap.googlemail.com'
def check_email(username): 
    mailbox = imaplib.IMAP4_SSL(GOOGLE_IMAP_SERVER, '993') 
    password = getpass.getpass(prompt="Enter you Google password: ") 
    mailbox.login(username, password)
    mailbox.select('Inbox')
    typ, data = mailbox.search(None, 'ALL')
    for num in data[0].split():
        typ, data = mailbox.fetch(num, '(RFC822)')

        print 'Message %s\n%s\n' % (num, data[0][1])
        break
    mailbox.close()
    mailbox.logout()
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Email Download Example')
    parser.add_argument('--username', action="store", dest="username", default=getpass.getuser())
    given_args = parser.parse_args() 
    username = given_args.username
    check_email(username)

如果您运行此脚本,这将显示以下输出。为了移除数据的私人部分,我们截断了一些用户数据。

$$ python 5_5_check_remote_email_via_imap.py --username=<USER_NAME>
Enter your Google password: 
Message 1
Received: by 10.140.142.16; Sat, 17 Nov 2007 09:26:31 -0800 (PST)
Message-ID: <...>@mail.gmail.com>
Date: Sat, 17 Nov 2007 09:26:31 -0800
From: "Gmail Team" <mail-noreply@google.com>
To: "<User Full Name>" <USER_NAME>@gmail.com>
Subject: Gmail is different. Here's what you need to know.
MIME-Version: 1.0
Content-Type: multipart/alternative; 
 boundary="----=_Part_7453_30339499.1195320391988"

------=_Part_7453_30339499.1195320391988
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: 7bit
Content-Disposition: inline

Messages that are easy to find, an inbox that organizes itself, great
spam-fighting tools and built-in chat. Sound cool? Welcome to Gmail.

To get started, you may want to:
[TRUNCATED]

它是如何工作的...

前面的脚本从命令行获取 Google 用户名并调用 check_email() 函数。此函数使用 imaplibIMAP4_SSL() 类创建一个 IMAP 邮箱,该类使用 Google 的 IMAP 服务器和默认端口进行初始化。

然后,此函数使用 getpass 模块的 getpass() 方法捕获的密码登录邮箱。通过在 mailbox 对象上调用 select() 方法选择收件箱文件夹。

mailbox 对象有许多有用的方法。其中两个是 search()fetch(),它们用于获取第一封电子邮件。最后,在 mailbox 对象上调用 close()logout() 方法来结束 IMAP 连接会更安全。

通过 Gmail SMTP 服务器发送带附件的电子邮件

您想从您的 Google 电子邮件账户向另一个账户发送电子邮件。您还需要将文件附加到这条消息上。

准备工作

要运行此配方,您应该有一个 Google 或其他服务提供商的电子邮件账户。

如何做到这一点...

我们可以创建一个电子邮件消息,并将 Python 的 python-logo.gif 文件附加到电子邮件消息中。然后,这条消息从一个 Google 账户发送到另一个账户。

列表 4.6 展示了如何从您的 Google 账户发送电子邮件的方法:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 5
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import os
import getpass
import re
import sys
import smtplib

from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText

SMTP_SERVER = 'smtp.gmail.com'
SMTP_PORT = 587

def send_email(sender, recipient):
    """ Send email message """
    msg = MIMEMultipart()
    msg['Subject'] = 'Python Email Test'
    msg['To'] = recipient
    msg['From'] = sender
    subject = 'Python email Test'
    message = 'Images attached.'
    # attach image files
    files = os.listdir(os.getcwd())
    gifsearch = re.compile(".gif", re.IGNORECASE)
    files = filter(gifsearch.search, files)
    for filename in files:
        path = os.path.join(os.getcwd(), filename)
        if not os.path.isfile(path):
            continue
        img = MIMEImage(open(path, 'rb').read(), _subtype="gif")

        img.add_header('Content-Disposition', 'attachment', filename=filename)
        msg.attach(img)

    part = MIMEText('text', "plain")
    part.set_payload(message)
    msg.attach(part)

    # create smtp session
    session = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
    session.ehlo()
    session.starttls()
    session.ehlo
    password = getpass.getpass(prompt="Enter you Google password: ") 
    session.login(sender, password)
    session.sendmail(sender, recipient, msg.as_string())
    print "Email sent."
    session.quit()

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Email Sending Example')
    parser.add_argument('--sender', action="store", dest="sender")
    parser.add_argument('--recipient', action="store", dest="recipient")
    given_args = parser.parse_args()
    send_email(given_args.sender, given_args.recipient)

运行以下脚本,如果你正确提供了你的 Google 账户详细信息,将输出发送电子邮件到任何电子邮件地址的成功信息。运行此脚本后,你可以检查收件人的电子邮件账户以验证电子邮件是否已实际发送。

$ python 5_6_send_email_from_gmail.py --sender=<USERNAME>@gmail.com –recipient=<USER>@<ANOTHER_COMPANY.com>
Enter you Google password: 
Email sent.

它是如何工作的...

在这个食谱中,在 send_email() 函数中创建了一个电子邮件消息。此函数提供了一个 Google 账户,电子邮件消息将从该账户发送。通过调用 MIMEMultipart() 类创建消息头对象 msg,然后在其上添加主题、收件人和发件人信息。

Python 的正则表达式处理模块用于过滤当前路径上的 .gif 图像。然后,使用 email.mime.image 模块的 MIMEImage() 方法创建图像附件对象 img。向此对象添加正确的图像头,最后,使用之前创建的 msg 对象将图像附加。我们可以在 for 循环中附加多个图像文件,如本食谱所示。我们也可以以类似的方式附加纯文本附件。

要发送电子邮件消息,我们创建一个 SMTP 会话。我们在这个会话对象上调用一些测试方法,例如 ehlo()starttls()。然后,使用用户名和密码登录到 Google SMTP 服务器,并调用 sendmail() 方法发送电子邮件。

为你的(基于 Python 的)Web 服务器编写 CGI guestbook

通用网关接口 (CGI) 是 Web 编程中的一个标准,通过它可以使用自定义脚本生成 Web 服务器输出。你希望捕获用户浏览器中的 HTML 表单输入,将其重定向到另一个页面,并确认用户操作。

如何操作...

我们首先需要运行一个支持 CGI 脚本的 Web 服务器。我们将我们的 Python CGI 脚本放在一个 cgi-bin/ 子目录中,然后访问包含反馈表单的 HTML 页面。提交此表单后,我们的 Web 服务器将表单数据发送到 CGI 脚本,我们将看到该脚本产生的输出。

列表 5.7 显示了 Python Web 服务器如何支持 CGI:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 5
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import os
import cgi
import argparse
import BaseHTTPServer
import CGIHTTPServer
import cgitb 
cgitb.enable()  ## enable CGI error reporting
def web_server(port):
    server = BaseHTTPServer.HTTPServer
    handler = CGIHTTPServer.CGIHTTPRequestHandler #RequestsHandler
    server_address = ("", port)
    handler.cgi_directories = ["/cgi-bin", ]
    httpd = server(server_address, handler)
    print "Starting web server with CGI support on port: %s ..." %port
    httpd.serve_forever()
if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='CGI Server Example')
    parser.add_argument('--port', action="store", dest="port", type=int, required=True)
    given_args = parser.parse_args()
    web_server(given_args.port)

以下截图显示了启用 CGI 的 Web 服务器正在提供内容:

如何操作...

如果你运行这个食谱,你将看到以下输出:

$ python 5_7_cgi_server.py --port=8800
Starting web server with CGI support on port: 8800 ...
localhost - - [19/May/2013 18:40:22] "GET / HTTP/1.1" 200 -

现在,你需要从你的浏览器访问 http://localhost:8800/5_7_send_feedback.html

你将看到一个输入表单。我们假设你向此表单提供以下输入:

Name:  User1
Comment: Comment1

以下截图显示了在 Web 表单中输入的用户评论:

如何操作...

然后,你的浏览器将被重定向到 http://localhost:8800/cgi-bin/5_7_get_feedback.py,在那里你可以看到以下输出:

User1 sends a comment: Comment1

用户评论在浏览器中显示:

如何操作...

它是如何工作的...

我们使用了一个基本的 HTTP 服务器设置,它可以处理 CGI 请求。Python 在 BaseHTTPServerCGIHTTPserver 模块中提供了这些接口。

处理器配置为使用 /cgi-bin 路径来启动 CGI 脚本。不能使用其他路径来运行 CGI 脚本。

位于 5_7_send_feedback.html 的 HTML 反馈表单显示了一个非常基础的 HTML 表单,其中包含以下代码:

<html>
   <body>
         <form action="/cgi-bin/5_7_get_feedback.py" method="post">
                Name: <input type="text" name="Name">  <br />
                Comment: <input type="text" name="Comment" />
                <input type="submit" value="Submit" />
         </form>
   </body>
</html>

注意,表单方法为 POST,并且操作设置为 /cgi-bin/5_7_get_feedback.py 文件。该文件的内容如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 5
# This program requires Python 2.7 or any later version
import cgi
import cgitb 
# Create instance of FieldStorage 
form = cgi.FieldStorage() 
# Get data from fields
name = form.getvalue('Name')
comment  = form.getvalue('Comment')
print "Content-type:text/html\r\n\r\n"
print "<html>"
print "<head>"
print "<title>CGI Program Example </title>"
print "</head>"
print "<body>"
print "<h2> %s sends a comment: %s</h2>" % (name, comment)
print "</body>"
print "</html>"

在这个 CGI 脚本中,从 cgilib 调用了 FieldStorage() 方法。这返回一个表单对象以处理 HTML 表单输入。这里解析了两个输入(namecomment),使用的是 getvalue() 方法。最后,脚本通过回显一行信息来确认用户输入,表示用户 x 已发送评论。

第六章 屏幕抓取和其他实用应用

在本章中,我们将涵盖以下主题:

  • 使用 Google Maps API 搜索商业地址

  • 使用 Google Maps URL 搜索地理坐标

  • 在维基百科中搜索文章

  • 搜索 Google 股票报价

  • 在 GitHub 上搜索源代码仓库

  • 从 BBC 读取新闻源

  • 爬取网页中存在的链接

简介

本章展示了您可以编写的某些有趣的 Python 脚本,用于从网络中提取有用的信息,例如,搜索商业地址、特定公司的股票报价或新闻机构的最新新闻。这些脚本展示了 Python 如何在不与复杂的 API 通信的情况下以更简单的方式提取简单信息。

按照这些配方,您应该能够编写用于复杂场景的代码,例如,查找有关业务的信息,包括位置、新闻、股票报价等。

使用 Google Maps API 搜索商业地址

您想搜索您所在地区一家知名企业的地址。

准备工作

您可以使用 Python 地理编码库pygeocoder来搜索本地商业。您需要使用pipeasy_installPyPI安装此库,通过输入$ pip install pygeocoder$ easy_install pygeocoder

如何做到这一点...

让我们使用几行 Python 代码找到知名英国零售商 Argos Ltd.的地址。

列表 6.1 提供了一个简单的地理编码示例,用于搜索商业地址,如下所示:

#!/usr/bin/env python

# Python Network Programming Cookbook -- Chapter - 6
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

from pygeocoder import Geocoder

def search_business(business_name):

  results = Geocoder.geocode(business_name)

  for result in results:
    print result

if __name__ == '__main__':
  business_name =  "Argos Ltd, London" 
  print "Searching %s" %business_name
  search_business(business_name)

此配方将打印出 Argos Ltd.的地址,如所示。输出可能会根据您安装的地理编码库的输出略有不同:

$ python 6_1_search_business_addr.py
Searching Argos Ltd, London 

Argos Ltd, 110-114 King Street, London, Greater London W6 0QP, UK

它是如何工作的...

此配方依赖于 Python 第三方地理编码库。

此配方定义了一个简单的函数search_business(),它接受业务名称作为输入并将其传递给geocode()函数。geocode()函数可以根据您的搜索词返回零个或多个搜索结果。

在此配方中,geocode()函数将业务名称 Argos Ltd.,伦敦作为搜索查询。作为回报,它给出了 Argos Ltd.的地址,即 110-114 King Street,伦敦,大伦敦 W6 0QP,英国。

参见

pygeocoder库功能强大,具有许多有趣和有用的地理编码功能。您可以在开发者的网站上找到更多详细信息,网址为bitbucket.org/xster/pygeocoder/wiki/Home

使用 Google Maps URL 搜索地理坐标

有时您可能需要一个简单的函数,通过仅提供该城市的名称即可给出该城市的地理坐标。您可能对安装任何第三方库来完成此简单任务不感兴趣。

如何做到这一点...

在这个简单的屏幕抓取示例中,我们使用谷歌地图 URL 查询城市的纬度和经度。用于查询的 URL 可以在对谷歌地图页面进行自定义搜索后找到。我们可以执行以下步骤从谷歌地图中提取一些信息。

让我们使用argparse模块从命令行获取一个城市的名称。

我们可以使用urllib模块的urlopen()函数打开地图搜索 URL。如果 URL 正确,这将给出 XML 输出。

现在,处理 XML 输出以获取该城市的地理坐标。

列表 6.2 帮助使用谷歌地图查找城市的地理坐标,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 6
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
import argparse
import os
import urllib

ERROR_STRING = '<error>'

def find_lat_long(city):
  """ Find geographic coordinates """
  # Encode query string into Google maps URL
    url = 'http://maps.google.com/?q=' + urllib.quote(city) + 
'&output=js'
    print 'Query: %s' % (url)

  # Get XML location from Google maps
    xml = urllib.urlopen(url).read()

    if ERROR_STRING in xml:
      print '\nGoogle cannot interpret the city.'
      return
    else:
    # Strip lat/long coordinates from XML
      lat,lng = 0.0,0.0
      center = xml[xml.find('{center')+10:xml.find('}',xml.find('{center'))]
      center = center.replace('lat:','').replace('lng:','')
      lat,lng = center.split(',')
      print "Latitude/Longitude: %s/%s\n" %(lat, lng)

    if __name__ == '__main__':
      parser = argparse.ArgumentParser(description='City Geocode 
Search')
      parser.add_argument('--city', action="store", dest="city", 
required=True)
      given_args = parser.parse_args() 

      print "Finding geographic coordinates of %s" 
%given_args.city
      find_lat_long(given_args.city)

如果您运行此脚本,您应该看到以下类似的内容:

$ python 6_2_geo_coding_by_google_maps.py --city=London 
Finding geograhic coordinates of London 
Query: http://maps.google.com/?q=London&output=js 
Latitude/Longitude: 51.511214000000002/-0.119824 

它是如何工作的...

此配方从命令行获取一个城市的名称并将其传递给find_lat_long()函数。此函数使用urllib模块的urlopen()函数查询谷歌地图服务并获取 XML 输出。然后,搜索错误字符串'<error>'。如果没有出现,这意味着有一些好的结果。

如果您打印出原始 XML,它是一长串为浏览器生成的字符流。在浏览器中,显示地图的层可能很有趣。但在我们的情况下,我们只需要纬度和经度。

从原始 XML 中,使用字符串方法find()提取纬度和经度。这是搜索关键字"center"。此列表键具有地理坐标信息。但它还包含额外的字符,这些字符使用字符串方法replace()被移除。

您可以尝试这个配方来找出世界上任何已知城市的纬度/经度。

在维基百科中搜索文章

维基百科是一个收集关于几乎任何事物的信息的绝佳网站,例如,人物、地点、技术等等。如果您想从 Python 脚本中在维基百科上搜索某些内容,这个配方就是为您准备的。

这里有一个例子:

在维基百科中搜索文章

准备工作

您需要使用pipeasy_install通过输入$ pip install pyyaml$ easy_install pyyaml从 PyPI 安装pyyaml第三方库。

如何做...

让我们在维基百科中搜索关键字Islam并按行打印每个搜索结果。

列表 6.3 解释了如何在维基百科中搜索一篇文章,如下所示:

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Python Network Programming Cookbook -- Chapter - 6
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications

import argparse
import re
import yaml
import urllib
import urllib2

SEARCH_URL = 'http://%s.wikipedia.org/w/api.php?action=query&list=search&srsearch=%s&sroffset=%d&srlimit=%d&format=yaml'

class Wikipedia:

  def __init__(self, lang='en'):
    self.lang = lang

  def _get_content(self, url):
    request = urllib2.Request(url)
    request.add_header('User-Agent', 'Mozilla/20.0')

    try:
      result = urllib2.urlopen(request)
      except urllib2.HTTPError, e:
        print "HTTP Error:%s" %(e.reason)
      except Exception, e:
        print "Error occurred: %s" %str(e)
      return result

  def search_content(self, query, page=1, limit=10):
    offset = (page - 1) * limit
    url = SEARCH_URL % (self.lang, urllib.quote_plus(query), 
offset, limit)
    content = self._get_content(url).read()

    parsed = yaml.load(content)
    search = parsed['query']['search']
    if not search:
    return

    results = []
    for article in search:
      snippet = article['snippet']
      snippet = re.sub(r'(?m)<.*?>', '', snippet)
      snippet = re.sub(r'\s+', ' ', snippet)
      snippet = snippet.replace(' . ', '. ')
      snippet = snippet.replace(' , ', ', ')
      snippet = snippet.strip()

    results.append({
      'title' : article['title'].strip(),
'snippet' : snippet
    })

    return results

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Wikipedia search')
  parser.add_argument('--query', action="store", dest="query", 
required=True)
  given_args = parser.parse_args()

  wikipedia = Wikipedia()
  search_term = given_args.query
  print "Searching Wikipedia for %s" %search_term 
  results = wikipedia.search_content(search_term)
  print "Listing %s search results..." %len(results)
  for result in results:
    print "==%s== \n \t%s" %(result['title'], result['snippet'])
  print "---- End of search results ----"

运行此配方查询维基百科关于伊斯兰的结果如下:

$ python 6_3_search_article_in_wikipedia.py --query='Islam' 
Searching Wikipedia for Islam 
Listing 10 search results... 
==Islam== 
 Islam. (
ˈ
 | 
ɪ
 | s | l | 
ɑː
 | m 
الإسلام
, ar | ALA | al-
ʾ
Isl
ā
m  æl
ʔɪ
s
ˈ
læ
ː
m | IPA | ar-al_islam. ... 

==Sunni Islam== 
 Sunni Islam (
ˈ
 | s | u
ː
 | n | i or 
ˈ
 | s | 
ʊ
 | n | i |) is the 
largest branch of Islam ; its adherents are referred to in Arabic as ... 
==Muslim== 
 A Muslim, also spelled Moslem is an adherent of Islam, a monotheistic Abrahamic religion based on the Qur'an —which Muslims consider the ... 
==Sharia== 
 is the moral code and religious law of Islam. Sharia deals with 
many topics addressed by secular law, including crime, politics, and ... 
==History of Islam== 
 The history of Islam concerns the Islamic religion and its 
adherents, known as Muslim s. " "Muslim" is an Arabic word meaning 
"one who ... 

==Caliphate== 
 a successor to Islamic prophet Muhammad ) and all the Prophets 
of Islam. The term caliphate is often applied to successions of 
Muslim ... 
==Islamic fundamentalism== 
 Islamic ideology and is a group of religious ideologies seen as 
advocating a return to the "fundamentals" of Islam : the Quran and 
the Sunnah. ... 
==Islamic architecture== 
 Islamic architecture encompasses a wide range of both secular 
and religious styles from the foundation of Islam to the present day. ... 
---- End of search results ---- 

它是如何工作的...

首先,我们收集搜索文章的维基百科 URL 模板。我们创建了一个名为Wikipedia的类,它有两个方法:_get_content()search_content()。默认情况下,初始化时,该类将语言属性lang设置为en(英语)。

命令行查询字符串被传递给search_content()方法。然后它通过插入变量(如语言、查询字符串、页面偏移和要返回的结果数量)来构建实际的搜索 URL。search_content()方法可以可选地接受参数,偏移量由(page -1) * limit表达式确定。

搜索结果的内容是通过_get_content()方法获取的,该方法调用urlliburlopen()函数。在搜索 URL 中,我们设置了结果格式yaml,这基本上是为了纯文本文件。然后使用 Python 的pyyaml库解析yaml搜索结果。

搜索结果通过替换每个结果项中找到的正则表达式进行处理。例如,re.sub(r'(?m)<.*?>', '', snippet)表达式将替换片段字符串中的原始模式(?m)<.*?>。要了解更多关于正则表达式的信息,请访问 Python 文档页面,网址为docs.python.org/2/howto/regex.html

在维基百科术语中,每篇文章都有一个片段或简短描述。我们创建了一个字典项列表,其中每个项包含每个搜索结果的标题和片段。通过遍历这个字典项列表,结果被打印在屏幕上。

搜索谷歌股票报价

如果您对任何公司的股票报价感兴趣,此配方可以帮助您找到该公司的今日股票报价。

准备工作

我们假设您已经知道您喜欢的公司用于在任何证券交易所上市的符号。如果您不知道,可以从公司网站获取符号,或者直接在谷歌上搜索。

如何操作...

在这里,我们使用谷歌财经(finance.google.com/)来搜索给定公司的股票报价。您可以通过命令行输入符号,如下所示。

列表 6.4 描述了如何搜索谷歌股票报价,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 6
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications. 

import argparse
import urllib
import re
from datetime import datetime

SEARCH_URL = 'http://finance.google.com/finance?q='

def get_quote(symbol):
  content = urllib.urlopen(SEARCH_URL + symbol).read()
  m = re.search('id="ref_694653_l".*?>(.*?)<', content)
  if m:
    quote = m.group(1)
  else:
    quote = 'No quote available for: ' + symbol
  return quote

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Stock quote 
search')
  parser.add_argument('--symbol', action="store", dest="symbol", 
required=True)
  given_args = parser.parse_args() 
  print "Searching stock quote for symbol '%s'" %given_args.symbol 
  print "Stock  quote for %s at %s: %s" %(given_args.symbol , 
datetime.today(),  get_quote(given_args.symbol))

如果你运行此脚本,你将看到类似以下输出。在此,通过输入符号goog来搜索谷歌的股票报价,如下所示:

$ python 6_4_google_stock_quote.py --symbol=goog 
Searching stock quote for symbol 'goog' 
Stock quote for goog at 2013-08-20 18:50:29.483380: 868.86 

它是如何工作的...

此配方使用urlliburlopen()函数从谷歌财经网站获取股票数据。

通过使用正则表达式库re,它定位到第一个项目组中的股票报价数据。research()函数足够强大,可以搜索内容并过滤特定公司的 ID 数据。

使用此配方,我们搜索了谷歌的股票报价,该报价在 2013 年 8 月 20 日为868.86

在 GitHub 上搜索源代码仓库

作为一名 Python 程序员,您可能已经熟悉 GitHub (www.github.com),一个源代码共享网站,如下面的截图所示。您可以使用 GitHub 将源代码私密地分享给团队或公开地分享给全世界。它有一个很好的 API 接口,可以查询任何源代码仓库。这个食谱可能为您创建自己的源代码搜索引擎提供了一个起点。

在 GitHub 上搜索源代码仓库

准备工作

要运行此食谱,您需要通过输入 $ pip install requests$ easy_install requests 来安装第三方 Python 库 requests

如何操作...

我们希望定义一个 search_repository() 函数,它将接受作者名称(也称为程序员)、仓库和搜索键。作为回报,它将根据搜索键返回可用的结果。从 GitHub API 来看,以下是可以用的搜索键:issues_urlhas_wikiforks_urlmirror_urlsubscription_urlnotifications_urlcollaborators_urlupdated_atprivatepulls_urlissue_comment_urllabels_urlfull_nameownerstatuses_urlidkeys_urldescriptiontags_urlnetwork_countdownloads_urlassignees_urlcontents_urlgit_refs_urlopen_issues_countclone_urlwatchers_countgit_tags_urlmilestones_urllanguages_urlsizehomepageforkcommits_urlissue_events_urlarchive_urlcomments_urlevents_urlcontributors_urlhtml_urlforkscompare_urlopen_issuesgit_urlsvn_urlmerges_urlhas_issuesssh_urlblobs_urlmaster_branchgit_commits_urlhooks_urlhas_downloadswatchersnamelanguageurlcreated_atpushed_atforks_countdefault_branchteams_urltrees_urlorganizationbranches_urlsubscribers_urlstargazers_url

列表 6.5 给出了在 GitHub 上搜索源代码仓库详细信息的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 6
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

SEARCH_URL_BASE = 'https://api.github.com/repos'

import argparse
import requests
import json

def search_repository(author, repo, search_for='homepage'):
  url = "%s/%s/%s" %(SEARCH_URL_BASE, author, repo)
  print "Searching Repo URL: %s" %url
  result = requests.get(url)
  if(result.ok):
    repo_info = json.loads(result.text or result.content)
    print "Github repository info for: %s" %repo
    result = "No result found!"
    keys = [] 
    for key,value in repo_info.iteritems():
      if  search_for in key:
          result = value
      return result

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Github search')
  parser.add_argument('--author', action="store", dest="author", 
required=True)
  parser.add_argument('--repo', action="store", dest="repo", 
required=True)
  parser.add_argument('--search_for', action="store", 
dest="search_for", required=True)

  given_args = parser.parse_args() 
  result = search_repository(given_args.author, given_args.repo, 
given_args.search_for)
  if isinstance(result, dict):
    print "Got result for '%s'..." %(given_args.search_for)
    for key,value in result.iteritems():
    print "%s => %s" %(key,value)
  else:
    print "Got result for %s: %s" %(given_args.search_for, 
result)

如果您运行此脚本以搜索 Python 网络框架 Django 的所有者,您可以得到以下结果:

$ python 6_5_search_code_github.py --author=django --repo=django --search_for=owner 
Searching Repo URL: https://api.github.com/repos/django/django 
Github repository info for: django 
Got result for 'owner'... 
following_url => https://api.github.com/users/django/following{/other_user} 
events_url => https://api.github.com/users/django/events{/privacy} 
organizations_url => https://api.github.com/users/django/orgs 
url => https://api.github.com/users/django 
gists_url => https://api.github.com/users/django/gists{/gist_id} 
html_url => https://github.com/django 
subscriptions_url => https://api.github.com/users/django/subscriptions 
avatar_url => https://1.gravatar.com/avatar/fd542381031aa84dca86628ece84fc07?d=https%3A%2F%2Fidenticons.github.com%2Fe94df919e51ae96652259468415d4f77.png 
repos_url => https://api.github.com/users/django/repos 
received_events_url => https://api.github.com/users/django/received_events 
gravatar_id => fd542381031aa84dca86628ece84fc07 
starred_url => https://api.github.com/users/django/starred{/owner}{/repo} 
login => django 
type => Organization 
id => 27804 
followers_url => https://api.github.com/users/django/followers 

工作原理...

此脚本接受三个命令行参数:仓库作者(--author)、仓库名称(--repo)和要搜索的项目(--search_for)。这些参数通过 argpase 模块进行处理。

我们的 search_repository() 函数将命令行参数追加到固定的搜索 URL,并通过调用 requests 模块的 get() 函数接收内容。

默认情况下,搜索结果以 JSON 格式返回。然后使用 json 模块的 loads() 方法处理此内容。然后在结果中查找搜索键,并将该键的对应值返回给 search_repository() 函数的调用者。

在主用户代码中,我们检查搜索结果是否是 Python 字典的实例。如果是,则迭代打印键/值。否则,只打印值。

从 BBC 读取新闻源

如果你正在开发一个包含新闻和故事的社交网络网站,你可能对展示来自各种世界新闻机构(如 BBC 和路透社)的新闻感兴趣。让我们尝试通过 Python 脚本从 BBC 读取新闻。

准备中

此菜谱依赖于 Python 的第三方feedparser库。你可以通过运行以下命令来安装它:

$ pip install feedparser

或者

$ easy_install feedparser

如何操作...

首先,我们从 BBC 网站收集 BBC 的新闻源 URL。这个 URL 可以用作模板来搜索各种类型的新闻,如世界、英国、健康、商业和技术。因此,我们可以将显示的新闻类型作为用户输入。然后,我们依赖于read_news()函数,它将从 BBC 获取新闻。

列表 6.6 解释了如何从 BBC 读取新闻源,如下面的代码所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 6
# This program is optimized for Python 2.7\. 
# It may run on any other version with/without modifications.

from datetime import datetime
import feedparser 
BBC_FEED_URL = 'http://feeds.bbci.co.uk/news/%s/rss.xml'

def read_news(feed_url):
  try:
    data = feedparser.parse(feed_url)
  except Exception, e:
    print "Got error: %s" %str(e)

  for entry in data.entries:
    print(entry.title)
    print(entry.link)
    print(entry.description)
    print("\n") 

if __name__ == '__main__':
  print "==== Reading technology news feed from bbc.co.uk 
(%s)====" %datetime.today()

  print "Enter the type of news feed: "
  print "Available options are: world, uk, health, sci-tech, 
business, technology"
  type = raw_input("News feed type:")
  read_news(BBC_FEED_URL %type)
  print "==== End of BBC news feed ====="

运行此脚本将显示可用的新闻类别。如果我们选择技术作为类别,你可以获取最新的技术新闻,如下面的命令所示:

$ python 6_6_read_bbc_news_feed.py 
==== Reading technology news feed from bbc.co.uk (2013-08-20 19:02:33.940014)==== 
Enter the type of news feed:
Available options are: world, uk, health, sci-tech, business, technology 
News feed type:technology 
Xbox One courts indie developers 
http://www.bbc.co.uk/news/technology-23765453#sa-ns_mchannel=rss&ns_source=PublicRSS20-sa 
Microsoft is to give away free Xbox One development kits to encourage independent developers to self-publish games for its forthcoming console. 

Fast in-flight wi-fi by early 2014 
http://www.bbc.co.uk/news/technology-23768536#sa-ns_mchannel=rss&ns_source=PublicRSS20-sa 
Passengers on planes, trains and ships may soon be able to take advantage of high-speed wi-fi connections, says Ofcom. 

Anonymous 'hacks council website' 
http://www.bbc.co.uk/news/uk-england-surrey-23772635#sa-ns_mchannel=rss&ns_source=PublicRSS20-sa 
A Surrey council blames hackers Anonymous after references to a Guardian journalist's partner detained at Heathrow Airport appear on its website. 

Amazon.com website goes offline 
http://www.bbc.co.uk/news/technology-23762526#sa-ns_mchannel=rss&ns_source=PublicRSS20-sa 
Amazon's US website goes offline for about half an hour, the latest high-profile internet firm to face such a problem in recent days. 

[TRUNCATED]

它是如何工作的...

在这个菜谱中,read_news()函数依赖于 Python 的第三方模块feedparserfeedparser模块的parser()方法以结构化的方式返回源数据。

在这个菜谱中,parser()方法解析给定的源 URL。这个 URL 由BBC_FEED_URL和用户输入构成。

在调用parse()获取一些有效的源数据后,然后打印数据的内容,例如每个源条目的标题、链接和描述。

爬取网页中存在的链接

有时你希望在网页中找到特定的关键词。在网页浏览器中,你可以使用浏览器的页面搜索功能来定位术语。一些浏览器可以突出显示它。在复杂的情况下,你可能想深入挖掘并跟随网页中存在的每个 URL,以找到那个特定的术语。这个菜谱将为你自动化这个任务。

如何操作...

让我们编写一个search_links()函数,它将接受三个参数:搜索 URL、递归搜索的深度以及搜索关键词/术语,因为每个 URL 的内容中可能包含链接,而该内容可能包含更多要爬取的 URL。为了限制递归搜索,我们定义了一个深度。达到那个深度后,将不再进行递归搜索。

列表 6.7 给出了爬取网页中存在的链接的代码,如下面的代码所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 6
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import sys
import httplib
import re

processed = []

def search_links(url, depth, search):
  # Process http links that are not processed yet
  url_is_processed = (url in processed)
  if (url.startswith("http://") and (not url_is_processed)):
    processed.append(url)
    url = host = url.replace("http://", "", 1)
    path = "/"

    urlparts = url.split("/")
    if (len(urlparts) > 1):
      host = urlparts[0]
      path = url.replace(host, "", 1)

     # Start crawling
     print "Crawling URL path:%s%s " %(host, path)
     conn = httplib.HTTPConnection(host)
     req = conn.request("GET", path)
     result = conn.getresponse()

    # find the links
    contents = result.read()
    all_links = re.findall('href="(.*?)"', contents)

    if (search in contents):
      print "Found " + search + " at " + url

      print " ==> %s: processing %s links" %(str(depth), 
str(len(all_links)))
      for href in all_links:
      # Find relative urls
      if (href.startswith("/")):
        href = "http://" + host + href

        # Recurse links
        if (depth > 0):
          search_links(href, depth-1, search)
    else:
      print "Skipping link: %s ..." %url

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Webpage link 
crawler')
  parser.add_argument('--url', action="store", dest="url", 
required=True)
  parser.add_argument('--query', action="store", dest="query", 
required=True)
  parser.add_argument('--depth', action="store", dest="depth", 
default=2)

  given_args = parser.parse_args() 

  try:
    search_links(given_args.url,  
given_args.depth,given_args.query)
    except KeyboardInterrupt:
      print "Aborting search by user request."

如果你运行此脚本来搜索www.python.org中的python,你将看到以下类似的输出:

$ python 6_7_python_link_crawler.py --url='http://python.org' --query='python' 
Crawling URL path:python.org/ 
Found python at python.org 
 ==> 2: processing 123 links 
Crawling URL path:www.python.org/channews.rdf 
Found python at www.python.org/channews.rdf 
 ==> 1: processing 30 links 
Crawling URL path:www.python.org/download/releases/3.4.0/ 
Found python at www.python.org/download/releases/3.4.0/ 
 ==> 0: processing 111 links 
Skipping link: https://ep2013.europython.eu/blog/2013/05/15/epc20145-call-proposals ... 
Crawling URL path:www.python.org/download/releases/3.2.5/ 
Found python at www.python.org/download/releases/3.2.5/ 
 ==> 0: processing 113 links 
...
Skipping link: http://www.python.org/download/releases/3.2.4/ ... 
Crawling URL path:wiki.python.org/moin/WikiAttack2013 
^CAborting search by user request. 

它是如何工作的...

此菜谱可以接受三个命令行输入:搜索 URL(--url)、查询字符串(--query)和递归深度(--depth)。这些输入由argparse模块处理。

当使用之前的参数调用 search_links() 函数时,它将递归地遍历该给定网页上找到的所有链接。如果完成时间过长,你可能希望提前退出。因此,search_links() 函数被放置在一个 try-catch 块中,该块可以捕获用户的键盘中断操作,例如 Ctrl + C

search_links() 函数通过一个名为 processed 的列表来跟踪已访问的链接。这样做是为了使其全局可用,以便在所有递归函数调用中都能访问。

在单个搜索实例中,确保只处理 HTTP URL,以避免潜在的 SSL 证书错误。URL 被拆分为主机和路径。主要的爬取操作使用 httplibHTTPConnection() 函数启动。它逐渐发起 GET 请求,然后使用正则表达式模块 re 处理响应。这收集了响应中的所有链接。然后检查每个响应以查找搜索词。如果找到搜索词,它将打印该事件。

收集到的链接以相同的方式递归访问。如果找到任何相对 URL,该实例将通过在主机和路径前添加 http:// 转换为完整 URL。如果搜索深度大于 0,则激活递归。它将深度减少 1,并再次运行搜索函数。当搜索深度变为 0 时,递归结束。

第七章:编程跨越机器边界

在本章中,我们将介绍以下配方:

  • 使用 telnet 执行远程 shell 命令

  • 通过 SFTP 将文件复制到远程机器

  • 打印远程机器的 CPU 信息

  • 远程安装 Python 包

  • 远程运行 MySQL 命令

  • 通过 SSH 将文件传输到远程机器

  • 远程配置 Apache 以托管网站

简介

本章推荐一些有趣的 Python 库。这些配方旨在面向系统管理员和喜欢编写连接到远程系统并执行命令的代码的高级 Python 程序员。本章从使用内置的 Python 库telnetlib的轻量级配方开始。然后引入了著名的远程访问库Paramiko。最后,介绍了功能强大的远程系统管理库fabricfabric库受到经常为自动部署编写脚本的开发者的喜爱,例如部署 Web 应用程序或构建自定义应用程序的二进制文件。

使用 telnet 执行远程 shell 命令

如果您需要通过 telnet 连接到旧的网络交换机或路由器,您可以从 Python 脚本而不是使用 bash 脚本或交互式 shell 中这样做。此配方将创建一个简单的 telnet 会话。它将向您展示如何向远程主机执行 shell 命令。

准备工作

您需要在您的机器上安装 telnet 服务器并确保它已启动并运行。您可以使用针对您操作系统的包管理器来安装 telnet 服务器包。例如,在 Debian/Ubuntu 上,您可以使用apt-getaptitude来安装telnetd包,如下面的命令所示:

$ sudo apt-get install telnetd
$ telnet localhost

如何操作...

让我们定义一个函数,该函数将从命令提示符获取用户的登录凭证并连接到 telnet 服务器。

连接成功后,它将发送 Unix 的'ls'命令。然后,它将显示命令的输出,例如,列出目录的内容。

列表 7.1 显示了执行远程 Unix 命令的 telnet 会话的代码如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 7
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import getpass
import sys
import telnetlib

def run_telnet_session():
  host = raw_input("Enter remote hostname e.g. localhost:")
  user = raw_input("Enter your remote account: ")
  password = getpass.getpass()

  session = telnetlib.Telnet(host)

  session.read_until("login: ")
  session.write(user + "\n")
  if password:
    session.read_until("Password: ")
    session.write(password + "\n")

  session.write("ls\n")
  session.write("exit\n")

  print session.read_all()

if __name__ == '__main__':
  run_telnet_session()

如果您在本地机器上运行 telnet 服务器并运行此代码,它将要求您输入远程用户账户和密码。以下输出显示了在 Debian 机器上执行的 telnet 会话:

$ python 7_1_execute_remote_telnet_cmd.py 
Enter remote hostname e.g. localhost: localhost
Enter your remote account: faruq
Password: 

ls
exit
Last login: Mon Aug 12 10:37:10 BST 2013 from localhost on pts/9
Linux debian6 2.6.32-5-686 #1 SMP Mon Feb 25 01:04:36 UTC 2013 i686

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
You have new mail.
faruq@debian6:~$ ls 
down              Pictures               Videos
Downloads         projects               yEd
Dropbox           Public
env               readme.txt
faruq@debian6:~$ exit
logout

它是如何工作的...

此配方依赖于 Python 的内置telnetlib网络库来创建一个 telnet 会话。run_telnet_session()函数从命令提示符获取用户名和密码。使用getpass模块的getpass()函数来获取密码,因为这个函数不会让您看到屏幕上输入的内容。

为了创建一个 telnet 会话,您需要实例化一个Telnet()类,该类需要一个主机名参数来初始化。在这种情况下,使用localhost作为主机名。您可以使用argparse模块将主机名传递给此脚本。

可以使用read_until()方法捕获 telnet 会话的远程输出。在第一种情况下,使用此方法检测到登录提示。然后,通过write()方法(在这种情况下,与远程访问相同的机器)将带有新行换行的用户名发送到远程机器。同样,密码也被提供给远程主机。

然后,将ls命令发送以执行。最后,为了从远程主机断开连接,发送exit命令,并使用read_all()方法在屏幕上打印从远程主机接收到的所有会话数据。

通过 SFTP 将文件复制到远程机器

如果您想安全地将文件从本地机器上传或复制到远程机器,您可以通过安全文件传输协议SFTP)来实现。

准备工作

这个菜谱使用了一个强大的第三方网络库Paramiko,展示了如何通过 SFTP 进行文件复制的示例,如下所示命令。您可以从 GitHub(github.com/paramiko/paramiko)或 PyPI 获取Paramiko的最新代码:

$ pip install paramiko

如何做...

这个菜谱接受一些命令行输入:远程主机名、服务器端口、源文件名和目标文件名。为了简单起见,我们可以为这些输入参数使用默认值或硬编码值。

为了连接到远程主机,我们需要用户名和密码,这些可以从命令行中的用户那里获取。

列表 7.2 解释了如何通过 SFTP 远程复制文件,如下所示代码:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 7
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications. 

import argparse
import paramiko
import getpass

SOURCE = '7_2_copy_remote_file_over_sftp.py'
DESTINATION ='/tmp/7_2_copy_remote_file_over_sftp.py '

def copy_file(hostname, port, username, password, src, dst):
  client = paramiko.SSHClient()
  client.load_system_host_keys()
  print " Connecting to %s \n with username=%s... \n" 
%(hostname,username)
  t = paramiko.Transport((hostname, port)) 
  t.connect(username=username,password=password)
  sftp = paramiko.SFTPClient.from_transport(t)
  print "Copying file: %s to path: %s" %(SOURCE, DESTINATION)
  sftp.put(src, dst)
  sftp.close()
  t.close()

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Remote file copy')
  parser.add_argument('--host', action="store", dest="host", 
default='localhost')
  parser.add_argument('--port', action="store", dest="port", 
default=22, type=int)
  parser.add_argument('--src', action="store", dest="src", 
default=SOURCE)
  parser.add_argument('--dst', action="store", dest="dst", 
default=DESTINATION)

  given_args = parser.parse_args()
  hostname, port =  given_args.host, given_args.port
  src, dst = given_args.src, given_args.dst

  username = raw_input("Enter the username:")
  password = getpass.getpass("Enter password for %s: " %username)

  copy_file(hostname, port, username, password, src, dst)

如果您运行此脚本,您将看到类似以下输出的结果:

$ python 7_2_copy_remote_file_over_sftp.py 
Enter the username:faruq
Enter password for faruq: 
 Connecting to localhost 
 with username=faruq... 
Copying file: 7_2_copy_remote_file_over_sftp.py to path: 
/tmp/7_2_copy_remote_file_over_sftp.py 

工作原理...

这个菜谱可以接受连接到远程机器和通过 SFTP 复制文件的多种输入。

这个菜谱将命令行输入传递给copy_file()函数。然后,它创建一个 SSH 客户端,调用paramikoSSHClient类。客户端需要加载系统主机密钥。然后,它连接到远程系统,从而创建transport类的实例。实际的 SFTP 连接对象sftp是通过调用paramikoSFTPClient.from_transport()函数创建的。这个函数需要一个transport实例作为输入。

在 SFTP 连接就绪后,使用put()方法将本地文件通过此连接复制到远程主机。

最后,通过分别在每个对象上调用close()方法来清理 SFTP 连接和底层对象是个好主意。

打印远程机器的 CPU 信息

有时候,我们需要通过 SSH 在远程机器上运行一个简单的命令。例如,我们需要查询远程机器的 CPU 或 RAM 信息。这可以通过如下的 Python 脚本实现。

准备工作

您需要安装第三方包Paramiko,如下所示命令,从 GitHub 仓库github.com/paramiko/paramiko提供的源代码中安装:

$ pip install paramiko

如何做...

我们可以使用paramiko模块创建到 Unix 机器的远程会话。

然后,从本次会话中,我们可以读取远程机器的/proc/cpuinfo文件以提取 CPU 信息。

列表 7.3 给出了打印远程机器 CPU 信息的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 7
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import getpass
import paramiko

RECV_BYTES = 4096
COMMAND = 'cat /proc/cpuinfo'

def print_remote_cpu_info(hostname, port, username, password):
  client = paramiko.Transport((hostname, port))
  client.connect(username=username, password=password)

  stdout_data = []
  stderr_data = []
  session = client.open_channel(kind='session')
  session.exec_command(COMMAND)
  while True:
    if session.recv_ready():
      stdout_data.append(session.recv(RECV_BYTES))
      if session.recv_stderr_ready():
        stderr_data.append(session.recv_stderr(RECV_BYTES))
      if session.exit_status_ready():
        break

  print 'exit status: ', session.recv_exit_status()
  print ''.join(stdout_data)
  print ''.join(stderr_data)

  session.close()
  client.close()

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Remote file copy')
  parser.add_argument('--host', action="store", dest="host", 
default='localhost')
  parser.add_argument('--port', action="store", dest="port", 
default=22, type=int)    
  given_args = parser.parse_args()
  hostname, port =  given_args.host, given_args.port

  username = raw_input("Enter the username:")
  password = getpass.getpass("Enter password for %s: " %username)
  print_remote_cpu_info(hostname, port, username, password)

运行此脚本将显示指定主机的 CPU 信息,在本例中,为本地计算机,如下所示:

$ python 7_3_print_remote_cpu_info.py 
Enter the username:faruq
Enter password for faruq: 
exit status:  0
processor    : 0
vendor_id    : GenuineIntel
cpu family    : 6
model        : 42
model name    : Intel(R) Core(TM) i5-2400S CPU @ 2.50GHz
stepping    : 7
cpu MHz        : 2469.677
cache size    : 6144 KB
fdiv_bug    : no
hlt_bug        : no
f00f_bug    : no
coma_bug    : no
fpu        : yes
fpu_exception    : yes
cpuid level    : 5
wp        : yes
flags        : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush mmx fxsr sse sse2 syscall nx rdtscp lm constant_tsc up pni monitor ssse3 lahf_lm
bogomips    : 4939.35
clflush size    : 64
cache_alignment    : 64
address sizes    : 36 bits physical, 48 bits virtual
power management:

它是如何工作的...

首先,我们收集连接参数,如主机名、端口、用户名和密码。然后,将这些参数传递给print_remote_cpu_info()函数。

此函数通过调用paramikotransport类创建 SSH 客户端会话。之后使用提供的用户名和密码建立连接。我们可以使用 SSH 客户端上的open_channel()创建原始通信会话。为了在远程主机上执行命令,可以使用exec_command()

在向远程主机发送命令后,可以通过阻塞会话对象的recv_ready()事件来捕获远程主机的响应。我们可以创建两个列表,stdout_datastderr_data,并使用它们来存储远程输出和错误消息。

当命令在远程机器上退出时,可以使用exit_status_ready()方法检测,并使用join()字符串方法连接远程会话数据。

最后,可以使用每个对象的close()方法关闭会话和客户端连接。

远程安装 Python 包

在处理远程主机的前一个示例中,您可能已经注意到我们需要做很多与连接设置相关的事情。为了高效执行,最好将它们抽象化,只将相关的高级部分暴露给程序员。总是明确设置连接以执行远程命令既繁琐又慢。

Fabric (fabfile.org/),一个第三方 Python 模块,解决了这个问题。它只暴露了可以用来高效与远程机器交互的 API。

在本例中,将展示使用 Fabric 的简单示例。

准备工作

我们需要首先安装 Fabric。您可以使用 Python 打包工具pipeasy_install安装 Fabric,如下所示。Fabric 依赖于paramiko模块,它将自动安装。

$ pip install fabric

在这里,我们将使用 SSH 协议连接远程主机。因此,在远程端运行 SSH 服务器是必要的。如果您想使用本地计算机进行测试(假装访问远程机器),您可以在本地安装openssh服务器包。在 Debian/Ubuntu 机器上,可以使用包管理器apt-get完成此操作,如下所示:

$ sudo apt-get install openssh-server

如何操作...

这是使用 Fabric 安装 Python 包的代码。

列表 7.4 给出了远程安装 Python 包的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 7
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

from getpass import getpass
from fabric.api import settings, run, env, prompt

def remote_server():
  env.hosts = ['127.0.0.1']
  env.user = prompt('Enter user name: ')
  env.password = getpass('Enter password: ')

def install_package():
  run("pip install yolk")

与常规 Python 脚本相比,Fabric 脚本的运行方式不同。所有使用 fabric 库的函数都必须引用一个名为 fabfile.py 的 Python 脚本。在这个脚本中没有传统的 __main__ 指令。相反,你可以使用 Fabric API 定义你的方法,并使用命令行工具 fab 执行这些方法。因此,你不需要调用 python <script>.py,而是可以通过调用 fab one_function_name another_function_name 来运行定义在 fabfile.py 脚本中并位于当前目录下的 Fabric 脚本。

因此,让我们创建一个 fabfile.py 脚本,如下所示命令。为了简化,你可以从任何文件创建一个到 fabfile.py 脚本的文件快捷方式或链接。首先,删除任何之前创建的 fabfile.py 文件,并创建一个到 fabfile 的快捷方式:

$ rm -rf fabfile.py
$ ln -s 7_4_install_python_package_remotely.py fabfile.py

如果你现在调用 fabfile,它将在远程安装 Python 包 yolk 后产生以下输出:

$ ln -sfn 7_4_install_python_package_remotely.py fabfile.py
$ fab remote_server install_package
Enter user name: faruq
Enter password:
[127.0.0.1] Executing task 'install_package'
[127.0.0.1] run: pip install yolk
[127.0.0.1] out: Downloading/unpacking yolk
[127.0.0.1] out:   Downloading yolk-0.4.3.tar.gz (86kB): 
[127.0.0.1] out:   Downloading yolk-0.4.3.tar.gz (86kB): 100%  86kB
[127.0.0.1] out:   Downloading yolk-0.4.3.tar.gz (86kB): 
[127.0.0.1] out:   Downloading yolk-0.4.3.tar.gz (86kB): 86kB 
downloaded
[127.0.0.1] out:   Running setup.py egg_info for package yolk
[127.0.0.1] out:     Installing yolk script to /home/faruq/env/bin
[127.0.0.1] out: Successfully installed yolk
[127.0.0.1] out: Cleaning up...
[127.0.0.1] out: 

Done.
Disconnecting from 127.0.0.1... done.

它是如何工作的...

这个食谱演示了如何使用 Python 脚本远程执行系统管理任务。在这个脚本中有两个函数。remote_server() 函数设置 Fabric env 环境变量,例如主机名、用户、密码等。

另一个功能,install_package(),调用 run() 函数。这接受你在命令行中通常输入的命令。在这种情况下,命令是 pip install yolk。这使用 pip 安装 Python 包 yolk。与之前描述的食谱相比,使用 Fabric 运行远程命令的方法更简单、更高效。

在远程运行 MySQL 命令

如果你需要远程管理 MySQL 服务器,这个食谱就适合你。它将展示如何从 Python 脚本向远程 MySQL 服务器发送数据库命令。如果你需要设置一个依赖于后端数据库的 Web 应用程序,这个食谱可以作为你的 Web 应用程序设置过程的一部分使用。

准备工作

这个食谱也需要首先安装 Fabric。你可以使用 Python 打包工具 pipeasy_install 来安装 Fabric,如下所示命令。Fabric 依赖于 paramiko 模块,它将被自动安装。

$ pip install fabric

在这里,我们将使用 SSH 协议连接到远程主机。因此,在远程端运行 SSH 服务器是必要的。你还需要在远程主机上运行一个 MySQL 服务器。在 Debian/Ubuntu 系统上,可以使用包管理器 apt-get 来完成,如下所示命令:

$ sudo apt-get install openssh-server mysql-server

如何操作...

我们定义了 Fabric 环境设置和一些用于远程管理 MySQL 的函数。在这些函数中,我们不是直接调用 mysql 可执行文件,而是通过 echo 将 SQL 命令发送到 mysql。这确保了参数被正确传递给 mysql 可执行文件。

列表 7.5 给出了运行 MySQL 命令的远程代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 7
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

from getpass import getpass 
from fabric.api import run, env, prompt, cd

def remote_server():
  env.hosts = ['127.0.0.1']
# Edit this list to include remote hosts
  env.user =prompt('Enter your system username: ')
  env.password = getpass('Enter your system user password: ')
  env.mysqlhost = 'localhost'
  env.mysqluser = 'root'prompt('Enter your db username: ')
  env.password = getpass('Enter your db user password: ')
  env.db_name = ''

def show_dbs():
  """ Wraps mysql show databases cmd"""
  q = "show databases"
  run("echo '%s' | mysql -u%s -p%s" %(q, env.mysqluser, 
env.mysqlpassword))

def run_sql(db_name, query):
  """ Generic function to run sql"""
  with cd('/tmp'):
    run("echo '%s' | mysql -u%s -p%s -D %s" %(query, 
env.mysqluser, env.mysqlpassword, db_name))

def create_db():
  """Create a MySQL DB for App version"""
  if not env.db_name:
    db_name = prompt("Enter the DB name:")
  else:
    db_name = env.db_name
  run('echo "CREATE DATABASE %s default character set utf8 collate 
utf8_unicode_ci;"|mysql --batch --user=%s --password=%s --
host=%s'\
    % (db_name, env.mysqluser, env.mysqlpassword, env.mysqlhost), 
pty=True)

def ls_db():
  """ List a dbs with size in MB """
  if not env.db_name:
    db_name = prompt("Which DB to ls?")
  else:
    db_name = env.db_name
  query = """SELECT table_schema                                        
"DB Name", 
  Round(Sum(data_length + index_length) / 1024 / 1024, 1) "DB Size 
in MB" 
    FROM   information_schema.tables         
    WHERE table_schema = \"%s\" 
    GROUP  BY table_schema """ %db_name
  run_sql(db_name, query)

def empty_db():
  """ Empty all tables of a given DB """
  db_name = prompt("Enter DB name to empty:")
  cmd = """
  (echo 'SET foreign_key_checks = 0;'; 
  (mysqldump -u%s -p%s --add-drop-table --no-data %s | 
  grep ^DROP); 
  echo 'SET foreign_key_checks = 1;') | \
  mysql -u%s -p%s -b %s
  """ %(env.mysqluser, env.mysqlpassword, db_name, env.mysqluser, 
env.mysqlpassword, db_name)
  run(cmd)

为了运行此脚本,你应该创建一个快捷方式,fabfile.py。从命令行,你可以通过输入以下命令来完成:

$ ln -sfn 7_5_run_mysql_command_remotely.py fabfile.py

然后,你可以以各种形式调用 fab 可执行文件。

以下命令将显示数据库列表(使用 SQL 查询,show databases):

$ fab remote_server show_dbs

以下命令将创建一个新的 MySQL 数据库。如果你没有定义 Fabric 环境变量 db_name,将显示提示输入目标数据库名称。此数据库将使用 SQL 命令 CREATE DATABASE <database_name> default character set utf8 collate utf8_unicode_ci; 创建。

$ fab remote_server create_db

这个 Fabric 命令将显示数据库的大小:

$ fab remote_server ls_db()

以下 Fabric 命令将使用 mysqldumpmysql 可执行文件来清空数据库。此函数的行为类似于数据库截断,但会删除所有表。结果是创建了一个没有任何表的全新数据库:

$ fab remote_server empty_db()

以下将是输出:

$ $ fab remote_server show_dbs
[127.0.0.1] Executing task 'show_dbs'
[127.0.0.1] run: echo 'show databases' | mysql -uroot -p<DELETED>
[127.0.0.1] out: Database
[127.0.0.1] out: information_schema
[127.0.0.1] out: mysql
[127.0.0.1] out: phpmyadmin
[127.0.0.1] out: 

Done.
Disconnecting from 127.0.0.1... done.

$ fab remote_server create_db
[127.0.0.1] Executing task 'create_db'
Enter the DB name: test123
[127.0.0.1] run: echo "CREATE DATABASE test123 default character set utf8 collate utf8_unicode_ci;"|mysql --batch --user=root --password=<DELETED> --host=localhost

Done.
Disconnecting from 127.0.0.1... done.
$ fab remote_server show_dbs
[127.0.0.1] Executing task 'show_dbs'
[127.0.0.1] run: echo 'show databases' | mysql -uroot -p<DELETED>
[127.0.0.1] out: Database
[127.0.0.1] out: information_schema
[127.0.0.1] out: collabtive
[127.0.0.1] out: test123
[127.0.0.1] out: testdb
[127.0.0.1] out: 

Done.
Disconnecting from 127.0.0.1... done.

它是如何工作的...

此脚本定义了一些与 Fabric 一起使用的函数。第一个函数 remote_server() 设置环境变量。将本地回环 IP (127.0.0.1) 放到主机列表中。设置本地系统用户和 MySQL 登录凭证,并通过 getpass() 收集。

另一个函数利用 Fabric 的 run() 函数通过将命令回显到 mysql 可执行文件来向远程 MySQL 服务器发送 MySQL 命令。

run_sql() 函数是一个通用函数,可以用作其他函数的包装器。例如,empty_db() 函数调用它来执行 SQL 命令。这可以使你的代码更加有组织且更干净。

通过 SSH 将文件传输到远程机器

在使用 Fabric 自动化远程系统管理任务时,如果你想通过 SSH 在本地机器和远程机器之间传输文件,你可以使用 Fabric 内置的 get()put() 函数。这个菜谱展示了我们如何通过在传输前后检查磁盘空间来创建自定义函数,以智能地传输文件。

准备工作

此菜谱也需要首先安装 Fabric。你可以使用 Python 打包工具 pipeasy_install 来安装 Fabric,如下所示:

$ pip install fabric

在这里,我们将使用 SSH 协议连接远程主机。因此,在远程主机上安装和运行 SSH 服务器是必要的。

如何操作...

让我们先设置 Fabric 环境变量,然后创建两个函数,一个用于下载文件,另一个用于上传文件。

列表 7.6 给出了通过 SSH 将文件传输到远程机器的代码如下:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 7
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

from getpass import getpass
from fabric.api import local, run, env, get, put, prompt, open_shell

def remote_server():
  env.hosts = ['127.0.0.1']
  env.password = getpass('Enter your system password: ')
  env.home_folder = '/tmp'

def login():
  open_shell(command="cd %s" %env.home_folder)

def download_file():
  print "Checking local disk space..."
  local("df -h")
  remote_path = prompt("Enter the remote file path:")
  local_path = prompt("Enter the local file path:")
  get(remote_path=remote_path, local_path=local_path)
  local("ls %s" %local_path)

def upload_file():
  print "Checking remote disk space..."
  run("df -h")
  local_path = prompt("Enter the local file path:")
  remote_path = prompt("Enter the remote file path:")
  put(remote_path=remote_path, local_path=local_path)
  run("ls %s" %remote_path)

为了运行此脚本,你应该创建一个快捷方式,fabfile.py。从命令行,你可以通过输入以下命令来完成:

$ ln -sfn 7_6_transfer_file_over_ssh.py fabfile.py

然后,你可以以各种形式调用 fab 可执行文件。

首先,为了使用你的脚本登录到远程服务器,你可以运行以下 Fabric 函数:

$ fab remote_server login

这将为您提供最小化的 shell-like 环境。然后,您可以使用以下命令从远程服务器下载文件到本地机器:

$ fab remote_server download_file

同样,要上传文件,可以使用以下命令:

$ fab remote_server upload_file

在此示例中,通过 SSH 使用本地机器。因此,您必须在本地上安装 SSH 服务器才能运行这些脚本。否则,您可以修改remote_server()函数并将其指向远程服务器,如下所示:

$ fab remote_server login
[127.0.0.1] Executing task 'login'
Linux debian6 2.6.32-5-686 #1 SMP Mon Feb 25 01:04:36 UTC 2013 i686

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
You have new mail.
Last login: Wed Aug 21 15:08:45 2013 from localhost
cd /tmp
faruq@debian6:~$ cd /tmp
faruq@debian6:/tmp$ 

<CTRL+D>
faruq@debian6:/tmp$ logout

Done.
Disconnecting from 127.0.0.1... done.

$ fab remote_server download_file
[127.0.0.1] Executing task 'download_file'
Checking local disk space...
[localhost] local: df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/sda1              62G   47G   12G  81% /
tmpfs                 506M     0  506M   0% /lib/init/rw
udev                  501M  160K  501M   1% /dev
tmpfs                 506M  408K  505M   1% /dev/shm
Z_DRIVE              1012G  944G   69G  94% /media/z
C_DRIVE               466G  248G  218G  54% /media/c
Enter the remote file path: /tmp/op.txt
Enter the local file path: .
[127.0.0.1] download: chapter7/op.txt <- /tmp/op.txt
[localhost] local: ls .
7_1_execute_remote_telnet_cmd.py   7_3_print_remote_cpu_info.py           7_5_run_mysql_command_remotely.py  7_7_configure_Apache_for_hosting_website_remotely.py  fabfile.pyc  __init__.py  test.txt
7_2_copy_remote_file_over_sftp.py  7_4_install_python_package_remotely.py  7_6_transfer_file_over_ssh.py      fabfile.py                        index.html     op.txt       vhost.conf

Done.
Disconnecting from 127.0.0.1... done.

工作原理...

在此配方中,我们使用了一些 Fabric 的内置函数在本地机和远程机之间传输文件。local()函数在本地机上执行操作,而远程操作由run()函数执行。

在上传文件之前检查目标机器上的可用磁盘空间非常有用,反之亦然。

这是通过使用 Unix 命令df实现的。源文件路径和目标文件路径可以通过命令提示符指定,或者在无人值守的自动执行的情况下,可以在源文件中硬编码。

远程配置 Apache 托管网站

Fabric 函数可以作为普通用户和超级用户运行。如果您需要在远程 Apache Web 服务器上托管网站,则需要管理员用户权限来创建配置文件和重新启动 Web 服务器。此配方介绍了 Fabric 的sudo()函数,该函数在远程机器上以超级用户身份运行命令。在此,我们希望配置 Apache 虚拟主机以运行网站。

准备工作

此配方需要首先在您的本地机器上安装 Fabric。您可以使用 Python 打包工具pipeasy_install安装 Fabric,如下所示:

$ pip install fabric

在这里,我们将使用 SSH 协议连接远程主机。因此,在远程主机上安装和运行 SSH 服务器是必要的。还假设 Apache Web 服务器已安装在远程服务器上。在 Debian/Ubuntu 机器上,可以使用包管理器apt-get完成此操作,如下所示:

$ sudo apt-get install openssh-server apache2

如何操作...

首先,我们收集 Apache 安装路径和一些配置参数,例如,Web 服务器用户、组、虚拟主机配置路径和初始化脚本。这些参数可以定义为常量。

然后,我们设置两个函数,remote_server()setup_vhost(),使用 Fabric 执行 Apache 配置任务。

列表 7.7 提供了以下配置 Apache 远程托管网站的代码:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 7
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

from fabric.api import env, put, sudo, prompt
from fabric.contrib.files import exists

WWW_DOC_ROOT = "/data/apache/test/"
WWW_USER = "www-data"
WWW_GROUP = "www-data"
APACHE_SITES_PATH = "/etc/apache2/sites-enabled/"
APACHE_INIT_SCRIPT = "/etc/init.d/apache2 "

def remote_server():
  env.hosts = ['127.0.0.1']
  env.user = prompt('Enter user name: ')
  env.password = getpass('Enter your system password: ')

def setup_vhost():
  """ Setup a test website """
  print "Preparing the Apache vhost setup..."

  print "Setting up the document root..."
  if exists(WWW_DOC_ROOT):
    sudo("rm -rf %s" %WWW_DOC_ROOT)
  sudo("mkdir -p %s" %WWW_DOC_ROOT)

  # setup file permissions
  sudo("chown -R %s.%s %s" %(env.user, env.user, WWW_DOC_ROOT))

  # upload a sample index.html file
  put(local_path="index.html", remote_path=WWW_DOC_ROOT)
  sudo("chown -R %s.%s %s" %(WWW_USER, WWW_GROUP, WWW_DOC_ROOT))

  print "Setting up the vhost..."
  sudo("chown -R %s.%s %s" %(env.user, env.user, 
APACHE_SITES_PATH))

  # upload a pre-configured vhost.conf
  put(local_path="vhost.conf", remote_path=APACHE_SITES_PATH)
  sudo("chown -R %s.%s %s" %('root', 'root', APACHE_SITES_PATH))

  # restart Apache to take effect
  sudo("%s restart" %APACHE_INIT_SCRIPT)
  print "Setup complete. Now open the server path 
http://abc.remote-server.org/ in your web browser."

为了运行此脚本,应在您的宿主文件中添加以下行,例如,/etc/hosts

127.0.0.1 abc.remote-server.org abc 

您还应该创建一个快捷方式,fabfile.py。从命令行,您可以通过输入以下命令来完成此操作:

$ ln -sfn 7_7_configure_Apache_for_hosting_website_remotely.py 
fabfile.py

然后,您可以通过多种形式调用fab可执行文件。

首先,要使用您的脚本登录到远程服务器,您可以运行以下 Fabric 函数。这将产生以下输出:

$ fab remote_server setup_vhost
[127.0.0.1] Executing task 'setup_vhost'
Preparing the Apache vhost setup...
Setting up the document root...
[127.0.0.1] sudo: rm -rf /data/apache/test/
[127.0.0.1] sudo: mkdir -p /data/apache/test/
[127.0.0.1] sudo: chown -R faruq.faruq /data/apache/test/
[127.0.0.1] put: index.html -> /data/apache/test/index.html
[127.0.0.1] sudo: chown -R www-data.www-data /data/apache/test/
Setting up the vhost...
[127.0.0.1] sudo: chown -R faruq.faruq /etc/apache2/sites-enabled/
[127.0.0.1] put: vhost.conf -> /etc/apache2/sites-enabled/vhost.conf
[127.0.0.1] sudo: chown -R root.root /etc/apache2/sites-enabled/
[127.0.0.1] sudo: /etc/init.d/apache2 restart
[127.0.0.1] out: Restarting web server: apache2apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName
[127.0.0.1] out:  ... waiting apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.0.1 for ServerName
[127.0.0.1] out: .
[127.0.0.1] out: 

Setup complete. Now open the server path http://abc.remote-server.org/ in your web browser.

Done.
Disconnecting from 127.0.0.1... done.

运行此配方后,您可以在浏览器中打开并尝试访问您在主机文件(例如,/etc/hosts)上设置的路径。它应该在您的浏览器上显示以下输出:

It works! 
This is the default web page for this server.
The web server software is running but no content has been added, 
yet.

它是如何工作的...

此配方将初始 Apache 配置参数设置为常量,然后定义两个函数。在 remote_server() 函数中,放置了常用的 Fabric 环境参数,例如,主机、用户、密码等。

setup_vhost() 函数执行一系列特权命令。首先,它使用 exists() 函数检查网站的文档根路径是否已经创建。如果已存在,它将删除该路径并在下一步中重新创建。使用 chown 命令确保该路径由当前用户拥有。

在下一步中,它将一个裸骨 HTML 文件,index.html,上传到文档根路径。上传文件后,它将文件的权限重置为 web 服务器用户。

在设置文档根目录后,setup_vhost() 函数将提供的 vhost.conf 文件上传到 Apache 网站配置路径。然后,它将该路径的所有者设置为 root 用户。

最后,脚本重新启动 Apache 服务,以便激活配置。如果配置成功,当您在浏览器中打开 URL abc.remote-server.org/ 时,您应该看到之前显示的示例输出。

第八章. 与 Web 服务协作 – XML-RPC、SOAP 和 REST

在本章中,我们将涵盖以下食谱:

  • 查询本地 XML-RPC 服务器

  • 编写一个多线程、多调用 XML-RPC 服务器

  • 使用基本 HTTP 身份验证运行 XML-RPC 服务器

  • 使用 REST 从 Flickr 收集一些照片信息

  • 从 Amazon S3 Web 服务中搜索 SOAP 方法

  • 在 Google 上搜索自定义信息

  • 通过产品搜索 API 在 Amazon 上搜索书籍

简介

本章介绍了使用三种不同方法(即XML 远程过程调用XML-RPC)、简单对象访问协议SOAP)和表征状态转移REST))在 Web 服务中的一些有趣的 Python 食谱。Web 服务的理念是通过精心设计的协议在 Web 上使两个软件组件之间进行交互。接口是机器可读的。使用各种协议来促进 Web 服务。

在这里,我们提供了三个常用协议的示例。XML-RPC 使用 HTTP 作为传输媒介,通信使用 XML 内容进行。实现 XML-RPC 的服务器等待来自合适客户端的调用。客户端调用该服务器以执行具有不同参数的远程过程。XML-RPC 更简单,并考虑了最小安全性。另一方面,SOAP 有一套丰富的协议,用于增强远程过程调用。REST 是一种促进 Web 服务的架构风格。它使用 HTTP 请求方法操作,即GETPOSTPUTDELETE。本章介绍了这些 Web 服务协议和风格的实际应用,以实现一些常见任务。

查询本地 XML-RPC 服务器

如果你做很多 Web 编程,你很可能会遇到这个任务:从一个运行 XML-RPC 服务的网站上获取一些信息。在我们深入研究 XML-RPC 服务之前,让我们先启动一个 XML-RPC 服务器并与它进行通信。

准备工作

在这个食谱中,我们将使用 Python Supervisor 程序,这是一个广泛用于启动和管理多个可执行程序的程序。Supervisor 可以作为后台守护进程运行,可以监控子进程,并在它们意外死亡时重新启动。我们可以通过简单地运行以下命令来安装 Supervisor:

$pip install supervisor

如何操作...

我们需要为 Supervisor 创建一个配置文件。本食谱提供了一个示例配置。在这个例子中,我们定义了 Unix HTTP 服务器套接字和一些其他参数。注意rpcinterface:supervisor部分,其中rpcinterface_factory被定义为与客户端通信。

program:8_2_multithreaded_multicall_xmlrpc_server.py部分,我们使用 Supervisor 配置一个简单的服务器程序,通过指定命令和一些其他参数。

列表 8.1a 给出了最小化 Supervisor 配置的代码,如下所示:

[unix_http_server]
file=/tmp/supervisor.sock   ; (the path to the socket file)
chmod=0700                 ; socket file mode (default 0700)

[supervisord]
logfile=/tmp/supervisord.log 
loglevel=info                
pidfile=/tmp/supervisord.pid 
nodaemon=true               

[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface

[program:8_2_multithreaded_multicall_xmlrpc_server.py]
command=python 8_2_multithreaded_multicall_xmlrpc_server.py ; the 
program (relative uses PATH, can take args)
process_name=%(program_name)s ; process_name expr (default 
%(program_name)s)

如果你使用你喜欢的编辑器创建前面的管理员配置文件,你可以通过简单地调用它来运行管理员。

现在,我们可以编写一个 XML-RPC 客户端,它可以充当管理员代理,并给我们提供有关正在运行进程的信息。

列表 8.1b 给出了查询本地 XML-RPC 服务器的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 8
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.
import supervisor.xmlrpc
import xmlrpclib

def query_supervisr(sock):
    transport = supervisor.xmlrpc.SupervisorTransport(None, None,
                'unix://%s' %sock)
    proxy = xmlrpclib.ServerProxy('http://127.0.0.1',
            transport=transport)
    print "Getting info about all running processes via Supervisord..."
    print proxy.supervisor.getAllProcessInfo()

if __name__ == '__main__':
    query_supervisr(sock='/tmp/supervisor.sock')

如果你运行管理员守护进程,它将显示类似于以下内容的输出:

chapter8$ supervisord
2013-09-27 16:40:56,861 INFO RPC interface 'supervisor' initialized
2013-09-27 16:40:56,861 CRIT Server 'unix_http_server' running 
without any HTTP authentication checking
2013-09-27 16:40:56,861 INFO supervisord started with pid 27436
2013-09-27 16:40:57,864 INFO spawned: 
'8_2_multithreaded_multicall_xmlrpc_server.py' with pid 27439
2013-09-27 16:40:58,940 INFO success: 
8_2_multithreaded_multicall_xmlrpc_server.py entered RUNNING state, 
process has stayed up for > than 1 seconds (startsecs)

注意,我们的子进程 8_2_multithreaded_multicall_xmlrpc_server.py 已经启动。

现在,如果你运行客户端代码,它将查询管理员服务器的 XML-RPC 服务器接口并列出正在运行的进程,如下所示:

$ python 8_1_query_xmlrpc_server.py 
Getting info about all running processes via Supervisord...
[{'now': 1380296807, 'group': 
'8_2_multithreaded_multicall_xmlrpc_server.py', 'description': 'pid 
27439, uptime 0:05:50', 'pid': 27439, 'stderr_logfile': 
'/tmp/8_2_multithreaded_multicall_xmlrpc_server.py-stderr---
supervisor-i_VmKz.log', 'stop': 0, 'statename': 'RUNNING', 'start': 
1380296457, 'state': 20, 'stdout_logfile': 
'/tmp/8_2_multithreaded_multicall_xmlrpc_server.py-stdout---
supervisor-eMuJqk.log', 'logfile': 
'/tmp/8_2_multithreaded_multicall_xmlrpc_server.py-stdout---
supervisor-eMuJqk.log', 'exitstatus': 0, 'spawnerr': '', 'name': 
'8_2_multithreaded_multicall_xmlrpc_server.py'}]

它是如何工作的...

这个菜谱依赖于在后台运行配置了 rpcinterface 的管理员守护进程。管理员启动了另一个 XML-RPC 服务器,如下所示:8_2_multithreaded_multicall_xmlrpc_server.py

客户端代码有一个 query_supervisr() 方法,该方法接受一个管理员套接字参数。在这个方法中,使用 Unix 套接字路径创建了一个 SupervisorTransport 实例。然后,通过传递服务器地址和先前创建的 transport,通过实例化 xmlrpclibServerProxy() 类创建了一个 XML-RPC 服务器代理。

XML-RPC 服务器代理随后调用管理员的 getAllProcessInfo() 方法,该方法打印子进程的进程信息。这个过程包括 pidstatenamedescription 等等。

编写多线程多调用 XML-RPC 服务器

你可以让你的 XML-RPC 服务器同时接受多个调用。这意味着多个函数调用可以返回单个结果。除此之外,如果你的服务器是多线程的,那么你可以在单个线程中启动服务器后执行更多代码。程序的主线程将以这种方式不会被阻塞。

如何做...

我们可以创建一个继承自 threading.Thread 类的 ServerThread 类,并将一个 SimpleXMLRPCServer 实例封装为该类的属性。这可以设置为接受多个调用。

然后,我们可以创建两个函数:一个启动多线程、多调用的 XML-RPC 服务器,另一个创建对该服务器的客户端。

列表 8.2 给出了编写多线程、多调用 XML-RPC 服务器的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 8
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import xmlrpclib
import threading

from SimpleXMLRPCServer import SimpleXMLRPCServer

# some trivial functions
def add(x,y):
  return x+y

def subtract(x, y):
  return x-y

def multiply(x, y):
  return x*y

def divide(x, y):
  return x/y

class ServerThread(threading.Thread):
  def __init__(self, server_addr):
    threading.Thread.__init__(self)
    self.server = SimpleXMLRPCServer(server_addr)
    self.server.register_multicall_functions()
    self.server.register_function(add, 'add')
    self.server.register_function(subtract, 'subtract')
    self.server.register_function(multiply, 'multiply')
    self.server.register_function(divide, 'divide')

  def run(self):
    self.server.serve_forever()

def run_server(host, port):
  # server code
  server_addr = (host, port)
  server = ServerThread(server_addr)
  server.start() # The server is now running
  print "Server thread started. Testing the server..."

def run_client(host, port):
  # client code
  proxy = xmlrpclib.ServerProxy("http://%s:%s/" %(host, port))
  multicall = xmlrpclib.MultiCall(proxy)
  multicall.add(7,3)
  multicall.subtract(7,3)
  multicall.multiply(7,3)
  multicall.divide(7,3)
  result = multicall()
  print "7+3=%d, 7-3=%d, 7*3=%d, 7/3=%d" % tuple(result)

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Multithreaded 
multicall XMLRPC Server/Proxy')
  parser.add_argument('--host', action="store", dest="host", 
default='localhost')
  parser.add_argument('--port', action="store", dest="port", 
default=8000, type=int)
  # parse arguments
  given_args = parser.parse_args()
  host, port =  given_args.host, given_args.port
  run_server(host, port)
  run_client(host, port)

如果你运行此脚本,你将看到类似于以下内容的输出:

$ python 8_2_multithreaded_multicall_xmlrpc_server.py --port=8000
Server thread started. Testing the server...
localhost - - [25/Sep/2013 17:38:32] "POST / HTTP/1.1" 200 -
7+3=10, 7-3=4, 7*3=21, 7/3=2 

它是如何工作的...

在这个菜谱中,我们创建了一个继承自 Python 线程库的 Thread 类的 ServerThread 子类。这个子类初始化一个服务器属性,该属性创建一个 SimpleXMLRPC 服务器实例。XML-RPC 服务器地址可以通过命令行输入提供。为了启用多调用功能,我们在服务器实例上调用 register_multicall_functions() 方法。

然后,使用此 XML-RPC 服务器注册了四个简单的函数:add()subtract()multiply()divide()。这些函数的操作正好与它们的名称所暗示的相同。

为了启动服务器,我们将主机和端口传递给run_server()函数。使用之前讨论过的ServerThread类创建服务器实例。这个服务器实例的start()方法启动 XML-RPC 服务器。

在客户端,run_client()函数从命令行接受相同的 host 和 port 参数。然后通过调用xmlrpclib中的ServerProxy()类创建之前讨论过的 XML-RPC 服务器的代理实例。这个代理实例随后被传递给MultiCall类实例multicall。现在,前面提到的四个简单的 RPC 方法可以运行,例如addsubtractmultiplydivide。最后,我们可以通过单个调用获取结果,例如multicall()。结果元组随后在一行中打印出来。

使用基本 HTTP 身份验证运行 XML-RPC 服务器

有时,你可能需要实现 XML-RPC 服务器的身份验证。这个配方提供了一个基本 HTTP 身份验证的 XML-RPC 服务器的示例。

如何做到这一点...

我们可以创建SimpleXMLRPCServer的子类,并覆盖其请求处理器,以便当请求到来时,它将与给定的登录凭证进行验证。

列表 8.3a 给出了运行具有基本 HTTP 身份验证的 XML-RPC 服务器的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 8
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import xmlrpclib
from base64 import b64decode
from SimpleXMLRPCServer  import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler

class SecureXMLRPCServer(SimpleXMLRPCServer):

  def __init__(self, host, port, username, password, *args, 
**kargs):
    self.username = username
    self.password = password
    # authenticate method is called from inner class
    class VerifyingRequestHandler(SimpleXMLRPCRequestHandler):
      # method to override
      def parse_request(request):
        if\ SimpleXMLRPCRequestHandler.parse_request(request):
        # authenticate
          if self.authenticate(request.headers):
        return True
          else:
            # if authentication fails return 401
              request.send_error(401, 'Authentication\ failed 
ZZZ')
            return False
          # initialize
         SimpleXMLRPCServer.__init__(self, (host, port), 
requestHandler=VerifyingRequestHandler, *args, **kargs)

  def authenticate(self, headers):
    headers = headers.get('Authorization').split()
    basic, encoded = headers[0], headers[1]
    if basic != 'Basic':
      print 'Only basic authentication supported'
    return False
    secret = b64decode(encoded).split(':')
    username, password = secret[0], secret[1]
  return True if (username == self.username and password == 
self.password) else False

def run_server(host, port, username, password):
  server = SecureXMLRPCServer(host, port, username, password)
  # simple test function
  def echo(msg):
    """Reply client in  upper case """
    reply = msg.upper()
    print "Client said: %s. So we echo that in uppercase: %s" 
%(msg, reply)
  return reply
  server.register_function(echo, 'echo')
  print "Running a HTTP auth enabled XMLRPC server on %s:%s..." 
%(host, port)
  server.serve_forever()

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Multithreaded 
multicall XMLRPC Server/Proxy')
  parser.add_argument('--host', action="store", dest="host", 
default='localhost')
  parser.add_argument('--port', action="store", dest="port", default=8000, type=int)
  parser.add_argument('--username', action="store", 
dest="username", default='user')
  parser.add_argument('--password', action="store", 
dest="password", default='pass')
  # parse arguments
  given_args = parser.parse_args()
  host, port =  given_args.host, given_args.port
  username, password = given_args.username, given_args.password
  run_server(host, port, username, password)

如果运行此服务器,则默认可以看到以下输出:

$ python 8_3a_xmlrpc_server_with_http_auth.py 
Running a HTTP auth enabled XMLRPC server on localhost:8000...
Client said: hello server.... So we echo that in uppercase: HELLO 
SERVER...
localhost - - [27/Sep/2013 12:08:57] "POST /RPC2 HTTP/1.1" 200 -

现在,让我们创建一个简单的客户端代理,并使用与服务器相同的登录凭证。

列表 8.3b 给出了 XML-RPC 客户端的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 8
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import xmlrpclib

def run_client(host, port, username, password):
  server = xmlrpclib.ServerProxy('http://%s:%s@%s:%s' %(username, 
password, host, port, ))
  msg = "hello server..."
  print "Sending message to server: %s  " %msg
  print "Got reply: %s" %server.echo(msg)

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Multithreaded 
multicall XMLRPC Server/Proxy')
  parser.add_argument('--host', action="store", dest="host", 
default='localhost')
  parser.add_argument('--port', action="store", dest="port", 
default=8000, type=int)
  parser.add_argument('--username', action="store", 
dest="username", default='user')
  parser.add_argument('--password', action="store", 
dest="password", default='pass')
  # parse arguments
  given_args = parser.parse_args()
  host, port =  given_args.host, given_args.port
  username, password = given_args.username, given_args.password
  run_client(host, port, username, password)

如果你运行客户端,那么它将显示以下输出:

$ python 8_3b_xmprpc_client.py 
Sending message to server: hello server... 
Got reply: HELLO SERVER...

它是如何工作的...

在服务器脚本中,通过从SimpleXMLRPCServer继承创建SecureXMLRPCServer子类。在这个子类的初始化代码中,我们创建了VerifyingRequestHandler类,该类实际上拦截请求并使用authenticate()方法进行基本身份验证。

authenticate()方法中,HTTP 请求作为参数传递。该方法检查Authorization值的是否存在。如果其值设置为Basic,则使用base64标准模块中的b64decode()函数解码编码后的密码。在提取用户名和密码后,它随后与服务器最初设置的凭证进行验证。

run_server()函数中,定义了一个简单的echo()子函数,并将其注册到SecureXMLRPCServer实例中。

在客户端脚本中,run_client()简单地获取服务器地址和登录凭证,并将它们传递给ServerProxy()实例。然后通过echo()方法发送单行消息。

使用 REST 从 Flickr 收集一些照片信息

许多互联网网站通过它们的 REST API 提供 Web 服务接口。Flickr,一个著名的照片分享网站,有一个 REST 接口。让我们尝试收集一些照片信息来构建一个专门的数据库或其他与照片相关的应用程序。

如何做到这一点...

我们需要 REST URL 来执行 HTTP 请求。为了简化,这个菜谱中将 URL 硬编码。我们可以使用第三方requests模块来执行 REST 请求。它有方便的get()post()put()delete()方法。

为了与 Flickr Web 服务通信,您需要注册并获取一个秘密 API 密钥。这个 API 密钥可以放在local_settings.py文件中,或者通过命令行提供。

列表 8.4 展示了使用 REST 从 Flickr 收集一些照片信息的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 8
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import json
import requests

try:
    from local_settings import flickr_apikey
except ImportError:
    pass

def collect_photo_info(api_key, tag, max_count):
    """Collects some interesting info about some photos from Flickr.com for a given tag """
    photo_collection = []
    url =  "http://api.flickr.com/services/rest/?method=flickr.photos.search&tags=%s&format=json&nojsoncallback=1&api_key=%s" %(tag, api_key)
    resp = requests.get(url)
    results = resp.json()
    count  = 0
    for p in results['photos']['photo']:
        if count >= max_count:
            return photo_collection
        print 'Processing photo: "%s"' % p['title']
        photo = {}
        url = "http://api.flickr.com/services/rest/?method=flickr.photos.getInfo&photo_id=" + p['id'] + "&format=json&nojsoncallback=1&api_key=" + api_key
        info = requests.get(url).json()
        photo["flickrid"] = p['id']
        photo["title"] = info['photo']['title']['_content']
        photo["description"] = info['photo']['description']['_content']
        photo["page_url"] = info['photo']['urls']['url'][0]['_content']

        photo["farm"] = info['photo']['farm']
        photo["server"] = info['photo']['server']
        photo["secret"] = info['photo']['secret']

        # comments
        numcomments = int(info['photo']['comments']['_content'])
        if numcomments:
            #print "   Now reading comments (%d)..." % numcomments
            url = "http://api.flickr.com/services/rest/?method=flickr.photos.comments.getList&photo_id=" + p['id'] + "&format=json&nojsoncallback=1&api_key=" + api_key
            comments = requests.get(url).json()
            photo["comment"] = []
            for c in comments['comments']['comment']:
                comment = {}
                comment["body"] = c['_content']
                comment["authorid"] = c['author']
                comment["authorname"] = c['authorname']
                photo["comment"].append(comment)
        photo_collection.append(photo)
        count = count + 1
    return photo_collection     

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Get photo info from Flickr')
    parser.add_argument('--api-key', action="store", dest="api_key", default=flickr_apikey)
    parser.add_argument('--tag', action="store", dest="tag", default='Python')
    parser.add_argument('--max-count', action="store", dest="max_count", default=3, type=int)
    # parse arguments
    given_args = parser.parse_args()
    api_key, tag, max_count =  given_args.api_key, given_args.tag, given_args.max_count
    photo_info = collect_photo_info(api_key, tag, max_count)
    for photo in photo_info:
        for k,v in photo.iteritems():
            if k == "title":
                print "Showing photo info...."  
            elif k == "comment":
                "\tPhoto got %s comments." %len(v)
            else:
                print "\t%s => %s" %(k,v) 

您可以通过将 API 密钥放入local_settings.py文件或从命令行(通过--api-key参数)提供它来运行这个菜谱。除了 API 密钥外,还可以提供搜索标签和结果的最大计数参数。默认情况下,这个菜谱将搜索Python标签,并将结果限制为三个条目,如下面的输出所示:

$ python 8_4_get_flickr_photo_info.py 
Processing photo: "legolas"
Processing photo: ""The Dance of the Hunger of Kaa""
Processing photo: "Rocky"
 description => Stimson Python
Showiing photo info....
 farm => 8
 server => 7402
 secret => 6cbae671b5
 flickrid => 10054626824
 page_url => http://www.flickr.com/photos/102763809@N03/10054626824/
 description => &quot; 'Good. Begins now the dance--the Dance of the Hunger of Kaa. Sit still and watch.'

He turned twice or thrice in a big circle, weaving his head from right to left. 
Then he began making loops and figures of eight with his body, and soft, oozy triangles that melted into squares and five-sided figures, and coiled mounds, never resting, never hurrying, and never stopping his low humming song. It grew darker and darker, till at last the dragging, shifting coils disappeared, but they could hear the rustle of the scales.&quot;
(From &quot;Kaa's Hunting&quot; in &quot;The Jungle Book&quot; (1893) by Rudyard Kipling)

These old abandoned temples built around the 12th century belong to the abandoned city which inspired Kipling's Jungle Book.
They are rising at the top of a mountain which dominates the jungle at 811 meters above sea level in the centre of the jungle of Bandhavgarh located in the Indian state Madhya Pradesh.
Baghel King Vikramaditya Singh abandoned Bandhavgarh fort in 1617 when Rewa, at a distance of 130 km was established as a capital. 
Abandonment allowed wildlife development in this region.
When Baghel Kings became aware of it, he declared Bandhavgarh as their hunting preserve and strictly prohibited tree cutting and wildlife hunting...

Join the photographer at <a href="http://www.facebook.com/laurent.goldstein.photography" rel="nofollow">www.facebook.com/laurent.goldstein.photography</a>

© All photographs are copyrighted and all rights reserved.
Please do not use any photographs without permission (even for private use).
The use of any work without consent of the artist is PROHIBITED and will lead automatically to consequences.
Showiing photo info....
 farm => 6
 server => 5462
 secret => 6f9c0e7f83
 flickrid => 10051136944
 page_url => http://www.flickr.com/photos/designldg/10051136944/
 description => Ball Python
Showiing photo info....
 farm => 4
 server => 3744
 secret => 529840767f
 flickrid => 10046353675
 page_url => 
http://www.flickr.com/photos/megzzdollphotos/10046353675/

它是如何工作的...

这个菜谱展示了如何使用其 REST API 与 Flickr 进行交互。在这个例子中,collect_photo_info()标签接受三个参数:Flickr API 密钥、搜索标签和期望的搜索结果数量。

我们构建第一个 URL 来搜索照片。请注意,在这个 URL 中,方法参数的值是flickr.photos.search,期望的结果格式是 JSON。

第一次get()调用的结果存储在resp变量中,然后通过在resp上调用json()方法将其转换为 JSON 格式。现在,JSON 数据通过循环读取['photos']['photo']迭代器。创建一个photo_collection列表来返回经过信息整理后的结果。在这个列表中,每张照片信息由一个字典表示。这个字典的键是通过从早期的 JSON 响应和另一个GET请求中提取信息来填充的。

注意,为了获取关于照片的评论,我们需要进行另一个get()请求,并从返回的 JSON 的['comments']['comment']元素中收集评论信息。最后,这些评论被附加到一个列表中,并附加到照片字典条目中。

在主函数中,我们提取photo_collection字典并打印有关每张照片的一些有用信息。

从 Amazon S3 Web 服务搜索 SOAP 方法

如果您需要与实现简单对象访问过程(SOAP)的 Web 服务交互,那么这个菜谱可以帮助您找到一个起点。

准备工作

我们可以使用第三方SOAPpy库来完成这个任务。可以通过运行以下命令来安装它:

$pip install SOAPpy

如何操作...

在我们可以调用它们之前,我们创建一个代理实例并检查服务器方法。

在这个菜谱中,我们将与 Amazon S3 存储服务进行交互。我们已获取了 Web 服务 API 的测试 URL。执行这个简单任务需要一个 API 密钥。

列表 8.5 给出了从 Amazon S3 网络服务中搜索 SOAP 方法的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter – 8
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import SOAPpy

TEST_URL = 'http://s3.amazonaws.com/ec2-downloads/2009-04-04.ec2.wsdl'

def list_soap_methods(url):
    proxy = SOAPpy.WSDL.Proxy(url)
    print '%d methods in WSDL:' % len(proxy.methods) + '\n'
    for key in proxy.methods.keys():
 "Key Details:"
        for k,v in proxy.methods[key].__dict__.iteritems():
            print "%s ==> %s" %(k,v)

if __name__ == '__main__':
    list_soap_methods(TEST_URL)

如果你运行此脚本,它将打印出支持 Web 服务定义语言(WSDL)的可用方法的总数以及一个任意方法的详细信息,如下所示:

$ python 8_5_search_amazonaws_with_SOAP.py 
/home/faruq/env/lib/python2.7/site-packages/wstools/XMLSchema.py:1280: UserWarning: annotation is 
ignored
 warnings.warn('annotation is ignored')
43 methods in WSDL:

Key Name: ReleaseAddress
Key Details:
 encodingStyle ==> None
 style ==> document
 methodName ==> ReleaseAddress
 retval ==> None
 soapAction ==> ReleaseAddress
 namespace ==> None
 use ==> literal
 location ==> https://ec2.amazonaws.com/
 inparams ==> [<wstools.WSDLTools.ParameterInfo instance at 
0x8fb9d0c>]
 outheaders ==> []
 inheaders ==> []
 transport ==> http://schemas.xmlsoap.org/soap/http
 outparams ==> [<wstools.WSDLTools.ParameterInfo instance at 
0x8fb9d2c>]

它是如何工作的...

此脚本定义了一个名为list_soap_methods()的方法,它接受一个 URL 并通过调用SOAPpyWSDL.Proxy()方法来构建 SOAP 代理对象。可用的 SOAP 方法都位于此代理的方法属性下。

通过遍历代理的方法键来迭代,for循环仅打印单个 SOAP 方法的详细信息,即键的名称及其详细信息。

搜索谷歌以获取自定义信息

在谷歌上搜索以获取关于某物的信息似乎对许多人来说是日常活动。让我们尝试使用谷歌搜索一些信息。

准备工作

此食谱使用第三方 Python 库requests,可以通过以下命令安装:

$ pip install SOAPpy

如何操作...

谷歌有复杂的 API 来进行搜索。然而,它们要求你注册并按照特定方式获取 API 密钥。为了简单起见,让我们使用谷歌旧的普通异步 JavaScriptAJAX)API 来搜索有关 Python 书籍的一些信息。

列表 8.6 给出了搜索谷歌自定义信息的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 8
# This program is optimized for Python 2.7.# It may run on any other version with/without modifications.
import argparse
import json
import urllib
import requests

BASE_URL = 'http://ajax.googleapis.com/ajax/services/search/web?v=1.0' 

def get_search_url(query):
  return "%s&%s" %(BASE_URL, query)

def search_info(tag):
  query = urllib.urlencode({'q': tag})
  url = get_search_url(query)
  response = requests.get(url)
  results = response.json()

  data = results['responseData']
  print 'Found total results: %s' % 
data['cursor']['estimatedResultCount']
  hits = data['results']
  print 'Found top %d hits:' % len(hits)
  for h in hits: 
    print ' ', h['url']
    print 'More results available from %s' % 
data['cursor']['moreResultsUrl']

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Search info from 
Google')
  parser.add_argument('--tag', action="store", dest="tag", 
default='Python books')
  # parse arguments
  given_args = parser.parse_args()
  search_info(given_args.tag)

如果你通过指定--tag参数中的搜索查询来运行此脚本,那么它将搜索谷歌并打印出总结果数和前四个命中页面,如下所示:

$ python 8_6_search_products_from_Google.py 
Found total results: 12300000
Found top 4 hits:
 https://wiki.python.org/moin/PythonBooks
 http://www.amazon.com/Python-Languages-Tools-Programming-
Books/b%3Fie%3DUTF8%26node%3D285856
 http://pythonbooks.revolunet.com/
 http://readwrite.com/2011/03/25/python-is-an-increasingly-popu
More results available from 
http://www.google.com/search?oe=utf8&ie=utf8&source=uds&start=0&hl=en
&q=Python+books

它是如何工作的...

在此食谱中,我们定义了一个简短的功能get_search_url(),它从BASE_URL常量和目标查询中构建搜索 URL。

主要搜索函数search_info()接受搜索标签并构建查询。使用requests库进行get()调用。然后,将返回的响应转换为 JSON 数据。

通过访问'responseData'键的值从 JSON 数据中提取搜索结果。然后通过访问结果数据的相关键提取估计结果和命中数。然后将前四个命中 URL 打印到屏幕上。

通过产品搜索 API 搜索亚马逊上的书籍

如果你喜欢在亚马逊上搜索产品并将其中一些包含在你的网站或应用程序中,这个食谱可以帮助你做到这一点。我们可以看看如何搜索亚马逊上的书籍。

准备工作

此食谱依赖于第三方 Python 库bottlenose。你可以使用以下命令安装此库:

$ pip install  bottlenose

首先,你需要将你的亚马逊账户的访问密钥、秘密密钥和联盟 ID 放入local_settings.py。提供了一个带有书籍代码的示例设置文件。你也可以编辑此脚本并将其放置在此处。

如何操作...

我们可以使用实现了亚马逊产品搜索 API 的bottlenose库。

列表 8.7 给出了通过产品搜索 API 搜索亚马逊书籍的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 8
# This program is optimized for Python 2.7.
# It may run on any other version with/without modifications.

import argparse
import bottlenose
from xml.dom import minidom as xml

try:
  from local_settings import amazon_account
except ImportError:
  pass 

ACCESS_KEY = amazon_account['access_key'] 
SECRET_KEY = amazon_account['secret_key'] 
AFFILIATE_ID = amazon_account['affiliate_id'] 

def search_for_books(tag, index):
  """Search Amazon for Books """
  amazon = bottlenose.Amazon(ACCESS_KEY, SECRET_KEY, AFFILIATE_ID)
  results = amazon.ItemSearch(
    SearchIndex = index,
    Sort = "relevancerank",
    Keywords = tag
  )
  parsed_result = xml.parseString(results)

  all_items = []
  attrs = ['Title','Author', 'URL']

  for item in parsed_result.getElementsByTagName('Item'):
    parse_item = {}

  for attr in attrs:
    parse_item[attr] = ""
    try:
      parse_item[attr] = 
item.getElementsByTagName(attr)[0].childNodes[0].data
    except:
      pass
    all_items.append(parse_item)
  return all_items

if __name__ == '__main__':
  parser = argparse.ArgumentParser(description='Search info from 
Amazon')
  parser.add_argument('--tag', action="store", dest="tag", 
default='Python')
  parser.add_argument('--index', action="store", dest="index", 
default='Books')
  # parse arguments
  given_args = parser.parse_args()
  books = search_for_books(given_args.tag, given_args.index)    

  for book in books:
    for k,v in book.iteritems():
      print "%s: %s" %(k,v)
      print "-" * 80

如果你使用搜索标签和索引运行这个食谱,你可以看到一些类似以下输出的结果:

$ python 8_7_search_amazon_for_books.py --tag=Python --index=Books
URL: http://www.amazon.com/Python-In-Day-Basics-Coding/dp/tech-data/1490475575%3FSubscriptionId%3DAKIAIPPW3IK76PBRLWBA%26tag%3D7052-6929-7878%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D1490475575
Author: Richard Wagstaff
Title: Python In A Day: Learn The Basics, Learn It Quick, Start Coding Fast (In A Day Books) (Volume 1)
--------------------------------------------------------------------------------
URL: http://www.amazon.com/Learning-Python-Mark-Lutz/dp/tech-data/1449355730%3FSubscriptionId%3DAKIAIPPW3IK76PBRLWBA%26tag%3D7052-6929-7878%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D1449355730
Author: Mark Lutz
Title: Learning Python
--------------------------------------------------------------------------------
URL: http://www.amazon.com/Python-Programming-Introduction-Computer-Science/dp/tech-data/1590282418%3FSubscriptionId%3DAKIAIPPW3IK76PBRLWBA%26tag%3D7052-6929-7878%26linkCode%3Dxm2%26camp%3D2025%26creative%3D386001%26creativeASIN%3D1590282418
Author: John Zelle
Title: Python Programming: An Introduction to Computer Science 2nd Edition
---------------------------------------------------------------------
-----------

它是如何工作的...

这个食谱使用第三方bottlenose库的Amazon()类来创建一个对象,通过产品搜索 API 搜索亚马逊。这是通过顶级search_for_books()函数完成的。这个对象的ItemSearch()方法通过传递SearchIndexKeywords键的值来调用。它使用relevancerank方法对搜索结果进行排序。

搜索结果使用xml模块的minidom接口进行处理,该接口有一个有用的parseString()方法。它返回解析后的 XML 树形数据结构。这个数据结构上的getElementsByTagName()方法有助于找到物品的信息。然后查找物品属性,并将它们放置在解析物品的字典中。最后,所有解析的物品都附加到all_items()列表中,并返回给用户。

第九章:网络监控和安全

在本章中,我们将介绍以下菜谱:

  • 在你的网络上嗅探数据包

  • 使用 pcap dumper 将数据包保存到 pcap 格式

  • 在 HTTP 数据包中添加额外的头部

  • 扫描远程主机的端口

  • 自定义数据包的 IP 地址

  • 通过读取保存的 pcap 文件来回放流量

  • 扫描数据包的广播

简介

本章介绍了关于网络安全监控和漏洞扫描的一些有趣的 Python 菜谱。我们首先使用 pcap 库在网络中嗅探数据包。然后,我们开始使用 Scapy,这是一个瑞士军刀式的库,可以执行许多类似任务。使用 Scapy 展示了一些常见的包分析任务,例如将数据包保存到 pcap 格式、添加额外的头部和修改数据包的 IP 地址。

本章还包括一些关于网络入侵检测的高级任务,例如,从保存的 pcap 文件中回放流量和广播扫描。

在你的网络上嗅探数据包

如果你对你本地网络上的数据包嗅探感兴趣,这个菜谱可以作为起点。记住,你可能无法嗅探除目标机器之外的数据包,因为良好的网络交换机只会转发指定给机器的流量。

准备工作

为了使这个菜谱工作,你需要安装 pylibpcap 库(版本 0.6.4 或更高版本)。它可以在 SourceForge 上找到(sourceforge.net/projects/pylibpcap/)。

你还需要安装 construct 库,这个库可以通过 PyPI 中的 pipeasy_install 安装,如下命令所示:

$ easy_install construct

如何做...

我们可以提供命令行参数,例如网络接口名称和 TCP 端口号,以进行嗅探。

列表 9.1 给出了在网络上嗅探数据包的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 9
# This program is optimized for Python 2.6\. 
# It may run on any other version with/without modifications.

import argparse
import pcap
from construct.protocols.ipstack import ip_stack

def print_packet(pktlen, data, timestamp):
  """ Callback for printing the packet payload"""
  if not data:
    return

  stack = ip_stack.parse(data)
  payload = stack.next.next.next
  print payload

def main():
  # setup commandline arguments
  parser = argparse.ArgumentParser(description='Packet Sniffer')
  parser.add_argument('--iface', action="store", dest="iface", 
default='eth0')
  parser.add_argument('--port', action="store", dest="port", 
default=80, type=int)
  # parse arguments
  given_args = parser.parse_args()
  iface, port =  given_args.iface, given_args.port
  # start sniffing
  pc = pcap.pcapObject()
  pc.open_live(iface, 1600, 0, 100)
  pc.setfilter('dst port %d' %port, 0, 0)

  print 'Press CTRL+C to end capture'
  try:
    while True:
      pc.dispatch(1, print_packet)
  except KeyboardInterrupt:
    print 'Packet statistics: %d packets received, %d packets 
dropped, %d packets dropped by the interface' % pc.stats()

if __name__ == '__main__':
  main()

如果你运行此脚本并传递命令行参数,--iface=eth0--port=80,此脚本将嗅探来自你网页浏览器的所有 HTTP 数据包。因此,在运行此脚本后,如果你在浏览器中访问 www.google.com,你可以看到如下所示的原始数据包输出:

python 9_1_packet_sniffer.py --iface=eth0 --port=80 
Press CTRL+C to end capture
''
0000   47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a   GET / HTTP/1.1..
0010   48 6f 73 74 3a 20 77 77 77 2e 67 6f 6f 67 6c 65   Host: www.google
0020   2e 63 6f 6d 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e   .com..Connection
0030   3a 20 6b 65 65 70 2d 61 6c 69 76 65 0d 0a 41 63   : keep-alive..Ac
0040   63 65 70 74 3a 20 74 65 78 74 2f 68 74 6d 6c 2c   cept: text/html,
0050   61 70 70 6c 69 63 61 74 69 6f 6e 2f 78 68 74 6d   application/xhtm
0060   6c 2b 78 6d 6c 2c 61 70 70 6c 69 63 61 74 69 6f   l+xml,applicatio
0070   6e 2f 78 6d 6c 3b 71 3d 30 2e 39 2c 2a 2f 2a 3b   n/xml;q=0.9,*/*;
0080   71 3d 30 2e 38 0d 0a 55 73 65 72 2d 41 67 65 6e   q=0.8..User-Agen
0090   74 3a 20 4d 6f 7a 69 6c 6c 61 2f 35 2e 30 20 28   t: Mozilla/5.0 (
00A0   58 31 31 3b 20 4c 69 6e 75 78 20 69 36 38 36 29   X11; Linux i686)
00B0   20 41 70 70 6c 65 57 65 62 4b 69 74 2f 35 33 37    AppleWebKit/537
00C0   2e 33 31 20 28 4b 48 54 4d 4c 2c 20 6c 69 6b 65   .31 (KHTML, like
00D0   20 47 65 63 6b 6f 29 20 43 68 72 6f 6d 65 2f 32    Gecko) Chrome/2
00E0   36 2e 30 2e 31 34 31 30 2e 34 33 20 53 61 66 61   6.0.1410.43 Safa
00F0   72 69 2f 35 33 37 2e 33 31 0d 0a 58 2d 43 68 72   ri/537.31..X-Chr
0100   6f 6d 65 2d 56 61 72 69 61 74 69 6f 6e 73 3a 20   ome-Variations: 
0110   43 50 71 31 79 51 45 49 6b 62 62 4a 41 51 69 59   CPq1yQEIkbbJAQiY
0120   74 73 6b 42 43 4b 4f 32 79 51 45 49 70 37 62 4a   tskBCKO2yQEIp7bJ
0130   41 51 69 70 74 73 6b 42 43 4c 65 32 79 51 45 49   AQiptskBCLe2yQEI
0140   2b 6f 50 4b 41 51 3d 3d 0d 0a 44 4e 54 3a 20 31   +oPKAQ==..DNT: 1
0150   0d 0a 41 63 63 65 70 74 2d 45 6e 63 6f 64 69 6e   ..Accept-Encodin
0160   67 3a 20 67 7a 69 70 2c 64 65 66 6c 61 74 65 2c   g: gzip,deflate,
0170   73 64 63 68 0d 0a 41 63 63 65 70 74 2d 4c 61 6e   sdch..Accept-Lan
0180   67 75 61 67 65 3a 20 65 6e 2d 47 42 2c 65 6e 2d   guage: en-GB,en-
0190   55 53 3b 71 3d 30 2e 38 2c 65 6e 3b 71 3d 30 2e   US;q=0.8,en;q=0.
01A0   36 0d 0a 41 63 63 65 70 74 2d 43 68 61 72 73 65   6..Accept-Charse
01B0   74 3a 20 49 53 4f 2d 38 38 35 39 2d 31 2c 75 74   t: ISO-8859-1,ut
01C0   66 2d 38 3b 71 3d 30 2e 37 2c 2a 3b 71 3d 30 2e   f-8;q=0.7,*;q=0.
01D0   33 0d 0a 43 6f 6f 6b 69 65 3a 20 50 52 45 46 3d   3..Cookie: PREF=

….

^CPacket statistics: 17 packets received, 0 packets dropped, 0 
packets dropped by the interface

它是如何工作的...

这个菜谱依赖于 pcap 库中的 pcapObject() 类来创建嗅探器的实例。在 main() 方法中,创建了这个类的实例,并使用 setfilter() 方法设置了一个过滤器,以便只捕获 HTTP 数据包。最后,dispatch() 方法开始嗅探并将嗅探到的数据包发送到 print_packet() 函数进行后处理。

print_packet() 函数中,如果数据包包含数据,则使用 construct 库的 ip_stack.parse() 方法提取有效载荷。这个库对于低级数据处理很有用。

使用 pcap dumper 将数据包保存到 pcap 格式

pcap格式,缩写自数据包捕获,是保存网络数据的常见文件格式。有关 pcap 格式的更多详细信息,请参阅wiki.wireshark.org/Development/LibpcapFileFormat

如果你想要将捕获到的网络数据包保存到文件,并在以后重新使用它们进行进一步处理,这个菜谱可以为你提供一个工作示例。

如何做到这一点...

在这个菜谱中,我们使用Scapy库来嗅探数据包并将其写入文件。所有Scapy的实用函数和定义都可以通过通配符导入来导入,如下面的命令所示:

from scapy.all import *

这只是为了演示目的,不推荐用于生产代码。

Scapysniff()函数接受一个回调函数的名称。让我们编写一个回调函数,该函数将数据包写入文件。

列表 9.2 给出了使用 pcap dumper 将数据包保存为 pcap 格式的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 9
# This program is optimized for Python 2.7\. 
# It may run on any other version with/without modifications.

import os
from scapy.all import *

pkts = []
iter = 0
pcapnum = 0

def write_cap(x):
  global pkts
  global iter
  global pcapnum
  pkts.append(x)
  iter += 1
  if iter == 3:
    pcapnum += 1
    pname = "pcap%d.pcap" % pcapnum
    wrpcap(pname, pkts)
    pkts = []
    iter = 0

if __name__ == '__main__':
  print "Started packet capturing and dumping... Press CTRL+C to exit"
  sniff(prn=write_cap)

  print "Testing the dump file..."
  dump_file = "./pcap1.pcap"
  if os.path.exists(dump_file):
    print "dump fie %s found." %dump_file
    pkts = sniff(offline=dump_file)
    count = 0
    while (count <=2):
      print "----Dumping pkt:%s----" %count
      print hexdump(pkts[count])
      count += 1    
  else:
    print "dump fie %s not found." %dump_file

如果你运行此脚本,你将看到类似以下输出的输出:

# python 9_2_save_packets_in_pcap_format.py 
^CStarted packet capturing and dumping... Press CTRL+C to exit
Testing the dump file...
dump fie ./pcap1.pcap found.
----Dumping pkt:0----
0000   08 00 27 95 0D 1A 52 54  00 12 35 02 08 00 45 00   ..'...RT..5...E.
0010   00 DB E2 6D 00 00 40 06  7C 9E 6C A0 A2 62 0A 00   ...m..@.|.l..b..
0020   02 0F 00 50 99 55 97 98  2C 84 CE 45 9B 6C 50 18   ...P.U..,..E.lP.
0030   FF FF 53 E0 00 00 48 54  54 50 2F 31 2E 31 20 32   ..S...HTTP/1.1 2
0040   30 30 20 4F 4B 0D 0A 58  2D 44 42 2D 54 69 6D 65   00 OK..X-DB-Time
0050   6F 75 74 3A 20 31 32 30  0D 0A 50 72 61 67 6D 61   out: 120..Pragma
0060   3A 20 6E 6F 2D 63 61 63  68 65 0D 0A 43 61 63 68   : no-cache..Cach
0070   65 2D 43 6F 6E 74 72 6F  6C 3A 20 6E 6F 2D 63 61   e-Control: no-ca
0080   63 68 65 0D 0A 43 6F 6E  74 65 6E 74 2D 54 79 70   che..Content-Typ
0090   65 3A 20 74 65 78 74 2F  70 6C 61 69 6E 0D 0A 44   e: text/plain..D
00a0   61 74 65 3A 20 53 75 6E  2C 20 31 35 20 53 65 70   ate: Sun, 15 Sep
00b0   20 32 30 31 33 20 31 35  3A 32 32 3A 33 36 20 47    2013 15:22:36 G
00c0   4D 54 0D 0A 43 6F 6E 74  65 6E 74 2D 4C 65 6E 67   MT..Content-Leng
00d0   74 68 3A 20 31 35 0D 0A  0D 0A 7B 22 72 65 74 22   th: 15....{"ret"
00e0   3A 20 22 70 75 6E 74 22  7D                        : "punt"}
None
----Dumping pkt:1----
0000   52 54 00 12 35 02 08 00  27 95 0D 1A 08 00 45 00   RT..5...'.....E.
0010   01 D2 1F 25 40 00 40 06  FE EF 0A 00 02 0F 6C A0   ...%@.@.......l.
0020   A2 62 99 55 00 50 CE 45  9B 6C 97 98 2D 37 50 18   .b.U.P.E.l..-7P.
0030   F9 28 1C D6 00 00 47 45  54 20 2F 73 75 62 73 63   .(....GET /subsc
0040   72 69 62 65 3F 68 6F 73  74 5F 69 6E 74 3D 35 31   ribe?host_int=51
0050   30 35 36 34 37 34 36 26  6E 73 5F 6D 61 70 3D 31   0564746&ns_map=1
0060   36 30 36 39 36 39 39 34  5F 33 30 30 38 30 38 34   60696994_3008084
0070   30 37 37 31 34 2C 31 30  31 39 34 36 31 31 5F 31   07714,10194611_1
0080   31 30 35 33 30 39 38 34  33 38 32 30 32 31 31 2C   105309843820211,
0090   31 34 36 34 32 38 30 35  32 5F 33 32 39 34 33 38   146428052_329438
00a0   36 33 34 34 30 38 34 2C  31 31 36 30 31 35 33 31   6344084,11601531
00b0   5F 32 37 39 31 38 34 34  37 35 37 37 31 2C 31 30   _279184475771,10
00c0   31 39 34 38 32 38 5F 33  30 30 37 34 39 36 35 39   194828_300749659
00d0   30 30 2C 33 33 30 39 39  31 39 38 32 5F 38 31 39   00,330991982_819
00e0   33 35 33 37 30 36 30 36  2C 31 36 33 32 37 38 35   35370606,1632785
00f0   35 5F 31 32 39 30 31 32  32 39 37 34 33 26 75 73   5_12901229743&us
0100   65 72 5F 69 64 3D 36 35  32 30 33 37 32 26 6E 69   er_id=6520372&ni
0110   64 3D 32 26 74 73 3D 31  33 37 39 32 35 38 35 36   d=2&ts=137925856
0120   31 20 48 54 54 50 2F 31  2E 31 0D 0A 48 6F 73 74   1 HTTP/1.1..Host
0130   3A 20 6E 6F 74 69 66 79  33 2E 64 72 6F 70 62 6F   : notify3.dropbo
0140   78 2E 63 6F 6D 0D 0A 41  63 63 65 70 74 2D 45 6E   x.com..Accept-En
0150   63 6F 64 69 6E 67 3A 20  69 64 65 6E 74 69 74 79   coding: identity
0160   0D 0A 43 6F 6E 6E 65 63  74 69 6F 6E 3A 20 6B 65   ..Connection: ke
0170   65 70 2D 61 6C 69 76 65  0D 0A 58 2D 44 72 6F 70   ep-alive..X-Drop
0180   62 6F 78 2D 4C 6F 63 61  6C 65 3A 20 65 6E 5F 55   box-Locale: en_U
0190   53 0D 0A 55 73 65 72 2D  41 67 65 6E 74 3A 20 44   S..User-Agent: D
01a0   72 6F 70 62 6F 78 44 65  73 6B 74 6F 70 43 6C 69   ropboxDesktopCli
01b0   65 6E 74 2F 32 2E 30 2E  32 32 20 28 4C 69 6E 75   ent/2.0.22 (Linu
01c0   78 3B 20 32 2E 36 2E 33  32 2D 35 2D 36 38 36 3B   x; 2.6.32-5-686;
01d0   20 69 33 32 3B 20 65 6E  5F 55 53 29 0D 0A 0D 0A    i32; en_US)....
None
----Dumping pkt:2----
0000   08 00 27 95 0D 1A 52 54  00 12 35 02 08 00 45 00   ..'...RT..5...E.
0010   00 28 E2 6E 00 00 40 06  7D 50 6C A0 A2 62 0A 00   .(.n..@.}Pl..b..
0020   02 0F 00 50 99 55 97 98  2D 37 CE 45 9D 16 50 10   ...P.U..-7.E..P.
0030   FF FF CA F1 00 00 00 00  00 00 00 00               ............
None

它是如何工作的...

这个菜谱使用Scapy库的sniff()wrpacp()实用函数来捕获所有网络数据包并将它们写入文件。通过sniff()捕获数据包后,将调用该数据包的write_cap()函数。一些全局变量用于逐个处理数据包。例如,数据包存储在pkts[]列表中,并使用数据包和变量计数。当计数值为 3 时,将pkts列表写入名为pcap1.pcap的文件,将计数变量重置,以便我们可以继续捕获另外三个数据包并将它们写入pcap2.pcap,依此类推。

test_dump_file()函数中,假设在当前工作目录中存在第一个转储文件,pcap1.dump。现在,使用带有离线参数的sniff(),从文件中捕获数据包而不是从网络中捕获。在这里,使用hexdump()函数逐个解码数据包。然后,将数据包的内容打印到屏幕上。

在 HTTP 数据包中添加额外头

有时,你可能希望通过提供包含自定义信息的自定义 HTTP 头来操纵应用程序。例如,添加一个授权头对于在数据包捕获代码中实现 HTTP 基本认证非常有用。

如何做到这一点...

让我们使用Scapysniff()函数嗅探数据包,并定义一个回调函数modify_packet_header(),该函数为某些数据包添加额外的头。

列表 9.3 给出了在 HTTP 数据包中添加额外头的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 9
# This program is optimized for Python 2.7\. 
# It may run on any other version with/without modifications.

from scapy.all import *

def modify_packet_header(pkt):
  """ Parse the header and add an extra header"""
  if pkt.haslayer(TCP) and pkt.getlayer(TCP).dport == 80 and 
pkt.haslayer(Raw):
    hdr = pkt[TCP].payload.__dict__        
    extra_item = {'Extra Header' : ' extra value'}
    hdr.update(extra_item)     
    send_hdr = '\r\n'.join(hdr)
    pkt[TCP].payload = send_hdr

    pkt.show()

    del pkt[IP].chksum
    send(pkt)

if __name__ == '__main__':
  # start sniffing
  sniff(filter="tcp and ( port 80 )", prn=modify_packet_header)

如果你运行此脚本,它将显示捕获到的数据包;打印其修改后的版本并将其发送到网络,如下面的输出所示。这可以通过其他数据包捕获工具如tcpdumpwireshark进行验证:

$ python 9_3_add_extra_http_header_in_sniffed_packet.py 

###[ Ethernet ]###
 dst       = 52:54:00:12:35:02
 src       = 08:00:27:95:0d:1a
 type      = 0x800
###[ IP ]###
 version   = 4L
 ihl       = 5L
 tos       = 0x0
 len       = 525
 id        = 13419
 flags     = DF
 frag      = 0L
 ttl       = 64
 proto     = tcp
 chksum    = 0x171
 src       = 10.0.2.15
 dst       = 82.94.164.162
 \options   \
###[ TCP ]###
 sport     = 49273
 dport     = www
 seq       = 107715690
 ack       = 216121024
 dataofs   = 5L
 reserved  = 0L
 flags     = PA
 window    = 6432
 chksum    = 0x50f
 urgptr    = 0
 options   = []
###[ Raw ]###
 load      = 'Extra Header\r\nsent_time\r\nfields\r\naliastypes\r\npost_transforms\r\nunderlayer\r\nfieldtype\r\ntime\r\ninitialized\r\noverloaded_fields\r\npacketfields\r\npayload\r\ndefault_fields'
.
Sent 1 packets.

它是如何工作的...

首先,我们使用 Scapysniff() 函数设置数据包嗅探,指定 modify_packet_header() 作为每个数据包的回调函数。所有目标端口为 80(HTTP)且具有 TCP 和原始层的 TCP 数据包都被认为是修改对象。因此,当前数据包头部是从数据包的有效负载数据中提取出来的。

然后将额外的头部添加到现有的头部字典中。然后使用 show() 方法在屏幕上打印数据包,为了避免正确性检查失败,从数据包中移除数据包校验和。最后,数据包通过网络发送。

扫描远程主机的端口

如果你尝试通过特定端口连接到远程主机,有时你会收到“连接被拒绝”的消息。这可能是由于远程主机上的服务器可能已经关闭。在这种情况下,你可以尝试查看端口是否开放或处于监听状态。你可以扫描多个端口以识别机器上的可用服务。

如何做...

使用 Python 的标准套接字库,我们可以完成这个端口扫描任务。我们可以接受三个命令行参数:目标主机和起始端口和结束端口。

列表 9.4 提供了扫描远程主机端口的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 9
# This program is optimized for Python 2.7\. 
# It may run on any other version with/without modifications.

import argparse
import socket
import sys

def scan_ports(host, start_port, end_port):
  """ Scan remote hosts """
  #Create socket
  try:
    sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
  except socket.error,err_msg:
    print 'Socket creation failed. Error code: '+ str(err_msg[0]) 
+ ' Error mesage: ' + err_msg[1]
    sys.exit()

  #Get IP of remote host
  try:
    remote_ip = socket.gethostbyname(host)
  except socket.error,error_msg:
    print error_msg
  sys.exit()

  #Scan ports
  end_port += 1
  for port in range(start_port,end_port):
    try:
      sock.connect((remote_ip,port))
      print 'Port ' + str(port) + ' is open'
      sock.close()
      sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    except socket.error:
      pass # skip various socket errors

if __name__ == '__main__':
  # setup commandline arguments
  parser = argparse.ArgumentParser(description='Remote Port 
Scanner')
  parser.add_argument('--host', action="store", dest="host", 
default='localhost')
  parser.add_argument('--start-port', action="store", 
dest="start_port", default=1, type=int)
  parser.add_argument('--end-port', action="store", 
dest="end_port", default=100, type=int)
  # parse arguments
  given_args = parser.parse_args()
  host, start_port, end_port =  given_args.host, 
given_args.start_port, given_args.end_port
  scan_ports(host, start_port, end_port)

如果你运行此方法来扫描本地机器的端口 1100 以检测开放端口,你将得到类似以下的结果:

# python 9_4_scan_port_of_a_remote_host.py --host=localhost --start-port=1 --end-port=100
Port 21 is open
Port 22 is open
Port 23 is open
Port 25 is open
Port 80 is open

它是如何工作的...

这个方法演示了如何使用 Python 的标准套接字库扫描机器的开放端口。scan_port() 函数接受三个参数:主机名、起始端口和结束端口。然后,它分三步扫描整个端口范围。

使用 socket() 函数创建一个 TCP 套接字。

如果套接字创建成功,则使用 gethostbyname() 函数解析远程主机的 IP 地址。

如果找到了目标主机的 IP 地址,尝试使用 connect() 函数连接到该 IP。如果成功,则意味着端口是开放的。现在,使用 close() 函数关闭端口,并重复第一步以检查下一个端口。

自定义数据包的 IP 地址

如果你需要创建一个网络数据包并自定义源和目标 IP 或端口,这个方法可以作为起点。

如何做...

我们可以获取所有有用的命令行参数,例如网络接口名称、协议名称、源 IP、源端口、目标 IP、目标端口以及可选的 TCP 标志。

我们可以使用 Scapy 库创建自定义 TCP 或 UDP 数据包并将其发送到网络上。

列表 9.5 提供了自定义数据包 IP 地址的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 9
# This program is optimized for Python 2.7\. 
# It may run on any other version with/without modifications.

import argparse
import sys
import re
from random import randint

from scapy.all import IP,TCP,UDP,conf,send

def send_packet(protocol=None, src_ip=None, src_port=None, flags=None, dst_ip=None, dst_port=None, iface=None):
  """Modify and send an IP packet."""
  if protocol == 'tcp':
    packet = IP(src=src_ip, dst=dst_ip)/TCP(flags=flags, 
sport=src_port, dport=dst_port)
  elif protocol == 'udp':
  if flags: raise Exception(" Flags are not supported for udp")
    packet = IP(src=src_ip, dst=dst_ip)/UDP(sport=src_port, 
dport=dst_port)
  else:
    raise Exception("Unknown protocol %s" % protocol)

  send(packet, iface=iface)

if __name__ == '__main__':
  # setup commandline arguments
  parser = argparse.ArgumentParser(description='Packet Modifier')
  parser.add_argument('--iface', action="store", dest="iface", 
default='eth0')
  parser.add_argument('--protocol', action="store", 
dest="protocol", default='tcp')
  parser.add_argument('--src-ip', action="store", dest="src_ip", 
default='1.1.1.1')
  parser.add_argument('--src-port', action="store", 
dest="src_port", default=randint(0, 65535))
  parser.add_argument('--dst-ip', action="store", dest="dst_ip", 
default='192.168.1.51')
  parser.add_argument('--dst-port', action="store", 
dest="dst_port", default=randint(0, 65535))
  parser.add_argument('--flags', action="store", dest="flags", 
default=None)
  # parse arguments
  given_args = parser.parse_args()
  iface, protocol, src_ip,  src_port, dst_ip, dst_port, flags =  
given_args.iface, given_args.protocol, given_args.src_ip,\
  given_args.src_port, given_args.dst_ip, given_args.dst_port, 
given_args.flags
  send_packet(protocol, src_ip, src_port, flags, dst_ip, 
dst_port, iface)

为了运行此脚本,请输入以下命令:

tcpdump src 192.168.1.66
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 bytes
^C18:37:34.309992 IP 192.168.1.66.60698 > 192.168.1.51.666: Flags [S], seq 0, win 8192, length 0

1 packets captured
1 packets received by filter
0 packets dropped by kernel

$ sudo python 9_5_modify_ip_in_a_packet.py 
WARNING: No route found for IPv6 destination :: (no default route?)
.
Sent 1 packets.

它是如何工作的...

此脚本定义了一个send_packet()函数,用于使用Scapy构建 IP 数据包。它将源地址和目标地址以及端口号提供给它。根据协议,例如 TCP 或 UDP,它构建正确的数据包类型。如果是 TCP,则使用标志参数;如果不是,则引发异常。

为了构建 TCP 数据包,Sacpy提供了IP()/TCP()函数。同样,为了创建 UDP 数据包,使用IP()/UDP()函数。

最后,使用send()函数发送修改后的数据包。

通过读取保存的 pcap 文件重放流量

在玩网络数据包时,您可能需要通过从之前保存的pcap文件中读取来重放流量。在这种情况下,您希望在发送之前读取pcap文件并修改源或目标 IP 地址。

如何操作...

让我们使用Scapy读取之前保存的pcap文件。如果您没有pcap文件,可以使用本章的使用 pcap dumper 保存数据包到 pcap 格式方法来创建一个。

然后,从命令行解析参数,并将解析后的原始数据包传递给send_packet()函数。

列表 9.6 给出了通过从保存的pcap文件中读取来重放流量的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 9
# This program is optimized for Python 2.7\. 
# It may run on any other version with/without modifications.

import argparse
from scapy.all import *

def send_packet(recvd_pkt, src_ip, dst_ip, count):
  """ Send modified packets"""
  pkt_cnt = 0
  p_out = []

  for p in recvd_pkt:
    pkt_cnt += 1
    new_pkt = p.payload
    new_pkt[IP].dst = dst_ip
    new_pkt[IP].src = src_ip
    del new_pkt[IP].chksum
    p_out.append(new_pkt)
    if pkt_cnt % count == 0:
      send(PacketList(p_out))
      p_out = []

  # Send rest of packet
  send(PacketList(p_out))
  print "Total packets sent: %d" %pkt_cnt

if __name__ == '__main__':
  # setup commandline arguments
  parser = argparse.ArgumentParser(description='Packet Sniffer')
  parser.add_argument('--infile', action="store", dest="infile", 
default='pcap1.pcap')
  parser.add_argument('--src-ip', action="store", dest="src_ip", 
default='1.1.1.1')
  parser.add_argument('--dst-ip', action="store", dest="dst_ip", 
default='2.2.2.2')
  parser.add_argument('--count', action="store", dest="count", 
default=100, type=int)
  # parse arguments
  given_args = ga = parser.parse_args()
  global src_ip, dst_ip
  infile, src_ip, dst_ip, count =  ga.infile, ga.src_ip, 
ga.dst_ip, ga.count
  try:
    pkt_reader = PcapReader(infile)
    send_packet(pkt_reader, src_ip, dst_ip, count)
  except IOError:
    print "Failed reading file %s contents" % infile
    sys.exit(1)

如果您运行此脚本,它将默认读取保存的pcap文件pcap1.pcap,并在修改源和目标 IP 地址为1.1.1.12.2.2.2后发送数据包,如下所示。如果您使用tcpdump实用程序,您可以看到这些数据包传输。

# python 9_6_replay_traffic.py 
...
Sent 3 packets.
Total packets sent 3
----
# tcpdump src 1.1.1.1
tcpdump: verbose output suppressed, use -v or -vv for full protocol 
decode
listening on eth0, link-type EN10MB (Ethernet), capture size 65535 
bytes
^C18:44:13.186302 IP 1.1.1.1.www > ARennes-651-1-107-2.w2-
2.abo.wanadoo.fr.39253: Flags [P.], seq 2543332484:2543332663, ack 
3460668268, win 65535, length 179
1 packets captured
3 packets received by filter
0 packets dropped by kernel

它是如何工作的...

此方法使用ScapyPcapReader()函数从磁盘读取保存的pcap文件pcap1.pcap,该函数返回一个数据包迭代器。如果提供了命令行参数,则解析它们。否则,使用前述输出中所示默认值。

将命令行参数和数据包列表传递给send_packet()函数。此函数将新数据包放入p_out列表中,并跟踪处理过的数据包。在每个数据包中,有效载荷被修改,从而改变了源和目标 IP 地址。此外,删除了checksum数据包,因为它基于原始 IP 地址。

处理完一个数据包后,它立即通过网络发送。之后,剩余的数据包一次性发送。

扫描数据包广播

如果您遇到检测网络广播的问题,这个方法就是为您准备的。我们可以学习如何从广播数据包中找到信息。

如何操作...

我们可以使用Scapy嗅探到达网络接口的数据包。在捕获每个数据包后,可以通过回调函数处理它们以获取有用的信息。

列表 9.7 给出了扫描数据包广播的代码,如下所示:

#!/usr/bin/env python
# Python Network Programming Cookbook -- Chapter - 9
# This program is optimized for Python 2.7\. 
# It may run on any other version with/without modifications.

from scapy.all import *
import os
captured_data = dict()

END_PORT = 1000

def monitor_packet(pkt):
  if IP in pkt:
    if not captured_data.has_key(pkt[IP].src):
      captured_data[pkt[IP].src] = []

    if TCP in pkt:
      if pkt[TCP].sport <=  END_PORT:
        if not str(pkt[TCP].sport) in captured_data[pkt[IP].src]:
           captured_data[pkt[IP].src].append(str(pkt[TCP].sport))

  os.system('clear')
  ip_list = sorted(captured_data.keys())
  for key in ip_list:
    ports=', '.join(captured_data[key])
    if len (captured_data[key]) == 0:
      print '%s' % key
    else:
      print '%s (%s)' % (key, ports)

if __name__ == '__main__':
  sniff(prn=monitor_packet, store=0)

如果你运行这个脚本,你可以列出广播流量的源 IP 和端口。以下是一个示例输出,其中 IP 的第一个八位字节已被替换:

# python 9_7_broadcast_scanning.py
10.0.2.15
XXX.194.41.129 (80)
XXX.194.41.134 (80)
XXX.194.41.136 (443)
XXX.194.41.140 (80)
XXX.194.67.147 (80)
XXX.194.67.94 (443)
XXX.194.67.95 (80, 443)

它是如何工作的...

这个菜谱使用Scapysniff()函数在网络中嗅探数据包。它有一个monitor_packet()回调函数,用于处理数据包的后处理。根据协议,例如 IP 或 TCP,它将数据包排序到一个名为captured_data的字典中。

如果字典中尚未存在单个 IP,它将创建一个新的条目;否则,它将更新该特定 IP 的端口号。最后,它按行打印 IP 地址和端口。

posted @ 2025-09-22 13:19  绝不原创的飞龙  阅读(30)  评论(0)    收藏  举报