前文讲了网络之间传输协议TCP和UDP的连接和建立,以及如何域名解析找到双方主机。现在该讨论如何准备网络传输用的数据,以及可能遇到的错误。
字节和字符串
8个二进制位 (bit) 组成的字节 (Byte) 是IP网络上的通用传输单元。文本数据最重要的就是选择一种编码方式,将想要传输的字符转换成字节。
字节字符串,本质上是字符
Python中表示字节的方法:
第一种使用一个正好介于0-255的整数
-
第二种使用字节字符串. 可以使用
bytes()
将包含数字的列表转换成字节字符串。>>> 0b1100010 98 >>> 0b1100010 == 0o142 == 98 == 0x62 True
字节字符串的打印: 使用ASCII码作为简写形式,如果找不到对应ASCII码,则显示使用十六进制格式 \xNN
来表示。实际上是字符,比如 b'\x00\x01bcd'
, 注意它开头的 b
字符串
字符编码标准:
- ASCII (American Standard Code for Information InterChange, 美国标准信息转换码,128个)
- Unicode (Uni code, 已经收录10几万字符了)
Python 3 内部把字符串看做是由 Unicode 字符组成,已经对我们隐藏了细节。要处理的只是文件中或者网络上的数据。
操作:
-
编码 (Encoding): Unicode 字符 => 字节字符串
- 单字节编码,一个字节一个字符,最多256个字符
- 多字节编码,定长的 UTF-32,不定长的 UTF-8,BOM表示字节顺序
\xeff
- 解码 (Decoding):字节字符串 => Unicode字符串
错误:
- 已编码的字节字符串不符合提供的编码规则,因此解码失败 (UnicodeDecodeError):
b'\x80'.decode()
- 字符无法使用提供的编码方式编码,因此编码失败 (UnicodeEncodeError):
'dd'.encode('latin-1')
错误处理:使用正确编码,decode()/encode
加参数 ignore/repalce
字节顺序和二进制数
大端序和小端序
操作二进制用 struct 模块。
struct.pack('<i', 4253) // 小端
struct.pack('>i', 4253)
struc.unpack('<i', b'\x00\x80')
封帧和引用
UDP是数据报,不存在粘包问题。
TCP传输流,就会遇到问题:接收方何时停止调用 recv()
? 整个消息或数据何时完成传输完?何时能将接收到的信息作为一个整体去操作?
六个模式确保知道消息何时结束
模式一:只涉及数据发送,不关注响应。
发送方循环发送数据,直到所有数据都被传给 sendall()
, 然后 close()
;
接收方一直调用 recv()
, 直至 recv()
返回空。
模式二:一的变种,只不过两个方向上都发送
先通过流在一个方向发送,然后关闭该方向。接着在另一个方向发送。
模式三: 定长消息
双方约定好一个length。
模式四:使用特殊字符划分消息边界。
- 定界符要选用传输字符之外的字符,比如传输ASCII字符,用空字符串
\0
定界。 - 任意消息的话,可以使用转义,不过要处理事情太多,不建议。
模式五:每个消息前加上其长度作为前缀,流行选择。长度可以使用定长的二进制整数或者变长的整数字符串后加上一个文本定界符表示。
模式六:解决五中不知道消息长度的问题。将一条消息分为多个数据块发送,每个数据块前加上数据长度。信息结尾处,与发送方约定一个信号,比如长度为0的数据块。
块传输代码
#!/usr/bin/env python3
# Foundations of Python Network Programming, Third Edition
# https://github.com/brandon-rhodes/fopnp/blob/m/py3/chapter05/blocks.py
# Sending data over a stream but delimited as length-prefixed blocks.
import socket, struct
from argparse import ArgumentParser
// I 表示使用32位无符号整数,4B
header_struct = struct.Struct('!I') # messages up to 2**32 - 1 in length
def recvall(sock, length):
blocks = []
while length:
block = sock.recv(length)
if not block:
raise EOFError('socket closed with {} bytes left'
' in this block'.format(length))
length -= len(block)
blocks.append(block)
return b''.join(blocks)
def get_block(sock):
data = recvall(sock, header_struct.size)
(block_length,) = header_struct.unpack(data)
return recvall(sock, block_length)
// 这里为什么不用 sendall? 如果知道数据多长,是否一次发送无所谓了。
def put_block(sock, message):
block_length = len(message)
sock.send(header_struct.pack(block_length))
sock.send(message)
def server(address):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(address)
sock.listen(1)
print('Run this script in another window with "-c" to connect')
print('Listening at', sock.getsockname())
sc, sockname = sock.accept()
print('Accepted connection from', sockname)
sc.shutdown(socket.SHUT_WR)
while True:
block = get_block(sc)
if not block:
break
print('Block says:', repr(block))
sc.close()
sock.close()
def client(address):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(address)
sock.shutdown(socket.SHUT_RD)
put_block(sock, b'Beautiful is better than ugly.')
put_block(sock, b'Explicit is better than implicit.')
put_block(sock, b'Simple is better than complex.')
put_block(sock, b'')
sock.close()
if __name__ == '__main__':
parser = ArgumentParser(description='Transmit & receive blocks over TCP')
parser.add_argument('hostname', nargs='?', default='127.0.0.1',
help='IP address or hostname (default: %(default)s)')
parser.add_argument('-c', action='store_true', help='run as the client')
parser.add_argument('-p', type=int, metavar='port', default=1060,
help='TCP port number (default: %(default)s)')
args = parser.parse_args()
function = client if args.c else server
function((args.hostname, args.p))
pickle 与自定义定界符的格式
有的数据本身已有定界符,不需要封帧。pickle 可以将数据结构保存起来,以便在另一台机器使用。
import pickle
pickle.dump()
pickle.loads()
pickle 使用 .
作为结束符,loads 时 .
之后的内容不会读取,文件指针停留在此处,可以从此处用文件指针读。
数据格式
XML 与 JSON都很流行,文档的话 XML 更好,有结构。
二进制格式 Thrift, ProtoBuf
压缩
必要性:因为数据传输的时间远远多于 CPU 准备数据的时间
zlib.compress()
zlib.decompressobj()
zlib自己提供封帧,一般会在外面包一层封帧。
网络异常
针对套接字的异常:
-
OSERROR
: 网络传输所有阶段都可能遇到。 -
socket.gaierror
:getaddrinfo()
失败后返回, gai 是 get addr info 缩写。 -
socket.timeout
: 设置了超时参数
抛出异常
有两种思路:
完全不处理网络异常
-
将网络错误包装我们自己的异常
取决于我们的程序定位是库还是工具class DestiError(Exception): def __str__(self): return '%s: %s' % (self.arg[0], self.__cause__.error)
捕捉和报告网络异常
两种方法:
- granular 异常处理,对于每个网络调用都使用
try...except
- blanket 异常处理: 在一个代码块或功能块使用
try...except
,然后打印自己定义的错误。在顶层捕捉FatalError