第 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展示了如何获取设备的信息,如下所示:
#!usrbin/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.gethostname)
查看帮助信息,或者在浏览器中访问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地址,如下所示:
#!usrbin/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()
函数,如下所示:
#!usrbin/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()
函数,如下所示:
#!usrbin/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()
函数,如下所示:
#!usrbin/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
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()
函数,如下所示:
#!usrbin/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
异常,如下所示:
#!usrbin/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=<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.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展示了如何修改套接字的发送和接收缓冲区大小,如下所示:
#!usrbin/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展示了如何把套接字设为阻塞模式或非阻塞模式,如下所示:
#!usrbin/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 重用套接字地址
不管连接是被有意还是无意关闭,有时你想始终在同一个端口上运行套接字服务器。某些情况下,如果客户端程序需要一直连接指定的服务器端口,这么做就很有用,因为无需改变服务器端口。
1.11.1 实战演练
如果在某个端口上运行一个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展示了如何重用套接字地址,如下所示:
#!usrbin/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_addr()
这个攻略的输出如下所示:
$ python 1_10_reuse_socket_address.py
Old sock state: 0
New sock state: 1
Listening on port: 8282
1.11.2 原理分析
你可以在一个终端窗口运行这个脚本,然后在另一个终端窗口中输入telnet localhost 8282
,尝试连接这个服务器。关闭服务器程序后,还可以使用同一个端口再次连接。然而,如果你把设定SO_REUSEADDR
的那行代码注释掉,服务器将不会再次运行脚本。
1.12 从网络时间服务器获取并打印当前时间
很多程序要求设备的时间精准,例如Unix系统中的make
命令。设备上的时间可能不够准确,需要和网络中的时间服务器同步。
1.12.1 准备工作
你可以编写一个Python客户端,让设备上的时间和某个网络时间服务器同步。要完成这一操作,需要使用ntplib
,通过“网络时间协议”(Network Time Protocol,简称NTP)处理客户端和服务器之间的通信。如果你的设备中没有安装ntplib
,可以使用pip
或easy_install
从PyPI
中安装,命令如下:
pip install ntplib
1.12.2 实战演练
我们先要创建一个NTPClient
实例,然后在这个实例上调用request()
方法,把NTP服务器的地址传入方法。
代码清单1-11展示了如何从网络时间服务器上获取当前时间并打印出来,如下所示:
#!usrbin/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
1.12.3 原理分析
在这个攻略中,我们编写了一个NTP客户端,向NTP服务器pool.ntp.org
发起了一个NTP请求。响应使用ctime()
函数打印出来。
1.13 编写一个SNTP客户端
与前一个攻略不同,有时并不需要从NTP服务器上获取精确的时间。遇到这种情况,就可以使用NTP的简化版本,叫作“简单网络时间协议”。
1.13.1 实战演练
让我们不使用任何第三方库编写一个简单的SNTP客户端。
首先,定义两个常量:NTP_SERVER
和TIME1970
。NTP_SERVER
是客户端要连接的服务器地址,TIME1970
指1970年1月1日(也叫Epoch)。在http://www.epochconverter.com/上可以查看Epoch时间值,或者把时间转换成Epoch时间值。这个客户端通过UDP协议创建一个UDP套接字(SOCK_DGRAM
),用于连接服务器。然后,客户端要在一个数据包中把数据'\x1b' + 47 * '\0'
发给SNTP服务器。UDP客户端分别使用sendto()
和recvfrom()
方法发送和接收数据。
服务器返回的时间信息打包在一个数组中,客户端需要使用struct
模块取出数据。我们所需的数据是数组中的第11个元素。最后,我们要从取出的数据上减掉TIME1970
,得到真正的当前时间。
代码清单1-12展示了如何编写这个SNTP客户端,如下所示:
#!usrbin/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
1.13.2 原理分析
这个SNTP客户端创建一个套接字连接,然后通过协议发送数据。从NTP服务器(这里使用的是0.uk.pool.ntp.org
)收到数据后,使用struct
模块取出数据。最后,减去1970年1月1日对应的时间戳,再使用Python内置的time
模块提供的ctime()
方法打印时间。
1.14 编写一个简单的回显客户端/服务器应用
尝试过Python中socket
模块的基本API后,现在我们来编写一个套接字服务器和客户端。这里,你将有机会利用在前述攻略中掌握的基本知识。
1.14.1 实战演练
在这个例子中,不管服务器从客户端收到什么输入,都会将其回显出来。我们要使用Python中的argparse
模块,在命令行中指定TCP端口。服务器脚本和客户端脚本都要用到这个参数。
我们先来编写服务器。首先创建一个TCP套接字对象。然后设定启用重用地址,这样想运行多少次服务器就能运行多少次。我们把套接字绑定在本地设备的指定端口上。在监听阶段,把backlog
参数传入listen()
方法中,让服务器在队列中监听多个客户端。最后,等待客户端连接,向服务器发送一些数据。收到数据后,服务器会把数据回显给客户端。
代码清单1-13a展示了如何编写回显应用的服务器,如下所示:
#!usrbin/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展示了如何编写回显程序的客户端,如下所示:
#!usrbin/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)
1.14.2 原理分析
为了查看客户端和服务器之间的交互,要在一个终端里启动如下服务器脚本:
$ 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
连接到本地主机后,服务器还会输出以下消息:
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