Python客户端/服务器网络编程

在服务器响应客户端请求之前,首先创建一个通信端点它能够使服务器监听请求。一旦一个通信端点已经建立,监听服务器就可以进入无限循环中,等待客户端的连接并响应他们的请求。通信端点好比是允许通信的一些基础设施

客户端所需要做的只是创建它的单一通信端点,然后建立一个到服务器的连接。然后,客户端就可以发出请求,该请求包括任何必要的数据交换。一旦请求被服务器处理,且客户端收到结果或某种确认信息,此次通信就会被终止。



套接字:通信端点


套接字是计算机网络数据结构,它体现了上面所述的“通信端点”的概念。在任何类型的通信开始之前,网络应用程序都必须创建套接字,它就像是电话插孔,没有它将无法进行通信。



套接字有两种类型:基于文件的和面向网络的。


第一种套接字是基于文件的,并拥有一个“家族名字”AF_UNIX(又名AF_LOCAL),它代表地址家族:UNIX。AF_LOCAL将代替AF_UNIX,然而考虑到后向兼容性,很多系统都同时使用二者,只是对同一个常数使用不同的别名。Python本身仍然在使用AF_UNIX。


第二种套接字是基于网络的,它也有自己的家族名字AF_INET,它代表地址家族:因特网。另一个地址家族AF_INET6用于第6版因特网协议(IPv6)寻址。


此外,还有其他的地址家族,这些要么是专业的、过时的、很少使用的,要么是仍未实现的。在所有的地址家族中,目前AF_INET是使用得最广泛得。

总的来说,Python只支持AF_UNIX、AF_NETLINK、AF_TIPC和AF_INET家族。网络编程涉及AF_INET。


套接字地址:主机-端口号

如果套接字像一个电话插孔,那么主机名和端口号就像区号和电话号码的组合。在拥有硬件和通信的能力后,你还要知道电话打给谁。一个网络地址由主机名和端口号对组成。有效的端口号范围为0~65535(小于1024的端口号预留给了系统),众所周知的端口号列表可以在这个网站中查看:http://www.iana.org/assignments/port-numbers



套接字连接有两种风格:面向连接的和无连接的。

不管采用哪种地址家族,都有两种不同风格的套接字连接。


1、面向连接的套接字


面向连接意味着在进行通信之前必须先建立一个连接,例如使用电话系统给一个朋友打电话。这种类型的通信也称为虚拟电路或流套接字。

特点:每条消息可以拆分成多个片段,并且每一条消息片段都确保能够到达目的地,然后将它们按顺序组合在一起,最后将完整消息传递给正在等待的应用程序。

协议:实现这种连接类型的主要协议是传输控制协议(TCP)。为了创建TCP套接字,必须使用SOCK_STREAM作为套接字类型。TCP套接字的名字SOCK_STREAM基于流套接字的其中一种表示。因为这些套接字(AF_INET)使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合两种协议(TCP和IP)。


2、无连接的套接字


此类套接字是数据报类型的套接字,是一种无连接的套接字,这意味着在通信开始之前并不需要建立连接,例如邮政服务,信件和包裹或许并不能以发送顺序到达,事实上他们可能不会到达,为了将其添加到并发通信中,在网络中甚至可能存在重复的消息。

特点:在数据传输过程中并无法保证它的顺序性、可靠性或重复性。然而,数据报确实保存了记录边界,这意味着消息是以整体发送的,而并非首先分成多个片段。

协议:主要协议是用户数据报协议(UDP)。为了创建UDP套接字,必须使用SOCK_DGRAM作为套接字类型。因为这些套接字也使用因特网协议(IP)来搜寻网络中的主机,所以整个系统通常结合两种协议(UDP和IP)。




Python中的网络编程


在Python中使用的主要模块是socket模块,在这个模块中可以找到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套接字

tcpSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
创建UDP/IP套接字

udpSock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)



套接字对象(内置)方法

(以下方法只为简易的一部分,更多应参考API)


服务器套接字方法

 s.bind() 将地址(主机名、端口号对)绑定到套接字上   
 s.listen() 设置并启动TCP监听器
 s.accept() 被动接收TCP客户端连接,一直等待直到连接到达(阻塞)


客户端套接字方法

 s.connect() 主动发起TCP服务器连接     
 s.connect_ex() connect()的扩展版本,此时会以错误码的形式返回问题,而不是抛出一个异常   


普通的套接字方法

 s.recv() 接收TCP消息  
 s.send() 发送TCP消息
 s.sendall() 完整地发送TCP消息
 s.recvfrom() 接受UDP消息
 s.sendto() 发送UDP消息
 s.shutdown() 关闭连接
 s.close() 关闭套接字


面向阻塞的套接字方法

 s.setblocking() 设置套接字的阻塞或非阻塞模式
 s.settimeout() 设置阻塞套接字操作的超时时间
 s.gettimeout() 获取阻塞套接字操作的超时时间




创建TCP服务器


设计服务器的一种方式:


from socket import *

ss = socket( )                           #创建服务器套接字

ss.bind( )                              #套接字与地址绑定 

ss.listen( )                             #监听连接

inf_loop:                               #服务器无限循环 

    cs = ss.accept( )                     #接受客户端连接 → (开启服务器)

    comm_loop:                          #通信循环 → (通信开始)

        cs.recv( ) / cs.send( )             #对话(接受/发送) 

    cs.close( )                         #关闭客户端套接字

ss.close( )                             #关闭服务器套接字(可选)


(这仅仅是设计服务器的一种方式,一旦熟悉了服务器设计,那么你将能够按照自己的要求修改上面的伪代码来操作服务器)


  调用accept()函数之后,就开启了一个简单的(单线程)服务器,它会等待客户端的连接,默认情况下,accept()是阻塞的,这意味着执行将被暂停,直到一个连接到达。非阻塞模式的套接字可以参考其他文档与资料。

  一旦服务器接受了一个连接,就会返回(利用accept())一个独立的新的客户端套接字,用来与即将到来的消息进行交换,类似与将客户的电话切换给客服代表。这将能够空出主要的端口(原始服务器套接字),以使其能够接受新的客户端连接。

  一旦创建了临时套接字,通信就可以开始,通过使用这个新的套接字,客户端与服务器就可以开始参与发送和接受的对话中,直到连接终止。当一方关闭连接或者向对方发送一个空字符串时,通常就会关闭连接。在代码中,一个客户端连接关闭之后,服务器就会等待另一个客户端连接。



创建TCP客户端

 

from socket import *                    

cs = socket()                            #创建客户端套接字

cs.connect()                             #尝试连接服务器

comm_loop:                              #通信循环  

    cs.send()/cs.recv()                    #对话(发送/接受)

cs.close()                              #关闭客户端套接字



执行TCP服务器和客户端


在任何客户端试图连接之前,首先要启动服务器。服务器可以视为一个被动伙伴,因为必须首先建立自己,然后被动地等待连接,客户端是一个主动的合作伙伴,由它主动发起一个连接。



创建UDP服务器


from socket import *                        

ss = socket()                              #创建服务器套接字

ss.bind()                                 #绑定服务器套接字

inf_loop:                                 #服务器无限循环

    cs = ss.recvfrom() / ss.sendto()             #对话(接受/发送)

ss.close()                                #关闭服务器套接字


UDP服务器不需要TCP服务器那么多的设置,没有调用“监听传入的连接”,因为它不是面向连接的。另一个显著差异是,因为数据报套接字是无连接的,所以就没有为了成功通信而使一个客户端连接到一个独立的套接字“转换”操作,这种服务器仅仅接受消息并有可能回复数据。



创建UDP客户端


from socket import *

cs = socket()                               #创建客户端套接字

comm_loop:                                 #通信循环

    cs.sendto() / cs.recvfrom()                  #对话(发送/接受)

cs.close()                                 #关闭客户端套接字


UDP客户端循环工作方式几乎和TCP客户端完全一样。唯一的区别是,事先不需要建立与UDP服务器的连接,只是简单地发送一条消息并等待服务器的回复。




SocketServer模块


SocketServer模块是标准库中的一个高级模块(Python3.x中重命名为socketserver),它的目标是简化很多样板代码,他们是创建网络客户端和服务器所必须的代码。这个模块中有各种各样的类。


以下为一部分类:

 TCPServer / UDPServer 基础的网络同步TCP/UDP服务器
 UnixStreamServer / UnixDatagramServer 基于文件的基础同步TCP/UDP服务器
 ForkingTCPServer / ForkingUDPServer ForkingMixIn和TCPServer / UDPServer的组合
 ThreadingTCPServer / ThreadingUDPServer ThreadingMixIn和TCPServer / UDPServer的组合
 StreamRequestHandler / DatagramRequestHandler 实现TCP/UDP服务器的服务处理器



  使用SocketServer模块除了可以隐藏实现细节外,另一个不同之处是,我们现在使用类来编写应用程序。因为以面向对象的方式处理事务有助于组织数据,以及逻辑性地将功能放在正确的地方。你还会注意到,应用程序现在是事件驱动的,事件包括消息的发送和接受,这意味着只有在系统中的事件发生时,他们才会工作。
  在原始服务器循环中,我们阻塞等待请求,当接受到请求时就对其提供服务,然后继续等待。而此处的服务器循环中,并非在服务器中创建代码,而是定义一个处理程序,这样当服务器接收到一个传入的请求时,服务器就可以调用你的函数。



创建SocketServer TCP服务器


示例(Python2.x):

from SocketServer import (TCPServer as TCP, StreamRequestHandler as SRH)

HOST = ''
PORT = 21567
ADDR = (HOST, PORT)

class MyRequestHandler(SRH):
    def handle(self):
        self.wfile.write('%s' % self.rfile.readline())

tcpServ = TCP(ADDR, MyRequestHandler)
tcpServ.serve_forever()


相关解释:

  这里的请求处理程序MyRequestHandler,作为SocketServer中StreamRequestHandler的一个子类,并重写了它的handle()方法,该方法在基类Request中默认情况下没有任何行为。当接受到一个来自客户端的消息时,它就会调用handle()方法。
  StreamRequestHandler类将输入和输出套接字看作类似文件的对象,因此我们将使用readline()来获取客户端消息,并利用write()将字符串发送会客户端,因此在客户端和服务器代码中,需要额外的回车和换行符,但实际上,在服务器的代码中不会看到它,因为重用了那些来自客户端的符号。



创建SocketServer TCP客户端


SocketServer TCP客户端和原来的客户端的代码类似,主要不同点在于:


  SocketServer请求处理程序的默认行为是接受连接、获取请求,然后关闭连接。由于这个原因,应用程序不能在整个执行过程中都保持连接,因此每次向服务器发送消息时,都需要创建一个新的套接字。

  另外StreamRequestHandler类对待套接字通信就像文件一样,所以必须发送行终止符(回车和换行符),例如tcpCliSock.send('%s\r\n' % data),而服务器只是保留并重用这里发送的终止符。






版权声明:本文为u011589338原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。