下面是小凰凰的簡介,看下吧!
💗人生態度:珍惜時間,渴望學習,熱愛音樂,把握命運,享受生活
💗學習技能:網絡 -> 雲計算運維 -> 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項目實戰視頻教程,點擊此處 免費獲取一起進步哦!