本节本节将简要介绍使用套接字进行网络编程的知识。然而,在深入研究之前,将介绍一些
有关网络编程的背景信息,以及套接字如何应用于 Python 之中,然后展示如何使用 Python
的一些模块来创建网络应用程序。
**服务器:**服务器就是一系列硬件或软件,为一个或多个客户端(服务的用户)提供所需的“服务”。它存在唯一
目的就是等待客户端的请求,并响应它们(提供服务),然后等待更多请求。
**客户端:**客户端因特定的请求而联系服务器,并发送必要的数据,然后等待服务器的
回应,最后完成请求或给出故障的原因。
服务器无限地运行下去,并不断地处理请求;而客户端会对服务进行一次性请求,然后接收该服务,最后结束它们之间的事务。客户端在一段时间后可能会再次发出其他请求,但这些都被当作不同的事务。
图 2-1 因特网上客户端/服务器系统的典型概念图
**打印(打印机)**服务器是硬件服务器的一个例子。它们处理传入的打印作业并将其发送给系统中的打印机(或其他的打印设备)。这样的计算机通常可以通过网络进行访问,并且客户端计算机将向它发送打印请求。
硬件服务器的另一个例子就是文件服务器。这些通常都是拥有庞大通用存储容量的计算机,可以被客户端远程访问。客户端计算机会挂载服务器计算机上的磁盘,看起来好像这个磁盘就在本地计算机上一样。支持文件服务器的一个最流行的网络操作系统就是 Sun 公司的网络文件系统(NFS)。如果你正在访问一个网络磁盘驱动器,并且无法分辨它是在本地还是网络上,那么此时客户端/服务器系统就已经完成了它的任务。它的目标就是让用户得到与访问本地磁盘完全相同的体验,抽象起来就是正常的磁盘访问,而这些都是通过编程实现来确保以这种方式进行。
软件服务器也运行在一块硬件之上,但是没有像硬件服务器那样的专用外围设备(如打印机、磁盘驱动器等)。软件服务器提供的主要服务包括程序执行、数据传输检索、聚合、更新,或其他类型的编程或数据操作。现在一个更常见的软件服务器就是 Web 服务器。如果个人或公司想要运行自己的 Web 服务器,那么必须拥有一台或多台计算机,在上面安装希望提供给用户的 Web 页面和 Web 应用程序,然后启动 Web 服务器。一个这样的服务器的工作就是接受客户端请求,并向(Web)客户端(即用户计算机上的浏览器)回送 Web 页面,然后等待下一个客户端的请求。这些服务器一旦开启,都将可能永远运行。虽然它们并不能实现这一目标,但是它们会尽可能长时间地运行,除非受到一些外力驱使才会停止,如显式地关闭,或灾难性地关闭(由于硬件故障)。
数据库服务器是另一种类型的软件服务器。它们接受客户端的存储或检索请求,响应请求,然后等待更多的事务。与 Web 服务器类似,它们也是永远运行的。
我们将讨论的最后一类软件服务器就是窗体(window)服务器,几乎可以认为这些服务器是硬件服务器。它们运行在一台附带(外接)显示设备(如显示器)的计算机上。窗体客户端其实就是一些程序,这些程序需要一个窗口化的环境来运行。这些通常被当作图形用户界面(GUI)应用程序。如果在没有窗体服务器的情况下执行它们,也即意味着在一个基于文本的环境中,如 DOS 窗口或一个 UNIX shell 中,那么将无法启动它们。一旦能够访问窗体服务器,那么一切都会正常。
在网络领域,这种环境会变得更加有趣。窗体客户端通常的显示设备就是本地计算机上的服务器,但是在一些网络化的窗体环境(如 X Window 系统)中,也可以选择另一台计算机的窗体服务器作为一个显示设备。在这种情况下,你就可以在一台计算机上运行一个 GUI程序,而将它显示在另一台计算机上!
通信端点在服务器响应客户端请求之前,必须进行一些初步的设置流程来为之后的工作做准备。首先会创建一个通信端点,它能够使服务器监听请求。可以把服务器比作公司前台,或者应答公司主线呼叫的总机接线员。一旦电话号码和设备安装成功且接线员到达时,服务就可以开始了。
这个过程与网络世界一样,一旦一个通信端点已经建立,监听服务器就可以进入无限循环中,等待客户端的连接并响应它们的请求。当然,为了使公司电话接待员一直处于忙碌状态,我们绝不能忘记将电话号码放在公司信笺、广告或一些新闻稿上;否则,将没有人会打电话过来!
相似地,必须让潜在的客户知道存在这样的服务器来处理他们的需求;否则,服务器将永远不会得到任何请求。想象着创建一个全新的网站,这可能是最了不起的、劲爆的、令人惊异的、有用的并且最酷的网站,但如果该网站的 Web 地址或 URL 从来没有以任何方式广播或进行广告宣传,那么永远也不会有人知道它,并且也将永远不会看到任何访问者。
现在你已经非常了解了服务器是如何工作的,这就已经解决了较困难的部分。客户端比服务器端更简单,客户端所需要做的只是创建它的单一通信端点,然后建立一个到服务器的连接。然后,客户端就可以发出请求,该请求包括任何必要的数据交换。一旦请求被服务器处理,且客户端收到结果或某种确认信息,此次通信就会被终止。
套接字是计算机网络数据结构,它体现了上节中所描述的“通信端点”的概念。在任何类型的通信开始之前,网络应用程序必须创建套接字。可以将它们比作电话插孔,没有它将无法进行通信。
套接字最初是为同一主机上的应用程序所创建,使得主机上运行的一个程序(又名一个进程)与另一个运行的程序进行通信。这就是所谓的进程间通信(Inter Process Communication,IPC)。有两种类型的套接字:基于文件的和面向网络的。
**AF_UNIX:**UNIX 套接字是我们所讲的套接字的第一个家族,并且拥有一个“家族名字”AF_UNIX(又名 AF_LOCAL,在 POSIX1.g 标准中指定),它代表地址家族(address family):UNIX。包括 Python 在内的大多数受欢迎的平台都使用术语地址家族及其缩写 AF;其他比较旧的系统可能会将地址家族表示成域(domain)或协议家族(protocol family),并使用其缩写 PF 而 非 AF。类似地,AF_LOCAL(在 2000~2001 年标准化)将代替 AF_UNIX。然而,考虑到后向兼容性,很多系统都同时使用二者,只是对同一个常数使用不同的别名。Python 本身仍然在使用 AF_UNIX。
因为两个进程运行在同一台计算机上,所以这些套接字都是基于文件的,这意味着文件系统支持它们的底层基础结构。这是能够说得通的,因为文件系统是一个运行在同一主机上的多个进程之间的共享常量。
**AF_INET:**第二种类型的套接字是基于网络的,它也有自己的家族名字 AF_INET,或者地址家族:因特网。另一个地址家族 AF_INET6 用于第 6 版因特网协议(IPv6)寻址。此外,还有其他的地址家族,这些要么是专业的、过时的、很少使用的,要么是仍未实现的。在所有的地址家族之中,目前 AF_INET 是使用得最广泛的。
**AF_NETLINK:**Python 2.5 中引入了对特殊类型的 Linux 套接字的支持。套接字的 AF_NETLINK 家族(无连接[见 2.3.3 节])允许使用标准的 BSD 套接字接口进行用户级别和内核级别代码之间的 IPC。之前那种解决方案比较麻烦,而这个解决方案可以看作一种比前一种更加优雅且风险更低的解决方案,例如,添加新系统调用、/proc 支持,或者对一个操作系统的“IOCTL”。
**AF_TIPC :**针对 Linux 的另一种特性(Python 2.6 中新增)就是支持透明的进程间通信(TIPC)协议。TIPC 允许计算机集群之中的机器相互通信,而无须使用基于 IP 的寻址方式。Python 对TIPC 的支持以 AF_TIPC 家族的方式呈现。:
如果一个套接字像一个电话插孔——允许通信的一些基础设施,那么主机名和端口号就像区号和电话号码的组合。然而,拥有硬件和通信的能力本身并没有任何好处,除非你知道电话打给谁以及如何拨打电话。一个网络地址由主机名和端口号对组成,而这是网络通信所需要的。此外,并未事先说明必须有其他人在另一端接听;否则,你将听到这个熟悉的声音“对不起,您所拨打的电话是空号,请核对后再拨”。你可能已经在浏览网页的过程中见过一个网络类比,例如“无法连接服务器,服务器没有响应或者服务器不可达。”有效的端口号范围为 0~65535(尽管小于 1024 的端口号预留给了系统)。如果你正在使用 POSIX 兼容系统(如 Linux、Mac OS X 等),那么可以在/etc/services 文件中找到预留端口号的列表(以及服务器/协议和套接字类型)。众所周知的端口号列表可以在这个网站中查看:http://www.iana.org/assignments/port-numbers。
不管你采用的是哪种地址家族,都有两种不同风格的套接字连接。第一种是面向连接的,这意味着在进行通信之前必须先建立一个连接,例如,使用电话系统给一个朋友打电话。这种类型的通信也称为虚拟电路或流套接字。
面向连接的通信提供序列化的、可靠的和不重复的数据交付,而没有记录边界。这基本上意味着每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。
实现这种连接类型的主要协议是传输控制协议(更为人熟知的是它的缩写 TCP)。为 了创建 TCP 套接字,必须使用 SOCK_STREAM 作为套接字类型。TCP 套接字的名字SOCK_STREAM 基于流套接字的其中一种表示。因为这些套接字(AF_INET)的网络版本使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合这两种协议(TCP 和 IP)来进行(当然也可以使用 TCP 和本地[非网络AF_LOCAL,AF_UNIX]套接字,但是很明显此时并没有使用 IP)。
与虚拟电路形成鲜明对比的是数据报类型的套接字,它是一种无连接的套接字。这意味着,在通信开始之前并不需要建立连接。此时,在数据传输过程中并无法保证它的顺序性、可靠性或重复性。然而,数据报确实保存了记录边界,这就意味着消息是以整体发送的,而并非首先分成多个片段,例如,使用面向连接的协议。
使用数据报的消息传输可以比作邮政服务。信件和包裹或许并不能以发送顺序到达。事实上,它们可能不会到达。为了将其添加到并发通信中,在网络中甚至有可能存在重复的消息。既然有这么多副作用,为什么还使用数据报呢(使用流套接字肯定有一些优势)?由于面向连接的套接字所提供的保证,因此它们的设置以及对虚拟电路连接的维护需要大量的开销。然而,数据报不需要这些开销,即它的成本更加“低廉”。因此,它们通常能提供更好的性能,并且可能适合一些类型的应用程序。
实现这种连接类型的主要协议是用户数据报协议(更为人熟知的是其缩写 UDP)。为 了创建 UDP 套接字,必须使用 SOCK_DGRAM 作为套接字类型。你可能知道,UDP 套接字的SOCK_DGRAM 名字来自于单词“datagram”(数据报)。因为这些套接字也使用因特网协议来寻找网络中的主机,所以这个系统也有一个更加普通的名字,即这两种协议(UDP 和 IP)的组合名字,或 UDP/IP。
既然你知道了所有关于客户端/服务器架构、套接字和网络方面的基础知识,接下来就让我们试着将这些概念应用到 Python 中。本节中将使用的主要模块就是 socket 模块,在这个模块中可以找到 socket()函数,该函数用于创建套接字对象。套接字也有自己的方法集,这些方法可以实现基于套接字的网络通信。
要创建套接字,必须使用 socket.socket()函数,它一般的语法如下。
socket(socket_family,socket_type,protocol=0)
其中,socket_family 是 AF_UNIX 或 AF_INET(如前所述),socket_type 是 SOCK_STREAM或 SOCK_DGRAM(也如前所述)。protocol 通常省略,默认为 0。
所以,为了创建 TCP/IP 套接字,可以用下面的方式调用 socket.socket()。
from socket import socket, SOCK_STREAM, AF_INET
# 因为socket木块属性多,可以使用下面方式导入模块
# from socket import *
#创建tcp套接字
tcpsocket = socket(AF_INET,SOCK_STREAM)
同样,为了创建 UDP/IP 套接字,需要执行以下语句。
from socket import socket, SOCK_STREAM, AF_INET
# 因为socket木块属性多,可以使用下面方式导入模块
# from socket import *
#创建udp套接字
udpsocket = socket(AF_INET,SOCK_DGRAM)
因为有很多 socket 模块属性,所以此时使用“from module import *”这种导入方式可以接受,不过这只是其中的一个例外。如果使用“from socket import *”,那么我们就把 socket属性引入到了命名空间中。虽然这看起来有些麻烦,但是通过这种方式将能够大大缩短代码。
一旦有了一个套接字对象,那么使用套接字对象的方法将可以进行进一步的交互。
下面列出了最常见的套接字方法。在下一节中,我们将使用其中的一些方法创建 TCP和 UDP 客户端与服务器。虽然我们专注于网络套接字,但这些方法与使用本地/不联网的套接字时有类似的含义。
服务器套接字方法
名 称 | 描 述 |
---|---|
s.bind() | 将地址(主机名、端口号对)绑定到套接字上 |
s.listen() | 设置并启动TCP监听器 |
s.accept() | 被动接受TCP客户端连接,一只等待直到连接到达(阻塞函数) |
客户端套接字方法
名 称 | 描 述 |
---|---|
s.connect() | 主动发起TCP服务器连接 |
s.conect_ex() | connect()的拓展版本,此时会以错误的形式返回问题代替抛出一个异常 |
普通的套接字方法
名 称 | 描 述 |
---|---|
s.recv() | 接受TCP消息 |
s.recv_into() | 接受TCP消息到指定的缓冲区 |
s.send() | 发送TCP消息 |
s.sendall() | 完整的发送TCP消息 |
s.recvfrom() | 接受UDP消息 |
s.recvfrom_into() | 接受UDP消息到指定的缓冲区 |
s.sendto() | 发送UDP消息 |
s.getpeername() | 连接到套接字(TCP)的远程地址 |
s.getsocketname() | 当前套接字的地址 |
s.getsockopt() | 返回给定套接字选项的值 |
s.setsockopt() | 设置给定套接字选项的值 |
s.shutdown() | 关闭连接 |
s.close() | 关闭套接字 |
s.detach() | 在未关闭文件描述符的情况下关闭套接字,返回文件描述符 |
s.ioctl | 控制套件字的模式(仅支持Windows) |
面向阻塞的套接字方法
名 称 | 描 述 |
---|---|
s.setblocking() | 设置套接字的阻塞或非阻塞模式 |
s.settimeout() | 设置阻塞套接字操作的超市时间 |
s.gettimeout() | 获取阻塞套接字操作的超市时间 |
面向文件的套接字方法
名 称 | 描 述 |
---|---|
s.fileno() | 套接字的文件描述符 |
s.makefile() | 创建与套接字关联的文件对象 |
数据属性
名 称 | 描 述 |
---|---|
s.family | 套接字家族 |
s.type | 套接字类型 |
s.proto | 套接字协议 |
首先,我们将展现创建通用 TCP 服务器的一般伪代码,然后对这些代码的含义进行一般性的描述。
server_socket = socket() #创建服务器套接字
server_socket.bind() #套接字与地址绑定
server_socket.listen() #监听连接
while True: #服务器无限循环
client_socket = server_socket.accept() #接受客户端连接
while True: #通信循环
client_socket.recv()/client_socket.send() #对话(接受消息/发送消息)
client_close() #关闭客户端套接字
server_socket.close() #关闭服务器套接字(可选),一般不会关闭服务器套接字,在实现智能的退出方案时使用
所有套接字都是通过使用 socket.socket()函数来创建的。因为服务器需要占用一个端口并等待客户端的请求,所以它们必须绑定到一个本地地址。因为 TCP 是一种面向连接的通信系统,所以在 TCP 服务器开始操作之前,必须安装一些基础设施。特别地,TCP 服务器必须监听(传入)的连接。一旦这个安装过程完成后,服务器就可以开始它的无限循环。
调用 accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接。默认情况下,accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达。另外,套接字确实也支持非阻塞模式,可以参考文档或操作系统教材,以了解有关为什么以及如何使用非阻塞套接字的更多细节。
一旦服务器接受了一个连接,就会返回(利用 accept())一个独立的客户端套接字,用来与即将到来的消息进行交换。使用新的客户端套接字类似于将客户的电话切换给客服代表。当一个客户电话最后接进来时,主要的总机接线员会接到这个电话,并使用另一条线路将这个电话转接给合适的人来处理客户的需求。
这将能够空出主线(原始服务器套接字),以便接线员可以继续等待新的电话(客户请求),而此时客户及其连接的客服代表能够进行他们自己的谈话。同样地,当一个传入的请求到达时,服务器会创建一个新的通信端口来直接与客户端进行通信,再次空出主要的端口,以使其能够接受新的客户端连接。
一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接收的对话中,直到连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接。
在代码中,一个客户端连接关闭之后,服务器就会等待另一个客户端连接。最后一行代码是可选的,在这里关闭了服务器套接字。其实,这种情况永远也不会碰到,因为服务器应该在一个无限循环中运行。在示例中这行代码用来提醒读者,当为服务器实现一个智能的退出方案时,建议调用 close()方法。例如,当一个处理程序检测到一些外部条件时,服务器就应该关闭。在这些情况下,应该调用一个 close()方法。
**注:**我们没在该例子中实现这一点,但将一个客户端请求切换到一个新线程或进程来完成客户端处理也是相当普遍的。SocketServer 模块是一个以 socket 为基础而创建的高级套接字通信模块,它支持客户端请求的线程和多进程处理。可以参考文档或在第 4 章的练习部分获取 SocketServer 模块的更多信息。
实例2.1
**TCP 服务器程序:**它接受客户端发送的数据字符串,并将其打上时间戳(格式:[时间戳]数据)并返回给客户端;
# 导入模块和模块的所有属性
from socket import *
from time import ctime
# 地址信息
HOST = '' #空代表他可以使用任何可用的地址
PORT = 9998 #可以选择一个随机的端口号
ADDRESS = (HOST,PORT) #bind()接受元祖参数(HOST,PORT)
BUFSIZ = 1024 # 缓冲区大小,根据性能可调节
# 创建tcp套接字
tcp_server_socket = socket(AF_INET,SOCK_STREAM)
# 绑定通信地址
tcp_server_socket.bind(ADDRESS)
# 开启tcp监听器,设置连接被转接或拒绝之前,传入连接请求的最大数为5。
tcp_server_socket.listen(5)
while True:
print("waiting for connection...")
# 被动的等待连接,accept()函数为阻塞函数,直到有连接是才继续往下执行
tcp_client_socket,client_addr= tcp_server_socket.accept()
print("...connected from:",(client_addr))
# 当有连接时,进入循环,进行通信
while True:
# 等待接受客户端的消息,resv()函数也是阻塞函数
data = tcp_client_socket.recv(BUFSIZ)
if not data: # 如果接受到客户端的信息为空退出循环结束通信
break
# 如果客户端发送的信息非空,将接受到的信息和服务器时间,发送给客户端
data = ctime() + data.decode()
tcp_client_socket.send(data.encode())
# 退出通信后,关闭客户端套接字,连接断开
tcp_client_socket.close()
#
#
tcp_server_socket.close()
创建客户端比服务器要简单得多。与对 TCP 服务器的描述类似,本节将先给出附带解释
的伪代码,然后揭示真相。
client_socket = socket() #创建客户端套接字
client_socket.conect() # 尝试连接服务器
while True: #通信循环
client_socket.send()/client_socket.recv() #对话(发送信息/接受信息)
client_socket.close() #关闭客户端套接字
正如前面提到的,所有套接字都是利用 socket.socket()创建的。然而,一旦客户端拥有了一个套接字,它就可以利用套接字的 connect()方法直接创建一个到服务器的连接。当连接建立之后,它就可以参与到与服务器的一个对话中。最后,一旦客户端完成了它的事务,它就可以关闭套接字,终止此次连接。示例 2-3 :
# 导入模块和模块的所有属性
from socket import *
# 地址信息
HOST = 'localhost' #空代表他可以使用任何可用的地址
PORT = 9998 #可以选择一个随机的端口号
ADDRESS = (HOST,PORT) #connect()接受元祖参数(HOST,PORT)
BUFSIZ = 1024 # 缓冲区大小,根据性能可调节
# 创建tcp套接字
tcp_server_socket = socket(AF_INET,SOCK_STREAM)
# 绑定通信地址
tcp_server_socket.connect(ADDRESS)
# 循环通信
while True:
data = input(">>>:")
if not data:
break
tcp_server_socket.send(data.encode())
data = tcp_server_socket.recv(BUFSIZ)
if not data:
break
print(data.decode('utf-8'))
tcp_server_socket.close()
现在,运行服务器和客户端程序,看看它们是如何工作的。然而,应该先运行服务器还是客户端呢?当然,如果先运行客户端,那么将无法进行任何连接,因为没有服务器等待接受请求。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动地等待连接。另一方面,客户端是一个主动的合作伙伴,因为它主动发起一个连接。换句话说:首先启动服务器(在任何客户端试图连接之前)。
在该示例中,使用相同的计算机,但是完全可以使用另一台主机运行服务器。如果是这种情况,仅仅需要修改主机名就可以了(当你在不同计算机上分别运行服务器和客户端以此获得你的第一个网络应用程序时,这将是相当令人兴奋的!)。
UDP 服务器不需要 TCP 服务器那么多的设置,因为它们不是面向连接的。除了等待传入的连接之外,几乎不需要做其他工作。
ss = socket() # 创建服务器套接字
ss.bind() # 绑定服务器套接字
while True: # 服务器无限循环进行通信
cs = ss.recvfrom()/ss.sendto() #通信(接收信息/发送信息)
ss.close() #关闭服务器套接字
从以上伪代码中可以看到,除了普通的创建套接字并将其绑定到本地地址(主机名/端口号对)外,并没有额外的工作。无限循环包含接收客户端消息、打上时间戳并返回消息,然后回到等待另一条消息的状态。再一次,close()调用是可选的,并且由于无限循环的缘故,它并不会被调用,但它提醒我们,它应该是我们已经提及的优雅或智能退出方案的一部分。
UDP 和 TCP 服务器之间的另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使一个客户端连接到一个独立的套接字“转换”的操作。这些服务器仅仅接受消息并有可能回复数据。
你将会在示例 2-6 的 tsUserv.py 中找到代码,这是前面给出的 TCP 服务器的 UDP 版本,它接受一条客户端消息,并将该消息加上时间戳然后返回客户端。
# 导入模块及模块的属性
from socket import *
from time import ctime
# 设置通信地址信息
HOST = ""
PORT = 20000
ADDR = (HOST,PORT)
# 设定缓冲区大小
BUFSIZ = 1024
# 创建服务器套接字并绑定通信地址
udp_server_sock = socket(AF_INET,SOCK_DGRAM)
udp_server_sock.bind(ADDR)
# 循环通信
while True:
print("waiting for message......")
data,addr = udp_server_sock.recvfrom(BUFSIZ)
data = "%s:%s"%(ctime().encode(),data)
udp_server_sock.sendto(data.encode(),addr)
print("received from and returned to:",addr)
# 关闭udp服务器套接字
udp_server_sock.close()
在 UNIX 启动行后面,导入 time.ctime()和 socket 模块的所有属性,就像 TCP 服务器设置中的一样。
HOST 和 PORT 变量与之前相同,原因与前面完全相同。对 socket()的调用的不同之处仅仅在于,我们现在需要一个数据报/UDP 套接字类型,但是 bind()的调用方式与 TCP服务器版本的相同。再一次,因为 UDP 是无连接的,所以这里没有调用“监听传入的连接”。
一旦进入服务器的无限循环之中,我们就会被动地等待消息(数据报)。当一条消息到达时,我们就处理它(通过添加一个时间戳),并将其发送回客户端,然后等待另一条消息。如前所述,套接字的 close()方法在这里仅用于显示。
在本节中所强调的 4 个客户端中, UDP 客户端的代码是最短的。它的伪代码如下所示。
# 导入模块及模块的属性
from socket import *
# 设置通信地址信息
HOST = "localhost"
PORT = 20000
ADDR = (HOST,PORT)
# 设定缓冲区大小
BUFSIZ = 1024
# 创建服务器套接字
udp_client_sock = socket(AF_INET,SOCK_DGRAM)
# 循环通信
while True:
data = input(">>>:")
if not data:
break
udp_client_sock.sendto(data.encode(),ADDR)
data,ADDR = udp_client_sock.recvfrom(BUFSIZ)
if not data:
break
print(data.decode())
# 关闭udp客户端套接字
udp_client_sock.close()
事实上,之所以输出客户端的信息,是因为可以同时接收多个客户端的消息并发送回复消息,这样的输出有助于指示消息是从哪个客户端发送的。利用 TCP 服务器,可以知道消息来自哪个客户端,因为每个客户端都建立了一个连接。注意,此时消息并不是“waiting for connection”,而是“waiting for message”。
除了现在熟悉的 socket.socket()函数之外,socket 模块还提供了更多用于网络应用开发的属性。下列列出了一些最受欢迎的属性。
数据属性
属性名称 | 描述 |
---|---|
AF_UNIX、AF_INET、AF_INET6、AFNETLINK、AF_TIPC | Python中支持的套接字地址家族 |
SOCK_STREAM、SOCK_DGRAM | 套接字类型(TCP=流,UDP=数据报) |
Has_ipv6 | 指示是否支持IPv6的布尔标记 |
异常
属性名称 | 描述 |
---|---|
herror | 主机和地址相关错误 |
gaierror | 地址相关错误 |
time | 超时时间 |
error | 套接字相关错误 |
函数
属性名称 | 描述 |
---|---|
socket() | 以给定的地址家族、套接字类型和协议类型(可选)创建一个套接字对象 |
socketpair() | 以给定的地址家族、套接字类型和协议类型(可选)创建一个套接字对象 |
create_conection() | 常规函数,他接收一个地址(主机名,端口号)对,返回套接字对象 |
fromfd() | 以一个打开的文件描述符创建一个套接字对象 |
ssl() | 通过套接字启动一个安全套接字层连接,不执行证书验证 |
getaddrinfo() | 获取一个五元组序列形式的地址信息 |
getnameinfo() | 给定一个套接字地址,返回(主机名,端口号)二元组 |
getfqdn() | 返回完整的域名 |
gethostname() | 返回当前的主机名 |
gethostbyname() | 将一个主机名映射到它的IP地址 |
gethostbyname_ex() | gethostbyname()的扩展版本,它返回主机名、别名主机集合和 IP 地址列表 |
gethostbyaddr() | 将一个 IP 地址映射到 DNS 信息;返回与 gethostbyname_ex()相同的 3 元组 |
getprotobyname() | 将一个协议名(如‘tcp’)映射到一个数字 |
getservbyname()/getservbyport() | 将一个服务名映射到一个端口号,或者反过来;对于任何一个函数来说,协议名都是可选的 |
ntohl()/ntohs() | 将来自网络的整数转换为主机字节顺序 |
htonl()/htons() | 将来自主机的整数转换为网络字节顺序 |
inet_aton()/inet_ntoa() | 将 IP 地址八进制字符串转换成 32 位的包格式,或者反过来(仅用于 IPv4 地址) |
inet_pton()/inet_ntop() | 将 IP 地址字符串转换成打包的二进制格式,或者反过来(同时适用于 IPv4 和 IPv6 地址) |
getdefaulttimeout()/setdefaulttimeout() | 以秒(浮点数)为单位返回默认套接字超时时间;以秒(浮点数)为单位设置默认套接字超时时间 |
要获取更多信息,请参阅 Python 参考库中的 socket 模块文档。
SocketServer 是标准库中的一个高级模块(Python 3.x 中重命名为 socketserver),它的目标是简化很多样板代码,它们是创建网络客户端和服务器所必需的代码。这个模块中有为你创建的各种各样的类,如表 2-3 所示。
通过复制前面展示的基本 TCP 示例,我们将创建一个 TCP 客户端和服务器。你会发现它们之间存在明显的相似性,但是也应该看到我们如何处理一些繁琐的工作,于是你不必担心样板代码。这些代表了你能够编写的最简单的同步服务器(为了将你的服务器配置为异步运行,可以查看本章末尾的练习)。
除了为你隐藏了实现细节之外,另一个不同之处是,我们现在使用类来编写应用程序。因为以面向对象的方式处理事务有助于组织数据,以及逻辑性地将功能放在正确的地方。你还会注意到,应用程序现在是事件驱动的,这意味着只有在系统中的事件发生时,它们才会工作。
表2-3 SocketServer 模块类
类 | 描 述 |
---|---|
BaseServer | 包含核心服务器功能和 mix-in 类的钩子;仅用于推导,这样不会创建这个类的实例;可以用 TCPServer 或 UDPServer 创建类的实例 |
TCPServer/UDPServer | 基础的网络同步 TCP/UDP 服务器 |
UnixStreamServer/UnixDatagramServer | 基于文件的基础同步 TCP/UDP 服务器 |
ForkingMixIn/ThreadingMixIn | 核心派出或线程功能;只用作 mix-in 类与一个服务器类配合实现一些异步性;不能直接实例化这个类 |
ForkingTCPServer/ForkingUDPServer | ForkingMixIn 和 TCPServer/UDPServer 的组合 |
BaseRequestHandler | 包含处理服务请求的核心功能;仅仅用于推导,这样无法创建这个类的实例;可以使用 StreamRequestHandler 或DatagramRequestHandler 创建类的实例 |
ThreadingTCPServer/ThreadingUDPServer | ThreadingMixIn 和 TCPServer/UDPServer 的组合 |
StreamRequestHandler/DatagramRequestHandler | 实现 TCP/UDP 服务器的服务处理器 |
事件包括消息的发送和接收。事实上,你会看到类定义只包括一个用来接收客户端消息的事件处理程序。所有其他的功能都来自使用的 SocketServer 类。此外,GUI 编程(见第 5章)也是事件驱动的。你会立即注意到它们的相似性,因为最后一行代码通常是一个服务器的无限循环,它等待并响应客户端的服务请求。它工作起来几乎与本章前面的基础 TCP 服务器中的无限 while 循环一样。
在原始服务器循环中,我们阻塞等待请求,当接收到请求时就对其提供服务,然后继续等待。在此处的服务器循环中,并非在服务器中创建代码,而是定义一个处理程序,这样当服务器接收到一个传入的请求时,服务器就可以调用你的函数。
在下面示例中,首先导入服务器类,然后定义与之前相同的主机常量。其次是请求处理
程序类,最后启动它。更多细节请查看下面的代码片段。
from socketserver import (TCPServer as TCP,StreamRequestHandler as SRH)
from time import ctime
HOST = ''
PORT = 21000
ADDR = (HOST,PORT)
class MyRequestHandler(SRH):
def handle(self):
print("...connected from:",self.client_address)
self.wfile.write('%s:%s'%(ctime(),self.rfile.readline()))
tcp_server = TCP(ADDR,MyRequestHandler)
print('waiting for connection......')
tcp_server.serve_forever()
我们得到了请求处理程序 MyRequestHandler,作为 SocketServer中 StreamRequestHandler 的一个子类,并重写了它的 handle()方法,该方法在基类 Request 中默认情况下没有任何行为。
def handle(self):
pass
当接收到一个来自客户端的消息时,它就会调用 handle()方法。而 StreamRequestHandler类将输入和输出套接字看作类似文件的对象,因此我们将使用 readline()来获取客户端消息,并利用 write()将字符串发送回客户端。
因此,在客户端和服务器代码中,需要额外的回车和换行符。实际上,在代码中你不会看到它,因为我们只是重用那些来自客户端的符号。除了这些细微的差别之外,它看起来就像以前的服务器。
最后的代码利用给定的主机信息和请求处理类创建了 TCP 服务器。然后,无限循环地等待并服务于客户端请求。
如下面示例所示,这里的客户端很自然地非常像最初的客户端,比服务器像得多,但必须稍微调整它以使其与新服务器很好地工作。
"""
这是一个时间戳 TCP 客户端,它知道如何与类似文件的
SocketServer 类 StreamRequest Handler 对象通信。
"""
# 导入模块和模块的所有属性
from socket import *
# 地址信息
HOST = 'localhost' #空代表他可以使用任何可用的地址
PORT = 21000 #可以选择一个随机的端口号
ADDRESS = (HOST,PORT) #connect()接受元祖参数(HOST,PORT)
BUFSIZ = 1024 # 缓冲区大小,根据性能可调节
# 循环通信
while True:
# 创建tcp套接字
tcp_server_socket = socket(AF_INET, SOCK_STREAM)
# 绑定通信地址
tcp_server_socket.connect(ADDRESS)
data = input(">>>:")
if not data:
break
data = '%s\r\n' % data
tcp_server_socket.send(data.encode())
data = tcp_server_socket.recv(BUFSIZ)
if not data:
break
print(data.decode('utf-8'))
tcp_server_socket.close()
SocketServer 请求处理程序的默认行为是接受连接、获取请求,然后关闭连接。由于这个原因,我们不能在应用程序整个执行过程中都保持连接,因此每次向服务器发送消息时,都需要创建一个新的套接字。
这种行为使得 TCP 服务器更像是一个 UDP 服务器。然而,通过重写请求处理类中适当的方法就可以改变它。不过,我们将其留作本章末尾的一个练习。
除了客户端现在有点“由内而外”(因为我们必须每次都创建一个连接)这个事实之外,其他一些小的区别已经在服务器代码的逐行解释中给出:因为这里使用的处理程序类对待套接字通信就像文件一样,所以必须发送行终止符(回车和换行符)。而服务器只是保留并重用这里发送的终止符。当得到从服务器返回的消息时,用 strip()函数对其进行处理并使用由 print声明自动提供的换行符。
此时的输出与最初的 TCP 客户端和服务器的输出类似。然而,你应该会发现,我们连接了服务器两次。
Twisted 是一个完整的事件驱动的网络框架,利用它既能使用也能开发完整的异步网络应用程序和协议。它提供了大量的支持来建立完整的系统,包括网络协议、线程、安全性和身份验证、聊天/ IM、DBM 及 RDBMS 数据库集成、Web/因特网、电子邮件、命令行参数、GUI 集成工具包等。
使用 Twisted 来实现简单的例子,有点小题大做,但是你必须开始使用它,并且该应用程序就相当于网络应用程序的“hello world”。与 SocketServer 类似,Twisted 的大部分功能都存在于它的类中。特别是对于该示例,我们将使用 Twisted 因特网组件中的 reactor 和 protocol 子包中的类。
当开发低级套接字程序时,经常配合使用 select 模块和 socket 模块。select 模块提供了 select()函数,该函数管理套接字对象集合。它所做的最有用的一个事情就是接收一套套接字,并监听它们活动的连接。select()函数将会阻塞,直到至少有一个套接字已经为通信做好准备,而当其发生时,70 第 1 部分 通用应用主题它将提供一组准备好读信息的集合(它还可以确定哪些套接字准备好写入,虽然它不像前一种操作那么常见)。
模 块 | 描 述 |
---|---|
socket | 正如本章讨论的,它是低级网络编程接口 |
asyncore/asynchat | 提供创建网络应用程序的基础设施,并异步地处理客户端 |
select | 在一个单线程的网络服务器应用中管理多个套接字连接 |
SocketServer | 高级模块,提供网络应用程序的服务器类,包括 forking 或 threading 簇 |
在创建服务器方面,async*和 SocketServer 模块都提供更高级的功能。它们以 socket 和/ 或 select 模块为基础编写,能够使客户端/服务器系统开发更加迅速,因为它们已经自动处理了所有的底层代码。你需要做的所有工作就是以自己的方式创建或继承适当的基类。正如前面所提到的,SocketServer 甚至提供了将线程或新进程集成到服务器的功能,它提供了一个更像并行处理的客户端请求的流程。
虽然在标准库中 async*提供了唯一的异步开发支持,但是在前一节中,我们引入了一个比旧版本更加强大的第三方包 Twisted。虽然本章中我们已经看到的示例代码稍长于粗糙的脚本,但是 Twisted 提供了一个更加强大和灵活的框架,并且已经实现了很多协议。可以在http://twistedmatrix.com 网站上找到更多关于 Twisted 的消息。
Concurrence 是一个更现代化的网络框架,它是荷兰社交网络 Hyves 的后台引擎。Concurrence 是一个搭配了 libevent 的高性能 I/O 系统,libevent 是一个低级事件回调调度系统。Concurrence 是一个异步模型,它使用轻量级线程(执行回调)以事件驱动的方式进行线程间通信和消息传递工作。可以在 http://opensource.hyves.org/concurrence 网址找到更多关于Concurrence 的信息。
现代网络框架遵循众多异步模型(greenlet、generator 等)之一来提供高性能异步服务器。这些框架的其中一个目标就是推动异步编程的复杂性,以允许用户以一种更熟悉的同步方式进行编码。
本章介绍的主题主要是在 Python 中利用套接字进行网络编程,以及如何使用低层协议套件(如 TCP/IP 和 UDP/IP)创建自定义应用程序。如果你想开发高级 Web 和网络应用程序,我们强烈鼓励你阅读第 3 章(因特网客户端编程)。