一、介紹
UDP是一種不可靠的、無連線的、基於資料包的傳輸層協議。相比於TCP就比較簡單,像寫信一樣,直接打包丟過去,就不用管了,而不用TCP這樣的反覆確認。所以UDP的優勢就是速度快,開銷小。但是隨之而來的就是不穩定,面向無連線的,無法確認資料包。會導致丟包問題。
二、丟包原因
1、服務未啟動或出現故障,但是資料包依然傳送出去,目標地址和埠沒有任何程序在監聽,這些資料包將被丟棄。
2、緩衝區滿,資料包溢位丟失。在實際情況中,如果處理的速度比較慢,會導致資料包堆積在緩衝區,當緩衝區滿時,傳送的資料無處存放就會丟失。另一種情況是傳送的資料包非常大時,可能這個資料包直接超出了緩衝區的大小,也會導致資料丟失。最後一種情況和第一種差不多,由於傳送的速率過快,導致處理不及時。
Client
import socket
import time
def main():
server_host = "127.0.0.1"
server_port = 8888
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client_sock:
i = 0
while True:
message = b"Hello, server!"
client_sock.sendto(message, (server_host, server_port))
i = i + 1
time.sleep(0.001)
if i == 100000:
break
if __name__ == "__main__":
main()
Serevr
import socket
import time
def main():
host = "127.0.0.1"
port = 8888
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server_sock:
server_sock.bind((host, port))
i= 0
while True:
data, client_addr = server_sock.recvfrom(1024)
print("接收來自", client_addr, "的訊息:", data.decode())
if i==0:
time.sleep(10)
i+=1
print(i)
if __name__ == "__main__":
main()
這裡的客戶端傳送了100000個資料包,在服務端特意設定處理第一個資料包後停止10秒模擬資料處理時間。在這種情況下,就會因為速度過快,緩衝區滿而導致資料包丟失。服務端最後的列印為
可以看到只接收到了96521個資料包,後面的因為緩衝區滿的原因全部丟失。這裡不會像TCP一樣堆積資料包會粘包,UDP不會,而是會一次取一個,按順序取。不同的設定的緩衝區大容量不同。
三、避免丟包
既然我們知道了丟包的原因,那麼在好實際開發中我們應儘量避免丟包問題。
1、在接收端人為建立緩衝區,也即是說,如果一個資料包處理的時間很長,那麼我們可以將接收和處理分開,將接收的資料儲存到程式碼層面。
2、再遇見資料包很大時,可以採用分片多次傳輸,最後將資料在接收端彙總處理,避免資料堆積。
3、解決方案:接收處理分離
這裡使用多程序來處理資料,與接收資料使用不同的執行緒,互不影響,這樣不會導致資料包的接收速度,所以緩衝區不會堆積,避免資料包的丟失。手動建立了一個本地資料緩衝區,使用一個列表將接收的資料儲存,使用多程序不斷處理。這裡相當於佇列是一個本地緩衝區,可以避免資料丟包,但是需要注意的是本地緩衝區不能也不能超過大小。
Client
import socket
import time
def main():
server_host = "127.0.0.1"
server_port = 8888
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client_sock:
i = 0
while True:
message = b"Hello, server!"
client_sock.sendto(message, (server_host, server_port))
i = i + 1
time.sleep(0.001)
if i == 100000:
break
if __name__ == "__main__":
main()
Server
from multiprocessing import Queue
import socket
import time
from multiprocessing import Process
def task(data_list:Queue):
'''模擬處理處理'''
while True:
data = data_list.get()
time.sleep(10)
def main():
host = "127.0.0.1"
port = 8888
data_list = Queue()
i= 0
work = Process(target=task, args=(data_list,))
work.daemon = True
work.start()
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server_sock:
server_sock.bind((host, port))
while True:
data, _ = server_sock.recvfrom(1024)
data_list.put(data)
i+=1
print(i)
if __name__ == "__main__":
main()
四、解決丟包
1、回覆機制
Server
import socket
def main():
host = "127.0.0.1"
port = 8888
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server_sock:
server_sock.bind((host, port))
while True:
data, client_addr = server_sock.recvfrom(1024)
print("接收到來自", client_addr, "的訊息:", data.decode())
ack_message = "ACK".encode()
server_sock.sendto(ack_message, client_addr)
if __name__ == "__main__":
main()
Client
import socket
import time
def main():
server_host = "127.0.0.1"
server_port = 8888
message = ["Hello, server!"]*10
timeout = 2
i = 0
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client_sock:
client_sock.settimeout(timeout)
while i<len(message):
try:
client_sock.sendto(message[i].encode(), (server_host, server_port))
print(f"傳送訊息: {message[i]}--{i}")
ack, _ = client_sock.recvfrom(1024)
if ack.decode() == "ACK":
print("接收到確認訊息: ACK")
i+=1
continue
except socket.timeout:
print(f"未接收到確認訊息,重傳資料包")
time.sleep(1)
if __name__ == "__main__":
main()
這裡透過回傳機制確定資料正常到達,服務端接收到資料必須在指定時間內給予回覆,否則預設資料包丟失,將上一次訊息重發,這樣可以解決資料丟包。(注意服務端必須給予回覆,否則將會一直收到重複訊息。)
2、奇偶檢驗
用於檢測資料包是否錯誤,這裡指的是資料包破損,導致資料包是不完整的,這時候使用回覆機制無法找到錯誤,這裡使用奇偶檢驗就可以解決這個問題。客戶端除了在指定時間內需要接收資料外,還要根據回覆的訊息判斷資料包是否破損。
Server
import socket
def verify_and_correct(data):
'''檢驗奇偶檢驗碼'''
received_data = data[:-1]
received_parity = data[-1]
calculated_parity = sum(bytearray(received_data)) % 256
if calculated_parity == received_parity:
return received_data.decode(), True
else:
return None, False
def main():
host = "127.0.0.1"
port = 8888
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as server_sock:
server_sock.bind((host, port))
while True:
data, client_addr = server_sock.recvfrom(1024)
message, is_correct = verify_and_correct(data)
if is_correct:
print("接收到來自", client_addr, "的訊息:", message)
ack_message = "True".encode()
server_sock.sendto(ack_message, client_addr)
else:
print("接收到來自", client_addr, "的錯誤訊息")
ack_message = "False".encode()
server_sock.sendto(ack_message, client_addr)
if __name__ == "__main__":
main()
Client
import socket
def calculate_parity(data):
'''計算奇偶檢驗碼'''
parity = sum(bytearray(data)) % 256
return parity
def main():
server_host = "127.0.0.1"
server_port = 8888
message = "Hello, server!"
message_bytes = message.encode()
parity_byte = calculate_parity(message_bytes)
packet = message_bytes + parity_byte
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as client_sock:
while True:
client_sock.sendto(packet, (server_host, server_port))
print(f"傳送訊息: {message}")
client_sock.settimeout(3)
try:
ack, _ = client_sock.recvfrom(1024)
if ack.decode() == "True":
print("資料已成功接收")
break
else:
print("資料破損,重傳中...")
except socket.timeout:
print("超時,重傳中...")
if __name__ == "__main__":
main()
3、前向糾錯
這種情況比較複雜,是透過更復雜的編碼方案規則,在資料中新增冗餘資料用於資料糾錯。根據自己定義的一套規則,將判斷規則需要的資料,新增到資料包中,冗餘資料用於來糾錯。例如海明碼(這裡不做具體舉例,因為比較複雜)
五、總結
UDP(使用者資料包協議)是一種無連線的傳輸層協議,因其不保證資料包的順序到達和不具備內建重傳機制,導致在網路擁塞、接收緩衝區溢位或傳送頻率過快等情況下容易出現丟包現象。為應對這些問題,可以在應用層實現重傳機制、使用前向糾錯碼等方法。這些方法在一定程度上可以緩解UDP通訊中的丟包問題,提高資料傳輸的可靠性和效率。