python网络编程原理_图解Python网络编程

本文详细介绍了Python网络编程的基本原理,包括IP地址、端口、套接字的使用,以及TCP和UDP两种通信模型。同时,文章讨论了Python中`socket`模块的函数、方法和属性,强调了并发编程在TCP服务器中的重要性。此外,还提到了`asynchat`和`socketserver`模块在提高网络程序效率方面的应用。
摘要由CSDN通过智能技术生成

本篇索引

(1)基本原理

本篇指的网络编程,仅仅是指如何在两台或多台计算机之间,通过网络收发数据包;而不涉及具体的应用层功能(如Web服务器、 邮件收发、网络爬虫等等),那些属于应用编程的范畴,需要了解的可参看下一篇 Internet 应用编程。

关于使用Python进行网络通信编程,简单的例子网络上一搜一大把,但基本都是仅仅几行最简单的套接字代码, 用来做个小实验可以,但并不能实用。因为大多数Python的书和文档着重点在于讲Python语法, 并不会太细地把网络编程的底层原理给你讲清楚,比如:同步/异步的关系、线程并发监听的实现架构等等。 如果你要了解那些知识,需要去看《Unix网络编程》、《TCP/IP详解-卷1》之类的书。

本篇试图在讲Python网络编程的基础上,把涉及到的原理稍带整理一起描述一下。 一方面希望能帮到想进一步掌握Python网络编程的初学者、另一方面也方便我自己快速查阅用。

● IP地址、端口

每台电脑(服务器)都有一个固定的IP地址,而一台服务器上可能运行若干个不同的程序, 每个程序提供一种服务(比如:邮件服务程序、Web服务程序等等),每个不同的服务程序会占用一个端口号(也有占有多个端口的,比较少见), 端口(port)是一个16位数字,范围从065535。其中0~1023为保留端口,保留给特定的网络协议使用 (比如:HTTP固定使用80端口、HTTPS固定使用443端口)。一般你自己的服务程序可任意使用10000以上的端口。 它们的示意关系如下图所示:

由于要访问一个服务程序需要知道“一个IP地址和一个端口号”,因此两者加一起合称一个“地址(address)”。 在Python中,一个地址(address)一般用一个元组来表示,形如:address = (ipaddr, port)。

● 套接字

服务程序与客户端程序进行通行,需要通过一个叫做 socket(套接字)的媒介。socket 的本意是“插口”, 在网络通信中一般把它翻译成“套接字”。套接字的作用,就相当于在服务器程序和客户端程序之间建立了一根虚拟的专线, 服务器程序和客户端程序可以分别通过自己这端的套接字,向对方写入和读出数据 (在Python中,套接字一般为一个 socket 类型的实例),如此即可实现服务器和客户端的数据通信。 在服务器程序中,同一个端口可生成若干个套接字,每个套接字跟一个特定的客户端进行通信。 在客户端,如果与一个服务程序通信,一般只需生成一个套接字即可。 如下图所示:

● 编码问题

由于网络是以ascii文本格式传输数据的,而在Python3中,所有字符串都是Unicode编码的。 因此,将字符串通过网络发送时必须转码。而从网络收到数据时,也必须进行解码以转换成Python的字符串。

发送时,可使用字符串的encode()方法进行转码,也可直接使用内置的bytes类型。 接收时,可使用字符串的decode()方法进行解码。

# 转码示例

s.send('Hello world!'.encode('ascii')) # 方法一:使用encode()转码

s.send(b'Hello world!') # 方法二:直接发送bytes类型(字节序列)

# 解码示例

recv_data = s.recv(1024)

recv_str = recv_data.decode('ascii') # 使用decode()解码

(2)socket模块

socket模块提供了最原始的仿UNIX的网络编程方式,因为它非常底层,所以很适合用来说明网络编程的概念, 但在实际工作中基本上不太会直接用socket模块去编写网络程序。实际工作中, 一般都会使用Python库中提供的更加方便的模块或类(比如SocketServer等)来编写网络程序。

● 基本的UDP编程模型

UDP的编程模型比较简单,虽然服务器 socket 和客户端 socket 也是一对一通信,但是一般发完数据就放手, 服务器程序不需要花心思去管理多个客户端的连接。大体流程示意可参看下图:

在服务器程序端,先生成一个套接字,然后通过bind() 方法绑定到本地地址和特定端口,之后就可以通过recvfrom()方法监听客户端数据了。 recvfrom()方法为阻塞运行,即:如果客户端没有新的数据进来,服务器程序会僵在这里, 只有等到客户端有新的数据进来,这个方法才会返回,然后继续运行后面的语句。 上图是一个基本示意,各个方法的详细解释可参看后文的表格。

以下为一个UDP服务器程序的示例:

# UdpServer.py

# 功能:接收客户端数据,将客户端发过来的字符串加个头“echo:”再回发过去)

import socket

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

s.bind(("", 10000)) # 服务器程序绑定本地10000端口,空字符串表示本地IP地址

while True:

data, address = s.recvfrom(256)

print("Received a connection from %s" % str(address))

s.sendto(b"echo:" + data, address)

以下为UDP客户端测试程序:

# UdpClient.py

import socket

s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # AF_INET指IPv4,SOCK_DGRAM指UDP,后面会有详释

s.sendto(b"Hello", ("127.0.0.1", 10000)) # 服务器地址和端口(客户端一般会由操作系统随机分配发送端口)

resp, addr = s.recvfrom(256)

print(resp)

s.sendto(b"World", ("127.0.0.1", 10000))

resp, addr = s.recvfrom(256)

print(resp)

s.close()

需要注意的是,在网络编程中,服务器程序和客户端程序是需要一定配合的, 需要避免进入双方都在等对方数据的卡住状态,如下图所示:

● 基本的TCP编程模型

使用UDP通信的服务器程序一般不太需要太复杂的编程技术。而如果使用TCP通信, 不使用“并发”或“异步”或“select()”编程技术基本是没法实用的。在实用中,一般只要使用这三种技术中的一种就可以了。 简单来说:“并发”是指多进程或多线程编程;“异步”是指在操作系统中先注册某种事件,当这个事件发生时, 由操作系统回调你事先注册的函数;“select()”方法后面会专门解释。

这里为说明概念,先演示最原始的单进程、单线程、什么技术都不用的原始TCP通信模型,如下图所示:

以下为一个TCP服务器程序示例:

# TcpServer.py

# 功能:接收客户端的TCP连接,打印客户端发送过来的字符串,并将服务器本地时间发给客户端

from socket import *

import time

s = socket(AF_INET, SOCK_STREAM) # AF_INET指IPv4,SOCK_STREAM至TCP,后面会有详释

s.bind(('', 10001)) # 服务器程序绑定本地10000端口

s.listen(5)

while True:

s1, addr = s.accept()

print("Got a connection from %s" %str(addr))

data = s1.recv(1024)

print("Received: %s" %data.decode('ascii'))

timestr = time.ctime(time.time()) + "\r\n"

s1.send(timestr.encode('ascii'))

s1.close()

以下为TCP客户端测试程序:

# TcpClient.py

from socket import *

s = socket(AF_INET, SOCK_STREAM)

s.connect(('127.0.0.0.1', 10001))

s.send(b'Hello')

tm = s.recv(1024)

s.close()

print("The time is %s" % tm.decode('ascii'))

TCP的编程需要服务器程序管理若干个 socket,所以编程模型与上面的UDP略有不同, 多了一个listen()和accept()步骤。listen()等会儿再讲, 先讲accept()。

在示例程序中我们可以看到s1, addr = s.accept()的用法。其中,s 是原始的用于监听端口10001的套接字实例, accept()方法会阻塞运行。当有客户端发起connect()连接时,accept()方法会接受这个连接, 并返回一个元组:分别是新套接字实例 s1 、客户端地址 addr。s1 用于与这个客户端通信,s 仍然用于监听端口10001, 看有没有新的客户端连入。

之后运行的recv()方法,也是阻塞运行的。当这个客户端没有发送新的数据过来时, 服务器主流程就会僵在这里,无法继续往下运行。如果有新的客户端请求连接时,只能在操作系统中排队等待。 前面的listen()方法就是用来定义操作系统中这个等待队列的长度的, 其入参即可指定操作系统中在这个监听套接字 s 上允许排队等待的最大客户端数量。 以前,在不使用前面提到的并发等3个编程技巧时,一般这个值需要为1024或者更多, 而如果使用了并发等编程技巧,一般这个值只需要5就足够了。

当 s1 与客户端通讯完毕,需要调用close()方法关闭这个套接字。 在套接字关闭后,程序主流程再次回到上面的s1, addr = s.accept()语句,继续监听新的连接。 若此时已经有客户端在操作系统中排队等待,则会立即从操作系统中取出一个等待的客户端,然后建立新的套接字实例。 若无等待的客户端,则本语句会阻塞,直到下一次有客户端connect()进来时,再返回。

很显然,这种同时只能处理一个客户端连接的服务器程序是没法用的, 如果前一个客户端与服务器通信的时间比较长,那新的客户端连接请求只能在操作系统中排队等待, 而无法立即与服务器建立通信,后面我们将看到,如何用并发等编程技术解决这个问题。

以下为一个通信时间较常的TCP客户端测试程序:

# TcpClient.py

from socket import *

import time

s = socket(AF_INET, SOCK_STREAM)

s.connect(('127.0.0.0.1', 10001))

time.sleep(5) # 与服务器建立连接后,不放手,先等5秒钟再发送数据

s.send(b'Hello')

tm = s.recv(1024)

s.close()

print("The time is %s" % tm.decode('ascii'))

你可以开2个终端运行这个通信时间较常的客户端程序,看看服务器是怎样反应的。

另外,可以比较一下以前用纯C语言写TCP服务器程序,作为参考:

// TcpServer.c

#include

#include

#include

int main(int argc, char **argv) {

int listenfd, connfd;

char buff[4096];

time_t ticks;

struct sckaddr_in servaddr;

listenfd = Socket(AF_INET, SOCK_STREAM, 0);

memset(&servaddr, 0, sizeof(servaddr));

servaddr.sin_family = AF_INET;

servaddr.sin_addr.s_addr = htonl(INADDR_ANY);

servaddr.sin_port = htons(13);

Bind(linstenfd, (SA *)&servaddr, sizeof(servaddr));

Listen(listenfd, 1024);

for(;;) {

connfd = Accept(listenfd, (SA *) NULL, NULL);

ticks = time(NULL);

snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));

Write(connfd, buff, strlen(buff));

Close(connfd);

}

}

● 采用并发技术的TCP编程模型

并发是指采用子进程或多线程方式进行编程。并发编程的核心思想是,当与客户端的连接建立后, 在主线程(或父进程)内不要有使用recv()等可能造成阻塞的行为, 这些有可能导致阻塞的行为都通信都交给其他后台线程(或子进程)去做, 主线程(或父进程)永远只阻塞在accept()上,负责监听新的连接并立即处理。

以下以线程并发为例,示意并发的TCP的编程模型:

以下为一个线程并发的TCP服务器程序:

# TcpServerThreading.py

from socket import *

import time, threading

s = socket(AF_INET, SOCK_STREAM)

s.bind(('', 10001))

s.listen(5)

thread_list = []

def client_commu(client_socket):

data = client_socket.recv(1024)

print("Received: %s" %data.decode('ascii'))

timestr = time.ctime(time.time()) + "\r\n"

client_socket.send(timestr.encode('ascii'))

client_socket.close()

while True:

ns, addr = s.accept()

print("Got a connection from %s" %str(addr))

t = threading.Thread(target=client_commu, args=(ns,))

t.daemon = True # 将新线程处理成后台线程,主线程结束时将不等待后台线程

thread_list.append(t)

t.start()

代码比较简单,很容易看懂。核心思想就如前述:每来一个新的客户端连接,就开一个新线程负责与这个客户端通信, 而主线程永远只阻塞在accept()上监听新连接。

● socket模块的函数

以下为 socket 模块中可用的函数、方法、属性的详细解释,大部分都同 UNIX 中的同名用法。

(1)模块函数

函数或变量说明

模块变量

has_ipv6

布尔值,支持IPv6时为 True。

连接相关

socket(family, type [,proto])

新建套接字,返回一个 SocketType 类型的实例。

family为IP层协议,常用:AF_INET(IPv4)、AF_INET6(IPv6)。

type为套接字类型,常用:SOCK_DGRM(UDP)、SOCK_STREAM(TCP)。

proto为协议编号,通常省略(默认为0)

socketpair([family [,type [,proto]]])

仅适用于创建family为 AF_UNIX 的“UNIX域”套接字。该概述主要用于设置 os.fork()

创建的进程之间的通信。例如:父进程调用 socketpair() 创建一对套接字,然后父进程和子进程

就可以使用这些套接字相互通信了。

fromfd(fd, family, type [,proto])

通过整数文件描述符创建套接字对象,文件描述符必须引用之前创建的套接字。

该方法返回一个 SocketType 实例。

create_connection(address [,timeout])

建立与address的TCP连接,并返回已连接的套接字对象。

address为:(host, port) 形式的元组,timeout指定一个可选的超时期。

查看主机信息

gethostname()

返回本机的主机名。

getfqdn([name])

若忽略入参name,则返回本机主机名。其他详查文档。

gethostbyname(hostname)

将主机名hostname(如:'www.python.org')转换为 IP 地址。不支持 IPV6。

这个函数会自动去查询Internet上的地址。

gethostbyname_ex(hostname)

将主机名hostname(如:'www.python.org')转换为 IP 地址。

但返回元组:(hostname, aliaslist, ipaddrlist),其中hosthame是主机名,

aliaslist是同一个地址的可选主机名列表,ipaddrlist是同一个主机上同一个接口的IPv4地址列表。

gethostbyaddr(ip_address)

返回信息与上面 gethostbyname_ex() 相同,但入参为IP地址。

getaddrinfo(host, port [,family [,socktype [,proto [,flags]]]])

给定关于主机的host和port信息,返回值为包含5个元素的元组:

(family, socktype, proto, cannonname, sockaddr),可视为 gethostbyname() 函数的增强版。

getnameinfo(address, flags)

给定套接字地址address(为(ipaddr, port) 形式的元组),将其转换为

flag指定的地址信息,主要用于获取与地址有关的详细信息。详可查看文档。

查询协议信息

getprotobyname(protocolname)

将协议名称(如:'icmp')转换为协议编号(如:IPROTO_ICMP的值),

以便传给 socket() 函数的第3个参数。

getservbyname(servicename [,protocolname])

将 Internet 服务名称和协议名称转换为该服务的端口号。

protocolname可以为:'tcp'或'udp'。例如:getservbyname('ftp','tcp')

getservbyport(port [,protocolname])

与上面相反,通过端口号查询服务名称。如果没有任何服务用于指定端口,

则引发 socket.error 错误。

超时信息

getdefaulttimeout()

返回默认的套接字超时秒数(浮点数),None表示不设置任何超时期。

setdefaulttimeout(timeout)

为新建的套接字对象设置默认超时期,入参为超时秒数(浮点数),若为 None 表示没有超时(默认值)

转码相关

htonl(x)

将主机的32位整数x转为网络字节顺序(大尾)。

htons(x)

将主机的32位整数x转为网络字节顺序(小尾)。

ntohl(x)

将来自网络的32位整数(大尾)x转换为主机字节顺序。

ntohs(x)

将来自网络的32位整数(小尾)x转换为主机字节顺序。

inet_aton(ip_string)

将字符串形式的IPv4地址(如:'127.0.0.1')转换成32位二进制分组格式,用作地址的原始编码。

返回值是由4个二进制字符组成的字符串(如:b'\x7f\x00\x00\x01')。在将地址传递给C程序时比较有用。

inet_ntoa(packed_ip)

与上面 inet_aton() 功能相反。常用于从C程序传来的地址数据解包。

inet_pton(family, ip_string)

功能与上面 inet_aton() 类似,但支持IPv6,family可指定地址族。

inet_ntop(family, packed_ip)

与 inet_pton() 功能相反,用于解包地址。

(2)套接字属性和方法

属性和方法说明

属性

s.family

套接字地址族(如:AF_INET)。

s.type

套接字类型(如:SOCK_STREAM)。

s.proto

套接字协议编号。

连接相关方法

s.bind(address)

通常为服务器用。将套接字绑定到特定地址和端口。address为元组形式的:

(hostname, port),注意 hostname 必须要加引号,空字符串、'localhost'都表示本机IP地址。

s.listen(backlog)

通常为服务器用。指定操作系统能在本端口上最大可以等待的还未被accept()处理的连接数量。

s.accept()

通常为服务器用。接受连接并返回 (conn, address),其中conn是新的套接字对象,

可以用这个新的套接字和某个连入的特定客户端通讯。

address是另一端的套接字地址端口信息,为(hostname, port)元组。

s.connect(address)

通常为客户端用。连接到远端address指定的地址和端口(为 (hostname, port) 元组形式)。

如果有错误则引发 socket.error。

s.connect_ex()

与上类似,但是成功时返回0,失败时返回 errno 的值。

s.close()

关闭套接字。服务器客户端都可使用。

s.shutdown(how)

关闭1个或2个连接。若how为 s.SHUT_RD,则不允许接收;

若为 s.SHUT_WR,则不允许发送;若为 s.SHUT_RDWR,则接收和发送都不允许。

UDP 数据读写

s.recvfrom(bufsize [,flags])

UDP专用。返回 (data, address) 对,address为 (hostname, port) 元组形式。

bufsize指定要接收的最大字节数。flags通常可以忽略(默认为0),

详可查看文档。

s.recvfrom_info(buffer [,nbytes [,flags]])

与 recvfrom() 类似,但接收的数据存储在入参对象buffer中,

nbytes指定要接收的最大字节数,如忽略则最大为buffer大小。

flags同上。

s.sendto(string [,flags] ,address)

UDP专用。将string发送到address指定的地址和端口

(为 (hostname, port) 元组形式)。返回发送的字节数。flags同上。

TCP 数据读写

s.recv(bufsize [,flags])

接收套接字数据,数据以字符串形式返回。bufsize指定要接收的字节数。

flags通常可以忽略(默认为0),详可查看文档。

s.recv_into(buffer [,nbytes [,flags]])

与 recv() 类似,但将数据写入支持缓冲区接口的对象buffer中,

nbytes指定要接收的最大字节数,如忽略则最大为buffer大小。

flags含义同上。

s.send(string [,flags])

将string中的数据发送到套接字,flags含义同上。

返回发送的字节数量(可能小于string中的字节数),如有错误则抛出异常。

s.sendall(string [,flags])

将string中的数据发送到套接字,但在返回之前会尝试发送所有数据。

成功则返回 None,失败则抛出异常。flags含义同上。

套接字参数相关方法

s.getsockname()

返回套接字自己的地址端口,通常为一个元组:(ipaddr, port)。

s.getpeername()

返回远端套接字的地址端口,通常为一个元组:(ipaddr, port),并非所有系统都支持该函数。

s.gettimeout()

返回当前套接字的超时秒数(浮点数),如果没有设置超时期,则返回None。

s.getsockopt(level, optname [,buflen])

返回套接字选项的值。level 定义选项的级别,

optname为特定的选项。 buflen表示接收选项的最大长度,通常可忽略。

s.settimeout(timeout)

设置套接字操作的超时秒数(浮点数),设None表示没有超时。如果发生超时,

则引发 socket.timeout 异常。

s.setblocking(flag)

若flag设为0,则套接字为非阻塞模式。在非阻塞模式下,

s.recv() 和 s.send() 调用将立即返回,若 s.recv() 没有发现任何数据、或者

s.send() 无法立即发送数据,那么将引发 socket.error 异常。

s.setsockopt(level, optname, value)

设置给定套接字选项的值。参数含义同 s.getsockopt()

文件相关

s.fileno()

返回套接字的文件描述符。

s.makefile([mode [,bufsize]])

创建与套接字关联的文件对象。mode和bufsize的含义与内置

open() 函数相同,文件对象使用套机子文件描述符的复制版本。

s.ioctl()

受限访问 Windows 上的 WSAIoctol 接口。详可查阅文档。

● socket模块的异常

socket模块定义了以下异常:

异常说明

error

继承自OSError,表示与套接字或地址有关的错误。它返回一个 (errno, mesg) 元组(错误编号、错误消息)

以及底层调用返回的错误。

herror

继承自OSError,表示与地址有关的错误。它返回一个 (errno, mesg) 元组(错误编号、错误消息)。

timeout

继承自OSError,套接字操作超时时出现的异常。异常值是字符串 'timeout'。

gaierror

继承自OSError,表示 getaddrinfo()和 getnameinfo() 函数中与地址有关的错误。

它返回一个 (errno, mesg) 元组(错误编号、错误消息)。

errno 为socket模块中定义的以下常量之一:

常量描述常量描述

EAI_ADDRFAMILY

不支持地址族

EAI_NODATA

没有与节点名称相关的地址

EAI_AGAIN

名称解析暂时失效

EAI_NONAME

未提供节点名称或服务名称

EAI_BADFLAGS

标志无效

EAI_PROTOCOL

不支持该协议

EAI_BADHINTS

提示不当

EAI_SERVICE

套接字类型不支持该服务名称

EAI_FAIL

不可恢复的名称解析失败

EAI_SOCKTYPE

不支持该套接字类型

EAI_FAMILY

主机不支持的地址组

EAI_SYSTEM

系统错误

EAI_MEMORY

内存分配失败

(3)select模块

select模块可使用select()和poll()系统调用。

select()通常用来实现轮询,可以在不使用线程或子进程的情况下,

实现与多个客户端进行通讯。它的用法直接模仿原始UNIX中的select()系统调用。

在 Linux 中,它可以用于文件、套接字、管道;在 Windows 中,它只能用于套接字。

poll()函数可以直接利用Linux底层的poll()系统调用,

Windows不支持poll()函数。

● select()

使用select()实现同时与多个客户端通信的核心编程思想是:select()

函数可以阻塞在多个套接字上,只要这些套接字中有一个收到数据或收到连接,

select()就会返回,并且在返回值中包含这个收到数据的套接字。

然后用户自己的服务器程序可以根据返回值自行判断,是哪个客户端对应的套接字收到了数据,

若返回的套接字是最原始的监听套接字,则说明有新客户端的连接请求。

select()函数的语法如下:

select(rlist, wlist, xlist [,timeout])

查询一组文件描述符的输入、输出和异常状态。前3个参数rlist、wlist、 xlist都是列表,每个列表包含一系列文件描述符或类似文件描述符的对象(当某个对象具有 fileno() 方法时,它就是类似文件描述符的对象,比如:套接字)。 rlist为输入文件描述符的列表、wlist为输出文件描述符的列表、 xlist为异常文件描述符的列表,这3个列表都可以是空列表。

一般情况下,本函数为阻塞运行,即当入参的上述3个列表中若没有事件发生,则本函数将阻塞挂起。 timeout参数为指定的超时秒数(浮点数),若忽略则为阻塞运行, 若为0则函数仅将执行一次轮询并立即返回。

当有事件在入参的3个列表中发生时,本函数即返回。返回值是一个列表元组:(rs, ws, xs), rs 是入参rlist的子集,为rlist中发生期待事件的文件描述符列表; 比如:若入参rlist为一系列套接字,若有一个或多个套接字收到数据, 那么select()将返回,并且在 rs 中包含这些收到数据的套接字。

同样的:ws 是入参wlist的子集,只要wlist 中的任何一个或多个文件描述符允许写入,那么select()将立即在 ws 中返回这个子集。 因此,往入参wlist中放入元素时必须十分小心。 最后,xs 是入参xlist的子集。

如果超时时没有对象准备就绪,那么将返回3个空列表。如果发生错误,那么将触发 select.error 异常。

以下为一个使用 select() 实现的服务器例子,功能为在服务器屏幕打印从客户端收到的任何数据,直到客户端关闭连接为止:

# select_server.py

import socket, select

s = socket.socket()

s.bind(('', 10001))

s.listen(5)

inputs = [s]

while True:

rs, ws, es = select.select(inputs, [], []) # 阻塞运行,若无新的事件本函数会挂起

for r in rs:

if r is s:

c, addr = s.accept()

print('Got connection from', addr)

inputs.append(c)

else:

try:

data = r.recv(1024)

disconnected = not data

except socket.error:

disconnected = True

if disconnected:

print(r.getpeername(), 'disconnected')

inputs.remove(r)

else:

print(data)

上面程序中,入参 inputs 的初始值只包含一个监听套接字s,当收到客户端的连接请求时, select()函数会返回,并且在 rs 中包含这个套接字。 然后s.accept()会新生成一个套接字 c,服务器程序会将其放入 inputs 列表。 以后若是收到这个客户端的数据,则select()返回时的 rs 中会包含这个新套接字 c, 若是收到其他客户端的连接请求时,则select()返回时的 rs 中会包含原始套接字 s。 之后的程序靠判断 rs 中究竟是哪个套接字,来决定后续的行为。

最后,若客户端调用close()关闭连接(本质上是发送一个长度为0的数据:b''), 则服务器收到这个0长度数据后,在屏幕打印关闭连接的客户端地址,并将这个与之对应的套接字移出 input 队列。

● poll()

poll()函数可创建利用poll()系统调用的“轮询对象”,Windows不支持 poll() 函数。

poll()返回的轮询对象支持以下方法:

方法说明

p.register(fd [,eventmask])

注册新的文件描述符fd,fd为一个文件描述符、 或一个类似文件描述符的对象(当某个对象具有fileno() 方法时,它就是类似文件描述符的对象, 比如套接字)。eventmask可取值见下表,可以“按位或”。 如果忽略eventmask,则仅检查 POLLIN, POLLPRI, POLLOUT 事件。

p.unregister(fd)

从轮询对象中删除文件描述符fd,如果没有注册,则引发 KeyError 异常。

p.poll([timeout])

对所有已注册的文件描述符进行轮询。timeout位可选的超时毫秒数(浮点数)。 返回一个元组列表,列表中每个元组的形式为:(fd, event),其中 fd 是文件描述符列表、 event 是指示事件的位掩码(含义见下表)。 例如,要检查 POLLIN 事件,只需使用event & POLLIN测试值即可。 如果返回空列表,则表示到达超时值且没有发生任何事件。

eventmask和event支持的事件:

常量描述常量描述

POLLIN

可用于读取的数据

POLLERR

错误情况

POLLPRI

可用于读取的紧急数据

POLLHUP

保持状态

POLLOUT

准备写入

POLLNVAL

无效请求

以下为一个使用 poll() 实现的服务器例子:

import socket, select

s = socket.socket()

s.bind(('', 8009))

s.listen(5)

fdmap = {}

p = select.poll()

p.register(s)

while True:

events = p.poll() # 阻塞运行,若无新的事件本函数会挂起

for fd, event in events:

if fd == s.fileno():

c, addr = s.accept()

print('Got connection from', addr)

p.register(c)

fdmap[c.fileno()] = c

elif event & select.POLLIN:

data = fdmap[fd].recv(1024)

if not data:

print(fdmap[fd].getpeername(), 'disconnected')

p.unregister(fd)

del fdmap[fd]

else:

print(data)

总体来说,poll()的使用比select()略为简单。上面程序中,首先通过 p.register(s)注册要监听的套接字,然后调用events = p.poll() 等待连接或数据,当p.poll()返回时,即遍历其返回值。若fd为监听套接字 s 的文件描述符,则通过调用s.accept()新建一个与此客户端通信的套接字, 然后其通过p.register(c)注册进监听事件,再将这个套接字放入字典 fdmap 以备以后可直接通过 fd 拿出套接字。

之后,每当收到新的数据,若非监听套接字 s 收到数据,则说明是与客户端通信的某个套接字 c 收到了数据,则通过data = fdmap[fd].recv(1024)把数据收进来。若收到数据长度为0, 说明是用户端关闭套接字,则在本处理程序中,使用p.unregister(fd) 解除对这个套接字的监听。最后在 fdmap 字典中删除这个套接字的索引。

(4)asyncore模块

asyncore模块用来编写“异步”网络程序(内部核心原理是使用select()系统调用), 它可以用于希望提供并发性但又无法使用多线程(或子进程)的环境。

回忆一下异步的核心思想:当发生某事件时(比如收到客户端数据、或收到新的客户端连接请求等等), 由操作系统来回调运行你先前为这个事件定义好的函数或方法。这些事先定义好的函数或方法只会由操作系统来调用, 而不会影响你自己程序的主流程。

不过由于asyncore模块过于底层,一般工作中不太会直接使用asyncore模块编写网络程序, 而会用其他更高级的模块(如:asynchat等),这里仅仅用asyncore模块来说明异步网络编程的基本方法。

asyncore模块主要提供了一个 dispatcher 类,其所有功能都几乎都由 dispatcher 类提供, dispatcher 类内部封装了一个普通套接字对象,其初始化语法如下:

dispatcher([sock])

上面的 dispatch() 函数定义事件驱动型非阻塞套接字对象(比较拗口哈)。sock是现有的套接字对象。 如果忽略该参数,则后面需使用 create_socket() 方法创建套接字。一般我们在编程中通过继承 dispatcher 类并重定义它的一些方法,来实现自己需要的功能。

● dispatcher 对象支持以下方法

方法或函数说明

可重定义的基类方法

d.handle_accept()

收到新连接时系统会自动调用该方法。

d.handle_connect()

作为客户端进行连接。

d.handle_close()

套接字关闭时系统会自动调用该方法。

d.handle_error()

发生未捕获的异常时系统会自动调用该方法。

d.handle_expt()

收到套接字外带数据时系统会自动调用该方法。

d.handle_read()

从套接字收到新数据时,系统会自动调用该方法。

d.handle_write()

当 d.writable() 方法返回True时,系统会自动调用该方法。

d.readable()

内部的select()方法使用该函数查看对象是否准备读取数据,如果是则返回 True。 接下来系统会自动调用 d.handle_read() 来读取数据。

d.writable()

select()方法使用该函数查看对象是否想写入数据,如果是则返回 True。

底层方法(直接操作其内部的套接字)

d.create_socket(family, type)

新建套接字,参数含义与底层 socket() 相同。

d.bind(address)

将套接字绑定到address,address是一个 (host, port) 元组。

d.listen([backlog])

监听传入连接,参数含义与底层 listen() 相同。

d.accept()

接受连接,返回元组 (client, addr),其中client是新建的套接字对象, addr是客户端的地址/端口元组。

d.close()

关闭套接字

d.connect(address)

建立连接,address是一个 (host, port) 元组。

d.recv(size)

最大接收size个字节,返回空字符串表示客户端已关闭了通道。

d.send(data)

发送数据data(字符串)

asyncore 模块的函数

loop([timeout [,use_poll [,map[,count]]]])

无限轮询事件。使用 select() 函数进行轮询,如果use_poll参数为True, 则使用 poll() 进行轮询。timeout表示超时秒数,默认为30秒。 map是一个字典,包含所有要监视的通道。count 指定返回之前要执行的轮询操作次数(默认为None,即一直轮询,直到所有通道关闭)

● asyncore的使用示例

下例展示了一个asyncore的服务器程序,它的功能是:当收到客户端发送过来的任何数据时, 在服务器屏幕上显示这个收到的数据,并将服务器本地时间发送给客户端。 由客户端决定何时关闭连接。

# asyncore_server.py

import asyncore

import socket

import time

# 该类仅处理“接受连接”事件

class asyncore_server(asyncore.dispatcher):

def __init__(self, port):

asyncore.dispatcher.__init__(self)

self.create_socket(socket.AF_INET, socket.SOCK_STREAM)

self.bind(('', port))

self.listen(5)

def handle_accept(self):

client, addr = self.accept()

return asyncore_tcp_handler(client)

# 该类为每个具体客户端生成一个实例,并处理服务器和这个客户端的通讯

class asyncore_tcp_handler(asyncore.dispatcher):

def __init__(self, sock = None):

asyncore.dispatcher.__init__(self, sock)

self.writable_flag = False

def handle_read(self):

recv_data = self.recv(4096)

if len(recv_data) > 0:

print(recv_data)

self.writable_flag = True

def writable(self):

return self.writable_flag

def handle_write(self):

timestr = time.ctime(time.time()) + "\r\n"

bytes_sent = self.send(timestr.encode('ascii'))

self.writable_flag = False

def handle_close(self):

print('The client is closed.')

self.close()

a = asyncore_server(10001) # 创建监听服务器

asyncore.loop() # 无限轮询

程序要点分析如下:

(1)本程序定义了一个 asycore_server 类和一个 asyncore_tcp_handler 类, 都继承自asyncore.dispatcher。前者(asycore_server类)用于监听所有的新连接事件, 后者(asyncore_tcp_handler类)用于处理与某个已建立连接的具体服务端通信。

(2)程序的最下面两行:先建立一个 asycore_server 的实例,然后进入无限循环, 监听 10001 端口的所有新连接事件。

(3)当有新的客户端连入时,系统会自动回调此监听实例的 handle_accept() 方法, 在这个方法中,我们通过调用底层的accept()方法,得到一个新的套接字 client, 并用这个新套接字生成一个 ascycore_tcp_handler 实例,负责与这个客户端一对一通信。

(4)当已建立连接的客户端向服务器发送数据时,系统会自动调用 asyncore_tcp_handler 实例的 handle_read()方法。在这个方法中,我们通过调用底层的recv()方法, 得到客户端法来的数据,并将其 print 到服务器屏幕上,然后将我们自定义的实例属性 writable_flag设为 True。

(5)由于我们已经重写了实例的writable()方法,当我们在上面将实例属性 writable_flag设为 True时,这个writable()方法也会返回 True。 由于系统在后台不停地在监视writable()方法的返回值,当发现这个方法返回值为 True时,系统即自动调用本实例的 handle_write()方法。

(6)在handle_write()方法中,我们通过调用底层方法send(), 将本地时间发送给客户端。发送完后别忘了将writable_flag属性设回 False, 否则系统会不停地调用handle_write()方法。

(7)当客户端提出关闭连接时(即客户端调用:close()方法), 系统回会自动调用本实例的handle_close()方法,我们可以在此方法中调用底层的 close()方法,关闭服务端与此客户端的连接的连接,然后本实例就会自动销毁。

以下是一个客户端的例子,用来测试这个服务器:

from socket import *

import time

s = socket(AF_INET, SOCK_STREAM)

s.connect(('127.0.0.1', 10001))

# 第一次发送数据并接收

s.send('Hello'.encode('ascii'))

tm = s.recv(1024)

print("The time is %s" % tm.decode('ascii'))

# 等待1秒钟

time.sleep(1)

# 第二次发送数据并接收

s.send('World'.encode('ascii'))

tm = s.recv(1024)

print("The time is %s" % tm.decode('ascii'))

# 关闭连接

s.close()

(5)asynchat模块

asynchat模块将asyncore的底层I/O功能进行了封装,提供了更高级的编程接口, 非常适用于基于简单请求/响应机制的网络协议(如 HTTP)。

asynchat模块提供了一个名为async_chat的基类,用户需要继承这个基类, 并自定义两个必要的方法:incoming_data()和found_terminator()。 当网络收到数据时,系统会自动调用incoming_data()方法。

对于发送数据,async_chat在内部实现了一个 FIFO 队列, 用户可以通过调用push()方法将要发送的数据压入队列,然后就不用管了, 系统会自动在网络可发送时,将 FIFO 队列中的数据发送出去。

可使用以下函数,定义async_chat的实例,sock是与客户端一对一通信的套接字对象。

async_chat([sock])

async_chat的实例除了继承了asyncore.dispatcher基类提供的方法之外, 还具有以下自己的方法:

方法说明

a.collect_incoming_data(data)

通道收到数据时系统会自动调用该方法。data是本实例套接字通道收到的数据, 用户必须自己实现该方法,在该方法中用户通常需要将收到的数据保存起来已供后续处理。

a.set_terminator(term)

设置本实例套接字通道的终止符,term可以是字符串、整数或者 None。 如果term是字符串,则在输入流出现该字符串时,系统会自动调用 a.found_terminator()方法。如果term是整数,则它指定一次收的字节数, 当通道收到指定的字节数后,系统自动调用方法。 如果term是 None,则持续收集数据。

a.get_terminator()

返回本实例套接字通道的终止符。

a.found_terminator()

当本实例的套接字通道收到由本实例的set_terminator()方法设置终止符时, 系统会自动调用该方法。该方法必须由用户实现。 通常,它会处理此前由collect_incoming_data()方法保存的数据。

a.push(data)

将数据压入 FIFO 队列,data是要发送的字节序列。

a.discard_buffers()

丢弃 FIFO 队列中保存的所有数据。

a.close_when_done()

将 None 压入 FIFO 队列,表示传出数据流已到达文件尾。 当系统从 FIFO 中读到 None 时将关闭本套接字通道。

a.push_with_producer(producer)

将一生产者对象producer加入到生产者 FIFO 队列。 producer可以是任何具有方法more()的对象。 重复调用本方法可以将多个生产者对象推入生产者 FIFO 队列。

simple_producer(data [,buffer_size])

这是 asynchat 模块为a.push_with_producer()单独定义的类, 可以用来创建简单的生产者对象,从字节序列data生成数据块, buffer_size指定数据块大小(默认512)。

asynchat 模块总是和 asyncore 模块一起使用。一般使用asyncore.dispatch实例来监听端口, 然后由 asynchat 模块的async_chat的子类实例来处理与每个客户端的连接。下面是一个简单的实例, 服务器在屏幕打印任何从客户端收到的数据,当发现终止符b'\r\n\r\n'时, 向客户端发送服务器本地时间,并关闭这个套接字。

# asynchat_server.py

import asynchat, asyncore, socket

import time

class asyncore_http(asyncore.dispatcher):

def __init__(self, port):

asyncore.dispatcher.__init__(self)

self.create_socket(socket.AF_INET, socket.SOCK_STREAM)

self.bind(('', port))

self.listen(5)

def handle_accept(self):

client, addr = self.accept()

return asynchat_tcp_handler(client)

class asynchat_tcp_handler(asynchat.async_chat):

def __init__(self, conn=None):

asynchat.async_chat.__init__(self, conn)

self.data = []

self.got_terminator = False

self.set_terminator(b'\r\n\r\n')

def collect_incoming_data(self, data):

if not self.got_terminator:

self.data.append(data)

print(data)

def found_terminator(self):

self.got_terminator = True

timestr = time.ctime(time.time()) + "\r\n"

self.push(timestr.encode('ascii'))

self.close_when_done()

a = asyncore_http(10001)

asyncore.loop()

以上例子对比前面的纯使用 asyncore 模块的例子,在写与客户端通信的程序时,要简洁很多。

(6)socketserver模块

socketserver模块包括很多TCP、UDP、UNIX域 套接字服务器实现的类,用它们来编写服务器程序非常方便。 要使用该模块,用户必须继承并实现2个类:一个是 Handler 类(事件处理程序)、一个是 Server 类(服务器程序)。 这两个类需要配合使用。

● Handler 类(事件处理程序)

用户需要自定义一个 Handler 类(继承自基类BaseRequestHandler), 其中需自定义实现以下方法:

方法说明

h.setup()

对本实例进行一些初始化工作,默认情况下,它不执行任何操作。 如果用户希望在处理网络连接前,先作一些配置工作(如建立 SSL 连接), 那么可以改写该方法。

h.handle()

当 Server 类监听到新的客户端连接请求或收到来自已连接的客户端的数据, 系统将自动回调这个函数。在这个函数中,用户可以自定处理客户端连接或数据。

h.finish()

完成h.handle()方法后,系统会自动回调此本方法作一些清理工作。 默认情况下,它不执行任何操作。如果执行h.setup()或h.handle()时发生异常, 则不会调用本方法。

BaseRequestHandler 实例的一些可用属性:

属性说明

h.request

对于 TCP 连接,是本实例内置的套接字对象。 对于 UDP 连接,是包好收到数据的字节字符串。

h.client_address

为客户端的(地址, 端口)元组。

h.server

本实例对应的 Server 实例。

h.rfile

可以像操作文件对象一样,从h.rfile读取客户端数据 (用例如:data = h.rfile.readline())。

h.wfile

可以像操作文件对象一样,向h.wfile写入数据, 这些数据会被传送到已建立连接的客户端 (用例如:h.wfile.write('Hello'.encode('ascii')) )。

BaseRequestHandler还有两个派生类,用于简化操作。 如果用户仅使用 TCP 进行通信,那么自定义的 Handler 类可继承自StreamRequestHandler类。 如果用户仅使用 UDP 进行通信,那么自定义的 Handler 类可继承自DatagramRequestHandler类。 在这两种情况下,用户仅需实现h.handle()方法就可以了。

● Server 类(服务器程序)

定义完上面的 Handler 类后,用户还需要定义一个 Server 类。 socketserver 模块提供了5个可供用户继承的类,分别是:

● BaseServer(address, handler);

● UDPServer(address, handler):继承自 BaseServer;

● TCPServer(address, handler):继承自 BaseServer;

● UnixDatagramServer(address, handler):继承自 UDPServer,UNIX域专用;

● UnixStreamServer(address, handler):继承自 TCPServer,UNIX域专用;

其中入参address为 (ipaddr, port) 元组,

handler为用户为此 Server 实例配对的自定义 Handler 类(注意是“类”,不是实例)。

用户可根据自己的连接类型,自行选择继承相应的 Server 类实现服务程序。

Server 实例具有以下共有方法和属性:

方法或属性说明

s.fileno()

返回本实例对应的套接字的文件描述符,使得本实例可供select()直接使用。

s.serve_forever()

进入无限循环,处理本实例对应端口的所有请求。

s.shutdown()

停止s.serve_forever()无限循环。

s.server_address

本实例监听的(地址, 端口)元组。

s.socket

本实例对应的套接字对象。

s.RequestHandlerClass

本实例对应的 Handler 类(事件处理)。

Server 还可以定义以下“类变量”来配置一些基本参数;以下的“类方法”一般不必动,但也可以改写:

类变量或类方法说明

Server.socket_type

服务器使用的套接字类型,如socket.SOCK_STREAM或

socket.SOCK_DGRAM等。

Server.address_family

服务器套接字使用的地址族,如:socket.AF_INET等。

Server.request_queue_size

传递给套接字的listen()方法的队列值大小,默认值为 5。

Server.timeout

服务器等待新请求的超时秒数,超时期结束后,服务器会自动回调本类的

Server.handle_timeout()类方法。

Server.allow_reuse_address

布尔标志,指示套接字是否允许重用地址。在程序终止后,一般其他程序若要使用本端口,

需要等几分钟时间。但若此标志为 True,则其他程序可在本程序结束后立即使用本端口。

默认为 False。

Server.bind()

对服务器执行bind()操作。

Server.activate()

对服务器执行listen()操作。

Server.handle_timeout()

服务器发生超时时会自动回调本方法。

Server.handle_error(request, client_address)

此方法处理操作过程中发生的未处理异常,若要获取关于上一个异常的信息,

可使用 traceback 模块的sys.exc_info()或其他函数。

Server.verify_request(request, client_address)

在进一步处理之前,如果需要验证连接,则可以重新定义本方法。

本方法可以实现防火墙功能或执行某写验证。

● socketserver 使用示例

以下为一个单进程、单线程的 socketserver 服务器程序示例:

# my_socketserver.py

from socketserver import TCPServer, StreamRequestHandler

import time

class MyTCPHandler(StreamRequestHandler):

def handle(self):

print('Got connection from: ', self.client_address)

while True:

recv_data = self.request.recv(1024)

if len(recv_data):

print(recv_data)

if b'\r\n' in recv_data:

resp = time.ctime() + "\r\n"

self.request.send(resp.encode('ascii'))

else:

print(self.client_address, ' Disconnected')

break;

class MyTCPServer(TCPServer):

allow_reuse_address = True

serv = MyTCPServer(('', 10001), MyTCPHandler)

serv.serve_forever()

在上面的示例程序中,用户定义了两个继承类:MyTcpHandler 用于处理客户端连接和客户端数据, MyTcpServer 用于定义服务器类。

(1)在主程序中,先初始化一个 serv 实例,并为其绑定服务器地址/端口和 Handler 类。之后, 即调用 serv 实例的 serve_forever() 方法,进入无限循环监听端口。 此时会在 serv 实例内部自动生成一个 MyTCPHandler 的实例,用以监听服务器端口并处理数据。

(2)当客户端发起连接时,系统会自动回调内部 MyTCPHandler 的实例的handle()方法。 在此方法中,示例程序使用while True:和self.request.recv()结构, 接收从客户端发来的数据。

(3)若客户端发来普通数据,则在服务器在屏幕上打印这个发来的数据。 若客户端发来的数据中含有换行符 b'\r\n',则处理程序将本地时间发送给客户端。

(4)若客户端关闭连接(即发送长度为0的数据),则处理程序通过break语句退出 while True:循环,并结束handle()方法,此时服务端也会在内部关闭连接, 并销毁这个内部的 MyTCPHandler 实例。再生成一个新的 MyTCPHandler 实例来监听和处理下一次客户端的连接。

(5)需要理解的是:对于这种单进程单线程的服务器程序,当前一个客户端与服务器程序还处于连接状态时, 下一个客户端是无法连入这个服务器程序的,只能在操作系统层面等待(listen()函数的入参 即是用来指示:这个端口在操作系统层面可以等待的客户端的队列的长度)。 只有当前一个客户端关闭连接后,服务器程序才能从操作系统的等待队列中,取出下一个客户端进行处理。

以下是客户端测试程序的例子:

# client.py

from socket import *

import time

s = socket(AF_INET, SOCK_STREAM)

s.connect(('127.0.0.1', 10001))

s.send('Hello'.encode('ascii'))

time.sleep(1)

s.send('World\r\n'.encode('ascii'))

tm = s.recv(1024)

print("The time is %s" % tm.decode('ascii'))

s.close()

● socketserver的并发处理

在前面的例子中,服务器程序不能同时处理多个客户端的连接,只能等一个客户端关闭连接后, 再处理下一个客户端的数据。socketserver 模块提供了非常方便的并发扩展功能, 只要将上面的程序稍作修改,就能变成“子进程”或“多线程”并发模式,同时处理若干个客户端的连接。

简单来讲,socketserver 模块提供了几个UDPServer和TCPServer的派生类, 用以实现并发功能,这些派生类分别是:

● ForkingUDPServer(address, handler):UDPServer 的子进程并发版(Windows不支持);

● ForkingTCPServer(address, handler):TCPServer 的子进程并发版(Windows不支持);

● TheadingUDPServer(address, handler):UDPServer 的多线程并发版;

● TheadingTCPServer(address, handler):TCPServer 的多线程并发版;;

在实际使用中,只要从以上几个类继承实现自己的 Server 类就可以了。对,就是这么简单!

比如,对于上面的服务器示例程序,只要将程序中的TCPServer改成TheadingTCPServer,

就变成了多进程并发服务器程序,程序会为每个客户端连接创建一个独立的线程,可同时与多个客户端进行通信。

修改后的多线程版服务器程序如下:

# my_socketserver.py

from socketserver import ThreadingTCPServer, StreamRequestHandler

import time

class MyTCPHandler(StreamRequestHandler):

def handle(self):

print('Got connection from: ', self.client_address)

while True:

recv_data = self.request.recv(1024)

if len(recv_data):

print(recv_data)

if b'\r\n' in recv_data:

resp = time.ctime() + "\r\n"

self.request.send(resp.encode('ascii'))

else:

print(self.client_address, ' Disconnected')

break;

class MyTCPServer(ThreadingTCPServer):

allow_reuse_address = True

serv = MyTCPServer(('', 10001), MyTCPHandler)

serv.serve_forever()

对于ForkingUDPServer和ForkingTCPServer,额外有以下控制属性:

属性说明

max_children

子进程的最大数量

timeout

收集僵尸进程的操作时间间隔

active_children

跟踪正在运行多少个活动进程

对于TheadingUDPServer和TheadingTCPServer,额外有以下控制属性:

属性说明

daemon_threads

若设为True,则这些线程都变成后台线程,会随主线程退出而退出。 默认为 False。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值