文章大纲
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()