在所有具有 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
|
在导入 socket
模块之后,我调用了 gethostbyname
类方法将这个域名解析成 IP 地址。
现在,我们要讨论基本的 socket
方法,并通过 socket 进行通信。您应该熟悉 Python 解释器。
|
要新创建一个 socket,可以使用 socket
类的 socket
方法。这是一个类方法,因为还没有得到可以应用方法的 socket
对象。socket
方法与 BSD API 类似,下面是创建流(TCP) socket 和数据报(UDP)socket 的方法:
清单 4. 创建流和数据报 socket
|
在这种情况中,会返回一个 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 地址是一个组合,包括一个接口地址和一个端口号。由于 Python 可以很简单地表示元组,因此地址和端口也可以这样表示。下面表示的是接口地址 192.168.1.1 和端口 80:
( '192.168.1.1', 80 )
也可以使用完整的域名,例如:
( 'www.ibm.com', 25 )
这个例子非常简单,当然比使用 C 编写相同功能的程序时对 sockaddr_in
进行操作要简单很多。下面的讨论给出了 Python 中地址的例子。
|
服务器 socket 通常会在网络上提供一个服务。由于服务器和客户机的 socket 是使用不同的方式创建的,因此我们将分别进行讨论。
在创建 socket 之后,可以使用 bind
方法来绑定一个地址,listen
方法可以将其设置为监听状态,最后 accept
方法可以接收一个新的客户机连接。下面展示了这种用法:
清单 5. 使用服务器 socket
|
对于这个服务器来说,使用地址 ('', 2525)
就意味着接口地址中使用了通配符 ('')
,这样可以接收来自这个主机上的任何接口的连接。还说明要绑定到端口 2525 上。
注意此处 accept
方法不但要返回一个新的 socket
对象,它表示了客户机连接(newsock
);而且还要返回一个地址对(socket 端的远程地址和端口号)。Python 的 SocketServer
模块可以对这个过程进一步进行简化,正如上面展示的一样。
虽然也可以创建数据报服务器,不过这是无连接的,因此没有对应的 accept
方法。下面的例子创建一个数据报服务器 socket:
清单 6. 创建一个数据报服务器 socket
|
后面对于 socket I/O 的讨论将说明 I/O 是如何为流 socket 和数据报 socket 工作的。
现在,让我们来看一下客户机是如何创建 socket 并将其连接到服务器上的。
|
客户机 socket 的创建和连接机制与服务器 socket 相似。在创建 socket 之前,都需要一个地址 —— 不是本地绑定到这个 socket 上(就像服务器 socket 的情况那样),而是标识这个 socket 应该连接到什么地方。假设在这个主机的 IP 地址 '192.168.1.1' 和端口 2525 上有一个服务器。下面的代码可以创建一个新的 socket,并将其连接到定义的服务器上:
清单 7. 创建一个流 socket 并将其连接到服务器上
|
对于数据报 socket 来说,处理过程稍有不同。回想一下,数据报从本质上来说都是没有连接的。可以这样考虑:流 socket 是两个点之间的通信管道,而数据报 socket 是基于消息的,可以同时与多个点进行通信。下面是一个数据报客户机的例子。
清单 8. 创建一个数据报 socket 并将其连接到服务器上
|
尽管我们使用了 connect
方法,但是此处是有区别的:在客户机和服务器之间并不存在真正的 连接。此处的连接是对以后 I/O 的一个简化。通常在数据报 socket 中,必须在所发送的数据中提供目标地址的信息。通过使用 connect
,我们可以使用客户机对这些信息进行缓存,并且 send
方法的使用可以与流 socket 情况一样(只不过不需要目标地址)。可以再次调用 connect
来重新指定数据报客户机消息的目标。
|
通过流 socket 发送和接收数据在 Python 中是很简单的。有几个方法可以用来通过流 socket 传递数据(例如 send
、recv
、read
和 write
)。
第一个例子展示了流 socket 的服务器和客户机。在这个例子中,服务器会回显从客户机接收到的信息。
回显流服务器如清单 9 所示。在创建一个新的流 socket 之前,需要先绑定一个地址(接收来自任何接口和 45000 端口的连接),然后调用 listen
方法来启用到达的连接。这个回显服务器然后就可以循环处理各个客户机连接了。它会调用 accept
方法并阻塞(即不会返回),直到有新的客户机连接到它为止,此时会返回新的客户机 socket,以及远程客户机的地址信息。使用这个新的客户机 socket,我们可以调用 recv
来从另一端接收一个字符串,然后将这个字符串写回这个 socket。然后立即关闭这个 socket。
清单 9. 简单的 Python 流回显服务器
|
清单 10 显示了与清单 9 的回显服务器对应的客户机。在创建一个新的流程 socket 之前,需要使用 connect
方法将这个 socket 连接到服务器上。当连接之后(connect
方法返回),客户机就会使用 send
方法输出一条简单的文本消息,然后使用 recv
方法等待回显。print
语句用来显示所读取的内容。当这个过程完成之后,就执行 close
方法关闭 socket。
清单 10. 简单的 Python 流回显客户机
|
|
数据报 socket 天生就是无连接的,这意味着通信需要提供一个目标地址。类似,当通过一个 socket 接收消息时,必须同时返回数据源。recvfrom
和 sendto
方法可以支持其他地址,正如您在数据报回显服务器和客户机实现中可以看到的一样。
清单 11 显示了数据报回显服务器的代码。首先创建一个 socket,然后使用 bind
方法绑定到一个地址上。然后进入一个无限循环来处理客户机的请求。recvfrom
方法从一个数据报 socket 接收消息,并返回这个消息以及发出消息的源地址。这些信息然后会被传入 sendto
方法,将这些消息返回到源端。
清单 11. 简单的 Python 数据报回显服务器
|
数据报客户机更加简单。在创建数据报 socket 之后,我们使用 sendto
方法将一条消息发送到一个指定的地址。(记住:数据报是无连接的。)在 sendto
完成之后,我们使用 recv
来等待回显的响应,然后打印所收到的信息。注意此处我们并没有使用 recvfrom
,这是因为我们对两端的地址信息并不感兴趣。
清单 12. 简单的 Python 数据报回显客户机
|
|
socket 在缺省情况下有一些标准的行为,但是可以使用一些选项来修改 socket 的行为。我们可以使用 setsockopt
方法来修改 socket 的选项,并使用 getsockopt
方法来读取 socket 选项的值。
在 Python 中使用 socket 选项非常简单,正如清单 13 所示。在第一个例子中,我们读取的是 socket 发送缓冲区的大小。在第二个例子中,我们获取 SO_REUSEADDR
选项的值(重用 TIME_WAIT
中的地址),然后来启用它。
清单 13. 使用 socket 选项
|
SO_REUSEADDR
选项通常是在 socket 服务器的开发中使用的。可以增大 socket 的发送和接收缓冲区,从而获得更好的性能,但是记住您是在一个解释脚本中进行操作的,因此可能不会带来太多益处。
|
Python 作为 select
模块的一部分提供了异步 I/O 的功能。这种特性与 C 的 select 机制类似,但是更加简单。我们首先对 select
进行简介,然后解释如何在 Python 中使用。
select
方法允许对多个 socket 产生多个事件或多个不同的事件。例如,您可以告诉 select
当 socket 上有数据可用时、当可以通过一个 socket 写入数据时以及在 socket 上发生错误时,都要通知您;可以同时为多个 socket 执行这些操作。
在 C 使用位图的地方,Python 使用列表来表示要监视的描述符,并且返回那些满足约束条件的描述符。在下面的例子中,等待从标准输入设备上输入信息:
清单 14. 等待 stdin 的输入
|
传递给 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 )