解決tcp粘包問題


目錄html

什麼是粘包(演示粘包現象)算法

解決粘包shell

實際應用緩存


什麼是粘包

首先只有tcp有粘包現象,udp沒有粘包服務器

socket收發消息的原理網絡

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

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

粘包問題的根源性能

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

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

tcp和udp協議

TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。

UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。

tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略。

補充

拆包的發生狀況

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

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

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

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

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

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

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

總結

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

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


演示粘包現象

兩種狀況下會發生粘包

發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據量很小,會合到一塊兒,產生粘包),這是因爲tcp的優化算法。

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

第一種狀況:

客戶端屢次間隔時間短,數據量小的發送數據

  1 #服務端
  2 import socket
  3 
  4 def main():
  5     ip_port= ('127.0.0.1',4444)
  6     back_log=5
  7     buffer_size=1024
  8 
  9     s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #基於tcp的網絡通訊
 10     s1.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
 11     s1.bind(ip_port) #綁定ip和端口
 12     s1.listen(back_log)  # 最多鏈接幾個客戶端
 13     conn, addr = s1.accept()
 14 
 15     data1=conn.recv(buffer_size)
 16     data2=conn.recv(buffer_size)
 17     data3=conn.recv(buffer_size)
 18     print('第一次',data1.decode('utf-8'))
 19     print('第二次',data2.decode('utf-8'))
 20     print('第三次',data3.decode('utf-8'))
 21     conn.close()
 22     s1.close()
 23 
 24 if __name__ == '__main__':
 25     main()
 26 
 27 
 28 #客戶端
 29 import socket
 30 
 31 def main():
 32     ip_port = ('127.0.0.1', 4444)
 33 
 34     buffer_size = 1024
 35 
 36     s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 37 
 38     s2.connect(ip_port)  # 鏈接服務端
 39 
 40     data1 = 'hello'
 41     s2.send(data1.encode('utf-8'))
 42     data2 ='wrold'
 43     s2.send(data2.encode('utf-8'))
 44     data3 = 'pop'
 45     s2.send(data3.encode('utf-8'))
 46     s2.close()
 47 
 48 if __name__ == '__main__':
 49     main()
 50 
演示

能夠看出來服務端在第一次就把三次發送的數據都接收了,這就是粘包,服務端不知道一次讀取多少的數據,一次所有讀取出來。

首先咱們要知道並非客戶端發幾回,服務端就要接收幾回,一次發的數據也能夠三次讀取出來,收發信息都是從本身的內核緩存區讀取。


第二種狀況:

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

  1 #服務端
  2 import socket
  3 
  4 def main():
  5     ip_port= ('127.0.0.1',4444)
  6     back_log=5
  7     buffer_size=1024
  8 
  9     s1 = socket.socket(socket.AF_INET,socket.SOCK_STREAM) #基於tcp的網絡通訊
 10     s1.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
 11     s1.bind(ip_port) #綁定ip和端口
 12     s1.listen(back_log)  # 最多鏈接幾個客戶端
 13     conn, addr = s1.accept()
 14 
 15     data1=conn.recv(5)
 16     data2=conn.recv(buffer_size)
 17 
 18     print('第一次',data1.decode('utf-8'))
 19     print('第二次',data2.decode('utf-8'))
 20 
 21     conn.close()
 22     s1.close()
 23 
 24 if __name__ == '__main__':
 25     main()
 26 
 27 
 28 #客戶端
 29 import socket
 30 
 31 def main():
 32     ip_port = ('127.0.0.1', 4444)
 33 
 34     s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 35 
 36     s2.connect(ip_port)  # 鏈接服務端
 37 
 38     data1 = 'hellowroldpop'
 39     s2.send(data1.encode('utf-8'))
 40     s2.close()
 41 
 42 if __name__ == '__main__':
 43     main()
演示

服務端讀取數據沒有所有讀取出來,致使第一次應該接收完的數據還要第二次讀取出來。


解決粘包

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

第一種解決方法

  1 #服務端
  2 import socket
  3 
  4 def main():
  5     ip_port = ('127.0.0.1', 4444)
  6     back_log = 5
  7     buffer_size = 1024
  8 
  9     s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於tcp的網絡通訊
 10     s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 11     s1.bind(ip_port)  # 綁定ip和端口
 12     s1.listen(back_log)  # 最多鏈接幾個客戶端
 13     conn, addr = s1.accept()
 14 
 15     while True:
 16      #接收數據大小
 17         length= conn.recv(buffer_size).decode('utf-8')<br>     #爲防止客戶端連續發包,迴應
 18         conn.send('ready'.encode('utf-8'))
 19         length=int(length)
 20 
 21         recv_size=0   #已經接收到數據的大小
 22         recv_msg=b''  #已經接收到的數據<br>
 23      #接收數據
 24         while recv_size<length:
 25             r_msg = conn.recv(buffer_size)
 26             recv_msg+=r_msg
 27             recv_size +=len(r_msg)
 28        #另外一種方法接收數據的方法<br>
 29             #recv_msg+=conn.recv(buffer_size)
 30             #recv_size=len(recv_msg)
 31 
 32     s1.close()
 33 
 34 if __name__ == '__main__':
 35     main()
 36 
 37 
 38 
 39 #客戶端
 40 import socket
 41 
 42 def main():
 43     ip_port = ('127.0.0.1', 4444)
 44 
 45     buffer_size = 1024
 46 
 47     s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 48 
 49     s2.connect(ip_port)  # 鏈接服務端
 50 
 51     while True:
 52 
 53         data1 = input('input:')<br>     #將數據大小轉爲字符型而後編碼發出去
 54         s2.send(str(len(data1)).encode('utf-8'))<br>      #接收服務端的迴應
 55         server_Ready=s2.recv(buffer_size)<br>      #接收到服務端迴應
 56         if server_Ready==b'ready':
 57             s2.send(data1.encode('utf-8'))
 58     s2.close()
 59 
 60 
 61 if __name__ == '__main__':
 62     main()
 63 
總結:客戶端在發送數據時,先發送數據大小,這時不能把數據內容一塊兒發送出去,服務端第一次接收的時候,並不知道該讀取多少的數據大小和多少的數據內容,因此仍是會形成粘包,咱們的解決辦法是,服務端獲取到數據大小後,要回應一次,而後根據數據大小來循環讀取內容。

這種方法很差,須要服務端多發一次迴應,這很影響服務端的性能。

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

第二種解決方法

爲字節流加上自定義固定長度報頭,報頭中包含字節流長度,而後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,而後再取真實數據

struct模塊

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

>>> struct.pack('i',1111111111111) #第一個參數是要封裝的格式類型,第二個參數是要封裝的內容

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個封裝數據的範圍,只要在這個範圍裏面,就能夠把內容封裝成固定大小

  1 #服務端
  2 
  3 import socket
  4 import struct
  5 
  6 def main():
  7     ip_port = ('127.0.0.1', 4444)
  8     back_log = 5
  9     buffer_size = 1024
 10 
 11     s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 基於tcp的網絡通訊
 12     s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 13     s1.bind(ip_port)  # 綁定ip和端口
 14     s1.listen(back_log)  # 最多鏈接幾個客戶端
 15     conn, addr = s1.accept()
 16 
 17     while True:
 18 
 19         length_data= conn.recv(4)
 20         length=struct.unpack('i',length_data)[0]
 21 
 22         recv_size=0   #已經接收到數據的大小
 23         recv_msg=b''  #已經接收到的數據
 24 
 25         while recv_size<length:
 26             r_msg = conn.recv(buffer_size)
 27             recv_msg+=r_msg
 28             recv_size +=len(r_msg)
 29 
 30             #recv_msg+=conn.recv(buffer_size)
 31             #recv_size=len(recv_msg)
 32 
 33 
 34     s1.close()
 35 
 36 
 37 if __name__ == '__main__':
 38     main()
 39 
 40 
 41 #客戶端
 42 
 43 import socket
 44 import struct
 45 
 46 def main():
 47     ip_port = ('127.0.0.1', 4444)
 48 
 49     buffer_size = 1024
 50 
 51     s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 52 
 53     s2.connect(ip_port)  # 鏈接服務端
 54 
 55     while True:
 56 
 57         data1 = input('input:')
 58         length=len(data1)
 59         #定製包頭 i爲4個字節,因此接收方爲四個字節,這個大小並非輸入的大小,而是封裝固定的大小
 60         data_length=struct.pack('i',length) #使用struct,直接將int轉爲二進制型數據傳輸,對方使用struct解包
 61         s2.send(data_length)
 62 
 63         s2.send(data1.encode('utf-8'))
 64     s2.close()
 65 
 66 if __name__ == '__main__':
 67     main()

總結:客戶端把數據長度封裝成一個固定大小的數據,這時服務端就能夠指定讀取固定大小的內容,不會讀取數據的內容,服務端只要根據數據長度再來接收數據內容就行了,因此客戶端連續兩次發數據,不會粘包,由於服務端每次接收都只接收了本次該接收的數據。

實際應用

#服務端
from socket import  *
import subprocess
import struct


def main():
    ip_port=('127.0.0.1',8080)
    back_log=5
    buffer_size=1024

    s1 = socket(AF_INET,SOCK_STREAM)
    s1.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
    s1.bind(ip_port)
    s1.listen(back_log)

    while True:
        conn,addr=s1.accept()

        while True:
            try:
                #收信息
                 cmd = conn.recv(buffer_size)
                 if not cmd:break
                 print('收到的命令是:',cmd.decode('utf-8'))


                 #執行命令
                 res = subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                        stderr=subprocess.PIPE,
                                        stdout=subprocess.PIPE,
                                        stdin=subprocess.PIPE)

                 err = res.stderr.read()
                 if  err:
                     cmd_res=err
                 else:
                     cmd_res=res.stdout.read()

                 if not cmd_res:
                     cmd_res='執行成功'.encode('gbk')

                 length=len(cmd_res)
                #第一次發送數據大小
                 data_length = struct.pack('i', length)  # 使用struct,直接將int轉爲二進制型數據傳輸,對方使用struct解包
                 conn.send(data_length)
                #發信息
                #注意:執行的結果默認jbk編碼方式,因此客戶端必須使用gbk方式解碼
                 conn.send(cmd_res)

            except Exception:
                break
        conn.close()
    s1.close()  # 關閉服務端套接字


if __name__ == '__main__':
    main()


#客戶端
from socket import  *
import struct

def main():
    ip_port=('127.0.0.1',8080)
    buffer_size=1024

    s1 = socket(AF_INET,SOCK_STREAM)
    s1.connect(ip_port)

    while True:
        cmd = input('-->')
        if not cmd:continue
        if cmd =='quite':break
        s1.send(cmd.encode('utf-8'))
        length_data =s1.recv(4)
        length = struct.unpack('i', length_data)[0]

        recv_size = 0  # 已經接收到數據的大小
        recv_msg = b''  # 已經接收到的數據

        while recv_size < length:
            r_msg = s1.recv(buffer_size)
            recv_msg += r_msg
            recv_size += len(r_msg)


            # recv_msg+=conn.recv(buffer_size)
            # recv_size=len(recv_msg)

        print('命令執行結果:',recv_msg.decode('gbk'))

    s1.close()

if __name__=='__main__':
    main()
總結

若是沒有粘包的處理,服務端把命令執行的結果發給客戶端的時候,數據太大,客戶端一次沒有接收完,在客戶端第二次執行命令的時候,就會把第一次沒有讀取完的部分也讀取出來,這屬於咱們剛纔說的第二種粘包的狀況。

有了粘包的處理,只要服務端把結果發過來,就算超過網卡的限制(拆包發送),客戶端能保證在循環的過程當中接收完結果。

轉載連接:https://www.cnblogs.com/-wenli/p/10178436.html

相關文章
相關標籤/搜索