前文講了網(wǎng)絡(luò)之間傳輸協(xié)議TCP和UDP的連接和建立,以及如何域名解析找到雙方主機(jī)∷遥現(xiàn)在該討論如何準(zhǔn)備網(wǎng)絡(luò)傳輸用的數(shù)據(jù),以及可能遇到的錯(cuò)誤洋闽。
字節(jié)和字符串
8個(gè)二進(jìn)制位 (bit) 組成的字節(jié) (Byte) 是IP網(wǎng)絡(luò)上的通用傳輸單元罩缴。文本數(shù)據(jù)最重要的就是選擇一種編碼方式趾牧,將想要傳輸?shù)淖址D(zhuǎn)換成字節(jié)铃在。
字節(jié)字符串遥诉,本質(zhì)上是字符
Python中表示字節(jié)的方法:
第一種使用一個(gè)正好介于0-255的整數(shù)
-
第二種使用字節(jié)字符串. 可以使用
bytes()
將包含數(shù)字的列表轉(zhuǎn)換成字節(jié)字符串须床。>>> 0b1100010 98 >>> 0b1100010 == 0o142 == 98 == 0x62 True
字節(jié)字符串的打宇砹稀: 使用ASCII碼作為簡(jiǎn)寫形式,如果找不到對(duì)應(yīng)ASCII碼,則顯示使用十六進(jìn)制格式 \xNN
來(lái)表示钠惩。實(shí)際上是字符柒凉,比如 b'\x00\x01bcd'
, 注意它開(kāi)頭的 b
字符串
字符編碼標(biāo)準(zhǔn):
- ASCII (American Standard Code for Information InterChange, 美國(guó)標(biāo)準(zhǔn)信息轉(zhuǎn)換碼篓跛,128個(gè))
- Unicode (Uni code, 已經(jīng)收錄10幾萬(wàn)字符了)
Python 3 內(nèi)部把字符串看做是由 Unicode 字符組成膝捞,已經(jīng)對(duì)我們隱藏了細(xì)節(jié)。要處理的只是文件中或者網(wǎng)絡(luò)上的數(shù)據(jù)愧沟。
操作:
-
編碼 (Encoding): Unicode 字符 => 字節(jié)字符串
- 單字節(jié)編碼蔬咬,一個(gè)字節(jié)一個(gè)字符,最多256個(gè)字符
- 多字節(jié)編碼沐寺,定長(zhǎng)的 UTF-32林艘,不定長(zhǎng)的 UTF-8,BOM表示字節(jié)順序
\xeff
- 解碼 (Decoding):字節(jié)字符串 => Unicode字符串
錯(cuò)誤:
- 已編碼的字節(jié)字符串不符合提供的編碼規(guī)則混坞,因此解碼失敗 (UnicodeDecodeError):
b'\x80'.decode()
- 字符無(wú)法使用提供的編碼方式編碼狐援,因此編碼失敗 (UnicodeEncodeError):
'dd'.encode('latin-1')
錯(cuò)誤處理:使用正確編碼,decode()/encode
加參數(shù) ignore/repalce
字節(jié)順序和二進(jìn)制數(shù)
大端序和小端序
操作二進(jìn)制用 struct 模塊究孕。
struct.pack('<i', 4253) // 小端
struct.pack('>i', 4253)
struc.unpack('<i', b'\x00\x80')
封幀和引用
UDP是數(shù)據(jù)報(bào)啥酱,不存在粘包問(wèn)題。
TCP傳輸流蚊俺,就會(huì)遇到問(wèn)題:接收方何時(shí)停止調(diào)用 recv()
? 整個(gè)消息或數(shù)據(jù)何時(shí)完成傳輸完?何時(shí)能將接收到的信息作為一個(gè)整體去操作逛万?
六個(gè)模式確保知道消息何時(shí)結(jié)束
模式一:只涉及數(shù)據(jù)發(fā)送泳猬,不關(guān)注響應(yīng)。
發(fā)送方循環(huán)發(fā)送數(shù)據(jù)宇植,直到所有數(shù)據(jù)都被傳給 sendall()
, 然后 close()
;
接收方一直調(diào)用 recv()
, 直至 recv()
返回空得封。
模式二:一的變種,只不過(guò)兩個(gè)方向上都發(fā)送
先通過(guò)流在一個(gè)方向發(fā)送指郁,然后關(guān)閉該方向忙上。接著在另一個(gè)方向發(fā)送。
模式三: 定長(zhǎng)消息
雙方約定好一個(gè)length闲坎。
模式四:使用特殊字符劃分消息邊界疫粥。
- 定界符要選用傳輸字符之外的字符,比如傳輸ASCII字符腰懂,用空字符串
\0
定界梗逮。 - 任意消息的話,可以使用轉(zhuǎn)義绣溜,不過(guò)要處理事情太多慷彤,不建議。
模式五:每個(gè)消息前加上其長(zhǎng)度作為前綴,流行選擇底哗。長(zhǎng)度可以使用定長(zhǎng)的二進(jìn)制整數(shù)或者變長(zhǎng)的整數(shù)字符串后加上一個(gè)文本定界符表示岁诉。
模式六:解決五中不知道消息長(zhǎng)度的問(wèn)題。將一條消息分為多個(gè)數(shù)據(jù)塊發(fā)送跋选,每個(gè)數(shù)據(jù)塊前加上數(shù)據(jù)長(zhǎng)度涕癣。信息結(jié)尾處,與發(fā)送方約定一個(gè)信號(hào)野建,比如長(zhǎng)度為0的數(shù)據(jù)塊属划。
塊傳輸代碼
#!/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位無(wú)符號(hào)整數(shù),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? 如果知道數(shù)據(jù)多長(zhǎng)候生,是否一次發(fā)送無(wú)所謂了同眯。
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 與自定義定界符的格式
有的數(shù)據(jù)本身已有定界符,不需要封幀唯鸭。pickle 可以將數(shù)據(jù)結(jié)構(gòu)保存起來(lái)须蜗,以便在另一臺(tái)機(jī)器使用。
import pickle
pickle.dump()
pickle.loads()
pickle 使用 .
作為結(jié)束符目溉,loads 時(shí) .
之后的內(nèi)容不會(huì)讀取明肮,文件指針停留在此處,可以從此處用文件指針讀缭付。
數(shù)據(jù)格式
XML 與 JSON都很流行柿估,文檔的話 XML 更好,有結(jié)構(gòu)陷猫。
二進(jìn)制格式 Thrift秫舌, ProtoBuf
壓縮
必要性:因?yàn)閿?shù)據(jù)傳輸?shù)臅r(shí)間遠(yuǎn)遠(yuǎn)多于 CPU 準(zhǔn)備數(shù)據(jù)的時(shí)間
zlib.compress()
zlib.decompressobj()
zlib自己提供封幀,一般會(huì)在外面包一層封幀绣檬。
網(wǎng)絡(luò)異常
針對(duì)套接字的異常:
-
OSERROR
: 網(wǎng)絡(luò)傳輸所有階段都可能遇到足陨。 -
socket.gaierror
:getaddrinfo()
失敗后返回, gai 是 get addr info 縮寫。 -
socket.timeout
: 設(shè)置了超時(shí)參數(shù)
拋出異常
有兩種思路:
完全不處理網(wǎng)絡(luò)異常
-
將網(wǎng)絡(luò)錯(cuò)誤包裝我們自己的異常
取決于我們的程序定位是庫(kù)還是工具class DestiError(Exception): def __str__(self): return '%s: %s' % (self.arg[0], self.__cause__.error)
捕捉和報(bào)告網(wǎng)絡(luò)異常
兩種方法:
- granular 異常處理娇未,對(duì)于每個(gè)網(wǎng)絡(luò)調(diào)用都使用
try...except
- blanket 異常處理: 在一個(gè)代碼塊或功能塊使用
try...except
墨缘,然后打印自己定義的錯(cuò)誤。在頂層捕捉FatalError