網絡編程-----黏包問題
一,黏包現象
我們通過一段簡單程序來看看黏包現象:
import socket sk=socket.socket() sk.bind((‘127.0.0.1‘,8090)) sk.listen() conn,addr=sk.accept() while True: cmd=input(">>>") if cmd==‘q‘: conn.send(b‘q‘) break conn.send(cmd.encode(‘gbk‘)) res=conn.recv(1024).decode(‘gbk‘) print(res) conn.close() sk.close()server端
imsocket import subprocess sk=socket.socket() sk.connect((‘127.0.0.1‘,8090)) while True: cmd=sk.recv(1024).decode(‘gbk‘) if cmd==‘q‘: break res=subprocess.Popen(cmd,shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) sk.send(res.stdout.read()) sk.send(res.stderr.read()) sk.close()client端
同時執行多條命令之後,得到的結果很可能只有一部分,在執行其他命令時又會接到之前執行的另外一部分結果,這種就是黏包。
只有tcp有黏包現象,udp不會黏包。
二,黏包成因
tcp協議中的數據傳遞
tcp協議的拆包機制
當發送端緩沖區的長度大於網卡的MTU時,tcp會將這次發送的數據拆成幾個數據包發送出去。
MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。 大部分網絡設備的MTU都是1500。
如果本機的MTU比網關的MTU大,大的數據包就會被拆開來傳送,這樣會產生很多數據包碎片,增加丟包率,降低網絡速度。
面向流的通信特點和Nagle(優化)算法
TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。
收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然後進行封包。
這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
對於空消息:tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上消息頭發送過去。
可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
View Code
基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,
在何處結束
此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一個TCP段。若連續幾次需要send的數據都很少,
通常TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。
三,會發生黏包的兩種情況
1,發送方的緩存機制:發送端需要等緩沖區滿才發送出去,造成黏包(發送數據時間間隔很短,數據很小,會合到一起,產生黏包)
例:連續send兩次且數據很小
import socket sk = socket.socket() sk.connect((‘127.0.0.1‘,8090)) sk.send(b‘hello‘) sk.send(b‘egg‘) sk.close()發送端
import socket sk = socket.socket() sk.bind((‘127.0.0.1‘,8090)) sk.listen() conn,addr = sk.accept() ret2 = conn.recv(10) print(ret2) conn.close() sk.close()接收端
2,接收方的緩存機制:接收不及時接收緩沖區的包,造成多個包接收(客戶端發送一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿走上次剩余的數據,產生黏包。)
例:連續recv兩次且第一個recv接收的數據小
import socket sk = socket.socket() sk.connect((‘127.0.0.1‘,8090)) sk.send(b‘hello,egg‘) sk.close()發送端
import socket sk = socket.socket() sk.bind((‘127.0.0.1‘,8090)) sk.listen() conn,addr = sk.accept() ret = conn.recv(2) ret2 = conn.recv(10) print(ret) print(ret2) conn.close() sk.close()接收端
總結:
1,表面上看,黏包問題主要是因為發送端和接收端的緩存機制、tcp協議面向流通信的特點。
2,實際上,主要還是因為接收端不知道消息之間的界限,不知道一次性提取多少次字節的數據所造成的。
四,黏包的解決
1,將要發送的字節流總大小發給接收端,然後接收端來一個循環接收完所有數據。
import socket sk = socket.socket() sk.bind((‘127.0.0.1‘,8080)) sk.listen() conn,addr = sk.accept() while True: cmd = input(‘>>>‘) if cmd == ‘q‘: conn.send(b‘q‘) break conn.send(cmd.encode(‘gbk‘)) num = conn.recv(1024).decode(‘utf-8‘) # 2048 conn.send(b‘ok‘) res = conn.recv(int(num)).decode(‘gbk‘) print(res) conn.close() sk.close()服務端
import socket import subprocess sk = socket.socket() sk.connect((‘127.0.0.1‘,8080)) while True: cmd = sk.recv(1024).decode(‘gbk‘) if cmd == ‘q‘: break res = subprocess.Popen(cmd,shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) std_out = res.stdout.read() std_err = res.stderr.read() sk.send(str(len(std_out)+len(std_err)).encode(‘utf-8‘)) #2000 sk.recv(1024) # ok sk.send(std_out) sk.send(std_err) sk.close()客戶端
問題:程序的運行速度遠快於網絡傳輸速度,所以在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網路延遲帶來的性能損耗。send sendto在超過一定範圍的時候會報錯
2,進階方案
struck模塊,該模塊可以把一個類型,例,數字,轉換成固定的長度bytes類型
import struct s=struct.pack(‘i‘,23) # i ,整數類型,不管數字有多大 ,都轉換成4個字節長度 print(s) #b‘\x84\x1a\x00\x00‘ s1=struct.unpack(‘i‘,s) #取出數字 print(s1) #元組(23,)
import json,struct #假設通過客戶端上傳1T:1073741824000的文件a.txt #為避免粘包,必須自定制報頭 header={‘file_size‘:1073741824000,‘file_name‘:‘/a/b/c/d/e/a.txt‘,‘md5‘:‘8f6fbf8347faa4924a76856701edb0f3‘} #1T數據,文件路徑和md5值 #為了該報頭能傳送,需要序列化並且轉為bytes head_bytes=bytes(json.dumps(header),encoding=‘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格式 header=json.loads(json.dumps(header)) #提取報頭 #最後根據報頭的內容提取真實的數據,比如 real_data_len=s.recv(header[‘file_size‘]) s.recv(real_data_len)View Code
使用struck解決黏包:
把報頭做成字典,字典裏要包含將要發送真實數據的詳細信息,然後json序列化,然後用struck將序列化後的數據長度打包成4個字節。
發送時:先發報頭長度,再編碼報頭內容然後發送,最後發真實內容。
接收時:先收報頭長度,用struct取出來,根據取出的長度收取報頭內容,解碼,反序列化,從反序列化的結果中取出待取數據的詳細信息,然後去取真實的數據內容
# 發送端 import os import json import struct import socket sk = socket.socket() sk.connect((‘127.0.0.1‘,8090)) buffer = 1024 # 發送文件 head = {‘filepath‘:r‘F:\Program Files\feiq\Recv Files\day23‘, ‘filename‘:r‘04 python fullstack s9day23 組合2.mp4‘, ‘filesize‘:None} file_path = os.path.join(head[‘filepath‘],head[‘filename‘]) filesize = os.path.getsize(file_path) head[‘filesize‘] = filesize json_head = json.dumps(head) # 字典轉成了字符串 bytes_head = json_head.encode(‘utf-8‘) # 字符串轉bytes # 計算head的長度 head_len = len(bytes_head) # 報頭的長度 pack_len = struct.pack(‘i‘,head_len) sk.send(pack_len) # 先發報頭的長度 sk.send(bytes_head) # 再發送bytes類型的報頭 with open(file_path,‘rb‘) as f: while filesize: print(filesize) if filesize >= buffer: content = f.read(buffer) # 每次讀出來的內容 sk.send(content) filesize -= buffer else: content = f.read(filesize) sk.send(content) break sk.close()客戶端
import json import socket import struct sk = socket.socket() sk.bind((‘127.0.0.1‘,8090)) sk.listen() buffer = 1024 conn,addr = sk.accept() # 接收 head_len = conn.recv(4) head_len = struct.unpack(‘i‘,head_len)[0] json_head = conn.recv(head_len).decode(‘utf-8‘) head = json.loads(json_head) filesize = head[‘filesize‘] with open(head[‘filename‘],‘wb‘) as f: while filesize: print(filesize) if filesize >= buffer: content = conn.recv(buffer) f.write(content) filesize -= buffer else: content = conn.recv(filesize) f.write(content) break conn.close() sk.close()服務端
五,socket的更多方法
服務端套接字函數
s.bind() 綁定(主機,端口號)到套接字
s.listen() 開始TCP監聽
s.accept() 被動接受TCP客戶的連接,(阻塞式)等待連接的到來
客戶端套接字函數
s.connect() 主動初始化TCP服務器連接
s.connect_ex() connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
公共用途的套接字函數
s.recv() 接收TCP數據
s.send() 發送TCP數據
s.sendall() 發送TCP數據
s.recvfrom() 接收UDP數據
s.sendto() 發送UDP數據
s.getpeername() 連接到當前套接字的遠端的地址
s.getsockname() 當前套接字的地址
s.getsockopt() 返回指定套接字的參數
s.setsockopt() 設置指定套接字的參數
s.close() 關閉套接字
面向鎖的套接字方法
s.setblocking() 設置套接字的阻塞與非阻塞模式
s.settimeout() 設置阻塞套接字操作的超時時間
s.gettimeout() 得到阻塞套接字操作的超時時間
面向文件的套接字的函數
s.fileno() 套接字的文件描述符
s.makefile() 創建一個與該套接字相關的文件
View Code
網絡編程-----黏包問題