前文講了網(wǎng)絡(luò)之間傳輸協(xié)議TCP和UDP的連接和建立,以及如何域名解析找到雙方主機(jī)?,F(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)寫(xiě)形式,如果找不到對(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 縮寫(xiě)。 -
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