文章大纲
Python 中有很多库实现了常见的网络协议,可以专注于程序逻辑而不必关心底层线路传输的问题。
常用的模块
标准库中有很多的网络模块,常用的有 scocket
和 SocketServer
相关的模块。
socket
网络编程中的一个基本组件是套接字(socket)。套接字本质上是一个信息通道,两端各有一个程序。
套接字分为两类:服务器套接字和客户端套接字。创建服务器套接字后,让它等待连接请求的到来,它将监听某个网络地址(由 IP 和 PORT 组成),直到客户端套接字建立连接,随后服务器和客户端就可以进行通信了。
客户端套接字处理起来比服务器套接字要容易,它只需连接,完成任务后断开连接就可以了,服务器套接字需要随时准备处理客户端连接,有时还须处理多个连接。
套接子是模块 socket
中的 socket
类的实例。_class_ socket.socket(_family=AF_INET_, _type=SOCK_STREAM_, _proto=0_, _fileno=None_)
实例化套接字时最多可以指定三个参数:
- 地址族,默认为
socket.AF_INET
(ipv4)socket.AF_INET6
是 ipv6 - 流套接字(默认)(
socket.SOCK_STREAM
)还是数据包套接字(socket.SOCK_DGRAM
) - 协议号通常为零并且可以省略
创建普通套接字时,不用提供任何参数。
服务器套接字先调用 bind
方法,再调用 listen
方法来监听特定的地址。
随后客户端套接字调用 connect
方法,指定服务器套接字 bind
方法指定的地址。
方法 listen
接受一个参数,用于指定最多可以有多少个连接在队列中等待接纳,到达这个数量后将开始拒绝。
服务器套接字开始监听后,就可以接受客户端的连接了,通过调用 accept
方法来完成。这个方法将阻断到客户端连接到来为止,然后返回一个格式为 (client, address)
的元组,其中 client
是一个客户端套接字, address
是客户端的地址。这种模式称为同步网络编程。
服务器处理客户端的连接,然后再次调用 accept
以接着等待新连接的到来,通常是在一个无限循环中完成的。
为了传输数据,套接子提供了两个方法 send
和 recv
。
要发送数据,可调用方法 send
并提供一个 byte
类型的数据。要接收数据,可调用 recv
方法,并指定最多接受多少个字节的数据,1024 是个不错的选择。
服务器套接字示例:
import socket
HOST, PORT = '', 1024
with socket.socket() as s:
s.bind((HOST, PORT))
s.listen(5)
while True:
c, addr = s.accept()
print('Client: {}, Addr: {}'.format(c, addr))
data = c.recv(1024)
c.send(data.upper())
c.close
客户端套接字示例:
import sys, socket
HOST, PORT = "localhost", 1024
with socket.socket() as s:
data = "".join(sys.argv[1:])
s.connect((HOST, PORT))
s.send(data.encode())
received = s.recv(1024)
print(str(received))
模块 urllib 和 urllib2
这两个模块能够通过网络访问文件,urllib2
相比较 urllib
而言能实现 HTTP 身份验证或 Cookie ,或者编写扩展来处理自己的协议,对于简单的下载 urllib
足以。
打开远程文件
使用模块 urllib.request
中的 urlopen
方法,将 URL 作为参数,将访问网页文件对象。urlopen
返回的类似于文件的对象支持 close
, read
, readline
和 readlines
方法,还支持迭代。
示例:
from urllib.request import urlopen
import re
data = urlopen('http://www.imxcai.com')
text = data.read().decode("utf-8")
m = re.search('<a href="([^"]+)" .*?>Python</a>', text, re.IGNORECASE)
print(m.group(1))
下载远程文件
要下载文件可以使用 urllib
中的 urlretrieve
方法。该函数不返回类似于文件的对象,而是返回一个格式为 (filename, headers)
的元组,filename
是本地文件的名称,自动创建的,headers
包含远程文件的信息。
urlretrieve
第一个参数指定下载的资源URL,第二个参数可以指定下载的数据存放位置,如果不指定将存放在临时位置,可以调用 urlcleanup
且不提供任何参数就可以帮助清空数据。
示例:
>>> from urllib.request import urlretrieve
>>> data = urlretrieve('http://www.imxcai.com')
>>> data
('/tmp/tmp969pi230', <http.client.HTTPMessage object at 0x7f1dec0c30e0>)
其它模块
Python 标准库中的一些与网络相关的模块:
模块 | 描述 |
---|---|
asynchat | 包含补充asyncore的功能 |
asyncore | 异步套接字处理程序 |
cgi | 基本的CGI支持 |
Cookie | Cookie对象操作,主要用于服务器 |
cookielib | 客户端Cookie支持 |
电子邮件(包括MIME)支持 | |
ftplib | FTP客户端模块 |
gopherlib | Gopher客户端模块 |
httplib | HTTP 客户端模块 |
imaplib | IMAP4客户端模块 |
mailbox | 读取多种邮箱格式 |
mailcap | 通过mailcap文件访问MIME配置 |
mhlib | 访问MH邮箱 |
nntplib | NNTP客户端模块 |
poplib | POP客户端模块 |
robotparser | 解析Web服务器robot文件 |
SimpleXMLRPCServer | 一个简单的XML-RPC服务器 |
smtpd | SMTP服务器模块 |
smtplib | SMTP客户端模块 |
telnetlib | Telnet客户端模块 |
urlparse | 用于解读URL |
xmlrpclib | XML-RPC客户端支持 |
SocketServer 及相关的类
模块 SocketServer
是标准库提供的服务器框架基石,这个框架包括 BaseHTTPServer
、 SimpleHTTPServer
、CGIHTTPServer
SimpleXMLRPCServer
和 DocXMLRPCServer
等服务器。它们都在基础服务器的基础功能上添加了各种功能:
+------------+
| BaseServer |
+------------+
|
v
+-----------+ +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+ +------------------+
|
v
+-----------+ +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+ +--------------------+
SocketServer
包含四个基本的服务器:
TCPServer
支持 TCP 套接字流UDPServer
支持 UDP 数据包套接字UnixServer
UnixDatagramServer
基于 SockerServer
的极简服务器示例:
from socketserver import TCPServer, StreamRequestHandler
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('Got connection from {}'.format(addr))
self.wfile.write(b'Thank you for connecting')
server = TCPServer(('', 1234), Handler)
server.serve_forever()
多个连接
前面的服务器解决方法都是同步的:意味着不能同时处理多个客户端的连接请求,只能处理完一个然后再处理第二个。
处理多个连接的主要方式有三种:分叉(forking)、线程化和异步I/O 。
分叉占用的资源较多,且在客户端很多时可伸缩性不佳,而线程化可能带来同步问题。
分叉和线程是什么?
fork
是一个 UNIX 术语,对进程进行 fork ,基本上是赋值它,这样得到的两个进程都将从当前位置开始继续往下执行,且每个进程都有自己的内存副本。原来的进程为父进程,复制的进程为子进程,进程能够判断它们是原始进程还是子进程,因此能够执行不同的操作。
在分叉的服务器中,对于每个客户端连接,都将通过分叉创建一个子进程,父进程继续监听新连接,而子进程负责处理客户端请求。客户端请求结束后,子进程直接退出,由于分叉出来的进程并行运行,因此客户端无需等待。
鉴于分叉占用的资源较多,还有另一种解决方法:线程化。
线程是轻量级的进程(子进程),都位于同一个进程中并共享内存。由于线程共享内存,必须确保它们不会彼此干扰或同时修改同一项数据,否则将引起混乱,这属于同步问题。
使用 SocketServer 实现分叉和线程化
使用 SocketServer
创建分叉或线程化服务器非常简单,仅当 handle
需要很长时间才能执行完毕时,分叉和线程化才能提供帮助。
分叉服务器示例:
from socketserver import TCPServer, ForkingMixIn, StreamRequestHandler
from time import sleep
class Server(ForkingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('Got connection from {}.'.format(addr))
sleep(1000)
self.wfile.write(b'Thank you for connecting!')
server = Server(('', 1234), Handler)
server.serve_forever()
多个客户端同时连接,此时系统上的进程可以看到 fork 出了多个子进程:
ps -axf -o pid,ppid,comm | grep 72716
72716 6765 | | | | \_ python
72746 72716 | | | | \_ python
72789 72716 | | | | \_ python
72843 72716 | | | | \_ python
72870 72716 | | | | \_ python
72918 72716 | | | | \_ python
72945 72716 | | | | \_ python
73016 72716 | | | | \_ python
线程化服务器示例:
from socketserver import TCPServer, StreamRequestHandler, ThreadingMixIn
from time import sleep
class Server(ThreadingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print('Got connection from {}.'.format(addr))
sleep(1000)
self.wfile.write(b'Thank you for connecting!')
server = Server(('', 1234), Handler)
server.serve_forever()
查看线程的信息:
pstree -p 75035
python(75035)─┬─{python}(75131)
├─{python}(75183)
├─{python}(75210)
├─{python}(75246)
├─{python}(75273)
├─{python}(75314)
└─{python}(75348)
使用 select 和 poll 实现异步 I/O
当服务器与客户端通信时,来自客户端的数据可能时断时续,可以通过分叉和线程化解决,还有另一种做法是只处理当前正在通信的客户端,无需不断监听,只需监听后将客户端加入队列即可。这是框架 asyncore/asynchat
和 Twisted
采取的方法。这种方法的基石是函数 select
或 poll
。两个函数都位于 select
模块中,其中 poll
可伸缩性高,但只有 UNIX 系统支持。
函数 select
接受三个必不可少的参数和一个可选参数,其中前三个参数为序列,第四个参数为超时时间,单位是秒。
三个序列分别表示需要输入和输出以及发生异常的连接,如果没有指定超时时间,select
将阻断到有文件描述符准备就绪时,如果超时时间为零,将不断轮询。
select
返回三个序列,也就是一个长度为 3 的元组,每个序列都包含相应参数中处于活动状态的文件描述符。返回的第一个序列包含有数据需要读取的所有输入文件描述符。
使用 select
的服务器示例:
import socket, select
s = socket.socket()
host = ''
port = 1234
s.bind((host, port))
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(b'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)
测试客户端:
import sys, socket
from time import sleep
HOST, PORT = "localhost", 1234
with socket.socket() as s:
data = "".join(sys.argv[1:])
s.connect((HOST, PORT))
while True:
sleep(3)
s.send(data.encode())
received = s.recv(1024)
print(str(received))
输出效果:
b'Got connection from' ('127.0.0.1', 59308)
b'client_A'
b'client_A'
b'Got connection from' ('127.0.0.1', 51866)
b'client_A'
b'Client_B'
b'client_A'
b'Client_B'
b'client_A'
b'Client_B'
方法 poll
使用起来比 select
容易。
调用 poll
时,会返回一个轮询对象,可使用 register
方法项这个对象注册文件描述符,注册后可以通过 unregister
将其删除。
注册对象后,可调用其方法 poll
,将返回一个包含 (fd, event)
元组的列表。fd
为文件描述符,event
是发生的事件,它是一个位掩码,是一个整数,各个位对应不同的事件。
各个事件是用 select
模块中的常量表示的,要检查指定的位是否为1 ,可以使用按位与运算符:if event & select.POLLIN: ...
select
模块中轮询事件常量:
事件名 | 描述 |
---|---|
POLLIN | 文件描述符中有需要读取的数据 |
POLLPRI | 文件描述符中有需要读取的紧急数据 |
POLLOUT | 文件描述符为写入数据做好了准备 |
POLLERR | 文件描述符出现了错误状态 |
POLLHUP | 挂起。连接已断开 |
POLLNVAL | 无效请求。连接未打开 |
使用 poll
的简单服务器示例:
import socket, select
s = socket.socket()
host, port = '', 1234
s.bind((host, port))
fdmap = {s.fileno(): s}
s.listen(5)
p = select.poll()
p.register(s)
while True:
events = p.poll()
for fd, event in events:
if fd in fdmap:
c, addr = s.accept()
print('Got connectin from {}.'.format(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)
Twisted
Twisted 是由 Twisted Matrix Laboratories 开发的,是一个事件驱动的 Python 网络框架。
Twisted 是一个功能及其丰富的框架,支持 Web 服务器和客户端、SSH2、
SMTP、POP3、IMAP4、AIM、ICQ、IRC、MSN、Jabber、NNTP、DNS等。
编写 Twisted 服务器
Twisted
采用的是基于事件的方法,只需实现处理如下情形的事件处理程序:
- 客户端发起连接
- 有数据到来
- 客户端断开连接
专用类可在基本类的基础上定义更细致的事件。
事件处理程序是在协议中定义的,还需一个工厂,它能够在新连接到来时创建这样的协议对象。
如果指向创建自定义协议类的实例,可使用 Twisted
自带的工厂 twisted.internet.protocol
中的 Factory
类。
编写自定义协议时,将模块 twisted.internet.protocol
中的 Protocol
作为超类。有新连接时调用事件处理程序 connectionMade
;连接中断时调用 connectionLost
, 来自客户端的数据通过处理程序 dataReceived
接收的。
不能使用事件处理策略来向客户端发送数据。需要使用对象 self.transport
来完成,包含一个 write
方法,还有一个 client
属性,包含客户端的地址。
使用 Twisted 创建的简单服务器示例:
from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factory, connectionDone
from twisted.python.failure import Failure
class SimpleLogger(Protocol):
def connectionLost(self, reason: Failure = ...) -> None:
return super().connectionLost(reason)
def connectionMade(self):
print('Got a connection')
return super().connectionMade()
def dataReceived(self, data: bytes) -> None:
print(data)
return super().dataReceived(data)
factory = Factory()
factory.protocol = SimpleLogger
reactor.listenTCP(1234, factory)
reactor.run()