tcp粘包問題

1、什麼是粘包

注意:只有TCP有粘包現象,UDP永遠不會粘包,由於TCP是基於數據流的協議,而UDP是基於數據報的協議算法

 

發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。shell

例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束json

所謂粘包問題主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的。緩存

此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據negal優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。服務器

            negal優化算法:會將數據量小的,且時間間隔較短的數據一次性發給對方
網絡

  1. TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。
  2. UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。
  3. tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略

udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x個字節的數據就算完成,如果y>x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠數據結構

tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。socket

 

兩種狀況下會發生粘包。tcp

發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)ide

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(10)
data2=conn.recv(10)

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()
服務端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello'.encode('utf-8'))
s.send('feng'.encode('utf-8'))
客戶端

 

接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包) 

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(2) #一次沒有收完整
data2=conn.recv(10)#下次收的時候,會先取舊的數據,而後取新的

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()
服務端
#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello feng'.encode('utf-8'))
客戶端

 

拆包的發生狀況

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

補充問題一:爲什麼tcp是可靠傳輸,udp是不可靠傳輸

tcp在數據傳輸時,發送端先把數據發送到本身的緩存中,而後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則從新發送數據,因此tcp是可靠的

而udp發送數據,對端是不會返回確認信息的,所以不可靠

補充問題二:send(字節流)和recv(1024)及sendall

recv裏指定的1024意思是從緩存裏一次拿出1024個字節的數據

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

 

2、解決粘包問題的方法

粘包問題的關鍵在於:

接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端知曉,而後接收端來一個死循環接收完全部數據

解決方法一

 

#_*_coding:utf-8_*_
import socket,subprocess
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(ip_port)
s.listen(5)

while True:
    conn,addr=s.accept()
    print('客戶端',addr)
    while True:
        msg=conn.recv(1024)
        if not msg:break
        res=subprocess.Popen(msg.decode('utf-8'),shell=True,\
                            stdin=subprocess.PIPE,\
                         stderr=subprocess.PIPE,\
                         stdout=subprocess.PIPE)
        err=res.stderr.read()
        if err:
            ret=err
        else:
            ret=res.stdout.read()
        data_length=len(ret)
        conn.send(str(data_length).encode('utf-8'))
        data=conn.recv(1024).decode('utf-8')
        if data == 'recv_ready':
            conn.sendall(ret)
    conn.close()
服務端

 

#_*_coding:utf-8_*_
import socket,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))
    length=int(s.recv(1024).decode('utf-8'))
    s.send('recv_ready'.encode('utf-8'))
    send_size=0
    recv_size=0
    data=b''
    while recv_size < length:
        data+=s.recv(1024)
        recv_size+=len(data)


    print(data.decode('utf-8'))
客戶端

該方法的缺點:

程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字節流長度,這種方式會放大網絡延遲帶來的性能損耗

 

解決方法二

 1 import json,struct
 2 #假設經過客戶端上傳1T:1073741824000的文件a.txt
 3 
 4 #爲避免粘包,必須自定製報頭
 5 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T數據,文件路徑和md5值
 6 
 7 #爲了該報頭能傳送,須要序列化而且轉爲bytes
 8 head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化並轉成bytes,用於傳輸
 9 
10 #爲了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個字節
11 head_len_bytes=struct.pack('i',len(head_bytes)) #這4個字節裏只包含了一個數字,該數字是報頭的長度
12 
13 #客戶端開始發送
14 conn.send(head_len_bytes) #先發報頭的長度,4個bytes
15 conn.send(head_bytes) #再發報頭的字節格式
16 conn.sendall(文件內容) #而後發真實內容的字節格式
17 
18 #服務端開始接收
19 head_len_bytes=s.recv(4) #先收報頭4個bytes,獲得報頭長度的字節格式
20 x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度
21 
22 head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式
23 header=json.loads(json.dumps(header)) #提取報頭
24 
25 #最後根據報頭的內容提取真實的數據,好比
26 real_data_len=s.recv(header['file_size'])
27 s.recv(real_data_len)

推薦使用(代碼詳解):

from socket import *
import subprocess                      #啓動一個新的進程並與之通訊
import struct                          #該模塊能夠把一個類型,如數字,轉成固定長度的bytes
import json

server=socket(AF_INET,SOCK_STREAM)
server.bind(('127.0.0.1',8080))
server.listen(5)

while True:
    conn,client_addr=server.accept() #(鏈接對象,客戶端的ip和端口)
    # print(client_addr)
    while True:
        try:
            cmd=conn.recv(1024)                                 #接收系統命令
            obj=subprocess.Popen(cmd.decode('utf-8'),           #該系統命令必須是字符串格式,因此必須進行解碼
                                 shell=True,
                                 stdout=subprocess.PIPE,        #正確命令輸出結果
                                 stderr=subprocess.PIPE         #錯誤命令輸出結果
                                 )
            stdout=obj.stdout.read()                            #從管道中讀取正確的結果
            stderr=obj.stderr.read()                            #從管道中讀取錯誤的結果

            # 一、製做報頭                                        #模擬文件的上傳和下載,則報頭中應該包括文件名,文件的大小,文件的md5值
            header_dic={
                'total_size':len(stdout) + len(stderr),         #真實數據的總大小
                'md5':'123svsaef123sdfasdf',
                'filename':'a.txt'
            }
            header_json = json.dumps(header_dic)         #將報頭這種數據類型即字典轉換成json格式(是一種json格式的字符串),能夠基於網絡傳輸
            header_bytes = header_json.encode('utf-8')   #將json格式的字符串轉換成bytes,基於網絡進行傳輸給客戶端

            # 二、先發送報頭的長度
            header_size=len(header_bytes)                #將bytes類型報頭的長度(通常也就幾百個字節)
            conn.send(struct.pack('i',header_size))      #經過struct模塊將報頭的長度轉換成固定長度的bytes大小(i格式是4個字節大小),並將固定報頭長度 發送給客戶端

            # 三、發送報頭
            conn.send(header_bytes)                      #將報頭的內容發送給客戶端

            # 四、發送真實的數據
            conn.send(stdout)                            #將正確的真實數據發送給客戶端
            conn.send(stderr)                            #將錯誤的真實數據發送給客戶端
        except ConnectionResetError:
            break
    conn.close()
server.close()
服務端
from socket import *
import struct
import json

client=socket(AF_INET,SOCK_STREAM)
client.connect(('127.0.0.1',8080))
# print(client)

while True:
    cmd=input('>>>: ').strip()
    if not cmd:continue                                  #輸入的命令不能爲空
    client.send(cmd.encode('utf-8'))                     #將字符串形式的命令轉換成bytes類型,發送給服務端
    #一、先收報頭的長度
    header_size=struct.unpack('i',client.recv(4))[0]     #將服務端打包過來報頭的長度進行解包(是一個元組,第一次元素是報頭的大小),解析出報頭的長度
    print(header_size)

    #二、接收報頭
    header_bytes=client.recv(header_size)                #已接收報頭的長度,能夠經過報頭的長度來接收bytes類型的報頭

    #三、解析報頭
    header_json=header_bytes.decode('utf-8')             #將bytes類型的報頭的解碼成json格式的字符串
    header_dic=json.loads(header_json)                   #將json格式的字符串反序列化成字符串,也就是拿到了字典形式的報頭
    print(header_dic)                                    #打印報頭

    total_size=header_dic[ 'total_size']                 #拿到字典形式的報頭,就能夠經過key值,取到服務端發送真實數據的總大小
    # print(total_size) #1025
    #四、根據報頭內的信息,收取真實的數據

    recv_size=0
    res=b''
    while recv_size < total_size:                        #循環接收真實數據
        recv_data=client.recv(1024)                      #每次接收數據的大小1024個字節
        res+=recv_data                                   #每次接收的真實數據拼接到空字符串中
        recv_size+=len(recv_data)                        #每循環一次接收的大小加每次接收真實數據的大小的長度
    print(res.decode('gbk'))                             #接收完真實數據將其解碼
client.close()
客戶端

咱們能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)

發送時:

先發報頭長度

再編碼報頭內容而後發送

最後發真實內容

 

接收時:

先手報頭長度,用struct取出來

根據取出的長度收取報頭內容,而後解碼,反序列化

從反序列化的結果中取出待取數據的詳細信息,而後去取真實的數據內容

相關文章
相關標籤/搜索