Posts
Python网络编程(第3版)

Python网络编程(第3版)

那会儿想学网络编程相关的 python稍微熟一点 知乎上的推荐书单之一

1. C/S网络编程

1.1 协议栈与库

复杂网络建立在简单网络服务的基础之上 virtualenv可以从当前的py环境剥离出来一个较为纯净的py环境 anaconda是重量级的包管理工具, 其功能包括virtualenv,但不限于此 miniconda则为轻量级的anaconda

1.2 应用层

到应用层这里就是requests这种高度封装,对很多情况都有了解决方案,进行适当的参数选择,使得http变得极其简洁

import requests
def get_token(appid: str, secret: str) -> (str, str):
    url = f"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}"
    res = requests.get(url).text
    res = json.loads(res)
    return res['access_token'], res['expires_in']

1.3 使用协议

google地图的api也需要绑定项目id之类的了,换成微信小程序版本

import http.client
import json
 
# this function call doing encoding job for request param
from urllib.parse import quote_plus
 
def get_token(appid: str, secret: str) -> (str, str):
    path = f"/cgi-bin/token?grant_type=client_credential&appid={appid}&secret={secret}"
    conn = http.client.HTTPSConnection('api.weixin.qq.com')
    conn.request('GET', path)
    raw_res = conn.getresponse().read()
    res = json.loads(raw_res.decode('utf8'))
  
    print(res['access_token'], res['expires_in'])  
    return res['access_token'], res['expires_in']

1.4 原始网络会话

基于socket编程 惨痛的教训, 写请求header不能有空白,一个都不行, 尤其是python的缩进, 能让bug产生于无形之中

import socket
import ssl
 
 
 
def get_token(appid: str, secret: str) -> (str, str):
    req = '''
GET /cgi-bin/token?grant_type=client_credential&appid={}&secret={} HTTP/1.1rn
HOST: api.weixin.qq.comrn
User-Agent: wcw-py-demorn
Connection: closern
rn
'''
    
    req = req.format(appid, secret)
    
    sock = socket.socket()
    sock = ssl.wrap_socket(sock)
    sock.connect(('api.weixin.qq.com', 443))
    
    sock.sendall(req.encode('ascii'))
 
    raw_res = b''
    while 1:
        more = sock.recv(1 << 12)
        if not more: break
        raw_res += more
 
    sock.close()
    print(raw_res.decode('utf8'))
    return raw_res.decode('utf8')

TCP协议下的sendall方法的参数,在实际传输中会被分割成多个数据包

1.5 网络层级

应用层的具体网络服务, 往下是调用的是request, 解析url,参数,协议

抽象层级到了http协议,代码变长了些, 剥离出了Session

当抽象层级到了socket时 , 代码就更长了, 可以看到wraptls, 循环接受响应, 二进制流,句柄的退出

再往下就是操作系统级别的编程了, 传输层TCP与网络层的IP协议负责处理字符串的接受发送 IP连接不同的计算机,为TCP建立管道, TCP考虑数据在管道中的各种情况,丢包, 拥塞, 重传 链路层将数据传输到直连的机器, 直接涉及到硬件,网卡, 以太网端口

1.7 网际协议(IP)

计算机内部各个部件同样需要通信, 通信需要保障其会话不会被干扰

网络设备数据交换的基本单元为packet, 长度在几字节至几千字节, packet在物理层通常包含字节数据以及目的地址两个属性, 地址作为标识符

IP协议的设计抽象, 能让操作系统不需要去考虑数据在网络的如何被处理, 只负责发送、接受

1.8 IP地址

IPv4地址为4字节的整数,.分隔,如今已被用完, 当然要是没有NAT这类技术估计这一时间节点会更早来临

针对特定的场景, 有一些特殊的地址被预留下来 127.*.*.*预留给本机使用, 通常被用的只是127.0.0.1这一个,表示本机,开发测试中最为常见, 构成网络回环,亦可通过localhost访问

10.*.*.*172.16-31.*.*192.168.*.*预留给私有子网, 常见的场景有家庭,学校,工作单位, 云服务器上, 在场景内用此字段内的地址辨识机器, 出场景外会对应进行转换

1.9 路由

如何连接网络来传输数据到目的地址,这一过程为路由

已知的目的地址对主机来说,就相当是先验了 对本机地址127打头的地址就不需要过网卡这关 对私有地址也可以查询网段内信息, 不用到网关这一步

机器并不能识别*通配符, 通关掩码来遮盖已经确定的bit数 掩码的值一般为bit数, 第一个字节地址确定了掩码值就是8 当掩码为30,说明前三十位bit已被占用,只剩下2bit, 最后的地址字节只有三个地址

1.10 packet分组

IP支持的最大数据包为64k, 而以太网传输的MTU只有1500Byte,所以在实际传输过程中往往会将数据包进行切分,编号

分组的话,还是看具体的场景, 分组标记DF用于决定数据包是否分组 设置DF后, 将不会对数据包分组, 这时超出容量大小的数据包将会被丢弃, 失败后将有ICMP协议将错误信息返回给主机

IPv4地址未来将会被逐步向IPv6过渡, IPv6地址有16字节, 有多种表示方法, 常用的还是16进制的,:分隔


2. UDP

IP协议只确保路由能正确无误, 维持一个会话还需要两个特性

  • 多路复用, 将数据包打上标签,会话两端的数据可以相互区分
  • 可靠传输, 数据包,错误修复、丢失重传

UDP协议仅针对第一个特性, 对于丢包, 重包、乱序等错误还无法解决 TCP解决了这两个,但在性能上稍有劣势 通常在实时通信,允许丢包的场景比较适合udp, 此时的丢包并不影响下一刻 HTTP3QUIC协议为提升性能,也是基于udp进行改进

2.1 端口

多路复用机制, 允许多个会话共享同一介质 这里端口的多路复用, 是对会话建立完成的两台主机, 不同进程之间不串扰 HTTP2的多路复用, 针对同一连接,将数据包分流, 发起多重请求与响应

端口号的分配也有一定的惯例, IANA为主流服务都已分配了默认端口, 如DNS端口号为53 端口号为16位无符号整数 最重要、最常用的服务所分配的端口号,几乎都在0~1023, 这些端口号的监听就不要有想法了 1024~49151通常都能任意指定, 一些大型服务的端口号可以当初默认的了, 尽量还是要绕开,如mysql3306, postgresql5432, 通常的demo测试常用的是80808开头之类端口

pythonsocket模块有提供解析端口的方法 socket.getaddrinfo(), socket.getservbyname()

2.2 Socket

pythonsocket库实际上也是对操作系统c库提供的socket头文件进行封装 socket是一个通信端点, 操作系统用一个整数来对其进行标识,

socket涉及到的操作也只有读写操作,分别对应客户端的响应与请求、服务端的请求与响应 我们实际接触的所有操作均是通过套接字来实现

服务端与客户端进行socket编程的主要区别在于, 客户端通常是一次请求就退出程序, 服务端则是循环接受请求, 解析请求再针对性的返回响应 当然,服务端还是要先初始化参数, 绑定端口, 客户端要解析域名,获取IP地址

INET协议族比UNIX的更具通用性 对UDP socket, 类型需选择datagramsock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)

promiscuous

当未验证响应时, 攻击者可以轻易的伪装成服务端, 给客户端返回服务端应给的响应 将服务端代码挂起, 然后用另起一个脚本发送响应给客服端, 会发现客户端会轻易的退出 windowscmd开启服务, 不易将其切到后台挂起, unix类系统可以ctl+ z, 或是在终端命令后加&, fg命令回切

udp的不可靠性

真实的网络环境中往往会伴随着包的丢失, 数据报在传输时的有三种情形

  • 即将到达
  • 已丢失, 永不到达
    
  • 对方已挂 客户端接收响应时需要以上的情形, 往往也得通过循环来接收 考虑到包可能丢失, 请求重发必不可少,因此, socket库提供了定时器方法settimeout, 当然, 多重请求对服务端也是一种负担

连接

服务端显式监听端口, 客服端的隐式绑定由操作系统随机分配临时端口, socket对象的connect方法会缓存维持一个会话的信息, 不会混杂接受其他服务端的响应 对维护一个会话, 通常采用以下两种方法,

  • sendtorecvfrom指定对方地址
  • 在同一个connect方法下进行sendrecvgetpeername方法获取对方地址

请求ID

给消息添加请求ID的好处在于, 可以应对重传带来的重包, 在一定程度上可以防范欺骗 响应针对请求ID添加上序列号,客户端就能加以区分

2.3 绑定端口

当服务端绑定本机的外部地址时, 客户端请求自环地址127.0.0.1是会被拒绝的,同一机器上操作系统会负责通知客户端, 而实际网络中的服务端就没有这么好心了 本地程序使用本机地址可以与任意服务通信

服务端绑定的地址可能会影响外部主机的连接

一台主机是启用的多个服务是可以共用一个端口的

服务端通常还是绑定的自环地址, 然后再通过边缘服务nginx进行反向代理, 可以防止外部的恶意数据

2.4 UDP分组

传输的内容往往要远大于MTU1500B, 当UDP数据报大于1500B时, 数据报被拆分成多个小于MTU的 防火墙可以检测出会话传输的MTU大小

目前只有linux下, setsockopt方法才能对MTU进行修改 setsockopt可以设置一些网络配置, 如广播,路由等

如今多播已经发展的很成熟了, 广播的场景比较适合寝室断网不断电的时候跟室友联机了吧


3. TCP

3.1 TCP工作原理

TCP给每个数据包添加一个序列号,通过序列号来对数据包排序,检测丢失并进行重传, 序列号由字节数计数器实现, 初始序列号随机产生 TCP通过窗口来控制流量, 会根据网络情况来调整窗口大小

3.2 TCP场景

TCP几乎是互联网通信的默认选择,

TCP连接的建立的三次握手, 以及连接断开的四次挥手 建立全双工通信,要求会话双方同步序列号, 所以握手至少需要三次, SYNSYN-ACKACK 断开连接时,断开方仅关闭请求的发生,不断开响应接收,FINACKFIN-LAST_ACKACK

所以一次全双工的TCP会话至少需要7个数据包, 当会话需传递数据包极少时, udp会比较合适, 长时间的会话连接比较适合TCP

3.3 TCP的socket

udpsocket, 在connect方法后send,与指定地址的sendto行为一致 而tcpsocket, 会话是建立在connect方法的状态之上的,tcp scoketconnect的建立可能会失败

tcpPOSIX接口分为主动, 与被动 被动的tcp scoket为服务端的socket, 它用来维护这个公共的socket接口, 由其通知操作系统来再分派socket与外部主机互联 主动的tcp socket只负责特定的主机间互联

3.4 TCP编程

pythontcp编程方法与udp差不多, 但底层的实现区别还是挺大的 udp对未启动的服务端发请求,抛出的是ConnectionResetError,而tcp请求则是ConnectionRefusedError

udp代码中需要手动处理丢包重传,超时等问题, 而tcp代码这部分就要显得简单些, 网络栈帮我们实现了这些

udp数据报是自包含的,只有01两种状态 而tcp要考虑分流, 存在部分传输的问题, 所以tcp代码会有循环检查请求长度的代码

sent = 0
while sent < len(request):
    req = request[sent:]
    sent += sock.send(req)

被动socketaccept方法返回操作系统分配的主动socket与该socket的信息, 当调用其listen方法时, 将造成该被动socket无法交互任何信息 listen方法接受的整型参数表栈中的最大等待数

被动socketSO_REUSEADDR属性, 该属性可以保证服务挂掉重启时,上次绑定的地址能快速重用

3.6 死锁

tcp会话的两端有操作系统分配的缓冲区, 用于缓存还未来得及处理的消息, 服务端->客户端,未收到的响应, 客户端->服务端,未收到的请求 当缓冲区被占满, 消息传递的方法会被阻塞, 然后进程会被操作系统暂停

避免此类死锁,常用的两种方案

  1. 将会话两端的socket设为非阻塞, 缓冲区满时消息传递的方法会主动返回
  2. 读写操作的并行,或是一些异步的操作系统调用

udp没有流量控制, 无此现象

3.7 半连接

socket句柄与文件句柄一样,当句柄指针到达句柄末尾的SEEK_END时,继续再读就返回空值了

阻塞socket不受会话某一端的断开影响, 非阻塞socket则需要别的手段来检测socket状态

shutdown方法在保持socket状态的同时,关闭一个方向的通信, SHUT_RDSHUT_WRSHUT_RDWR,选择关闭的方向,

shutdown方法与close方法的不同, shutdown方法关闭该socket与所有相关进程的连接,close方法仅关闭该socket与当前进程的连接

单向socket不能直接实例化, 通常需创建一个正常的socket然后再调整方向, 对关闭连接的某个方向, 操作系统不同无意义的填充其缓冲区

makefile方法返回socket版本的句柄


4. socketDNS

4.1 主机名与socket

创建socket至少需要3个参数, 构造器参数,

  • 地址族,AF_INET最为通用, AF_UNIX适合本机内, 连接的是文件名
  • socket类型, 根据数据包的类型区分, udpSOCK_DGRAMtcpSOCK_STREAM
  • 协议, 上面两个参数确定后, 可选的协议就没多少了, 这本书只用到了IPPROTO_TCPIPPROTO_UDP

4.2 地址解析

getaddrinfo方法解析地址、域名, 返回socket的相关参数[(AF, socket-type, ipproto, '', (ip, port))...], 用于构造socket, 服务端绑定, 客户端服务连接

利用getaddrinfo方法解析某一服务在指定数据传输方式下的地址, 0为数字字段参数的通配符

socket.getaddrinfo(
            hostname_or_ip, 'www', 0, socket.SOCK_STREAM, 0,
            socket.AI_ADDRCONFIG | socket.AI_V4MAPPED | socket.AI_CANONNAME,
            )

根据需求,给getaddrinfo方法传不同的标记, 可以获取更多的地址信息, 实现更复杂的功能, 例如进行反向DNS查询等

4.3 DNS

TCP/IP为不同ip地址的建立会话,但ip地址并不适合记忆, 以及在大型的分布式服务上, ip不适合对服务进行标识 域名与ip是一对多的关系

对域名的解析, 须指定dns服务器, dns服务器负责响应域名与ip地址间的映射,

dns请求为udp报文, 因为其报文为低频短文本内容,

dns并不是唯一获取域名信息的方法, 操作系统有对应的机制hosts,会进行一定的缓存 dns请求也是由近及远, 本地的dns服务器通常经管理员配置, 手动或是DHCPpython很可能已有本地dns服务器的信息


5. 网络数据和网络错误

5.1 网络字节顺序

很多协议的数据在网络中会转换成文本串传输, 到达目的地后再被解析,例如一个整数会经历itoa, atoi

即使是字符串,实际传输的还是二进制的byte,网络字节顺序主要是指的跟机器相关的大小端对齐问题

POSIX接口的字节顺序转换接口为网络顺序转主机顺序的ntohl、主机顺序转网络顺序的htons

pythonstruct模块用于处理数据及其二进制表示的各种操作 packunpack函数,可以根据<(>)i对数据进行大小端的二进制转换

5.2 封帧与引用

POSIX标准下收发TCP报文需要用while循环收发数据包, python中对发送报文封装了sendall方法,却并未提供recv的对应封装

确保消息完整安全传输

  • 先完成一个方向上的数据传输, 然后再完成另一方向上流的数据传输, 同时双向的数据流动, 数据量大到一定程度就容易造成死锁
  • recv定长报文
def recvall(sock, n):
    data = ''
    while len(data) < n:
        more = sock.recv(n - len(data))
        if not more: raise EOFError(f'socket closed {len(data)} bytes into to a {n}-bytes message.')
        data += more
    return data
  • 消息的字符集合有限时, 用分解符划分消息边界, ``、xff
  • 对每条消息增加一个前缀字段, 描述其长度
  • 对每个数据块加上长度前缀, 以及消息的结束标志

pickle的分隔符为.

跨语言标准, json通过字符串表示,其标准要求用utf8编码, xml对文档更为适用, 二进制protobuffer

zlib压缩能识别出压缩数据何时到达结尾, 其后未压缩数据亦可访问

5.6 网络异常

涉及到socket操作的常见异常

  • OSError 最常见,当send方法让远端发回一个RST响应,接下来socket相关的任意操作均会抛出次异常
  • socket.gaierror, getaddrinfo方法解析不到地址时抛出
  • socket.timeout

异常在会话中产生时, 不易定位异常的位置, 这时需要手动继承Exception扩展自定义异常类, 帮助定位具体异常的位置


6. TLS/SSL

6.1 TLS无法保护的信息

tls加密的内容包括https会话的连接,以及响应内容、密码、cookie及认证信息

始终透明的信息有, 参与会话两端ip的地址信息, 端口号、DNS查询信息 会话外的第三方可以轻易的知道 数据块大小、会话的整体模式,区分出请求与相应, 已经数据的顺序

证书通常需要第三方的认证机构, 对个人的小型应用服务可以到letsencrypt申请免费的CA证书

6.4 tls负载移除

要在python应用程序中提供tls支持通常的做法

  • 用一个单独的守护进程/服务来提供tls, 易于修改与升级
  • 在服务端代码中添加ssl库的加密功能, ssl还不支持ECDSA签名

6.5 默认上下文

ssl模块的create_default_context函数用于初始化tls

purpose = ssl.Purpose.SERVER_AUTH  # server side use CLIENT_AUTH
context = ssl.create_default_context(purpose, cafile)
 
# server side
context.load_cert_chain(certfile)
 
context.wrap_socket(raw_sock, server_hostname=host)
# server side
context.wrap_socket(raw_sock, server_side=True)

服务端需要加载私钥, 然后用其wrap_socket方法把socket包一层就有了加密功能

tls可以加密会话的连接在于https连接后可以直接激活tls, 其tls协商是在连接建立之后完成

smtp协议的连接需明文来建立, 故对smtp内容加密时不能提前wrapsocket

https虽然可以提前wrapsocket, 但connectaccept方法在协商时异常会失败, 其次实际的加密功能是在连接已建立、显示调用do_handshake关闭自动协商的时候

6.6 选择加密算法与完美前向安全

考虑到向下兼容,默认上下文对数据的加密级别较为一般

前向安全问题指的是在时间节点之后获取早期的私钥对早期加密数据的破解, 目前椭圆曲线算法ECDHE能提供有效的加密 只有服务器定期丢弃维护的会话状态与密钥才能保证PFS

可以对上下文属性进行手动设置context.options |= ssl.option-nameset_ciphers方法设置加密算法,来要求客户端更新匹配的加密算法

主流协议几乎都支持tls, 对旧的普通协议进行扩展, 可以在会话中为其升级tls加密, 或是转换tcp的通用端口号, 会在连接建立后自动开始tls协商, 由于http的无状态, 仅支持修改80端口至443端口

ECDHE + RSA + AES128 + GCM + SHA256为当前openssl能够提供的最佳加密方案


7. 服务器架构

7.1 部署

对单机的网络服务而言, ip之间直连建立会话就能提供服务, 单机多服务,对不同端口、不同记录值, 可由nginx统筹来转发 对分布式的多机服务, 主流做法还是在服务边缘配置nginx的负载均衡, 实现反向代理,对用户隐藏实际的服务, dns会返回离用户最近的负载均衡站点

旧式的单体架构部署下,每个服务器实现其服务的所有功能, 然后创建daemon,安排相关的日志,配置文件,不同的状态应对机制 目前流行的微服务下的twelve factor app,只实现服务器其必备功能的最小集合, 可以通过环境变量连接任意后端

还有应用PaaSSaaS的网络服务, 借助平台的托管功能,单个服务可仅实现单个功能

7.3 单线程服务器

单线程下, 同一时钟频率,服务职能应对一个会话, listen参数大于0也只是将多个连接放入队列中

trace模块屏蔽标准模块输出, 监控每行代码运行时间 python -m trace -tg --ignore-dir=/usr

在关闭socket前,第一次后的recv方法的调用的结果返回会有延迟, 这是由于操作系统在tcp层的优化, 握手时就已经包含消息文本, 第一个recv尚未调用时已经有数据在缓冲区,send方法只要将消息推到缓冲区就返回

7.4 多线/进程服务器

进/线程区别在于是否共用相同的内存空间, 切换开销 python的多线程由与GIL而无法为每个线程分配独立的计算资源 进程间切换的时间开销远大于线程间

不绕过GILpython的多线程只能分配单核,资源的利用率低 多线程下, cpu针对每个线程申请的时间片来多路复用 操作系统会为每个线程分配调用listen方法的socket副本, 然后运行该线程下的accept

多进程下, 进程间的内存空间相互独立, 隔离性强, 增加操作系统负担, 也降低了崩溃的概率

7.5 异步服务器

现代操作系统网络栈支持异步编程在于

  • 网络栈可以提供系统调用,阻塞整个socket的连接池,
  • 允许send,recv读写操作立即返回, 延迟时准备好交互重试的环境

目前POSIX异步非阻塞的网络编程主要是通过epoll, 支持高并发, nginx就有用到

select模块封装了操作系统的epoll、 kqueue、poll接口,win32下只支持poll, 要实现一个基本的异步网络服务, 需处理好poll状态机的各个状态 可以用generator模拟无限长容器, dict来代替操作系统上下文的切换 对accept返回的socketsetblocking方法设置其非阻塞

asycio框架支持回调、协程两种风格的异步编程, 可在仅扫描socket的情况下完成与不同客户端会话直间的切换

7.6 inetd

开启inetd daemon服务仅需修改/etc/inetd.conf配置文件即可,inetd为每个端口调用bindlisten方法,

每当会话建立时会启动一个进程,设置nowait后会在连接关闭后保留其进程的监听状态 port stream tcp nowait user interpreter interpreter script.py 此时,脚本需不断调用accept来保持忙等待,以接受客户端发起的请求

python的标准IO流设置为文件流, 不过这并非对实际的fd进行操作, 当服务器调用C库则需要关闭fd 1,2,0 sys.stdin = open('/dev/null', 'r'); sys.stdout = sys.stderr = open('log', 'a', buffering=1)


8. 缓存与消息队列

8.1 Memcached

key-val类的主流NoSql主要是memcachedredis 虽然现在redismemcahced还要火一些, 但解决的问题还是类似的 主要是针对服务器响应慢, 缓存中间件, 将相对独立的数据部分孤立出来, 可供多端使用, 缓存内容均为可恢复可重建的计算结果

跟硬件层的L1、L2、L3 Cache差不太多, 当寻址未命中再向上一层级寻址,redis则是软件层, 结果键值为命中,再重新计算

实现是基于LRU的一个超大型哈希表, pyhton调用memcached api会自动触发pickle操作, 缓存的的是二进制pickle序列

针对时效性的缓存

  • memcached可以为每个键值设置timeout
  • 可以建立信息标识与键的映射, 通过映射实现值的更新

8.2 散列与分区

memcached服务为网络应用提供数据接口, 客户端对获取的多个memcached实例, 可以通过键的字符串hash值对sql分区, 来定位数于据其服务器集群的位置

客户端缓存相关的信息仅需要key与服务器列表, 相同的key-val应仅存储在同一机器上

选择hash值来进行分区的好处在于, 好的哈希算法能保证结果的均匀分布, 能独立于输入字符串的分布情况, 从而达到负载均衡, 书中案例为单词首字母分区

8.3 消息队列

目前常见的有rabbitmq,分布式的kafukaredis的键值存储也可以用作消息队列

消息队列负责封帧,保证消息的可靠自动传输, 不需要循环调用socketrecv方法, 能统筹多端的C/S服务,并确保消息准确无误

消息队列可支持客户端之间的拓扑扩展模式,

  • 管道, 队列随着站点的繁忙而变长
  • publisher-subscriberfanout
  • 请求-响应, 可运行大量的轻量级线程

使用场景

  • 用邮箱注册账号,服务端发送确认邮件,会先将邮箱放入一个消息队列, 走SMTP协议时从消息队列中取出邮箱对象, 发送失败会将邮箱重插入消息队列中
  • web服务器繁忙时,通过rpccpu bound的任务置入消息队列供多个后端服务器监听
  • 将大容量事件数据切分为小型消息流进而分析, 消息队列替代了日志系统、syslog之流的日志传输机制
  • 秒杀、抢单、抢票系统

publisher-subscriber 本书中的zmq,已经很老了额, 用蒙特卡洛方法估算圆周率, 点落入四分之一单位圆与矩形的概率比, 案例中消息队列为always_yes、judge, 过滤并接收服务端b的数据流, 计算以rpc形式推给应用端p, 结果推给客户端t


9. HTTP客户端

http用于获取与发布文档、 包括但不限于图像、pdf、音视频、html

9.1 客户端库

requests模块,基于urllib3的连接池

gunicorn为基于wsgiweb server容器, 通常用于pythonweb服务部署, 可以配合flask等框架 httpbin是一个小型的测试站点模块, gunicorn httpbin:app运行测试站点

9.2 端口、加密、封帧

当客户端也提供证书时, tls是允许服务器对客户端进行身份验证的, 当Host字段与证书不匹配, 客户端会被拒绝, 所以http1.1的请求首部必须包含Host字段

http1.1的请求在未收到响应时, 是不允许发送第二条请求报文的 请求报文包括三个部分, 用rn进行封帧, 尽管有封帧, 大多服务器还是会在读取请求时设置长度限制

  • 方法,路径,协议 GET / HTTP/1.1rn
  • key-val, Host: wuchengwei.icurn, 最后一个键值对后面还需要一组额外的rn
  • 消息体(可选)

对消息体的封帧方法有

  • Content-Length字段指定消息体长度信息
  • Transfer-Encoding字段设为chunk,进行更为复杂的传输层封帧手段, 块中就带有长度前缀, 16进制
  • Connection: Close,可以发送任意大小的消息体,此举无法判断连接关闭的缘由, 每个请求都需建立连接, 降低了协议效率

9.3 方法

大都是GETPOST的变体 TRACE用于调试, CONNECT用于协议的切换,并不涉及数据传输

9.6 缓存与验证

与其他网络服务一样, 缓存能大大提高效率, 浏览器客户端可以将页面持久化, 你会发现,有些网页即使断网了有时还是能打开其首页, 浏览器缓存的时间控制通常采用定时器Cache-Control:max-age=3600Vary字段可以设置不同级别的缓存 每次使用缓存前应发送条件请求,验证缓存的时效性,

  • Last-Modified字段记录服务上次更新的时间点,与其缓存的时间对比
  • IF-None-Match:UUIDETag

对同一路径, 针对不同用户返回不同结果, Cookie是最常见的选项,

9.7 传输编码

旨在提升传输效率, 压缩传输文件体积 客户端在Accept-Encoding:gzip, defalte字段中告知服务端编码的可选方法、服务端响应的Transfer-Encoding字段给客户端以确认

9.8 内容协商

内容协商可以根据用户的一些属性来针对性的返回不同响应, 最为直观的就是语言了, 对不同地区的用户, Accpet-Language通常会获取位置信息, 然后返回对应语言版本, q参数为权重参数

http客户端的api往往难控制Accept这类字段, 所以内容协商在一定长度上容易被忽略, requests中接受此类字段通过Session

9.9 内容类型

Content-Type字段通常为为服务端接收Accept字段信息后对应的响应体文本类型说明,

在多数据类型的Post API中, Content-Type声明上传数据类型, 获取对应的响应

9.10 http认证

授权错误码为401, 但通常会返回303的状态码, 认证信息需要单独传输

没有网络库完整实现协议的,requests.Sessionauth等其他设置, 也只是减免了Authorization字段所必要的base64编码操作

目前主流认证机制使用的是cookie, 包括统一主机内的Ajax

secure属性防止客户端在非加密请求携带cookie

cookie还往往被服务端用作跟踪用户的工具

requests.Session会自动进行cookie的跟踪

9.12 连接复用

随着tls的时间成本, 连接的反复建立也是巨大的时间损耗 Keep-Alive字段可以在客户端下载多个资源的时候对初始连接的复用

requests中的连接池会缓存最近的会话状态,以实现连接的复用


10 http服务器

pythonstl内置有一个上世纪90年代版本的服务器, 其请求路由会被直接转换成文件系统中的搜索路径, 仅支持服务挂载下的目录, 对目录的访问会定向到存在的index.htm 现代的http服务几乎都会用上nginx反向代理, 负责路由的转发, 可以映射到任意目录

10.1 WSGI

python早期的http编程通过CGIhttp.server来实现, CGI针对每个请求新开进程,而且可移植性很差

针对http编程的弱势, 在PEP333中提出的WSGI标准,现代的python3WSGI标准则对应PEP3333

WSGI程序的实现也是回调风格, 两个回调参数分别是CGI环境变量集合拓展的字典environ、 声明响应头部信息的start_response, 当服务app函数为生成器时,可在迭代中生成字节序列

WSGI中间间的优势在于其提供的可插拔性, 服务与框架、应用程序间

10.2 异步服务器与框架

WSGI还尚未支持基于协程、语言运行平台调度线程的异步服务器, 任然是面向传统的并行服务器, 任然存在IO阻塞

WSGI不能将可调用对象的控制权返还给主进程, 以至于主进程不能直接对其调度

不同的框架通常会设定不同的编程规范, 诸如,F家的Tornado, 大名鼎鼎的Django, 轻量级的flask

10.3 前/反向代理

代理其实也是一个服务器, 根据它充当的角色不同归为前/后向, 前向是充当客户端,反向则是充当服务端

随着TLS的普及,前向代理实施的难度变大了了不少, 仍存在的应用有科学上网代理, 扮演用户去抓取外网数据, 然后中转回内网

反向代理的广泛使用,已经是web服务的标配, ExpiresCache-Control字段标识缓存状态, 反向代理在缓存有效期内充当服务端, 承载大多数负载

反向代理服务器会先获取服务端的证书、私钥, 然后截断TLS, 让服务端不可见

10.4 四种架构

python社区中4中最基本的设计模式,

  • 服务器代码直接调用WSGIapi, 服务器引擎可以选择gunicorn, 若是实现异步服务器, 服务器与框架须在同一进程
  • 配置mod_wsgi中间件, 静态资源由C引擎直接返回, 动态资源由mod_wsgi开启deamon->pyhton脚本, 不适用异步框架
  • 后端gunicorn, 前端nginx反向代理, 我的个人站点采用的是此方法,再配上flask框架
  • 上一种架构的最前端再添加一层纯粹的反向代理VarnishVarnish可经由Fastly之类的cdn加速请求资源响应的返回

Apache服务器如今已经不再是服务边缘的首选了, 其性能各方面已被nginx碾压

反向代理的优势在于, nginx会对请求进行高效的预处理, 如长时间未响应的请求、畸形输入等, nginx会主动决绝无效请求 首选方案应为nginx+gunicorn, 纯api可只用gunicorn, 必要时配上Varnish充当一级缓存, 大型服务就第四种的三层架构

10.5 平台即服务

PaaS平台通常会提供负载均衡、版本控制、数据库管理、容器及其缓存等功能,开发者可以无需自己故障重启、DNS配置、环境配置,负载管理,但仍需web服务器来给程序提供监听端口

10.6 GET、POST模式和REST的问题

rest的4个约束条件

  • URI标识资源
  • 通过其表示形式操作资源;restfulhttp协议可以区分读写请求
  • 消息的自描述性; 请求首部字段
  • 超媒体 -> 应用状态引擎; 一般的api很难满足该约束 与rpc最直观的区别在于第一约束, rpc在应用层协议未暴露资源实体,是没有uri的路径的

10.7 不使用web框架编写wsgi可调用对象

web服务器负责监听端口, 可以不调用应用代码即可解析请求

web服务器只会将完整的请求递交给框架、应用代码

框架的作用主要是路由, 分发url, 对不支持的http请求会自动返回异常的状态码

不用框架来进行路由

  • 通过wsgi入口environ变量的字段, 条件判断来过滤筛选路由
  • 三方库的wrapper, 如webobwerkzeug, 也是对environ变量进行封装,简化过滤

flask是基于werkzeug构建的框架,同一作者


11 万维网

11.1 超媒体与URL

web的意义在于用机器实现对引用资源的寻址, http协议是为实现web而设计的

uri是更为广泛的概念,是url的超集, uri指代计算机可识别的概念性实体, 为其指定的名字为urn

更为完整的url组成, 协议的字段名为scheme scheme://(auth)@host:port/path?key=val&key1=val1#fragment

url解析

urllib3parse_ul接口通过正则几乎已将所有情况均考虑在内了, 返回包含各个字段的元组 scheme, auth, host, port, path, query, fragment = parse_url(url)

11.2 超文本标记语言

html已经到第五代标准了, css标准也到了第三代, 交互的脚本语言javascriptes标准也更是到了第十版, 虽然目前主流应用还是es6, 还需要考虑兼容es5,不过版本的升级更多的是弥补前代的不足已经对未来的展望, 经时间检验过的优良部分可定会被继承, 虽然形式可能会发生变化

静态页面没有数据绑定, 所有用户获取的均是相同的内容, 可由各个结点的cdn服务器加速内容响应返回

动态页面随用户的不同而不同, 随交互的不同而不同, 需绑定数据, 会用到字符串模板引擎, 将更新的绑定数据实时渲染到模板中达到动态的效果

11.3 数据库读写

根据应用的数据类型,选用对应的数据库进行管理数据, 然后就可以开启枯燥的CRUD生活了

python内置有便携式的轻量级数据库sqlitesqlite更适合数据规模小, 与应用绑定的场景

对中小应用、服务,最为广泛使用的关系数据库是社区办的mysql,免费,开源 NoSql中的key-value数据库,有如redismemcached, 结构化valueMongoDB,社交网络、推荐系统中图结构数据库neo4j

11.4 flask账单web应用

才发现jinja2的作者也是flask的作者 {{}}绑定数据, {% %}循环, {% set express %}简单计算

11.5 表单与HTTP方法

get方法的表单会把字段添加到url,作为请求路径, 所以不能用get方法传输密码等敏感信息

postputdelete方法会将字段放至消息体中, 路径不发生变化, 因此可受tls加密

上传大型负载可以基于MIME标准的表单编码multipart/form

post请求会造成状态变化, 浏览器会谨慎的对待, post请求下的页面重载浏览器会弹窗提示以确认, 为防止重载这时的弹窗, 通常有两种方法

  • js来检查以确保用户的输入准确
  • 表单提交后重定向页面

访问地址时使用get方法, 状态修改使用post

cookie为用户的验证信息, 服务端通过cookie来记录用户的先前会话状态, 当cookie被攻击者获取会造成极大的安全隐患

常用的用户名混淆方法有, Base64编码, 字节顺序交换、 常量掩码的异或操作

保证cookie不被伪造的3种方法

  • 使用数字签名对cookie签名, 可保留cookie的可读性,密钥和源代码需保存在不同位置
  • 完全加密
  • UUID库创建随机字符的cookie

xss

跨站脚本通过注入js脚本来劫持请求, 这时的应对手段是将标签标记</>进行转义

非持久xss将脚本置于外部,需用户手动触发,当误点了攻击者的入口连接, 请求字段会按攻击者的意愿被修改, 单次触发

持久xss则是将脚本注入到服务器, 这种攻击在每次访问页面就会被触发

csrf

跨站请求伪造主要发生在表单字段被攻击者摸清, 任何用户会访问的站点, 只要有攻击者注入的脚本, 都会在无形中发送post请求提交表单 应对csrf可以增加提交、构造表单的难度, 如增加额外的隐藏字段信息、隐藏表单属性

11.6 Django账单应用程序

django是全栈式的web框架, 内置有ORM、模板引擎、CSRF保护flask则需要配合sqlalchemydjango内容需要新开贴了 有名气的python web框架还有 TornadoBottlePyramid

11.8 websocket

websocket协议用来解决长轮询问题,wsgi不支持websocket

websocket会话建立与http类似, 然后通过请求字段与状态码协商,转换为websocket协议

websocket的帧数据系统与http区别较大, websocket下的socket可以同时双向发送消息

websocket编程有着大量的交互操作,js与服务端直接

websocket主要用于多端的实时交互, 如多人聊天室,群聊, 服务状态实时更新

11.9 网页抓取

robots.txt,站点的抓取限制, 服务条款

获取页面常用的3种方法

  • requests, 维护cookie、连接池的Session
  • seleniumwebdriver,仿人交互
  • mechanize, 已不更新, 介于浏览器与python

html解析库beautifule soup4 lxmlbs4要快不少,而且还提供多种选择元素,xpathcssselect


12 电子邮件构造与解析

email相关的协议

  • SMTP 将邮件推送至@后的域名服务器, 25端口
  • POP3 下载服务器邮件至本地, 110端口
  • IMAP 本地浏览服务器邮件, 143端口

本章的内容为以上三个协议的基础

12.1 email格式

RFC 5322是现行的email标准

  • emailascii文本形式表示
  • 行尾标记由CRLF组成rn
  • 一个email包含一个邮件头、空行、邮件体
  • 邮件头由多个key-value组成, keycaselessvalue一行容纳不下后续行需要缩进t

邮件头会记录邮件的基本信息, 收发方、路径、抄送、密送、日期、id、回复等

12.2 构造email

datemessage-id是邮件头中必须手动设置的, 编码、类型、MIME版本都有默认值

构造一个最基本的email

from email import message, policy, utils
import sys
msg = message.EmailMessage(policy=policy.SMTP)
msg['date'] = utils.formatdate(localtime=True)
msg['message-id'] = utils.make_msgid()
msg.set_content(text)
sys.stdout.buffer.write(msg.as_bytes())

message-id可以采用uuid算法生成标识, set_contentas_bytes方法对邮件体进行格式转换

12.3 添加HTML与多媒体

mime标准为emailascii提供扩展, 在content-type指定一个边界字符串, 用此边界将email拆分为多个子部件

边界字符串的标准是把开头设置为连字符, 然后可以给子部件也添加邮件头, 这样形成了邮件的嵌套结构

email.message.MIMEPart对象用来构造子部件实例, 指定其邮件头、邮件体, 然后父部件的attch方法添加子部件, 手动构造mime的场景比较少见

通常在构造email主部件时就可以构造mime, 涉及到的4个方法

  • set_content
  • add_related, 添加引用, 主消息体的必要资源, html主消息所需的css, js, img
  • add_alternative, 为适配多端,提供备选消息体, 同时将message-id配合连字符来拆分父子部件
  • add_attchment, 消息附件

引用资源也为子部件,故添加引用时也需要生成uuid标识

add_alternative格式化分割字符串, 连字符目前是采用的--===============id==, 分隔符会用作子标题,对子部件的每个消息进行标识

mimetype模块的guess_type函数可以对文件的mime类型及编码进行解析, 但无法解析八进制流, 所以解析失败的类型会被置为application/octet-stream

multipart/*的邮件消息使用边界字符串对mime分割, 作用在于分割某条消息的多个版本 对multipart字段

  • 当调用了add_related方法, 生成multipart/related类型子部件, 内容涵盖set_content方法中指定的所有相关内容
  • 当调用了add_alternative方法, 生成multipart/alternative类型子部件, 内容包含原始邮件及其衍生版本
  • 当调用add_attachment方法, 生成multipart/mixed类型子部件, 内容为所有附件

12.4 内容规范

构造mime的4个方法也有对应的调用规范

  • 内容为字符串时, 默认类型都是text/plain, subtype参数可以将文本指定为text/html
  • 内容为binary时, type参数指定类型文件imagesubtype指定具体类型jpeg
  • 内容的编码若仅用7bit即可就会采用ascii,不够则会采用base64编码,base64的缺点在于不可读,cte参数用来覆盖编码 如quoted-printable,指定后,会对长字节的字符进行转义替换, 这样会就不会出现大量的base64编码
  • 引用内容的cid参数, 需要包含在<>

12.5 email消息解析

EmailMessage的内置方法时,邮件需按字节读取, 然后用email模块处理, 比起手动解析, 这样的好处在于避免解码

email模块的message_from_binary_file负责将二进制字节转换为EmailMessage对象, get方法获取邮件头的key-value, get_body方法指定优先级参数preferencelist获取邮件体, 在新版本python中的iter_attachment方法可以捕获附件, 3.4版本的python对有附件的消息体,附件的多媒体消息扔需手动搜索,walk方法遍历再进行逐个判断

12.6 遍历mime部件

在使用EmailMessage方法实际解析的时候, 邮件体中包含不可打印信息、格式/编码错误时,邮件体对象可能会获取失败, python的解码错误会抛出UnicodeError类异常,然后退出程序,

手动解析email要牢记4个基本准则

  • 子部件的读取,需先调用is_multipart方法来判断此mime部件是否仍为嵌套结构
  • multipart部件,iter_parts方法可以获取其子部件
  • content-disposition字段用来标识部件是否为附件
  • 主类型为textmime部件内容,get_content方法解码得到的是str的文本,text以外的主类型均解码为byte

之前流程python看的协程都尼玛忘了, 动手实践少了点, 又没有记录、输出 后面重看流畅都python再慢慢补上

def walk(part, prefix=''):
    yield prefix, part
    for i, subpart in enumerate(part.iter_parts()):
        yield from walk(subpart, prefix + '.{}'.format(i))

12.7 邮件头编码

email.header模块的底层实现, Header类有邮件头编码方式的详细实现

12.8 解析日期

email.utilsformatdate函数默认返回当前的时间日期, 亦可接受底层的unix时间戳, format_datetime函数来接受datetime的事件对象

parsedateparsedate_tz实现formatdate的逆向操作, 返回表示时间的元组, 遵循c旧式风格时间

parsedate_to_datetime则返回一个完整的datetime对象

pytz模块支持对时间的运算


13 SMTP

rfc 5321SMTP目前最新的标准 提交和传输email时使用的smtp稍有不同

13.1 email客服端与web邮件服务

个人主机的25端口通常会被isp禁用,故不能被部署为邮件服务器, 这样的目的是防止其被病毒劫持

提交email由本地客户端发起,经过认证后,发送至tls协商starttls587端口, 465端口则要求在smtp启动前进行ssl加密 提交后的email在服务器之间多跳传输(多重dns路由, 垃圾过滤, ldap映射),此时的端口是25,且无需身份认证

如今的email客服端已经很大程度上被web给取代了, 浏览器集成了email必备的协议栈

13.2 smtp的用法

email从客户端提交邮件至服务器需要身份认证, eamil在服务器间传输可能会失败, 因此邮件往往需要如postfix,exim,qmailmta代理重传队列,负责在email传输失败时,将email再添加队列中

对一个smtp会话, 邮件头的收发字段相互独立,email在客服端提交前,邮件头会被再编辑, 如删掉密送字段, 添加辅助的id字段等, 这样可以达到对消息体的复用,实现订阅的发布

13.3 smtp模块

import sys, smtplib
 
message_template = """
To: {}
From: {}
Subject: test
 
hello from wcw
"""
 
def email_demo():
 
    server, fromaddr, toaddrs = 'localhost', 'wcw@test.com', ['344078971@qq.com',]
    message = message_template.format(', '.join(toaddrs), fromaddr)
 
    connection = smtplib.SMTP(server, port=25)
    connection.sendmail(fromaddr, toaddrs, message)
    connection.quit()

python -m smtpd -c DebuggingServer -n localhost:25 运行python内置的本地邮件服务debugger, 捕获本地SMTP发出的邮件信息, 真实场景跨域m, 需实现加密,认证等操作

13.4 错误处理与会话调试

smtplib的常见异常

  • gaierror, 查询地址错误
  • error, 网络、通信错误
  • herror, 地址异常
  • SMTPException, 会话异常

前三种异常由os级的tcp抛出, python负责捕捉后由smtplib打印出来,tcp正常连接情况下抛出的异常即为第四种 SMTP实例的set_debuglevel方法设置后能打印更详细的信息,如一些email协议命令,ehlo、rcot to

13.5 从EHLO获取信息

stmp服务器往往都对消息大小有限制, EHLO命令支持smtp扩展集esmtp, 能协商会话双方的smtp特性,

SMTP实例的ehlo, helo方法返回的状态码与http的类似, 值在200-300之间时,SMTP实例的esmtp_features字典属性将会保留协商的结果

13.6 使用tls/ssl发送email

smtphttp为同级协议, smtp会话的建立在两端协商之后, 支持ehlotls的必要条件

ehlo验证过后,SMTP实例需调用has_extn方法确认esmtp_feature属性中的starttls字段, 该字段也是smtp建立tls会话的必要条件

在此之后SMTP实例就能携带ssl上下文, 然后调用starttls方法初始化加密信道,再度验证tls下的ehlo, 这样就走完了一道完整的email加密程序

邮箱现在都改为授权码来登陆smtp服务器来

import sys, smtplib, socket
 
message_template = """To: {}
From: {}
 
Hello from wcw!
"""
 
def main():
    server, fromaddr, toaddrs = 'smtp.qq.com', '344078971@qq.com', ['344078971@qq.com']
    message = message_template.format(', '.join(toaddrs), fromaddr)
 
    username = '344078971'
    password = 'authorization code'
 
    try:
        connection = smtplib.SMTP_SSL(server, 465)
        try:
            connection.login(username, password)
        except smtplib.SMTPException as e:
            print("Authentication failed:", e)
            sys.exit(1)
        connection.sendmail(fromaddr, toaddrs, message)
    except (socket.gaierror, socket.error, socket.herror,
            smtplib.SMTPException) as e:
        print("Your message may not have been sent!")
        print(e)
        sys.exit(1)
    else:
        print("successful")
        connection.quit()
 
if __name__ == '__main__':
    main()

13.7 smtp认证

smtp认证要求发件方的账户信息, 无认证信息就能发送邮件的话, 推送垃圾邮件的门槛就会提高,进一步泛滥

代码部分在建立ssl上下文后增加一步, 调用SMTP实例的login方法提交用户名密码即可

13.8 tips

  • smtp不能完全确保消息的正确传输
  • SMTP实例的sendmail会在任一接受失败时抛出异常
  • ssl/tls需证书验证
  • smtplib模块的作用是将email提交到临近的smtp服务器

14 POP3

smtpemail协议的上行部分, pop3imap均为下行

pop3的功能是对服务器的email进行下载与删除,类似getdelete, 要进一步同步email状态就需要功能更强大的imap协议了

pop服务器对协议实现不太符合不标准

14.2 连接与认证

pop认证有用户/密码、apop两种,

popssl会话, 调用poplib模块POP3_SSL实例的userpass_方法来提交身份认证 有的pop服务器会根据连接修改email的状态标记

apop协议利用挑战-响应机制来避免明文密码,由于并不是真正意义的数据加密, 底层传输的数据包被捕获后仍会被观测到,代码上区别就是换成apop方法提交用户密码

认证失败时会抛出error_proto异常

认证后调用POP3_SSL实例的stat方法可以获取email总的统计信息, list方法返回逐条信息的编号、字节大小

14.4 下载与删除

list方法调用时给服务器发送LIST命令至pop服务器, 返回响应体、信息摘要、响应体大小

根据listing摘要信息编号,调用retrtopdele方法直接对email进行操作

  • top方法对email进行预览操作,不影响email状态
  • retr方法抓取email
  • dele方法进行删除操作, 调用前需考虑清楚是否需要备份

pop会话需尽量简短, 会话结束需调用quit方法来退出邮箱, 否则邮箱可能会锁死


15 IMAP

imap具备所有的pop功能外, 提供了更多的email功能

  • 永久分类归档
  • 状态标记
  • 文本搜索
  • 消息上传
  • 可靠消息同步, 维护唯一的持久化消息编号
  • 文件夹共享
  • 选择性下载

15.1 使用imap

imaplib的客户端接口仅提供非常基础的功能,需要手动处理请求与响应, IMAP4_SSL实例成功认证后,capabilities属性可用来查看当前imap服务器所支持的特性有哪些 list方法返回的文本串需要手动解析

IMAPClient基于imaplib,该实例的list_folders方法提供了解析功能, 并以python元组返回文件夹标记、分隔符、名称

标准文件夹标记,

  • \Noinferiors, 子文件夹, 不支持再度创建子节点
  • \Noselect, 只能包含子文件夹, 不支持消息结点
  • \Marked, 即使标记过,文件夹下后续还是可能有新消息进来
  • \Unmarked, 无新消息

imap协议是带状态的, 这就要求消息操作依赖于文件夹对象, 选择文件夹对象时指定只读属性对读操作上锁能优化磁盘性能

消息号与UID

消息号与UIDimap引用特定消息的方法, 消息号在连接建立时顺序分配,随会话结束而收回 UID与会话独立,对文件夹的在编辑不会改变UID,需设置UIDVALIDITY属性对UID进行校验

消息范围

消息的imap命令是可以进行批处理的, 消息号参数的指定支持消息列表格式, 支持通配符

摘要信息

IMAPClient实例调用select_folder方法返回的摘要为字典形式, 内容包括文件夹的统计、标记、自定义、UID、未读等相关信息, 其中UIDVALIDITY字段的字符串用于对客户端的消息UID进行比对校验, 并更新

下载整个邮箱

IMAPClientfetch方法用于在选择文件夹后,抓取文件夹指定消息号的全部内容, 指定1:*抓取文件夹,对应整个邮箱, 消息命令BODY[]请求整个消息体, PEEK方法指定查询操作,不对消息进行标记已读,配合email模块解析指定字段, 自定义打印摘要信息

单独下载消息

imap命令[]内传入字段、协议规范、标记等,可以在整个消息体的基础上进行query各个部分的消息体

mime为例, 首先还是调用list_folders指定文件夹,然后fetch方法传入消息命令,指定消息体部件,返回这些部件的消息摘要字典, 再度调用fetch, 根据摘要信息选取指定消息(传入摘要中的UID、消息号), 这时可以通过imap命令BODYSTRUCTURE以递归的形式只返回消息的mime

消息切片可以通过BODY[]<start, end>命令,返回区间内的bytes

消息标记

消息最常用的标准标记

  • \Answered 消息已经回复过
  • \Draft 草稿
  • \Flagged 标记, 有特殊意义
  • \Recent 最近消息, 该标记无法用常规手动删除, 在选择邮箱后自动被删除
  • \Seen 已读

IMAPClientfetch方法传入FLAGS命令可以抓取消息标记 , 然后调用get_flagsset_flagsadd_flagsremove_flags即可对标记进行编辑

删除消息

消息的删除可以通过消息标记, 将要删除的消息标记设置为\DeleteIMAPClient可以调用delete_message方法,传入UID列表,将列表类的消息都标记为\Delete, 然后调用expunge方法, 完成消息的实际删除,删除后还会对现存的消息进行再排序

搜索

search方法, 传入搜索命令字符串, 返回所有满足搜索命令的消息UID

操作文件夹与消息

imap对文件夹的增删操作需要进行错误检查

imap内部添加消息可以通过复制和添加,复制并无任何风险,添加新消息需要考虑行分隔符 imap的行尾结束标志也是CR-LF, python字符串方法splitlines可以识别\r \n \r\n三种行尾标志


16 TelnetSSH

16.1 命令行自动化

python远程命令行自动化工具

  • Ansible, 可用于管理大量远程机器的配置
  • Saltstack, 可在每台客户端上安装自己的代理,主机器推送消息至其余机器要更快

在终端输入命令时,遇到具有特殊意义的字符需要转义, shell可以运行子进程, 子进程的输出可以通过管道重定向作为新命令的输入

纯底层的unix命令行没有任何特殊字符或保留字符, 受特殊字符影响的是shell的语法解析器 换成操作系统级的子进程调用来执行终端命令是不会受特殊字符的影响的, python中开启子进程通过subprocess模块, call方法可以调用环境变量以及当前目录下的可执行程序 最常见的就是空格分隔符了, 文件名带有空格, 但在shell下文件却被当做两个

网络命令行会话通常还是通过shell协议, 直接与shell进行交互, 调用shell命令通过os.system pipes模块用来构造复杂的shell命令, 对每个参数调用quote方法,再进行空格拼接

windows命令行则是直接将所有文本直接传给了新开启的子进程, 然后再有进程来完成对字符串的解析 对此subprocess模块给windows命令行下提供了list2cmdline方法将类unix风格的命令解析为windows风格

终端

终端是回显输入,打印输出的一类设备, 终端作为输入方被视作人类用户, 与其连接的shell会将输出编码为可读形式

当输入不是从终端传给shell时, shell并不会打印其输出, 执行cat | bash,可以看到命令提示符的消失, Ctrl+Dcat命令发生结尾标志 sys.stdin.isatty方法判断当前输入是否为终端 psls命令在终端输入下会自动调整输出格式

终端的缓冲行为

当与程序交互的是文件、管道, 可读性反而成了性能累赘, 输出会被缓存,进而批量传递 在编写自动化代码涉及到终端与文本、管道输入切换时, 程序行为会发生变化,被挂起、输出被放入缓冲区,打印有延迟

目前标准的字符输入以行为基准, 字符可在行内增删,回车为结尾标志, unix针对大批量输出有暂停/继续( Ctrl + S/Ctrl + Q)功能,vim也有同样的设计

可通过stty命令对icanonixon设置进行修改以启/停上述功能

16.2 Telnet

telnet会建立一个信道,然后明文传输数据包, 不存在安全性的考量, 目前应用主要在小型嵌入式系统、内网通信、测试等

telnet会话的建立需要调用Telnet实例的read_util方法读取服务端的认证提示信息,再调用expect方法确认认证成功的通知信息,interact方法允许用户直接在终端上通信

telnet对控制信息的嵌入格式有规定,对数据于控制码进行区分,必要时需协商optiontelnetlib默认拒绝一切选项 对选项设置可以给set_option_negotiation_callback方法传一个回调函数

16.3 SSH

pythonparamiko库对SSH提供了很好的支持

SSH协议已经实现了多路复用,通过对信道的每个消息块添加信道标识符,多个信道可以共享同一个ssh socket, 能在同一连接内执行单独执行不同语义任务

ssh会话建立最耗时的部分在密钥的协商与认证上, 主机密钥默认在~/.ssh/knows_hosts文件中, 当密钥不存在时,建立会话都会出现提示信息,然后初始化host文件, 可以将公钥上传至服务器的ssh配置中实现免密登录

paramiko在实现ssh功能前需保证主机密钥已在配置文件中,否则需手动处理(继承MissingHostKeyPolicy)密钥缺失、无法识别的情况

ssh的认证可以通过paramikoSSHClient实例的connect方法,传入用户密码建立会话, 上传公钥后参数可以只传域名,

shell会话与独立命令

如果不编写交互式程序, 应该避免使用invoke_shell方法, 该方法不会等待远端shell的初始化就将消息推送, 以及命令的解析成本高

exec_command方法无需启动完整的shell会话即可传递命令, 相当是抽象成了subprocess模块,

invoke_shellexec_command均会隐式的建立一条信道以保证并行的稳定, 不同会话不串扰

sftp 通过ssh传输文件

sftp具有状态,支持绝对路径与相对路径, 也可以调用getcwdchdir方法进行路径切换 sftp可以通过fileopen方法获取信道下的文件对象 sftp对文件名的解析不支持shell的语法

调用SSHClient实例的open_sftp即可创建一个sftp对象, getput方法是对open方法的轻度封装,阻塞式传输

对于一些简单文件传输通过scp命令即可实现同样的功能, 用python编写sftp代码可实现更复杂的功能

要实现X11会话以及端口转发, 需要更底层一点的接口, 需要直接与客户端的transport对象交互, SSHClient调用get_transport方法返回transport对象


17 FTP

ftp的主要用途

  • 文件下载、匿名上传
  • 文件同步
  • 全功能文件管理

ftp为长连接,且不具备安全性,文件下载上传几乎已被http替代, 文件同步rsyncrdist能端对端更高效的实现

完整的文件系统访问ftp目前还尚未被替代, 当然也不是直接使用,而是解决安全性问题的sftp

ftp默认使用两个tcp连接

  • 控制信道,传输命令、结果确认
  • 数据信道

ftp会话

认证后, 需要客户端指定操作: 下载指定文件/目录,上传则通过修改目录 然后服务端再开启数据信道传递数据 可以在一个ftp会话中访问多个不同目录

文件下载/上传

数据传输的编码通常为文本或二进制,ftplib集成相关的方法,可以指定远程执行的命令,以及相关行为的回调

  • 文本按行传输,需手动添加行尾结束标识,对应FTP实例的retrlines方法
  • 二进制按块传输, 块大小基于传输层协议,对应FTP实例的retrbinary方法

与下载相对的方法为storlinesstorbinary, 区别在于第二个参数, 由远端返回数据以写操作的回调函数变成了本地读操作的文件句柄

对底层的实现, 选择目录后,调用错误检验的voidcmd方法, ntransfercmd方法获取tcp连接的socket及数据量的估算值

ftpliball_errors属性继承了所有的ftp异常用以捕获

扫描目录

获取目录信息

  • nlst函数, 目录内容的信息, 仅字符
  • dir函数, 目录内容信息及属性, 接受参数可以为对象的append方法

配合nlst函数即可实现对目录下的文件递归下载

ftplib模块同时还支持对远端目录的增删改操作, 故可基于ftp编写图形化远程文件系统应用


18 RPC

rpc需协商好序列化方式,使用上通常伴随着

  • 任务量大,被分发至不同机器
  • 任务依赖于远端数据信息

jsonxml具备自描述性,字节解析较为容易,不足在于其自描述性依赖于数据冗余, 会造成性能损耗

rpchttpsmtp协议更抽象, 约束更少,从表现上,可抽象为普通函数

18.1 rpc特性

  • rpc限制数据类型,单语言支持的rpc提供更多的数据类型,操作系统外的资源均可通过网络字节
  • 客户端可抛出异常
  • 需提供寻址方法, 简单的实现可以用ip、端口、url 自省功能静态语言通常会提供

编写rpc代码需要注意

  • 避免语言层面的bug
  • 单返回值
  • 类型转换, 将rpc不支持数据类型转换为支持类型

xmlrpc

xml只能包含结点、字符串、字符串属性,pythonxml提供原生支持, xmlrpc模块通过注册回调函数即可实现简单的xml-rpc服务 可以注册配置函数实现自省等额外功能

对具有自省功能的服务, 客户端可以调用listMethods方法获取rpc函数签名

xmlrpc不支持关键字参数, 传递的字典数据时要求其键的值为字符串

jsonrpc

jsonrpc针对数据,比xml的数据冗余要小得多, 原生支持bool数据类型 用jsonrpclib三方库实现jsonrpc服务于xml类似

自文档数据

xmlrpc针对静态语言数据类型为struct, 与jsonrpcobject的主要区别在于非字符串键值 在python中,兼容两种类型的解决方案是用字典列表替代, 进而的衍生产品具名数组具有更高的性能

python原生rpc系统

基于python序列化模块picklePyroRPyC

rpyc仅序列化immutable对象, 对mutable对象将会传递其标识符,适合用来协调不同网络位置的python对象