第 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后应该显示解释器提示符>>>,具体的输出如下所示:

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

1.2.2 实战演练

这个攻略很简短,可以直接写在Python解释器中。

首先,使用下面的命令导入Python中的socket库:

  1. >>> import socket

然后,调用socket库提供的gethostname()方法,把结果保存在一个变量中,如下所示:

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

这些操作可以使用内置的类方法,定义成一个独立的函数print_machine_info()

我们要在常用的__main__代码块中调用这个函数。运行时,Python会为某些内部变量赋值, 例如__name__。在这里,__name__表示调用程序的进程名。如果在命令行中运行脚本(如后面的命令所示),__name__的值是__main__。但是,如果在其他脚本中导入,情况就不同了。也就是说,如果在命令行中调用这个模块,会自动运行print_machine_info()函数;如果在其他脚本中导入,用户就要手动调用这个函数。

代码清单1-1展示了如何获取设备的信息,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter -1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications.
  5. import socket
  6. def print_machine_info():
  7. host_name = socket.gethostname()
  8. ip_address = socket.gethostbyname(host_name)
  9. print "Host name: %s" % host_name
  10. print "IP address: %s" % ip_address
  11. if __name__ == '__main__':
  12. print_machine_info()

若想运行这个脚本,要在命令行中指定源码文件,如下所示:

  1. $ python 1_1_local_machine_info.py

在我的设备上,显示了如下输出:

  1. Host name: debian6
  2. 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。在命令行中查看这两个函数的帮助信息,得到的输出如下:

  1. gethostname(...)
  2. gethostname() -> string
  3. Return the current host name.
  4. gethostbyname(...)
  5. gethostbyname(host) -> address
  6. 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地址,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter – 1
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. import socket
  6. def get_remote_machine_info():
  7. remote_host = 'www.python.org'
  8. try:
  9. print "IP address: %s" %socket.gethostbyname(remote_host)
  10. except socket.error, err_msg:
  11. print "%s: %s" %(remote_host, err_msg)
  12. if __name__ == '__main__':
  13. get_remote_machine_info()

运行上述代码会得到以下输出:

  1. $ python 1_2_remote_machine_info.py
  2. 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,然后执行下述命令:

  1. $ python 1_2_remote_machine_info.py
  2. 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.1192.168.0.1

代码清单1-3展示了如何定义convert_ip4_address()函数,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter – 1
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. import socket
  6. from binascii import hexlify
  7. def convert_ip4_address():
  8. for ip_addr in ['127.0.0.1', '192.168.0.1']:
  9. packed_ip_addr = socket.inet_aton(ip_addr)
  10. unpacked_ip_addr = socket.inet_ntoa(packed_ip_addr)
  11. print "IP Address: %s => Packed: %s, Unpacked: %s"\
  12. %(ip_addr, hexlify(packed_ip_addr), unpacked_ip_addr)
  13. if __name__ == '__main__':
  14. convert_ip4_address()

现在,运行这个攻略,会看到以下输出:

  1. $ python 1_3_ip4_address_conversion.py
  2. IP Address: 127.0.0.1 => Packed: 7f000001, Unpacked: 127.0.0.1
  3. 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(),解析几个端口,例如8025

代码清单1-4展示了如何定义find_service_name()函数,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 1
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. import socket
  6. def find_service_name():
  7. protocolname = 'tcp'
  8. for port in [80, 25]:
  9. print "Port: %s => service name: %s" %(port, socket.getservbyport(port, protocolname))
  10. print "Port: %s => service name: %s" %(53, socket.getservbyport(53, 'udp'))
  11. if __name__ == '__main__':
  12. find_service_name()

运行这个脚本,会看到如下输出:

  1. $ python 1_4_finding_service_name.py
  2. Port: 80 => service name: http
  3. Port: 25 => service name: smtp
  4. 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()函数,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter -
  3. # This program is optimized for Python 2.7.
  4. # It may run on any other version with/without modifications.
  5. import socket
  6. def convert_integer():
  7. data = 1234
  8. # 32-bit
  9. print "Original: %s => Long host byte order: %s, Network byte order: %s"\
  10. %(data, socket.ntohl(data), socket.htonl(data))
  11. # 16-bit
  12. print "Original: %s => Short host byte order: %s, Network byte order: %s"\
  13. %(data, socket.ntohs(data), socket.htons(data))
  14. if __name__ == '__main__':
  15. convert_integer()

运行这个攻略,会看到以下输出:

  1. $ python 1_5_integer_conversion.py
  2. Original: 1234 => Long host byte order: 3523477504, Network byte order: 3523477504
  3. 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()函数,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications
  5. import socket
  6. def test_socket_timeout():
  7. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  8. print "Default socket timeout: %s" %s.gettimeout()
  9. s.settimeout(100)
  10. print "Current socket timeout: %s" %s.gettimeout()
  11. if __name__ == '__main__':
  12. test_socket_timeout()

运行上述代码后,你会看到它是如何修改默认超时时间的,如下所示:

  1. $ python 1_6_socket_timeout.py
  2. Default socket timeout: None
  3. 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异常,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter – 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications.
  5. import sys
  6. import socket
  7. import argparse
  8. def main():
  9. # setup argument parsing
  10. parser = argparse.ArgumentParser(description='Socket Error Examples')
  11. parser.add_argument('--host', action="store", dest="host", required=False)
  12. parser.add_argument('--port', action="store", dest="port", type=int, required=False)
  13. parser.add_argument('--file', action="store", dest="file", required=False)
  14. given_args = parser.parse_args()
  15. host = given_args.host
  16. port = given_args.port
  17. filename = given_args.file
  18. # First try-except block -- create socket
  19. try:
  20. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  21. except socket.error, e:
  22. print "Error creating socket: %s" % e
  23. sys.exit(1)
  24. # Second try-except block -- connect to given host/port
  25. try:
  26. s.connect((host, port))
  27. except socket.gaierror, e:
  28. print "Address-related error connecting to server: %s" % e
  29. sys.exit(1)
  30. except socket.error, e:
  31. print "Connection error: %s" % e
  32. sys.exit(1)
  33. # Third try-except block -- sending data
  34. try:
  35. s.sendall("GET %s HTTP/1.0\r\n\r\n" % filename)
  36. except socket.error, e:
  37. print "Error sending data: %s" % e
  38. sys.exit(1)
  39. while 1:
  40. # Fourth tr-except block -- waiting to receive data from remote host
  41. try:
  42. buf = s.recv(2048)
  43. except socket.error, e:
  44. print "Error receiving data: %s" % e
  45. sys.exit(1)
  46. if not len(buf):
  47. break
  48. # write the received data
  49. sys.stdout.write(buf)
  50. if __name__ == '__main__':
  51. main()

1.8.2 原理分析

在Python中,可以使用argparse模块把命令行参数传入脚本以及在脚本中解析命令行参数。这个模块在Python 2.7中可用。如果使用较旧版本的Python,这个模块可以到“Python包索引”(Python Package Index,简称PyPI)中获取,使用easy_installpip安装。

这个攻略用到了三个命令行参数:主机名、端口号和文件名。上述脚本的使用方法如下:

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

如果提供的主机不存在,这个脚本会输出如下错误:

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

如果某个端口上没有服务,你却尝试连接到这个端口,则这个脚本会抛出连接超时异常,如下所示:

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

这个命令会返回如下错误,因为主机www.python.org监听的不是端口8080

  1. Connection error: [Errno 110] Connection timed out

不过,如果向正确的主机、正确的端口发起随意的请求,应用层可能无法捕获这一异常。例如,运行下述脚本,不会返回错误,但输出的HTML代码说明了脚本的问题:

  1. $ python 1_7_socket_errors.py --host=www.python.org --port=80 --file=1_7_socket_errors.py
  2. HTTP/1.1 404 Not found
  3. Server: Varnish
  4. Retry-After: 0
  5. content-type: text/html
  6. Content-Length: 77
  7. Accept-Ranges: bytes
  8. Date: Thu, 20 Feb 2014 12:14:01 GMT
  9. Via: 1.1 varnish
  10. Age: 0
  11. Connection: close
  12. <html>
  13. <head>
  14. <title> </title>
  15. </head>
  16. <body>
  17. 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_SIZERECV_BUF_SIZE。然后在一个函数中调用套接字实例的setsockopt()方法。修改之前,最好先检查缓冲区大小是多少。注意,发送和接收的缓冲区大小要分开设定。

代码清单1-8展示了如何修改套接字的发送和接收缓冲区大小,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter – 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications
  5. import socket
  6. SEND_BUF_SIZE = 4096
  7. RECV_BUF_SIZE = 4096
  8. def modify_buff_size():
  9. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM )
  10. # Get the size of the socket's send buffer
  11. bufsize = sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
  12. print "Buffer size [Before]:%d" %bufsize
  13. sock.setsockopt(socket.SOL_TCP, socket.TCP_NODELAY, 1)
  14. sock.setsockopt(
  15. socket.SOL_SOCKET,
  16. socket.SO_SNDBUF,
  17. SEND_BUF_SIZE)
  18. sock.setsockopt(
  19. socket.SOL_SOCKET,
  20. socket.SO_RCVBUF,
  21. RECV_BUF_SIZE)
  22. bufsize = sock.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF)
  23. print "Buffer size [After]:%d" %bufsize
  24. if __name__ == '__main__':
  25. modify_buff_size()

运行上述脚本后,会显示修改套接字缓冲区大小前后的变化。根据你所用操作系统的本地设定,得到的输出可能有所不同:

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

1.9.2 原理分析

在套接字对象上可调用方法getsockopt()setsockopt()分别获取和修改套接字对象的属性。setsockopt()方法接收三个参数:leveloptnamevalue。其中,optname是选项名,value是该选项的值。第一个参数所用的符号常量(SO_*等)可在socket模块中查看。

1.10 把套接字改成阻塞或非阻塞模式

默认情况下,TCP套接字处于阻塞模式中。也就是说,除非完成了某项操作,否则不会把控制权交还给程序。例如,调用connect() API后,连接操作会阻止程序继续往下执行,直到连接成功为止。很多情况下,你并不想让程序一直等待服务器响应或者有异常终止操作。例如,如果编写了一个网页浏览器客户端连接服务器,你应该考虑提供取消功能,以便在操作过程中取消连接。这时就要把套接字设置为非阻塞模式。

1.10.1 实战演练

我们来看一下在Python中有哪些选项。在Python中,套接字可以被设置为阻塞模式或者非阻塞模式。在非阻塞模式中,调用API后,例如send()recv()方法,如果遇到问题就会抛出异常。但在阻塞模式中,遇到错误并不会阻止操作。我们可以创建一个普通的TCP套接字,分别在阻塞模式和非阻塞模式中执行操作实验。

为了能在阻塞模式中处理套接字,首先要创建一个套接字对象。然后,调用setblocking(1)把套接字设为阻塞模式,或者调用setblocking(0)把套接字设为非阻塞模式。最后,把套接字绑定到指定的端口上,监听进入的连接。

代码清单1-9展示了如何把套接字设为阻塞模式或非阻塞模式,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications
  5. import socket
  6. def test_socket_modes():
  7. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  8. s.setblocking(1)
  9. s.settimeout(0.5)
  10. s.bind(("127.0.0.1", 0))
  11. socket_address = s.getsockname()
  12. print "Trivial Server launched on socket: %s" %str(socket_address)
  13. while(1):
  14. s.listen(1)
  15. if __name__ == '__main__':
  16. test_socket_modes()

运行这个攻略后,会启动一个简易服务器,开启阻塞模式,如下述命令所示:

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

1.10.2 原理分析

在这个攻略中,我们把1传给setblocking()方法,启用套接字的阻塞模式。类似地,可以把0传给这个方法,把套接字设为非阻塞模式。

这个功能在后面的一些攻略中会用到,到时再详细说明其真正作用。

1.11 重用套接字地址

不管连接是被有意还是无意关闭,有时你想始终在同一个端口上运行套接字服务器。某些情况下,如果客户端程序需要一直连接指定的服务器端口,这么做就很有用,因为无需改变服务器端口。

1.11.1 实战演练

如果在某个端口上运行一个Python套接字服务器,连接一次之后便终止运行,就不能再使用这个端口了。如果再次连接,程序会抛出如下错误:

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

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

创建套接字对象之后,我们可以查询地址重用的状态,比如说旧状态。然后,调用setsockopt()方法,修改地址重用状态的值。再按照常规的步骤,把套接字绑定到一个地址上,监听进入的客户端连接。在这个例子中,我们要捕获KeyboardInterrupt异常,这样按下Ctrl+C键后,Python脚本会终止运行,但不会显示任何异常消息。

代码清单1-10展示了如何重用套接字地址,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications
  5. import socket
  6. import sys
  7. def reuse_socket_addr():
  8. sock = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
  9. # Get the old state of the SO_REUSEADDR option
  10. old_state = sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR)
  11. print "Old sock state: %s" %old_state
  12. # Enable the SO_REUSEADDR option
  13. sock.setsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 )
  14. new_state = sock.getsockopt( socket.SOL_SOCKET, socket.SO_REUSEADDR )
  15. print "New sock state: %s" %new_state
  16. local_port = 8282
  17. srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  18. srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  19. srv.bind( ('', local_port) )
  20. srv.listen(1)
  21. print ("Listening on port: %s " %local_port)
  22. while True:
  23. try:
  24. connection, addr = srv.accept()
  25. print 'Connected by %s:%s' % (addr[0], addr[1])
  26. except KeyboardInterrupt:
  27. break
  28. except socket.error, msg:
  29. print '%s' % (msg,)
  30. if __name__ == '__main__':
  31. reuse_socket_addr()

这个攻略的输出如下所示:

  1. $ python 1_10_reuse_socket_address.py
  2. Old sock state: 0
  3. New sock state: 1
  4. 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,可以使用pipeasy_installPyPI中安装,命令如下:

  1. pip install ntplib

1.12.2 实战演练

我们先要创建一个NTPClient实例,然后在这个实例上调用request()方法,把NTP服务器的地址传入方法。

代码清单1-11展示了如何从网络时间服务器上获取当前时间并打印出来,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications
  5. import ntplib
  6. from time import ctime
  7. def print_time():
  8. ntp_client = ntplib.NTPClient()
  9. response = ntp_client.request('pool.ntp.org')
  10. print ctime(response.tx_time)
  11. if __name__ == '__main__':
  12. print_time()

在我的设备上,运行这个攻略后得到的输出如下:

  1. $ python 1_11_print_machine_time.py
  2. 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_SERVERTIME1970NTP_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客户端,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter - 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications
  5. import socket
  6. import struct
  7. import sys
  8. import time
  9. NTP_SERVER = "0.uk.pool.ntp.org"
  10. TIME1970 = 2208988800L
  11. def sntp_client():
  12. client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  13. data = '\x1b' + 47 * '\0'
  14. client.sendto(data, (NTP_SERVER, 123))
  15. data, address = client.recvfrom( 1024 )
  16. if data:
  17. print 'Response received from:', address
  18. t = struct.unpack( '!12I', data )[10]
  19. t -= TIME1970
  20. print '\tTime=%s' % time.ctime(t)
  21. if __name__ == '__main__':
  22. sntp_client()

这个攻略通过SNTP协议从网络时间服务器上获取当前时间并打印出来,如下所示:

  1. $ python 1_12_sntp_client.py
  2. Response received from: ('87.117.251.2', 123)
  3. 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展示了如何编写回显应用的服务器,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter – 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications.
  5. import socket
  6. import sys
  7. import argparse
  8. host = 'localhost'
  9. data_payload = 2048
  10. backlog = 5
  11. def echo_server(port):
  12. """ A simple echo server """
  13. # Create a TCP socket
  14. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  15. # Enable reuse address/port
  16. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  17. # Bind the socket to the port
  18. server_address = (host, port)
  19. print "Starting up echo server on %s port %s" % server_address
  20. sock.bind(server_address)
  21. # Listen to clients, backlog argument specifies the max no. of queued connections
  22. sock.listen(backlog)
  23. while True:
  24. print "Waiting to receive message from client"
  25. client, address = sock.accept()
  26. data = client.recv(data_payload)
  27. if data:
  28. print "Data: %s" %data
  29. client.send(data)
  30. print "sent %s bytes back to %s" % (data, address)
  31. # end connection
  32. client.close()
  33. if __name__ == '__main__':
  34. parser = argparse.ArgumentParser(description='Socket Server Example')
  35. parser.add_argument('--port', action="store", dest="port", type=int, required=True)
  36. given_args = parser.parse_args()
  37. port = given_args.port
  38. echo_server(port)

在客户端代码中,我们要创建一个客户端套接字,然后使用命令行参数中指定的端口连接服务器。客户端把消息Test message. This will be echoed发送给服务器之后,立即就会在几个数据片段中收到返回的消息。这里用到了两个try-except块,捕获交互过程中发生的任何异常。

代码清单1-13b展示了如何编写回显程序的客户端,如下所示:

  1. #!usrbin/env python
  2. # Python Network Programming Cookbook -- Chapter – 1
  3. # This program is optimized for Python 2.7. It may run on any
  4. # other Python version with/without modifications.
  5. import socket
  6. import sys
  7. import argparse
  8. host = 'localhost'
  9. def echo_client(port):
  10. """ A simple echo client """
  11. # Create a TCP/IP socket
  12. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  13. # Connect the socket to the server
  14. server_address = (host, port)
  15. print "Connecting to %s port %s" % server_address
  16. sock.connect(server_address)
  17. # Send data
  18. try:
  19. # Send data
  20. message = "Test message. This will be echoed"
  21. print "Sending %s" % message
  22. sock.sendall(message)
  23. # Look for the response
  24. amount_received = 0
  25. amount_expected = len(message)
  26. while amount_received < amount_expected:
  27. data = sock.recv(16)
  28. amount_received += len(data)
  29. print "Received: %s" % data
  30. except socket.errno, e:
  31. print "Socket error: %s" %str(e)
  32. except Exception, e:
  33. print "Other exception: %s" %str(e)
  34. finally:
  35. print "Closing connection to the server"
  36. sock.close()
  37. if __name__ == '__main__':
  38. parser = argparse.ArgumentParser(description='Socket Server Example')
  39. parser.add_argument('--port', action="store", dest="port", type=int, required=True)
  40. given_args = parser.parse_args()
  41. port = given_args.port
  42. echo_client(port)

1.14.2 原理分析

为了查看客户端和服务器之间的交互,要在一个终端里启动如下服务器脚本:

  1. $ python 1_13a_echo_server.py --port=9900
  2. Starting up echo server on localhost port 9900
  3. Waiting to receive message from client

然后,在另一个终端里运行客户端,如下所示:

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

连接到本地主机后,服务器还会输出以下消息:

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