Python中的网络通信
概述
在我们平时生活工作中,常常会接触到网络通信的内容,不管你是普通的用户,还是通信行业内的开发人员,都无法避免与网络通信打交道。我在初步学习python的过程中,对python的网络通信问题做了总结,所以写下这篇文章作为记录,也希望能给其他初学者一些引导和启发。这篇文章的主要内容如下:
1. 在深入讲解之前,我们先介绍一些背景信息;
2. 介绍套接字的概念;
3. 展示如何用Python来实现简单的网络应用程序。
客户端/服务器架构(C/S架构)
什么是客户端/服务器架构?总的来说,某一个特定的对象,并没有严格的客户端或者服务器之分,区分的标准在于,在实现网络通信的过程中,该对象的行为模式决定了它是客户端还是服务端。
服务器:为一个或者多个客户端(用户)提供所需“服务"的一系列硬件或者软件。工作流程可以简单概括为:等待请求、响应并提供服务、等待下一个请求。
客户端:因特定请求而联系服务器,接收服务并处理相关事务的一方。客户端可以持续向服务器发送请求,也可以在结束事务请求后不再发出请求。
因特网上典型的C/S架构通信端点与套接字(socket)
在服务器相应客户端的请求之前,双方需要进行一系列的准备工作。首先需要创建一个通信端点,服务器通过该通信端点监听客户端的请求(当然,实际的网络通信中,处理不同类型的消息会使用不同类型的通信端点,从而进行区分)。
在网络通信中,我们常用的一种通信端点为套接字(socket),在通信开始之前,网络应用程序都需要创建套接字。
套接字的分类
1. 基于文件的套接字
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。
2. 面向网络的套接字
它也有自己的家族名字 AF_INET,或者地址家族:因特网。另一个地址家族 AF_INET6 用于第 6 版因特网协议(IPv6)寻址。(还有其他的地址家族,这些要么是专业的、过时的、很少使用的,要么是仍未实现的。)在所有的地址家族之中,目前 AF_INET 是使用得最广泛的。
总的来说,Python 只支持 AF_UNIX、AF_NETLINK、AF_TIPC 和 AF_INET 家族。下面的内容中,我们将使用 AF_INET。
套接字地址:主机-端口对
正如我们在现实生活中打电话一样,套接字的通信需要一些记号用作识别功能。我们打电话的时候,可以通过区号和电话号码的组合找到要呼叫的用户;在socket通信中,我们通过主机-端口对找到通信的对象。
我们都知道,通过url或者ip地址,我们可以在互联网中找到特定的主机。在每一台主机中,不同的端口号会用作不同的通信用途。只要确定的主机和端口号,就可以在网络中唯一确定一个”通信端点“,也就是找到了所要进行通信的套接字的地址了。
需要说明的是,有效的端口号范围是0~65535(小于1024的端口号预留给了系统)。一般来说,其余的端口号我们都可以根据实际的需要进行使用。
面向连接的套接字与无连接的套接字
1. 面向连接的套接字(TCP)
也称为虚拟电路或流套接字。主要提供序列化的、可靠的、不重复的数据,它可以将消息拆分成多个片段,确保每一条片段都顺利到达目的地,然后按照顺序组合在一起,最后将完整的消息传递给正在等待的应用程序。
实现的主要协议是传输控制协议(TCP)。创建TCP套接字时,必须使用SOCK_STREAM作为套接字类型。
2. 无连接的套接字(UDP)
又称为数据报类型的套接字。在传输过程中无法保证其顺序性、可靠性、重复性,事实上所发送的报文有可能最后并没有到达,也有可能存在重复的消息。
我们之所以还继续使用数据报,这是相对于面向连接的套接字来看的。由于维护TCP连接需要大量的开销,发送数据报能够保证成本更加”低廉“,所以通常能提供更好的性能,并且可能更适合某一些类型的应用程序。
实现的主要协议是用户数据报协议(UDP)。创建UDP套接字,必须使用SOCK_DGRAM作为套接字类型。
Python中的网络编程
socket()模块函数
要创建套接字,必须使用socket.socket()函数,它的一般语法如下:
socket(socket_family, socket_type, protocol=0)
其中,socket_family是AF_UNIX或者AF_INET,socket_type是SOCK_STREAM或者SOCK_DGRAM,protocol通常省略,默认为0 。
在python编程中,我们首先在文件最开始的地方引用需要用到的模块,在这里,我们使用“from socket import *",通过这样的引用,就可以向下面这样直接创建套接字:
tcpSock = socket(AF_INET, SOCK_STREAM)
udpSock = socket(AF_INET, SOCK_DGRAM)
成功创建套接字后,我们就可以调用套接字对象的内置方法进行其他的操作了。具体的内容可以查看python的文档。
TCP通信
1. 创建TCP服务器
以下是一个TCP服务器程序,它接受客户端发送的字符串,将其打上时间戳之后,再返回给客户端。二话不说,先贴代码:
TCP时间戳服务器第1~4行:
声明运行环境,导入socket模块和time.ctime()。
第6~10行:
指定主机地址、工作端口号、接收缓存的长度。HOST和PORT共同组成ADDR,体现的就是上面我们所说的”主机-端口对“。服务器端的HOST为空,表示它可以使用任意可用的地址。
第12~14行:
创建套接字 ,把套接字绑定到服务器地址,开启TCP监听。
第16~32行:
进入服务器的无限循环,不断等待接收客户端的连接。在第18行,我们通过accept()获取到客户端的tcpCliSock和addr,于是后续可以通过这个tcpCliSock专门处理该客户端的事务(从而与其他请求的客户端区分开来)。在第22行,使用recv()接收消息,如果消息为空,则跳出循环,关闭当前客户端的连接,然后继续等待连接;如果不为空,则把消息解析出来,添加时间戳,经过重新编码成ASCII字节后,通过send()发送回去给客户端。在这个程序里,第32行不会执行,只是用于提醒编写程序的人,在使用这一套程序时,必须要考虑到合理科学的退出方式,正确地调用close()方法。
这里需要强调的是,由于python的编码问题,所以我们的消息在主机端时要进行解码后才能正确显示(如decode()),进行编码后才能发送到网络端去(如encode(),bytes())。不管是在服务器还是客户端,我们都需要考虑到这个情况。
2. 创建TCP客户端
创建客户端比服务器要简单很多。客户端与服务器建立连接,发送字符串给服务器,从服务器接收添加了时间戳的消息并打印。发送空字符串即可关闭套接字,退出这次连接。代码实例如下:
TCP时间戳客户端第1~3行:
声明运行环境,导入socket模块。
第5~9行:
指定主机地址、工作端口号、接收缓存的长度。HOST和PORT共同组成ADDR,体现的就是上面我们所说的”主机-端口对“。这里的HOST为服务器端所在主机的地址,由于我是在本地进行通信测试的,所以地址设置为127.0.0.1(localhost)。在实际网络通信的时候,根据具体的情况进行相应的修改。客户端填写的PORT必须与服务器填写的PORT对应才能正常通信。
第11~12行:
创建套接字 ,主动调用并通过connect()连接到服务器。
第14~25行:
进行无限循环:客户端填写要发送的消息,发送后等待接收服务器的回复,接收到结果后将结果打印。当客户端输入的内容为空,或者服务器断开连接、客户端接收失败时,即退出循环,调用close()函数,关闭客户端的套接字。
同样的,发送消息前,我们需要对数据进行编码,接收的结果,也需要进行解码后才能正常显示出来。如果我们想要将代码改成相应的ipv6的形式,我们只需要把HOST改成“::1”,sock_family改成AF_INET6即可。
UDP通信
1. 创建UDP服务器
UDP服务器实现的功能与TCP基本一致,主要的区别在于UDP服务器不是面向连接的,所以只需要等待客户端的请求,回复消息即可,不需要将成功连接的客户端“转换”到一个独立的套接字的操作。示例代码如下:
UDP时间戳服务器第1~4行:
声明运行环境,导入socket模块和time.ctime()。
第6~10行:
与TCP相同,指定主机地址、工作端口号、接收缓存的长度。HOST和PORT共同组成ADDR,体现的就是上面我们所说的”主机-端口对“。服务器端的HOST为127.0.0.1,表示它使用本地主机地址。
第12~13行:
创建套接字 ,把套接字绑定到服务器地址,绑定后直接等待接收,不需要监听。
第15~24行:
进入服务器的无限循环,不断等待接收客户端的连接。在第17行,使用recvfrom()接收消息,同时获取通信客户端的地址对信息。紧接着把消息解析出来,添加时间戳,经过重新编码成ASCII字节后,通过sendto()发送回去给客户端,此时由于服务器没有与客户端维护连接,所以要指定发送的地址对信息。同样的,在这个程序里,第24行不会执行,只是用于提醒编写程序的人,在使用这一套程序时,必须要考虑到合理科学的退出方式,正确地调用close()方法。
2. 创建UDP客户端
原理非常简单,上面都有提及到,就不作详细说明了。直接贴代码:
UDP时间戳客户端关于服务器退出的问题
最后再提以下服务器退出的问题。在开发中,一种相对比较友好的退出方式是:将服务器的while部分放在一个try-except的try子句中,并监控EOFError和KeyboardInterrupt异常,这样就可以在except或者finally子句中关闭服务器的套接字。
参考内容:
《Python核心编程(第三版)》[美] Wesley Chun 著,孙波翔、李斌、李晗 译。