Python網絡編程進階篇

下面是小凰凰的簡介,看下吧!
💗人生態度:珍惜時間,渴望學習,熱愛音樂,把握命運,享受生活
💗學習技能:網絡 -> 雲計算運維 -> python全棧( 當前正在學習中)
💗您的點贊、收藏、關注是對博主創作的最大鼓勵,在此謝過!
有相關技能問題可以寫在下方評論區,我們一起學習,一起進步。
後期會不斷更新python全棧學習筆記,秉着質量博文爲原則,寫好每一篇博文。

一、粘包現象

首先粘包現象是TCP獨有的,UDP中沒有!至於原因後面我會仔細講解。因此我拿入門篇中的一個TCP項目(遠程執行命令)來描述這個現象!:

# settings.py
IP_PORT = ('127.0.0.1',8082) # 如果遠程控制雲服務器,請改成公網ip+port,且安全組開放指定的端口
READ_SIZE = 2048

# tcp服務端
import subprocess
import socket
import settings
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 不用from導,可以明確看出AF_INET這些屬性都是模塊的,而不是socket類的。
server.bind(settings.IP_PORT)
server.listen(5)
while True:
    conn,addr = server.accept()
    while True:
        msg = conn.recv(settings.READ_SIZE)
        if len(msg) == 0:
            print('客戶端正常close,也會發送空數據給服務端!')
            break
        msg = msg.decode('utf-8')
        obj = subprocess.Popen(msg,shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
        std_msg = obj.stdout.read()
        err_msg = obj.stderr.read()
        msg = std_msg + err_msg
        conn.send(msg)
    conn.close()

# tcp客戶端
import socket
import settings
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(settings.IP_PORT)
while True:
    msg = input('請輸入您要執行的命令 >>>').strip()
    if msg == '':
        continue
    if msg == 'exit':
        break
    client.send(msg.encode('utf-8'))
    feedback = client.recv(settings.READ_SIZE)
    print(feedback.decode('utf-8'))
client.close()

我們客戶端先執行ifconfig,再執行ls /
在這裏插入圖片描述發現ls /得到的結果是ifconfig沒取乾淨的內容。這種現象就是粘包現象!

注意:光理論是不夠的,在此送大家一套2020最新Python全棧實戰視頻教程,點擊此處 免費獲取一起進步哦!

二、什麼是粘包

上面說只有TCP有粘包現象,UDP永遠不會粘包,爲何,請看下文!

1、socket收發消息的原理

在這裏插入圖片描述

2、爲什麼TCP出現粘包現象?

TCP是面向連接的,流式協議,如果send一個數據過大,他會採用分段傳輸。數據過小,會粘在一起傳輸(nagle算法),所以TCP傳輸數據,像一段水流一樣,流到客戶端,recv(1024),是最多接收1024字節,一般緩存區只要有數據,就會recv,我們這裏是本地做實驗,網絡延時基本不存在,所以,最多接收1024字節,那麼它肯定是接收了1024字節的,但是如果,網絡延時很高,水流到客戶端,起初只有幾個字節,那麼recv也只會收幾個字節,就造成了沒有收乾淨的情況,後面的水再流過來,還是會繼續放到緩存區,下次你再輸入的命令,取的就是上次沒取乾淨的內容了,一直這樣下去,每次執行命令返回的結果都是錯亂的。

3、爲什麼UDP不會出現粘包現象

然而UDP傳輸消息,是傳輸一個整個消息到服務端,且是不可靠的,如果你一次沒取完,剩下的就直接扔掉了。因此它不會出現粘包現象,一般UDP用於傳輸短數據,UDP可以用於聊天,你會發現聊天有些時候有字數限制,這是因爲要限制你的數據長度,過長就容易丟失,短的數據用UDP還是比較可靠的。

我們現在知道了是因爲沒收乾淨導致的粘包現象,因此我們接下來的解決方案,就是致力於使每次收包,都能收乾淨,才能執行下條命令!

補充知識點一:

當發送端緩衝區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去。

補充知識點二:

# send(字節流)與sendall的區別。
send的字節流是先放入己端緩存,然後由協議控制將緩存內容發往對端,如果待發送的字節流大小大於緩存剩餘空間,那麼數據丟失,用sendall就會循環調用send,數據不會丟失

三、解決粘包的low比處理方法

問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,以至於收不完數據,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據

# settings.py
IP_PORT = ('127.0.0.1',8082) # 如果遠程控制雲服務器,請改成公網ip+port,且安全組開放指定的端口
READ_SIZE = 2048

# tcp服務端
import subprocess
import socket
import settings
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 不用from導,可以明確看出AF_INET這些屬性都是模塊的,而不是socket類的。
server.bind(settings.IP_PORT)
server.listen(5)
while True:
    conn,addr = server.accept()
    while True:
        msg = conn.recv(settings.READ_SIZE)
        if len(msg) == 0:
            print('客戶端正常close,也會發送空數據給服務端!')
            break
        msg = msg.decode('utf-8')
        obj = subprocess.Popen(msg,shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
        std_msg = obj.stdout.read()
        err_msg = obj.stderr.read()
        msg = std_msg + err_msg
        conn.send(str(len(msg)).encode('utf-8'))
        flag = conn.recv(1024).decode('utf-8')
        if flag == '已收到數據長度,可以開始正式傳輸數據!':
            conn.sendall(msg)
    conn.close()

# tcp客戶端
import socket
import settings
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(settings.IP_PORT)
while True:
    msg = input('請輸入您要執行的命令 >>>').strip()
    if msg == '':
        continue
    if msg == 'exit':
        break
    client.send(msg.encode('utf-8'))
    data_len = int(client.recv(1024).decode('utf-8'))
    client.send('已收到數據長度,可以開始正式傳輸數據!'.encode('utf-8'))
    recv_data_len = 0
    data = b''
    while recv_data_len < data_len:
        feedback = client.recv(settings.READ_SIZE)
        recv_data_len += len(feedback)
        data += feedback
        print(data.decode('utf-8'))
client.close()

執行結果:
在這裏插入圖片描述爲何low:
程序的運行速度遠快於網絡傳輸速度,所以服務端在發送一段字節的數據前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗

四、解決粘包的大招

大招:自定義協議。當然我們解決的思想還是:自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據,只是知曉的手段更加高明!

1、解決粘包的大招思想

答:我們服務端在發送數據時,在數據上封裝一層固定長度的頭部,頭部裏面裝的就是數據的長度。我們客戶端接收的時候,先接受那固定長度的頭部,拿到數據的長度,然後再循環接收後面的數據,直到數據取完!

2、struct模塊

該模塊可以把一個類型,如數字,轉成固定長度的bytes

>>> res = struct.pack('i',111111111) # i是什麼?i是說明你要轉換的對象是一個整型數字,注意長度是有限的,如果你要轉換很大的數字,你需要指定l長整型或者q也可以
>>> res # i轉換出來固定長度爲4個字節
b'\xc7k\x9f\x06'
>>> struct.unpack('i',res) # 注意得到的是一個小元組
(111111111,)

在這裏插入圖片描述

3、大招源碼

# settings.py
IP_PORT = ('127.0.0.1',8082) # 如果遠程控制雲服務器,請改成公網ip+port,且安全組開放指定的端口
READ_SIZE = 2048

# tcp服務端
import subprocess
import socket
import struct
import settings
server = socket.socket(socket.AF_INET,socket.SOCK_STREAM) # 不用from導,可以明確看出AF_INET這些屬性都是模塊的,而不是socket類的。
server.bind(settings.IP_PORT)
server.listen(5)
while True:
    conn,addr = server.accept()
    while True:
        msg = conn.recv(settings.READ_SIZE)
        if len(msg) == 0:
            print('客戶端正常close,也會發送空數據給服務端!')
            break
        msg = msg.decode('utf-8')
        obj = subprocess.Popen(msg,shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE)
        std_msg = obj.stdout.read()
        err_msg = obj.stderr.read()
        msg = std_msg + err_msg
        msg_len = struct.pack('i',len(msg)) # pack的結果就是個字節類型的哈
        conn.send(msg_len) # 打個頭部,就是在數據之前發送,沒啥高深的
        conn.send(msg)
    conn.close()

# tcp客戶端
import socket
import settings
import struct
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(settings.IP_PORT)
while True:
    msg = input('請輸入您要執行的命令 >>>').strip()
    if msg == '':
        continue
    if msg == 'exit':
        break
    client.send(msg.encode('utf-8'))
    data_len_tuple = client.recv(4)
    data_len = struct.unpack('i',data_len_tuple)[0]
    recv_len = 0
    data = b''
    while recv_len < data_len:
        feedback = client.recv(settings.READ_SIZE)
        recv_len += len(feedback)
        data += feedback
    print(data.decode('utf-8'))
client.close()

先執行ps aux,再執行ls /
在這裏插入圖片描述成功!!像這樣我們加了個頭部,頭部長度固定,就是自定義了個協議,只是這個協議比較簡單而已。

4、存在的不足之處,定義更通用的協議

假如我是基於tcp實現的是文件的上傳和下載,把文件打開,把數據傳過來,我們不僅需要將文件裏面數據的長度先傳到客戶端,也要把文件名、文件的md5校驗碼(校驗文件完整性)也先數據傳一步傳過來,這樣我們才能打開一個相同名字的文件,把接收到的數據,寫入這個文件。

(1)解決思想
1. '服務端的操作:'我們可以創建一個字典,把需要先一步發送的東西,全放進去,然後用json將其序列化得到一個字符串,然後將其轉爲bytes類型,然後服務器會把這個bytes當作頭部傳輸過去,因此我們需要知道頭部的長度啊,因此我們需要struct把len(字典序列化後轉成的bytes類型)打成一個固定長度的'頭部的頭部'2. '客戶端的操作:'客戶端先recv4個字節的頭部,裏面存的是'字典序列化轉成的bytes類型的長度',unpack得到其長度假如爲length,然後再recv(length),得到'字典序列化轉成的bytes類型',然後decode,然後json.loads得到字典,這樣就拿到了你想要先數據一步傳輸過來的信息。再進行其他操作即可。
(2)源碼解析流程
import json,struct
# 假設通過客戶端上傳1T:1073741824000的文件a.txt

# 爲避免粘包,必須自定製報頭
header_dic={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} # 1T數據,文件路徑和md5值

# 爲了該報頭能傳送,需要序列化並且轉爲bytes
head_bytes=json.dumps(header_dic).encode('utf-8') # 序列化得到一個字符串並轉成bytes,用於傳輸

# 爲了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節
head_len_bytes=struct.pack('i',len(head_bytes)) # 這4個字節裏只包含了一個數字,該數字是報頭的長度

# 服務端開始發送
conn.send(head_len_bytes) # 先發報頭的長度,4個bytes
conn.send(head_bytes) # 再發報頭的字節格式
conn.sendall(文件內容) # 然後發真實內容的字節格式

# 客戶端開始接收
head_len_bytes=s.recv(4) # 先收報頭4個bytes,得到報頭長度的字節格式
x=struct.unpack('i',head_len_bytes)[0] # 提取報頭的長度
head_bytes=s.recv(x) # 按照報頭長度x,收取報頭的bytes格式
head_dic = json.loads(head_bytes.decode('utf-8')) # 提取字典,字典裏面的所有需要先數據一步傳入的信息,客戶端就拿到了

# 最後根據報頭的內容提取真實的數據,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)

五、socketserver實現併發

1、socketserver基於(TCP)實現併發

# 服務端
import socketserver
class MyHandler(socketserver.BaseRequestHandler):
    def handle(self):     # 這個方法必須定義,裏面寫TCP的通信邏輯
        #通信循環
        while True:
            try:
                data=self.request.recv(1024)  # self.request就是conn那個管道
                if len(data)==0:break # 發空表示客戶端斷開連接了,所以break掉通信循環
                self.request.send(data.upper()) # 相當於conn.send(data.upper())
            except Exception:
                break
s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyHandler,bind_and_activate=True)
s.serve_forever()       # 代表連接循環,不斷accept建立連接
# 每建立一個連接就會啓動一個線程(服務員),然後調用MyHandler類產生一個對象,調用該對像下的handle方法,與剛剛建立好的連接進行通信循環
# 客戶端,複製多份就是多個客戶端
import socket

client=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080)) # 指定服務端ip和端口

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg = 'exit':break
    client.send(msg.encode('utf-8'))
    data=client.recv(1024)
    print(data.decode('utf-8'))

client.close() # 客戶端輸入命令exit,跳出通信循環,客戶端close斷開連接

2、socketserver基於(UDP)實現併發

# 服務端
import socketserver
class MyHandler(socketserver.BaseRequestHandler):
    def handle(self): # 寫UDP通信邏輯
        data=self.request[0] # 首先你應該明白這個self,到底是啥,它是專爲某個客戶端創建MyHandler對象,這裏的request和tcp的不一樣,這裏的self.request[0],直接獲取到的是客戶端發來的數據。
        # UDP面向無連接,一次消息的發送,是一整個消息,想下每個消息,一個線程,對應一個Myhandler的對象,發送的數據我可以直接存儲在這個對象裏就行了。
        # TCP是面向連接的,一次連接,多次數據交互,所以我們必須還得使用recv、send操作,不能把數據存給對象的某個數據屬性,如果賦值給某個屬性,那麼會覆蓋,得到數據不完整。
        print('客戶端消息',data)
        self.request[1].sendto(data.upper(),self.client_address) # self.client_address獲取client的ip_port小元組,指定客戶端的地址端口

s=socketserver.ThreadingUDPServer(('127.0.01',8080),MyHandler)
s.serve_forever() # 來一個消息,就爲其分配一個線程,然後這個線程,執行MyHandler類得到一個對象,調用handle方法,
# 客戶端
import socket
client=socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
msg='client11111111111'
client.sendto(msg.encode('utf-8'),('127.0.0.1',8080))
data,server_addr=client.recvfrom(1024)
print(data)
client.close()

注意上述爲了更加清晰的解釋socketserver實現併發的原理,並沒有添加粘包的處理方式。可以自己嘗試。

注意:最後送大家一套2020最新企業Pyhon項目實戰視頻教程,點擊此處 免費獲取一起進步哦!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章