Python網(wǎng)絡(luò)編程筆記(四):網(wǎng)絡(luò)數(shù)據(jù)和網(wǎng)絡(luò)錯(cuò)誤

前文講了網(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é)的方法:

  1. 第一種使用一個(gè)正好介于0-255的整數(shù)

  2. 第二種使用字節(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
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容