在服务器响应客户端请求之前,首先创建一个通信端点,它能够使服务器监听请求。一旦一个通信端点已经建立,监听服务器就可以进入无限循环中,等待客户端的连接并响应他们的请求。通信端点好比是允许通信的一些基础设施。
客户端所需要做的只是创建它的单一通信端点,然后建立一个到服务器的连接。然后,客户端就可以发出请求,该请求包括任何必要的数据交换。一旦请求被服务器处理,且客户端收到结果或某种确认信息,此次通信就会被终止。
套接字:通信端点
套接字是计算机网络数据结构,它体现了上面所述的“通信端点”的概念。在任何类型的通信开始之前,网络应用程序都必须创建套接字,它就像是电话插孔,没有它将无法进行通信。
套接字有两种类型:基于文件的和面向网络的。
第一种套接字是基于文件的,并拥有一个“家族名字”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),而服务器只是保留并重用这里发送的终止符。