使用 Python 进行 socket 编程

(5)类定义语句 
所有的类都需要在这里定义。当模块被导入时 class 语句会被执行, 类也就会被定义。类
的文档变量是 class.__doc__。 
(7) 主程序 
无论这个模块是被别的模块导入还是作为脚本直接执行,都会执行这部分代码。通常这里
不会有太多功能性代码,而是根据执行的模式调用不同的函数。

大部分的Python 模块都是用于导入调用的,直接运行模块
应该调用该模块的回归测试代码。

时刻记住一个事实,那就是所有的模块都有能力来执行代码。最高级别的 Python 语句--
也就是说, 那些没有缩进的代码行在模块被导入时就会执行, 不管是不是真的需要执行。由
于有这样一个“特性”,比较安全的写代码的方式就是除了那些真正需要执行的代码以外, 几
乎所有的功能代码都在函数当中。 再说一遍,  通常只有主程序模块中有大量的顶级可执行代码,  
所有其它被导入的模块只应该有很少的顶级执行代码,所有的功能代码都应该封装在函数或类
当中。

核心笔记:__name__ 指示模块应如何被加载 
由于主程序代码无论模块是被导入还是被直接执行都会运行,  我们必须知道模块如何决定
运行方向。一个应用程序可能需要导入另一个应用程序的一个模块,以便重用一些有用的代码
(否则就只能用拷贝粘贴那种非面向对象的愚蠢手段)。这种情况下,你只想访问那些位于其
它应用程序中的代码,而不是想运行那个应用程序。因此一个问题出现了,“Python 是否有
一种方法能在运行时检测该模块是被导入还是被直接执行呢?” 答案就是......(鼓声雷
动).....没错! __name__ 系统变量就是正确答案。 
 
如果模块是被导入, __name__ 的值为模块名字 
如果模块是被直接执行, __name__ 的值为 '__main__'

变量无须事先声明 
变量无须指定类型 
变量在第一次被赋值时自动声明

注意任何追踪或调试程序会给一个对象增加一个额外的引用, 这会推迟该
对象被回收的时间。

它也负责检查那些虽然引用计数大于 0 但也应该被销毁的对象。 特定情形会导
致循环引用。

类似 os.linesep 这样的名字需要解释器做两次查询: (1) 查找 os 以确认它是一个模块, 
(2)在这个模块中查找 linesep 变量。因为模块也是全局变量, 我们多消耗了系统资源。如
果你在一个函数中类似这样频繁使用一个属性,我们建议你为该属性取一个本地变量别名。 变
量查找速度将会快很多--在查找全局变量之前, 总是先查找本地变量。 这也是一个让你的
程序跑的更快的技巧: 将经常用到的模块属性替换为一个本地引用。代码跑得更快,而也不用
老是敲那么长的变量名了。在我们的代码片段中,并没有定义函数,所以不能给你定义本地别
名的示例。不过我们有一个全局别名,至少也减少了一次名字查询

异常处理最适用的场合,是在没有合适的函数处理异常状况的时候。
这时程序员必须识别这些非正常的错误,并做出相应处理。

你一定还记得,对象的一系列固有行为和特性(比如支持哪些运算,具
有哪些方法)必须事先定义好。从这个角度看,类型正是保存这些信息的最佳位置。描述一种
类型所需要的信息不可能用一个字符串来搞定,所以类型不能是一个简单的字符串,这些信息
不能也不应该和数据保存在一起, 所以我们将类型定义成对象。

随着 Python 2.2 中类型和类的统一,类型对象在面向对象编程和日常对象使用中扮演着
更加重要的角色。从现在起, 类就是类型,实例是对应类型的对象。

我们会注意到比较操作是针对对象的值进行的, 也就是说比较的是对象的数值而不是对象
本身。在后面的部分我们会研究对象身份的比较。

Python 通过传递引用来处理对象。

在上面的例子中,您会注意到我们使用的是浮点数而不是整数。为什么这样?整数对象和
字符串对象是不可变对象,所以 Python 会很高效的缓存它们。这会造成我们认为 Python应该
创建新对象时,它却没有创建新对象的假象。看下面的例子: 
>>> a = 1 
>>> id(a) 
8402824 
>>> b = 1 
>>> id(b) 
8402824 
>>> 
>>> c = 1.0 
>>> id(c) 
8651220 
>>> d = 1.0 
>>> id(d) 
8651204 
Python 仅缓存简单整数,因为它认为在 Python 应用程序中这些小整数会经常被用到。当
我们在写作本书的时候,Python 缓存的整数范围是(-1, 100),不过这个范围是会改变的,所
以请不要在你的应用程序使用这个特性。

在学习编程的过程中, 我们一直接受这样的教育, 变量就像一个盒子, 里面装着变量的
值。在 Python 中, 变量更像一个指针指向装变量值的盒子。 对不可改变类型来说, 你无法
改变盒子的内容, 但你可以将指针指向一个新盒子。每次将另外的数字赋给变量的时候,实际
上创建了一个新的对象并把它赋给变量.(不仅仅是数字,对于所有的不可变类型,都是这么回
事)

拥有 C 背景的程序员一定熟悉传统除法――也就是说, 对整数操作数,会执行“地板除”
(floor,取比商小的最大整数。例如 5 除以 2 等于 2.5,其中“2”就称为商的“地板” ,即“地
板除”的结果。本书中使用“地板除”的说法是为了沿用原作者的风格,译者注) 。对浮点操作
数会执行真正的除法。然而,对第一次学编程的人或者那些依赖精确计算的人来说,可能就需
要多次调整代码才能得到自己想要的结果。

所谓工厂函数就是指这些内建函数都是类对象,  当你调用它们时, 实际上是创建了一个类实例。 

Python 不支持方法或函数重载, 因此你必须自己保证调用的就是你想要的函数或对象。
(参阅 Python 常见问答 4.75 节) 。幸运的是, 我们前面 4.3.1 小节提到的 type()内建函数可
以帮助你确认这一点。 一个名字里究竟保存的是什么?相当多, 尤其是这是一个类型的名字时。
确认接收到的类型对象的身份有很多时候都是很有用的。为了达到此目的,Python 提供了一
个内建函数 type(). type()返回任意 Python 对象对象的类型,而不局限于标准类型。让我们
通过交互式解释器来看几个使用 type()内建函数返回多种对象类型的例子:

sequence1 + sequence2 
该表达式的结果是一个包含 sequence1 和 sequence2 的内容的新序列.注意,这种方式看
起来似乎实现了把两个序列内容合并的概念,但是这个操作不是最快或者说最有效的。对字符
串来说,这个操作不如把所有的子字符串放到一个列表或可迭代对象中,然后调用一个 join
方法来把所有的内容连接在一起节约内存;类似地,对列表来说,我们推荐读者用列表类型的
extend()方法来把两个或者多个列表对象合并.当你需要简单地把两个对象的内容合并, 或者说
不能依赖于可变对象的那些没有返回值(实际上它返回一个 None)的内建方法来完成的时候时,
连接操作符还是很方便的一个选择。

因为 Python 是面向对象的, 所以你可以像下面这样直接访问一个序列的元素(不用先把它
赋值给一个变量): 
>>> print ('Faye', 'Leanna', 'Daylen')[1]  
Leanna 
这个特性在你调用一个返回值是序列类型的函数,并且你只对返回的序列中的一个或某几
个元素感兴趣时特别有用.

所谓浅拷贝就是只拷贝了对对象的索引,而不是重新建立了一个对象!如果你想完全的拷
贝一个对象(包括递归,如果你的对象是一个包含在容器中的容器),你需要用到深拷贝,关于浅
拷贝和深拷贝的更多信息会在本章的末尾讲到。

字符串是不可变类型,就是说改变一个字符串的元素需要新建一个新的字符串.

核心笔记:那些可以改变对象值的可变对象的方法是没有返回值的! 
Python 初学者经常会陷入一个误区:调用一个方法就返回一个值.最明显的例子就是
sort(): 
>>> music_media.sort()# 没有输出? 
>>> 
在使用可变对象的方法如 sort(),extend()和 reverse()的时候要注意,这些操作会在列表
中原地执行操作,也就是说现有的列表内容会被改变,但是没有返回值!是的,与之相反,字符串
方法确实有返回值: 
>>> 'leanna, silly girl!'.upper() 
'LEANNA, SILLY GIRL!' 
温习一下,字符串是不可变的 -- 不可变对象的方法是不能改变它们的值的,所以它们必须
返回一个新的对象.如果你确实需要返回一个对象,那么我们建议你看一下 Python2.4 以后加入
的 reversed()和 sorted()内建函数. 
它们像列表的方法一样工作,不同的是它们可以用做表达式,因为它们返回一个对象.同时
原来的那个列表还是那个列表,没有改变,而你得到的是一个新的对象.

当处理一组对象时,这个组默认是元组类型.

只有一个元素的元组需要在元组分割符里面加一个逗号(,)用以防止跟普通的分组操作符混淆.
如a = (1),a是int类型,a = (1,),a是元组.

虽然元组对象本身是不可变的,但这并不意味着元组包含的可变对象也不可变了。
所有的多对象的,逗号分隔的,没有明确用符号定义的,比如说像用方括号表示列表和用
圆括号表示元组一样,等等这些集合默认的类型都是元组.

对一个对象进行浅拷贝其实是新创建了一个类型跟原对
象一样,其内容是原来对象元素的引用,换句话说,这个拷贝的对象本身是新的,但是它的内容不
是.序列类型对象的浅拷贝是默认类型拷贝,并可以以下几种方式实施:(1)完全切片操作[:],(2)
利用工厂函数,比如 list(),dict()等,(3)使用 copy 模块的 copy 函数.

以下有几点关于拷贝操作的警告。 第一,非容器类型(比如数字,字符串和其他"原子"类型的
对象,像代码,类型和 xrange 对象等)没有被拷贝一说,浅拷贝是用完全切片操作来完成的.第二
如果元组变量只包含原子类型对象,对它的深拷贝将不会进行.如果我们把账户信息改成元组类
型,那么即便按我们的要求使用深拷贝操作也只能得到一个浅拷贝:

字典键必须是可哈希的,即每次哈希的结果都一样.
一个要说明的是问题是数字:值相等的数字表示相同的键。换句话来说,整型数字 1 和 浮点数 1.0 的哈希值是相同的,即它们
是相同的键。

当使用输入方法如 read() 或者 readlines() 从文件中读取行时, Python 并不会删除行结束
符. 这个操作被留给了程序员. 例如这样的代码在 Python 程序中很常见: 
f = open('myFile', 'r') 
data = [line.strip() for line in f.readlines()] 
f.close() 
类似地, 输出方法 write() 或 writelines() 也不会自动加入行结束符. 你应该在向文件写
入数据前自己完成:

try 语句块中异常发生点后的剩余语句永远不会到达(所以也永远不会执行). 一旦一个异常被
引发, 就必须决定控制流下一步到达的位置. 剩余代码将被忽略, 解释器将搜索处理器, 一旦找到
就开始执行处理器中的代码. 
如果没有找到合适的处理器, 那么异常就向上移交给调用者去处理, 这意味着堆栈框架立即回
到之前的那个. 如果在上层调用者也没找到对应处理器, 该异常会继续被向上移交, 直到找到合适
处理器. 如果到达最顶层仍然没有找到对应处理器, 那么就认为这个异常是未处理的,  Python 解释
器会显示出跟踪返回消息, 然后退出.

__builtins__ 模块和 __builtin__ 模块不能混淆。 虽然它们的名字相似——尤其对于新手来
说。 __builtins__ 模块包含内建名称空间中内建名字的集合。 其中大多数(如果不是全部的话)来
自 __builtin__ 模块, 该模块包含内建函数, 异常以及其他属性。 在标准 Python 执行环境下, 
__builtins__ 包含 __builtin__ 的所有名字。

加载顺序:名称空间是名称(标识符)到对象的映射。 向名称空间添加名称的操作过程涉及到绑定标识符到
指定对象的操作(以及给该对象的引用计数加 1 )。   《Python语言参考》 (Python Language Reference)
有如下的定义: 改变一个名字的绑定叫做重新绑定, 删除一个名字叫做解除绑定。

Python 解释器首先加载内建名称空间。 它由 __builtins__ 模块中的名字构成。 随后加载执
行模块的全局名称空间, 它会在模块开始执行后变为活动名称空间。 这样我们就有了两个活动的名
称空间。 如果在执行期间调用了一个函数, 那么将创建出第三个名称空间, 即局部名称空间。

那么确定作用域的规则是如何联系到名称空间的呢? 它所要做的就是名称查询. 访问一个属性
时, 解释器必须在三个名称空间中的一个找到它。 首先从局部名称空间开始, 如果没有找到, 解释
器将继续查找全局名称空间. 如果这也失败了, 它将在内建名称空间里查找。 如果最后的尝试也失
败了, 你会得到这样的错误:

Python 的一个有用的特性在于你可以在任何需要放置数据的地方获得一个名称空间。 我们已
经在前一章见到了这一特性, 你可以在任何时候给函数添加属性(使用熟悉的句点属性标识)。 

一个模块只被加载一次, 无论它被导入多少次。 这可以阻止多重导入时代码被多次执行。 例
如你的模块导入了 sys 模块, 而你要导入的其他 5 个模块也导入了它, 那么每次都加载 sys (或
是其他模块)不是明智之举! 所以, 加载只在第一次导入时发生。

因为 import 语句总是绝对导入的, 所以相对导入只应用于 from-import 语句。
语法的第一部分是一个句点, 指示一个相对的导入操作。 之后的其他附加句点代表当前 from
起始查找位置后的一个级别。

如果你不想让某个模块属性被 "from module import *" 导入 , 那么你可以给你不想导入的属
性名称加上一个下划线( _ )。 不过如果你导入了整个模块或是你显式地导入某个属性(例如 import 
foo._bar ), 这个隐藏数据的方法就不起作用了。

你有一个本身没有任何属性的类,使用它仅对数据提供一个名字空间,让你的类拥有像 Pascal 中的记录集(records)和 C
语言中的结构体(structures)一样的特性,或者换句话说,这样的类仅作为容器对象来共享名字空间。

如果需要,每个子类最好定义它自己的构造器,不然,基类的构造器会被调用。然而,如果子
类重写基类的构造器,基类的构造器就不会被自动调用了--这样,基类的构造器就必须显式写出
才会被执行.


体验 Python

下面我们先对 Python 是什么建立一个直观印象。

作为一种解释性语言,Python 很容易使用,并且能够快速验证我们的想法和开发原型软件。Python 程序可以作为一个整体进行解释,也可以一行行地解释。

可以在第一次运行 Python 时测试一下下面的 Python 代码,然后一次只输入一行试试。在 Python 启动之后,会显示一个提示符(>>>),可以在这里输入命令。注意在 Python 中,缩进非常重要,因此代码前面的空格不能忽略:

清单 1. 可以试验的几个 Python 例子

                                    

# Open a file, read each line, and print it out

for line in open('file.txt'):

  print line

# Create a file and write to it

file = open("test.txt", "w")

file.write("test line\n")

file.close()

# Create a small dictionary of names and ages and manipulate

family = {'Megan': 13, 'Elise': 8, 'Marc': 6}

# results in 8

family['Elise']

# Remove the key/value pair

del family['Elise']

# Create a list and a function that doubles its input.  Map the

#    function to each of the elements of the list (creating a new

#    list as a result).

arr = [1, 2, 3, 4, 5]

def double(x): return x*x

map(double, arr)

# Create a class, inherit by another, and then instantiate it and

#    invoke its methods.

class Simple:

  def __init__(self, name):

    self.name = name

  def hello(self):

    print self.name+" says hi."

class Simple2(Simple):

  def goodbye(self):

    print self.name+" says goodbye."

me = Simple2("Tim")

me.hello()

me.goodbye()

为什么使用 Python?

我们要学习和使用 Python 的一个原因是它非常流行。Python 用户的数量以及使用 Python 编写的应用程序的不断增长使这种努力是值得的。

在很多开发领域中都可以看到 Python 的踪迹,它被用来构建系统工具,用作程序集成的黏合剂,用来开发Internet 应用程序和快速开发原型。

Python与其他脚本语言相比也有一定的优势。它的语法非常简单,概念非常清晰,这使得 Python 非常容易学习。在使用复杂的数据结构(例如列表、词典和元组)时,Python也非常简单,而且可描述性更好。Python 还可以对语言进行扩充,也可以由其他语言进行扩充。

我发现Python 的语法使它比 Perl 的可读性和可维护性更好,但是比 Ruby 要差。与 Ruby 相比,Python的优点在于它有大量的库和模块可以使用。使用这些库和模块,只需要很少的代码就可以开发功能丰富的程序。

Python使用缩进格式来判断代码的作用域,这有些讨厌,但是Python 本身的简单性使这个问题已经微不足道了。

现在,让我们开始进入 Python 中的 socket 编程世界。

Python socket 模块

基本的 Pythonsocket 模块

Python 提供了两个基本的 socket 模块。第一个是 Socket,它提供了标准的BSD Sockets API。第二个是SocketServer,它提供了服务器中心类,可以简化网络服务器的开发。Python 使用一种异步的方式来实现这种功能,您可以提供一些插件类来处理服务器中应用程序特有的任务。表 1 列出了本节所涉及的类和模块。

表 1. Python 类和模块

类/模块

说明

Socket

低层网络接口(每个 BSD API)

SocketServer

提供简化网络服务器开发的类

让我们来看一下这些模块,以便理解它们是如何工作的。

Socket 模块

Socket模块提供了 UNIX® 程序员所熟悉的基本网络服务(也称为BSD API)。这个模块中提供了在构建 socket 服务器和客户机时所需要的所有功能。

这个API 与标准的 C API 之间的区别在于它是面向对象的。在C 中,socket 描述符是从 socket 调用中获得的,然后会作为一个参数传递给 BSD API 函数。在 Python 中,socket 方法会向应用 socket 方法的对象返回一个 socket 对象。表 2 给出了几个类方法,表 3 显示了一部分实例方法。

表 2. Socket 模块的类方法

类方法

说明

Socket

低层网络接口(每个 BSD API)

socket.socket(family, type)

创建并返回一个新的 socket 对象

socket.getfqdn(name)

将使用点号分隔的 IP 地址字符串转换成一个完整的域名

socket.gethostbyname(hostname)

将主机名解析为一个使用点号分隔的 IP 地址字符串

socket.fromfd(fd, family, type)

从现有的文件描述符创建一个 socket 对象

表 3. Socket 模块的实例方法

实例方法

说明

sock.bind( (adrs, port) )

将 socket 绑定到一个地址和端口上

sock.accept()

返回一个客户机 socket(带有客户机端的地址信息)

sock.listen(backlog)

将 socket 设置成监听模式,能够监听 backlog 外来的连接请求

sock.connect( (adrs, port) )

将 socket 连接到定义的主机和端口上

sock.recv( buflen[, flags] )

从 socket 中接收数据,最多 buflen 个字符

sock.recvfrom( buflen[, flags] )

从 socket 中接收数据,最多 buflen 个字符,同时返回数据来源的远程主机和端口号

sock.send( data[, flags] )

通过 socket 发送数据

sock.sendto( data[, flags], addr )

通过 socket 发送数据

sock.close()

关闭 socket

sock.getsockopt( lvl, optname )

获得指定 socket 选项的值

sock.setsockopt( lvl, optname, val )

设置指定 socket 选项的值

类方法实例方法 之间的区别在于,实例方法需要有一个 socket 实例(从 socket 返回)才能执行,而类方法则不需要。

SocketServer 模块

SocketServer模块是一个十分有用的模块,它可以简化 socket 服务器的开发。有关这个模块的使用的讨论已经远远超出了本教程的范围,但是我将展示一下它的基本用法,然后您可以参阅参考资料 一节中给出的链接。

考虑清单2 中给出的例子。此处,我们实现了一个简单的 “Hello World” 服务器,当客户机连接它时,它就会显示这样一条消息。我首先创建一个请求处理程序,它继承了 SocketServer.StreamRequestHandler 类。我们定义了一个名为 handle 的方法,它处理服务器的请求。服务器所做的每件事情都必须在这个函数的上下文中进行处理(最后,关闭这个 socket)。这个过程的工作方式非常简单,但是您可以使用这个类来实现一个简单的HTTP 服务器。在 handle 方法中,我们打一个招呼就退出了。

现在连接处理程序已经准备就绪了,剩下的工作是创建 socket 服务器。我们使用了 SocketServer.TCPServer 类,并提供了地址和端口号(要将服务器绑定到哪个端口上)以及请求处理方法。结果是一个 TCPServer 对象。调用 serve_forever 方法启动服务器,并使其对这个连接可用。


清单2. 用 SocketServer 模块实现一个简单的服务器

import SocketServer

class hwRequestHandler( SocketServer.StreamRequestHandler ):

  def handle( self ):

    self.wfile.write("Hello World!\n")

server = SocketServer.TCPServer( ("", 2525), hwRequestHandler )

server.serve_forever()

就是这样!Python 允许这种机制的任何变种,包括 UDPServers 以及派生进程和线程的服务器。请参阅 参考资料一节中更多信息的链接。

在所有具有 socket 的语言中,socket 都是相同的 —— 这是两个应用程序彼此进行通信的管道。

前提条件

不管是使用 Python、Perl、Ruby、Scheme 还是其他有用的语言(此处 有用 的意思是这种语言有socket 接口)来编写 socket 程序,socket 通常都是相同的。这是两个应用程序彼此进行通信的管道(这两个应用程序可以在同一台机器上,也可以位于两台不同的机器上)。

使用Python 这种具有 socket 编程功能的语言的区别在于,它有一些辅助的类和方法,可以简化 socket 编程。在本节中,我们将展示 Python 的 socket API。可以使用一个脚本来执行 Python 的解释器,如果您要自己执行 Python,就可以一次只输入一行代码。这样,就可以看到每个方法调用之后的结果了。

下面这个例子展示了如何与 Python 解释器进行交互。此处我们使用了 socket 类方法 gethostbyname 将一个完整的域名(www.ibm.com)解析成一个使用点号分隔的 IP 地址字符串('129.42.19.99'):

清单 3. 从解释器命令行中使用 socket

 [camus]$ python

Python 2.4 (#1, Feb 20 2005, 11:25:45)

[GCC 3.2.2 20030222 (Red Hat Linux 3.2.2-5)] on linux2

Type "help", "copyright", "credits" or "license" for more

   information.

>>> import socket

>>> socket.gethostbyname('www.ibm.com')

'129.42.19.99'

>>> 

在导入 socket 模块之后,我调用了 gethostbyname 类方法将这个域名解析成 IP 地址。

现在,我们要讨论基本的 socket 方法,并通过 socket 进行通信。您应该熟悉 Python 解释器。

创建和销毁 socket

要新创建一个 socket,可以使用 socket 类的 socket 方法。这是一个类方法,因为还没有得到可以应用方法的socket 对象。socket 方法与BSD API 类似,下面是创建流(TCP) socket 和数据报(UDP)socket 的方法:

清单 4. 创建流和数据报 socket

                                     

streamSock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

 

dgramSock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )

在这种情况中,会返回一个 socket 对象。AF_INET 符号(第一个参数)说明您正在请求的是一个 Internet 协议(IP)socket,具体来说是IPv4。第二个参数是传输协议的类型(SOCK_STREAM 对应TCP socket,SOCK_DGRAM 对应 UDP socket)。如果底层操作系统支持 IPv6,那么还可以指定 socket.AF_INET6 来创建一个 IPv6 socket。

要关闭一个已经连接的 socket,可以使用 close 方法:

streamSock.close()

最后,可以使用 del 语句删除一个 socket:

del streamSock

这个语句可以永久地删除 socket 对象。之后再试图引用这个对象就会产生错误。

Socket 地址

socket地址是一个组合,包括一个接口地址和一个端口号。由于Python 可以很简单地表示元组,因此地址和端口也可以这样表示。下面表示的是接口地址 192.168.1.1和端口 80:

('192.168.1.1', 80 )

也可以使用完整的域名,例如:

('www.ibm.com', 25 )

这个例子非常简单,当然比使用 C 编写相同功能的程序时对 sockaddr_in 进行操作要简单很多。下面的讨论给出了 Python 中地址的例子。

服务器 socket

服务器socket 通常会在网络上提供一个服务。由于服务器和客户机的 socket 是使用不同的方式创建的,因此我们将分别进行讨论。

在创建socket 之后,可以使用 bind 方法来绑定一个地址,listen 方法可以将其设置为监听状态,最后 accept 方法可以接收一个新的客户机连接。下面展示了这种用法:

清单 5. 使用服务器 socket

                                     

sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

sock.bind( ('', 2525) )

sock.listen( 5 )

newsock, (remhost, remport) = sock.accept()

对于这个服务器来说,使用地址 ('', 2525) 就意味着接口地址中使用了通配符 (''),这样可以接收来自这个主机上的任何接口的连接。还说明要绑定到端口 2525 上。

注意此处 accept 方法不但要返回一个新的 socket 对象,它表示了客户机连接(newsock);而且还要返回一个地址对(socket 端的远程地址和端口号)。Python 的 SocketServer 模块可以对这个过程进一步进行简化,正如上面展示的一样。

虽然也可以创建数据报服务器,不过这是无连接的,因此没有对应的accept 方法。下面的例子创建一个数据报服务器 socket:

清单 6. 创建一个数据报服务器 socket

                              

sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )

sock.bind( ('', 2525) )

后面对于socket I/O 的讨论将说明 I/O 是如何为流socket 和数据报 socket 工作的。

现在,让我们来看一下客户机是如何创建 socket 并将其连接到服务器上的。

客户机 socket

客户机socket 的创建和连接机制与服务器 socket 相似。在创建 socket 之前,都需要一个地址 —— 不是本地绑定到这个 socket 上(就像服务器 socket 的情况那样),而是标识这个 socket 应该连接到什么地方。假设在这个主机的 IP 地址 '192.168.1.1' 和端口 2525 上有一个服务器。下面的代码可以创建一个新的 socket,并将其连接到定义的服务器上:

清单 7. 创建一个流 socket 并将其连接到服务器上

sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

sock.connect( ('192.168.1.1', 2525) )

对于数据报 socket 来说,处理过程稍有不同。回想一下,数据报从本质上来说都是没有连接的。可以这样考虑:流 socket 是两个点之间的通信管道,而数据报 socket 是基于消息的,可以同时与多个点进行通信。下面是一个数据报客户机的例子。

清单 8. 创建一个数据报 socket 并将其连接到服务器上

sock = socket.socket( socket.AF_INET, sock.sock_DGRAM )

sock.connect( ('192.168.1.1', 2525) )

尽管我们使用了 connect 方法,但是此处是有区别的:在客户机和服务器之间并不存在真正的连接。此处的连接是对以后 I/O 的一个简化。通常在数据报socket 中,必须在所发送的数据中提供目标地址的信息。通过使用 connect,我们可以使用客户机对这些信息进行缓存,并且send 方法的使用可以与流 socket 情况一样(只不过不需要目标地址)。可以再次调用 connect 来重新指定数据报客户机消息的目标。

流 socket I/O

通过流socket 发送和接收数据在 Python 中是很简单的。有几个方法可以用来通过流 socket 传递数据(例如 send、recv、read 和 write)。

第一个例子展示了流 socket 的服务器和客户机。在这个例子中,服务器会回显从客户机接收到的信息。

回显流服务器如清单 9 所示。在创建一个新的流 socket 之前,需要先绑定一个地址(接收来自任何接口和 45000 端口的连接),然后调用 listen 方法来启用到达的连接。这个回显服务器然后就可以循环处理各个客户机连接了。它会调用 accept 方法并阻塞(即不会返回),直到有新的客户机连接到它为止,此时会返回新的客户机 socket,以及远程客户机的地址信息。使用这个新的客户机 socket,我们可以调用recv 来从另一端接收一个字符串,然后将这个字符串写回这个 socket。然后立即关闭这个 socket。

清单 9. 简单的 Python 流回显服务器

import socket

srvsock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

srvsock.bind( ('', 23000) )

srvsock.listen( 5 )

while 1:

  clisock, (remhost, remport) = srvsock.accept()

  str = clisock.recv(100)

  clisock.send( str )

  clisock.close()

清单10 显示了与清单 9 的回显服务器对应的客户机。在创建一个新的流程 socket 之前,需要使用 connect 方法将这个 socket 连接到服务器上。当连接之后(connect 方法返回),客户机就会使用 send 方法输出一条简单的文本消息,然后使用 recv 方法等待回显。print 语句用来显示所读取的内容。当这个过程完成之后,就执行 close 方法关闭 socket。

清单 10. 简单的 Python 流回显客户机

import socket

clisock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

clisock.connect( ('', 23000) )

clisock.send("Hello World\n")

print clisock.recv(100)

clisock.close()

数据报 socket I/O

数据报socket 天生就是无连接的,这意味着通信需要提供一个目标地址。类似,当通过一个 socket 接收消息时,必须同时返回数据源。recvfrom 和 sendto 方法可以支持其他地址,正如您在数据报回显服务器和客户机实现中可以看到的一样。

清单11 显示了数据报回显服务器的代码。首先创建一个 socket,然后使用 bind 方法绑定到一个地址上。然后进入一个无限循环来处理客户机的请求。recvfrom 方法从一个数据报 socket 接收消息,并返回这个消息以及发出消息的源地址。这些信息然后会被传入 sendto 方法,将这些消息返回到源端。

清单 11. 简单的 Python 数据报回显服务器

import socket

dgramSock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )

dgramSock.bind( ('', 23000) )

while 1:

  msg, (addr, port) = dgramSock.recvfrom( 100 )

  dgramSock.sendto( msg, (addr, port) )

数据报客户机更加简单。在创建数据报 socket 之后,我们使用 sendto 方法将一条消息发送到一个指定的地址。(记住:数据报是无连接的。)在 sendto 完成之后,我们使用 recv 来等待回显的响应,然后打印所收到的信息。注意此处我们并没有使用 recvfrom,这是因为我们对两端的地址信息并不感兴趣。

清单 12. 简单的 Python 数据报回显客户机

import socket

dgramSock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM )

dgramSock.sendto( "Hello World\n", ('', 23000) )

print dgramSock.recv( 100 )

dgramSock.close()

socket 选项

socket在缺省情况下有一些标准的行为,但是可以使用一些选项来修改socket 的行为。我们可以使用 setsockopt 方法来修改 socket 的选项,并使用 getsockopt 方法来读取 socket 选项的值。

在Python 中使用 socket 选项非常简单,正如清单13 所示。在第一个例子中,我们读取的是 socket 发送缓冲区的大小。在第二个例子中,我们获取SO_REUSEADDR 选项的值(重用 TIME_WAIT 中的地址),然后来启用它。

清单 13. 使用 socket 选项

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 )

# Get the state of the SO_REUSEADDR option

state = sock.getsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR )

# Enable the SO_REUSEADDR option

sock.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )

SO_REUSEADDR 选项通常是在 socket 服务器的开发中使用的。可以增大 socket 的发送和接收缓冲区,从而获得更好的性能,但是记住您是在一个解释脚本中进行操作的,因此可能不会带来太多益处。

异步 I/O

Python作为 select 模块的一部分提供了异步 I/O 的功能。这种特性与 C 的 select 机制类似,但是更加简单。我们首先对 select 进行简介,然后解释如何在 Python 中使用。

select 方法允许对多个 socket 产生多个事件或多个不同的事件。例如,您可以告诉 select 当 socket 上有数据可用时、当可以通过一个 socket 写入数据时以及在 socket 上发生错误时,都要通知您;可以同时为多个 socket 执行这些操作。

在 C 使用位图的地方,Python 使用列表来表示要监视的描述符,并且返回那些满足约束条件的描述符。在下面的例子中,等待从标准输入设备上输入信息:

清单 14. 等待 stdin 的输入

rlist, wlist, elist = select.select( [sys.stdin], [], [] )

print sys.stdin.read()

传递给 select 的参数是几个列表,分别表示读事件、写事件和错误事件。select 方法返回三个列表,其中包含满足条件的对象(读、写和异常)。在这个例子中,返回的rlist 应该是 [sys.stdin],说明数据在 stdin 上可用了。然后就可以使用 read 方法来读取这些数据。

select 方法也可以处理 socket 描述符。在下面的例子(请参阅清单 15)中,我们创建了两个客户机 socket,并将其连接到一个远程端上。然后使用 select 方法来确定哪个 socket 可以读取数据了。接着可以读取这些数据,并将其显示到 stdout 上。

清单 15. 展示处理多个 socket 的select 方法

import socket

import select

sock1 = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

sock2 = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

sock1.connect( ('192.168.1.1', 25) )

sock2.connect( ('192.168.1.1', 25) )

while 1:

  # Await a read event

  rlist, wlist, elist = select.select( [sock1, sock2], [], [], 5 )

  # Test for timeout

  if [rlist, wlist, elist] == [ [], [], [] ]:

    print "Five seconds elapsed.\n"

  else:

    # Loop through each socket in rlist, read and print the available data

    for sock in rlist:

      print sock.recv( 100 )

构建一个 Python 聊天服务器

一个简单的聊天服务器

现在您已经了解了 Python 中基本的网络 API;接下来可以在一个简单的应用程序中应用这些知识了。在本节中,将构建一个简单的聊天服务器。使用 Telnet,客户机可以连接到 Python 聊天服务器上,并在全球范围内相互进行通信。提交到聊天服务器的消息可以由其他人进行查看(以及一些管理信息,例如客户机加入或离开聊天服务器)。这个模型如图 1 所示。

图 1. 聊天服务器使用 select 方法来支持任意多个客户机

聊天服务器的一个重要需求是必须可以伸缩。服务器必须能够支持任意个流(TCP)客户机。

要支持任意个客户机,可以使用 select 方法来异步地管理客户机的列表。不过也可以使用服务器 socket 的 select 特性。select 的读事件决定了一个客户机何时有可读数据,而且它也可以用来判断何时有一个新客户机要连接服务器 socket 了。可以利用这种行为来简化服务器的开发。

接下来,我们将展示聊天服务器的 Python 源代码,并说明 Python 怎样帮助简化这种实现。

ChatServer 类

让我们首先了解一下 Python 聊天服务器类和 __init__ 方法 —— 这是在创建新实例时需要调用的构造函数。

这个类由4 个方法组成。run 方法用来启动服务器,并且允许客户机的连接。broadcast_string 和 accept_new_connection 方法在类内部使用,我们稍后就会讨论。

__init__ 方法是一个特殊的方法,它们会在创建一个类的新实例时调用。注意所有的方法都使用一个self 参数,这是对这个类实例本身的引用(与 C++ 中的 this 参数非常类似)。这个self 参数是所有实例方法的一部分,此处用来访问实例变量。

__init__ 方法创建了 3 个实例变量。port 是服务器的端口号(传递给构造函数)。srvsock 是这个实例的 socket 对象,descriptors 是一个列表,包含了这个类中的每个socket 对象。可以在 select 方法中使用这个列表来确定读事件的列表。

最后,清单 16 给出了 __init__ 方法的代码。在创建一个流 socket 之后,就可以启用 SO_REUSEADDR socket 选项了;这样如果需要,服务器就可以快速重新启动了。通配符地址被绑定到预先定义好的端口号上。然后调用 listen 方法,从而允许到达的连接接入。服务器 socket 被加入到 descriptors 列表中(现在只有一个元素),但是所有的客户机 socket 都可以在到达时被加入到这个列表中(请参阅 accept_new_connection)。此时会在 stdout 上打印一条消息,说明这个服务器已经被启动了。

清单 16. ChatServer 类的 init 方法

import socket

import select

class ChatServer:

  def __init__( self, port ):

    self.port = port;

    self.srvsock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )

    self.srvsock.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )

    self.srvsock.bind( ("", port) )

    self.srvsock.listen( 5 )

    self.descriptors = [self.srvsock]

    print 'ChatServer started on port %s' % port

  def run( self ):

    ...

  def broadcast_string( self, str, omit_sock ):

    ...

  def accept_new_connection( self ):

    ...

run 方法

run 方法对于聊天服务器来说是一个循环(请参阅清单 17)。在调用时,它还会进入一个无限循环,并在连接的客户机之间进行通信。

服务器的核心是 select 方法。我将 descriptor 列表(其中包含了所有服务器的 socket)作为读事件的列表传递给 select (写事件和异常事件列表都为空)。当检测到读事件时,它会作为 sread 返回。(我们忽略了 swrite 和 sexc 列表。)sread 列表包含要服务的 socket 对象。我们循环遍历这个sread 列表,检查每个找到的 socket 对象。

在这个循环中首先检查 socket 对象是否是服务器。如果是,就说明一个新的客户机正在试图连接,这就要调用accept_new_connection 方法。否则,就读取客户机的 socket。如果 recv 返回 NULL,那就关闭 socket。

在这种情况中,我们构建了一条消息,并将其发送给所有已经连接的客户机,然后关闭 socket,并从 descriptor 列表中删除对应的对象。如果recv 返回值不是 NULL,那么就说明已经有消息可用了,它被存储在 str 中。这条消息会使用 broadcast_string 发送给其他所有的客户机。


清单17. 聊天服务器的 run 方法是这个聊天服务器的核心

def run( self ):

  while 1:

    # Await an event on a readable socket descriptor

    (sread, swrite, sexc) = select.select( self.descriptors, [], [] )

    # Iterate through the tagged read descriptors

    for sock in sread:

      # Received a connect to the server (listening) socket

      if sock == self.srvsock:

        self.accept_new_connection()

      else:

        # Received something on a client socket

        str = sock.recv(100)

        # Check to see if the peer socket closed

        if str == '':

          host,port = sock.getpeername()

          str = 'Client left %s:%s\r\n' % (host, port)

          self.broadcast_string( str, sock )

          sock.close

          self.descriptors.remove(sock)

        else:

          host,port = sock.getpeername()

          newstr = '[%s:%s] %s' % (host, port, str)

          self.broadcast_string( newstr, sock )

辅助方法

在这个聊天服务器中有两个辅助方法,提供了接收新客户机连接和将消息广播到已连接的客户机上的功能。

当在到达连接队列中检测到一个新的客户机时,就会调用accept_new_connection 方法(请参阅清单 18)。accept 方法用来接收这个连接,它会返回一个新的socket 对象,以及远程地址信息。我们会立即将这个新的 socket 加入到 descriptors 列表中,然后向这个新的客户机输出一条消息欢迎它加入聊天。我创建了一个字符串来表示这个客户机已经连接了,使用broadcast_string 方法来成组地广播这条消息(请参阅清单 19)。

注意,除了要广播的字符串之外,还要传递一个 socket 对象。原因是我们希望有选择地忽略一些 socket,从而只接收特定的消息。例如,当一个客户机向一个组中发送一条消息时,这条消息应该发送给这个组中除了自己之外的所有人。当我们生成状态消息来说明有一个新的客户机正在加入该组时,这条消息也不应该发送给这个新客户机,而是应该发送给其他所有人。这种任务是在broadcast_string 中使用 omit_sock 参数实现的。这个方法会遍历 descriptors 列表,并将这个字符串发送给那些不是服务器 socket 且不是 omit_sock 的 socket。

清单 18. 在聊天服务器上接收一个新客户机连接

def accept_new_connection( self ):

  newsock, (remhost, remport) = self.srvsock.accept()

  self.descriptors.append( newsock )

  newsock.send("You're connected to the Python chatserver\r\n")

  str = 'Client joined %s:%s\r\n' % (remhost, remport)

  self.broadcast_string( str, newsock )

清单 19. 将一条消息在聊天组中广播

def broadcast_string( self, str, omit_sock ):

  for sock in self.descriptors:

    if sock != self.srvsock and sock != omit_sock:

      sock.send(str)

  print str,

实例化一个新的 ChatServer

现在您已经看到了 Python 聊天服务器(这只使用了不到 50 行的代码),现在让我们看一下如何在 Python 中实例化一个新的聊天服务器。

我们通过创建一个新的 ChatServer 对象来启动一个服务器(传递要使用的端口号),然后调用run 方法来启动服务器并允许接收所有到达的连接:

清单 20. 实例化一个新的聊天服务器

myServer = ChatServer( 2626 )

myServer.run()

现在,这个服务器已经在运行了,您可以从一个或多个客户机连接到这个服务器上。也可以将几个方法串接在一起来简化这个过程(如果需要简化的话):

清单 21. 串接几个方法

myServer = ChatServer( 2626 ).run()

 

这可以实现相同的结果。下面我们将展示 ChatServer 类的用法。

 

展示 ChatServer

下面就是 ChatServer 的用法。我们将展示 ChatServer 的输出结果(请参阅清单 22 )以及两个客户机之间的对话(请参阅清单 23 和 清单 24)。用户输入的文本以黑体形式表示。

清单 22. ChatServer 的输出

 [plato]$ python pchatsrvr.py

ChatServer started on port 2626

Client joined 127.0.0.1:37993

Client joined 127.0.0.1:37994

[127.0.0.1:37994] Hello, is anyone there?

[127.0.0.1:37993] Yes, I'm here.

[127.0.0.1:37993]  Client left 127.0.0.1:37993

清单 23. 聊天客户机 #1 的输出

 [plato]$ telnet localhost 2626

Trying 127.0.0.1...

Connected to localhost.localdomain (127.0.0.1).

Escape character is '^]'.

You're connected to the Python chatserver

Client joined 127.0.0.1:37994

[127.0.0.1:37994] Hello, is anyone there?

Yes, I'm here.

                                      ^]

telnet> close

Connection closed.

[plato]$

清单 24. 聊天客户机 #2 的输出

 [plato]$ telnet localhost 2626

Trying 127.0.0.1...

Connected to localhost.localdomain (127.0.0.1).

Escape character is '^]'.

You're connected to the Python chatserver

Hello, is anyone there?

[127.0.0.1:37993] Yes, I'm here.

[127.0.0.1:37993] Client left 127.0.0.1:37993

正如您在清单 22 中看到的那样,所有客户机之间的对话都会显示到 stdout 上,包括客户机的连接和断开消息。

高级网络类

网络模块

Python包括几个专门用于应用层协议的模块(它们是在标准的 socket模块上构建的)。可用模块有很多,提供了超文本传输协议(HTTP)、简单邮件传输协议(SMTP)、Internet 消息访问协议(IMAP)、邮局协议(POP3)、网络新闻传输协议(NNTP)、XML-PRC(远程过程调用)、FTP 以及很多其他的协议。

本节将展示表 4 中列出的模块的用法。

表 4. 有用的应用层协议模块

模块

所实现的协议

httplib

HTTP 客户机

smtplib

SMTP 客户机

poplib

POP3 客户机

httplib (HTTP 客户机)

HTTP 客户机接口在开发 Web 机器人或其他流 socket 时非常有用。Web 协议本质上是通过流 socket 进行请求/响应的。Python通过一个简单的 Web 接口来简化构建 Web 机器人的过程。

清单25 展示了 httplib 模块的用法。使用HTTPConnection 创建了一个 HTTP 的实例,这里需要提供想要连接的 Web 站点。使用这个新对象(httpconn),可以使用request 方法来请求文件。在 request 方法中,可以指定 HTTP GET 方法(从服务器上请求一个文件,而HEAD 只简单地获取有关这个文件的信息)。getresponse 方法会对HTTP 响应头进行解析,从而了解是否碰到了错误。如果成功地接收到了这个文件,那么这个新响应对象的 read 方法就返回并打印一条文本信息。

清单 25. 使用 httplib 构建一个简单的 HTTP 客户机

import httplib

httpconn = httplib.HTTPConnection("www-130.ibm.com")

httpconn.request("GET", "/developerworks/index.html")

resp = httpconn.getresponse()

if resp.reason == "OK":

  resp_data = resp.read()

  print resp_data

httpconn.close()

smptlib(SMTP 客户机)

SMTP 让您可以发送 e-mail 消息到一台邮件服务器上,这对于在网络系统中传递有关设备操作的状态非常有用。发送 e-mail 的 Python 模块非常简单,包括创建一个 SMTP 对象,使用 sendmail 方法发送一条 e-mail 消息,然后使用 quit 方法关闭这个连接。

清单26 中的例子展示了发送一个简单 e-mail 消息的方法。msg 字符串中包含了邮件的主体(它应该包含主题行)。

清单 26. 使用 smtplib 发送一条简短的 e-mail 消息

import smtplib

fromAdrs = 'mtj@mtjones.com'

toAdrs = 'you@mail.com'

msg = 'From: me@mail.com\r\nTo: you@mail.com\r\nSubject:Hello\r\nHi!\r\n'

mailClient = smtplib.SMTP('192.168.1.1')

mailClient.sendmail( fromAdrs, toAdrs, msg )

mailClient.quit                             

poplib(POP3 客户机)

POP3 是另外一个非常有用的应用层协议,在 Python 中也有一个这种模块。POP3 协议让您可以连接到一个邮件服务器上,并下载新的邮件,这对于远程命令来说非常有用—— 可以在 e-mail 消息的正文中嵌入命令。在执行嵌入的命令之后,可以使用 smptlib 向源返回一条 e-mail 消息。

清单27 展示了一个简单的应用程序,它连接到一台邮件服务器上,并为这个用户打印所有未读的 e-mail 的主题行。

poplib 相当简单,但是它为在服务器上搜集和管理 e-mail 提供了几种方法。在这个例子中,我们使用 POP3 方法创建了一个新的 POP3 对象,这个方法要指定邮件服务器。user 和 pass_ 方法将向服务器验证应用程序;stat 方法返回等待用户阅读的邮件的数目,以及所有消息的总字节数。

接下来,我们遍历每条可用消息,并使用 retr 方法来提取下一条 e-mail 消息。这个方法会返回这样一个列表:

(response,['line, ...], octets)

其中 response 是对某条消息的 POP3 响应,line list 表示 e-mail 消息的行号,最后一个元素octets 是这条 e-mail 消息的字节数。内部循环简单地遍历 e-mail 消息正文列表的第二个元素([1])。对于每一行,我们测试是否存在 'Subject:' ;如果存在,就打印这一行的内容。

在检查完所有的 e-mail 消息之后,就可以调用 quit 方法结束 POP3 会话。

如果不使用 retr 方法,还应该可以使用 top 方法提取 e-mail 消息的头信息。这个步骤可能速度更快,可以将需要传输到客户机的数据量降低至最少。

清单 27. 从 POP3 邮件服务器上接收e-mail 消息,并打印主题行的信息

import poplib

import re

popClient = poplib.POP3('192.168.1.1')

popClient.user('user')

popClient.pass_('password')

numMsgs, mboxSize = popClient.stat()

print "Number of messages ", numMsgs

print "Mailbox size", mboxSize

print

for id in range (numMsgs):

  for mail in popClient.retr(id+1)[1]:

    if re.search( 'Subject:', mail ):

      print mail

  print

popClient.quit()

 

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值