Redispy 源碼學(xué)習(xí)(三) --- RESP協(xié)議實(shí)現(xiàn)--編碼

經(jīng)過(guò)對(duì)RESP協(xié)議的閱讀,我們了解redis客戶端和服務(wù)端的通信方式。下面將根據(jù)resp協(xié)議使用python3實(shí)現(xiàn)其編碼,也就是將客戶端的查詢命令按照RESP協(xié)議編碼。

字符編碼

在處理resp編碼之前,有必要對(duì)字符的編碼做簡(jiǎn)單的介紹。計(jì)算機(jī)給人感覺(jué)很強(qiáng)大,可是它們處理的數(shù)據(jù)的基本構(gòu)成卻很簡(jiǎn)單。任何計(jì)算機(jī)里的數(shù)據(jù),無(wú)非都是一些二進(jìn)制的0或者1。這些0和1當(dāng)然不適合給人類(lèi)閱讀,人類(lèi)只寫(xiě)自己認(rèn)識(shí)的字符,例如hello world1 + 1之類(lèi)的字符。計(jì)算機(jī)當(dāng)然也會(huì)抗議,畢竟它們不懂。為了讓計(jì)算機(jī)能懂人類(lèi)可讀的字符,就需要把這些字符轉(zhuǎn)換成0或1組成的二進(jìn)制數(shù)據(jù)。這個(gè)轉(zhuǎn)換過(guò)程就是編碼,顧名思義,編碼的反方向就是解碼。

由于計(jì)算機(jī)是西方人搞出來(lái)的,美國(guó)人思來(lái)想去,拉丁字符才26個(gè),亂七八糟的標(biāo)點(diǎn)和美元百分好加起來(lái)也不過(guò)百多個(gè)。一個(gè)字節(jié)有8位,可以表示256種字符(2**8)。一個(gè)字節(jié)編碼符號(hào)綽綽有余。然后他們就依此指定了一個(gè)編碼表,即ASCII表。

可是沒(méi)多久,同樣是西方人的歐洲其他國(guó)家不干了,像法國(guó)德國(guó)這樣除了拉丁字符,還有類(lèi)似拼音聲調(diào)的字符,ASCII的規(guī)定就不夠了。不僅這些字符,中國(guó)的漢字,日本文字,阿拉伯文字等,都無(wú)法用ASCII表示。既然世界文化這么多,就只能想一個(gè)完全之策來(lái)大一統(tǒng)。

Unicode應(yīng)運(yùn)而生,簡(jiǎn)而言之就是使用2-4個(gè)字節(jié)來(lái)編碼。數(shù)量上肯定是足夠了,可是對(duì)于ASCII碼,無(wú)緣無(wú)故多出幾個(gè)字節(jié)來(lái)編碼顯然不合算,因此Unicde的一種實(shí)現(xiàn)utf-8就誕生了。utf-8兼容ascii方式,可以根據(jù)具體情況用1-4個(gè)字節(jié)來(lái)表示一個(gè)字符。例如一個(gè)漢字unicode編碼是一個(gè)長(zhǎng)度 \u534eutf-8的編碼則是三個(gè)字符長(zhǎng)度\xe5\x8d\x8e。

除了utf-8編碼,中文世界里常見(jiàn)的是gbk方式編碼。gbk和utf-8一樣,也是一種編碼方式,不過(guò)只對(duì)中國(guó)的漢字和少數(shù)幾個(gè)民族文字兼容。范圍上比utf-8要小。

python字符編碼

提及編碼,python2經(jīng)常出現(xiàn)UnicodeDecodeError錯(cuò)誤,尤其是爬蟲(chóng)的時(shí)候這個(gè)錯(cuò)誤常被人詬病,很多人轉(zhuǎn)向python3??墒侨绻悴磺寰幋a與解碼的問(wèn)題,python3也會(huì)出現(xiàn)UnicodeDecodeError異常。

python3中,所有字串都是unicode實(shí)現(xiàn)。也就是str類(lèi)型。字串可以編碼成bytes類(lèi)型,bytes類(lèi)型可以解碼成字串。

>>> s = 'hello 世界'
>>> type(s)
<class 'str'>
>>> s.encode('utf-8')
b'hello \xe4\xb8\x96\xe7\x95\x8c'
>>> type(s.encode('utf-8'))
<class 'bytes'>
>>> b = b'hello 世界'
  File "<stdin>", line 1
SyntaxError: bytes can only contain ASCII literal characters.
>>> b = b'hello \xe4\xb8\x96\xe7\x95\x8c'
>>> b
b'hello \xe4\xb8\x96\xe7\x95\x8c'
>>> b.decode('utf-8')
'hello 世界'
>>> len(b)
12

對(duì)于python2而言,引號(hào)定義的字串是utf-8或者gbk的編碼(依賴系統(tǒng))。使用u加字串定義的是unicode。因此py2也有encode和decode的方式。

無(wú)論py2還是py3,計(jì)算機(jī)內(nèi)存處理的字串都是unicode,當(dāng)寫(xiě)入文件或者在網(wǎng)絡(luò)IO流中,都應(yīng)該編碼成utf-8的格式(utf-8國(guó)際通用,就不必使用gbk了)。

解碼的時(shí)候就不能一概而論了。很多爬蟲(chóng)的程序中,被爬的網(wǎng)站比較古老,使用了gbk的編碼。若不假思索的就以u(píng)tf-8的方式decode,肯定會(huì)報(bào)錯(cuò)。使用requests庫(kù)的時(shí)候,很少出現(xiàn)字符解碼錯(cuò)誤,因?yàn)樗鼉?nèi)部有一個(gè)程序會(huì)先判斷目標(biāo)字符的編碼,然后再針對(duì)性的解碼。因此我們寫(xiě)程序的時(shí)候,解碼也應(yīng)該先猜除對(duì)方編碼。至于怎么猜,可以學(xué)習(xí)requests的方式。

resp 字符編碼

說(shuō)來(lái)那么多python的編碼,為得是下面RESP做鋪墊。根據(jù)redis.py 的源碼,編碼和解碼的方法掛載在Connection類(lèi)的下面。因此我們的客戶端調(diào)用代碼如下:

 args = ('PING',)
 packed_command =  Connection().pack_command(*args)
 print(packed_command)

調(diào)用打印的結(jié)果為 [b'*1\r\n$4\r\nPING\r\n'],和預(yù)期的編碼一樣。

Connection 類(lèi)

首先創(chuàng)建一個(gè)Connection類(lèi),我們需要初始化其編碼方式和編碼錯(cuò)誤。

class Connection(object):
    def __init__(self, encoding='utf-8', encoding_errors='strict'):
        self.encoding = encoding
        self.encoding_errors = encoding_errors
    
    def pack_command(self, *args):
        pass

編碼命令

接下來(lái)實(shí)現(xiàn)pack_command 方法。

    def pack_command(self, *args):
        """將redis命令安裝redis的協(xié)議編碼,返回編碼后的數(shù)組,如果命令很大,返回的是編碼后chunk的數(shù)組"""
        output = []
        command = args[0]
        if ' ' in command:
            args = tuple([Token(s) for s in command.split(' ')]) + args[1:]
        else:
            args = (Token(command),) + args[1:]

        buff = SYM_EMPTY.join(
                (SYM_STAR, b(str(len(args))), SYM_CRLF))

        for arg in map(self.encode, args):
            # 數(shù)據(jù)量特別大的時(shí)候,分成部分小的chunk
            if len(buff) > 6000 or len(arg) > 6000:
                buff = SYM_EMPTY.join((buff, SYM_DOLLAR, b(str(len(arg))), SYM_CRLF))
                output.append(buff)
                output.append(arg)
                buff = SYM_CRLF
            else:
                buff = SYM_EMPTY.join((buff, SYM_DOLLAR, b(str(len(arg))), SYM_CRLF, arg, SYM_CRLF))

        output.append(buff)
        return output

該方法首先判斷了命令的方式,是單命令(PING)還是復(fù)合命令(CONFIG SET)。然后針對(duì)這兩種方式分別使用Token編碼。Token即命令的頭標(biāo)簽。

然后使用SYM_EMPTY把字符頭標(biāo)簽進(jìn)行編碼。

def b(x):
    '''將`unicode`編碼成`bytes` 編碼格式位 `latin-1`'''
    return x.encode('latin-1') if not isinstance(x, bytes) else x

SYM_STAR = b('*')
SYM_DOLLAR = b('$')
SYM_CRLF = b('\r\n')
SYM_EMPTY = b('')

因?yàn)閭鬏數(shù)淖执畱?yīng)該是字節(jié)串(bytes)類(lèi)型,并且?guī)讉€(gè)符號(hào)都是ascii符號(hào),因此編碼成latin-1utf-8都是一樣的。

可以看見(jiàn)PING編碼的頭標(biāo)簽為'*1\r\nCONFIG SET的oken為b'*4\r\n'。

接下來(lái)就是一個(gè)迭代編碼除了token之外,編碼命令和參數(shù)。當(dāng)buff不大的時(shí)候,就直接按照RESP協(xié)議串聯(lián)即可。即token + $ + 字節(jié)串長(zhǎng)度+CRLF+參數(shù)+CRLF的方式

如果token和參數(shù)大于6000字節(jié)長(zhǎng)度,就把編碼的命令組合拆分為小長(zhǎng)度的chunk數(shù)組。

當(dāng)然,在迭代命令和參數(shù)之前,需要將這些字串編碼成字節(jié)串。即map(self.encode, args)的功能,對(duì)應(yīng)的encode方法如下:

    def encode(self, value):
        if isinstance(value, Token):
            return b(value.value)
        elif isinstance(value, bytes):
            return value
        elif isinstance(value, int):
            value = b(str(value))
        elif not isinstance(value, str):
            value = str(value)
        if isinstance(value, str):
            value = value.encode(self.encoding, self.encoding_errors)
        return value

此時(shí)可以看出頭標(biāo)簽使用Token封裝,便于此時(shí)encode成bytes字節(jié)串,同時(shí)為python2提供了兼容的接口。python2只要重寫(xiě)一個(gè)b函數(shù)即可。

總結(jié)

RESP編碼比較簡(jiǎn)單,源于RESP的協(xié)議設(shè)計(jì)精巧。代碼實(shí)現(xiàn)的內(nèi)容并不多。無(wú)非就是需要注意頭標(biāo)簽token的編碼,和當(dāng)命令參數(shù)特別長(zhǎng)的時(shí)候,拆分字節(jié)串為chunk數(shù)組來(lái)發(fā)送數(shù)據(jù)。此外還需要注意,任何網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù),都不能是直接的字符串,而是編碼成utf-8的字節(jié)串。

RESP的編碼并不復(fù)雜,更多挑戰(zhàn)在于如何解碼redis服務(wù)器的響應(yīng)。在解析響應(yīng)之前,我們應(yīng)該創(chuàng)建redis的連接,將編碼的命令發(fā)送到redis服務(wù)器。

文中相關(guān)代碼

最后編輯于
?著作權(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)容