第1章套接字、IPv4和简单的客户端/服务器编程
本章攻略:
打印设备名和IPv4地址
获取远程设备的IP地址
将IPv4地址转换成不同的格式
通过指定的端口和协议找到服务名
主机字节序和网络字节序之间相互转换
设定并获取默认的套接字超时时间
优雅地处理套接字错误
修改套接字发送和接收的缓冲区大小
把套接字改成阻塞或非阻塞模式
重用套接字地址
从网络时间服务器上获取并打印当前时间
编写一个SNTP客户端
编写一个简单的回显客户端/服务器应用
1.1简介
本章通过一些简单的攻略介绍Python的核心网络库。Python的socket模块提供了类方法和实例方法,二者的区别在于使用类方法时不需要创建套接字对象实例。这是一种很直观的方法。例如,打印设备的IP地址不需要创建套接字对象,而只需调用套接字的类方法。但是,如果要把数据发送给服务器程序,那么创建一个套接字对象来处理具体的操作则更加自然。本章介绍的攻略可以分成如下三类:
前几个攻略使用类方法获取关于主机、网络以及目标服务的有用信息;
随后的几个攻略使用实例方法,演示了常用的套接字操作,例如处理套接字超时、缓冲区大小和阻塞模式等;
. 最后,结合使用类方法和实例方法开发客户端,执行一些实际的任务,例如使设备时间与网络服务器同步,编写通用的客户端/服务器脚本。
你可以使用本章演示的方法编写自己的客户端/服务器应用。
1.2打印设备名和IPv4地址
有时,你需要快速查看设备的某些信息,例如主机名、IP地址和网络接口的数量等。这些信息使用Python脚本很容易获取。
1.2.1准备工作
编写代码之前先要在设备上安装Python。大多数Linux发行版都预装了Python。如果使用微软Windows操作系统,可以从Python的网站上下载二进制文件:http://www.python.org/download/。
要了解系统是否已经安装了Python,可以查阅操作系统的文档。在设备上安装好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. )))
1.2.2实战演练
这个攻略很简短,可以直接写在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()。
我们要在常用的__main__代码块中调用这个函数。运行时,Python会为某些内部变量赋值, 例如__name__。在这里,__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
在你的设备上,输出的内容根据系统的主机配置会有所不同。
1.2.3原理分析
import socket语句导入Python提供的一个核心网络库。然后调用两个工具函数:gethostname()和gethostbyname(host_name)。在命令行中可以输入help(socket.geth- ostname)查看帮助信息,或者在浏览器中访问http://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地址。
1.3获取远程设备的IP地址
有时需要把设备的主机名转换成对应的IP地址,例如快速查询域名。本攻略介绍一个简单的函数来完成这一操作。
1.3.1实战演练
如果想知道远程设备的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
1.3.2原理分析
这个攻略把gethostbyname()方法包装在用户定义的get_remote_machine_info()函数中,还引入了异常处理的概念。如上述代码所示,我们把主要的函数调用放在try-except块中,这就意味着,如果执行函数gethostbyname()的过程中发生了错误,这个错误将由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地址。
1.4将IPv4地址转换成不同的格式
如果要使用低层网络函数,有时普通的字符串形式的IP地址并不是很有用,需要把它们转换成打包后的32位二进制格式。
1.4.1实战演练
Python的socket库提供了很多用来处理不同IP地址格式的函数,这里我们使用其中的两个:inet_aton()和inet_ntoa()。
我们来定义convert_ip4_address()函数,调用inet_aton()和inet_ntoa()转换IP地址。我们要使用两个示例IP地址:127.0.0.1和192.168.0.1。
代码清单1-3展示了如何定义convert_ip4_address()函数,如下所示:
#!/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
1.4.2原理分析
在这个攻略中,使用for-in语句把两个字符串形式的IP地址转换成打包后的32位二进制格式,而且还调用了binascii模块中的hexlify函数,以十六进制形式表示二进制数据。
1.5通过指定的端口和协议找到服务名
如果想找到网络服务,最好知道该服务运行在TCP或UDP协议的哪个端口上。
1.5.1准备工作
如果知道网络服务使用的端口,可以调用socket库中的getservbyport()函数来获取服务的名字。调用这个函数时可以根据情况决定是否提供协议名。
1.5.2实战演练
我们来定义find_service_name()函数,在Python的for-in循环中调用函数getservbyport(),解析几个端口,例如80和25。
代码清单1-4展示了如何定义find_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
1.5.3原理分析
在这个攻略中,使用for-in语句遍历一组变量。在每次遍历中,获取端口对应的服务名。
1.6主机字节序和网络字节序之间相互转换
编写低层网络应用时,或许需要处理通过电缆在两台设备之间传送的低层数据。在这种操作中,需要把主机操作系统发出的数据转换成网络格式,或者做逆向转换,因为这两种数据的表示方式不一样。
1.6.1实战演练
Python的socket库提供了将数据在网络字节序和主机字节序之间相互转换的函数。你可能想了解这些函数,例如ntohl()和htonl()。
我们来定义convert_integer()函数,调用ntohl()和htonl()类函数来转换不同格式的数据。
代码清单1-5展示了如何定义convert_integer()函数,如下所示:
#!/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 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
1.6.2原理分析
在这个攻略中,我们以整数为例,演示了如何把它转换成网络字节序和主机字节序。socket库中的类函数ntohl()把网络字节序转换成了长整形主机字节序。函数名中的n表示网络;h表示主机;l表示长整形;s表示短整形,即16位。
1.7设定并获取默认的套接字超时时间
有时,你需要处理socket库某些属性的默认值,例如套接字超时时间。
1.7.1实战演练
你可以创建一个套接字对象实例,调用gettimeout()方法获取默认的超时时间,调用settimeout()方法设定一个超时时间。这种操作在开发服务器应用时很有用。
在test_socket_timeout()函数中,首先创建一个套接字对象,然后使用读取或者设定实例方法处理超时时间。
代码清单1-6展示了如何定义test_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()
运行上述代码后,你会看到它是如何修改默认超时时间的,如下所示:
$ python 1_6_socket_timeout.py
Default socket timeout: None
Current socket timeout: 100.0
1.7.2原理分析
在这段代码片段中,首先创建了一个套接字对象。套接字构造方法的第一个参数是地址族,第二个参数是套接字类型。然后,调用gettimeout()方法获取套接字超时时间,再调用settimeout()方法修改超时时间。传给settimeout()方法的参数可以是秒数(非负浮点数)也可以是None。这个方法在处理阻塞式套接字操作时使用。如果把超时时间设为None,则禁用了套接字操作的超时检测。
1.8优雅地处理套接字错误
在网络应用中,经常会遇到这种情况:一方尝试连接,但另一方由于网络媒介失效或者其他原因无法响应。Python的socket库提供了一个方法,能通过socket.error异常优雅地处理套接字错误。在这个攻略中会举几个例子。
1.8.1实战演练
我们来编写几个try-except代码块,每个块对应一种可能发生的错误。为了获取用户输入,可以使用argparse模块。这个模块的功能很强大,而不仅是可以使用sys.argv解析命令行参数。这些try-except代码块分别演示了常见的套接字操作,例如创建套接字对象、连接服务器、发送数据和等待应答。
下述攻略使用几行代码演示了如何处理异常。
代码清单1-7展示了如何处理socket.error异常,如下所示:
#!/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()
1.8.2原理分析
在Python中,可以使用argparse模块把命令行参数传入脚本以及在脚本中解析命令行参数。这个模块在Python 2.7中可用。如果使用较旧版本的Python,这个模块可以到“Python包索引”(Python Package Index,简称PyPI)中获取,使用easy_install或pip安装。
这个攻略用到了三个命令行参数:主机名、端口号和文件名。上述脚本的使用方法如下:
$ python 1_7_socket_errors.py –host=
如果提供的主机不存在,这个脚本会输出如下错误:
$ 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
)
unknown domain: ))
这个攻略用到了四个try-except块。除第二个块处理socket.gaierror异常之外,其他块都处理socket.error异常。socket.gaierror是地址相关的错误。除此之外还有两种异常:socket.herror,C API中抛出的异常;如果在套接字中使用settimeout()方法,套接字超时后会抛出socket.timeout异常。
1.9修改套接字发送和接收的缓冲区大小
很多情况下,默认的套接字缓冲区大小可能不够用。此时,可以将默认的套接字缓冲区大小改成一个更合适的值。
1.9.1实战演练
我们要使用套接字对象的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
1.9.2原理分析
在套接字对象上可调用方法getsockopt()和setsockopt()分别获取和修改套接字对象的属性。setsockopt()方法接收三个参数:level、optname和value。其中,optname是选项名,value是该选项的值。第一个参数所用的符号常量(SO_*等)可在socket模块中查看。
1.10把套接字改成阻塞或非阻塞模式
默认情况下,TCP套接字处于阻塞模式中。也就是说,除非完成了某项操作,否则不会把控制权交还给程序。例如,调用connect() API后,连接操作会阻止程序继续往下执行,直到连接成功为止。很多情况下,你并不想让程序一直等待服务器响应或者有异常终止操作。例如,如果编写了一个网页浏览器客户端连接服务器,你应该考虑提供取消功能,以便在操作过程中取消连接。这时就要把套接字设置为非阻塞模式。
1.10.1实战演练
我们来看一下在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)
1.10.2原理分析
在这个攻略中,我们把1传给setblocking()方法,启用套接字的阻塞模式。类似地,可以把0传给这个方法,把套接字设为非阻塞模式。
这个功能在后面的一些攻略中会用到,到时再详细说明其真正作用。
1.11重用套接字地址
不管连接是被有意还是无意关闭,有时你想始终在同一个端口上运行套接字服务器。某些情况下,如果客户端程序需要一直连接指定的服务器端口,这么做就很有用,因为无需改变服务器端口。