python網絡編程之粘包

粘包現象python

  說粘包以前,咱們先說兩個內容,1.緩衝區、2.windows下cmd窗口調用系統指令linux

   1  緩衝區(下面粘包現象的圖裏面還有關於緩衝區的解釋)
 

2 windows下cmd窗口調用系統指令(linux下沒有寫出來,你們仿照windows的去摸索一下吧)算法

    a.首先ctrl+r,彈出左下角的下圖,輸入cmd指令,肯定
  b.在打開的cmd窗口中輸入dir(dir:查看當前文件夾下的全部文件和文件夾),你會看到下面的輸出結果。
  另外還有ipconfig(查看當前電腦的網絡信息),在windows沒有ls這個指令(ls在linux下是查看當前文件夾下全部文件和文件夾的指令,和windows下的dir是相似的),那麼沒有這個指令就會報下面這個錯誤
3 粘包現象(兩種)
先上圖:(本圖是我作出來爲了讓小白同窗有個大體的瞭解用的,其中不少地方更加的複雜,那就須要未來你們有多餘的精力的時候去作一些深刻的研究了,這裏我就不帶你們搞啦)
 
 
MTU簡單解釋:
 
MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。
大部分網絡設備的MTU都是1500個字節,也就是1500B。若是本機一次須要發送的數據比網關的MTU大,
大的數據包就會被拆開來傳送,這樣會產生不少數據包碎片,增長丟包率,下降網絡速度
 
 

  關於上圖中提到的Nagle算法等建議你們去看一看Nagle算法、延遲ACK、linux下的TCP_NODELAY和TCP_CORK,這些內容等大家把python學好之後再去研究吧,網絡的內容實在太多啦,也就是說你們須要努力的過程還很長,加油!shell

  超出緩衝區大小會報下面的錯誤,或者udp協議的時候,你的一個數據包的大小超過了你一次recv能接受的大小,也會報下面的錯誤,tcp不會,可是超出緩存區大小的時候,確定會報這個錯誤。json

 

4 模擬一個粘包現象windows

    在模擬粘包以前,咱們先學習一個模塊subprocess。
import subprocess
cmd = input('請輸入指令>>>')
res = subprocess.Popen(
    cmd,                     #字符串指令:'dir','ipconfig',等等
    shell=True,              #使用shell,就至關於使用cmd窗口
    stderr=subprocess.PIPE,  #標準錯誤輸出,凡是輸入錯誤指令,錯誤指令輸出的報錯信息就會被它拿到
    stdout=subprocess.PIPE,  #標準輸出,正確指令的輸出結果被它拿到
)
print(res.stdout.read().decode('gbk'))
print(res.stderr.read().decode('gbk'))

注意:緩存

        若是是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼網絡

        且只能從管道里讀一次結果,PIPE稱爲管道。socket

     下面是subprocess和windows上cmd下的指令的對應示意圖:subprocess的stdout.read()和stderr.read(),拿到的結果是bytes類型,因此須要轉換爲字符串打印出來看。
 

好,既然咱們會使用subprocess了,那麼咱們就經過它來模擬一個粘包tcp

    tcp粘包演示(一):

      先從上面粘包現象中的第一種開始: 接收方沒有及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包) 
      server端代碼示例:
cket import *
import subprocess
 
ip_port=('127.0.0.1',8080)
BUFSIZE=1024
 
tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)
 
while True:
    conn,addr=tcp_socket_server.accept()
    print('客戶端>>>',addr)
 
    while True:
        cmd=conn.recv(BUFSIZE)
        if len(cmd) == 0:break
 
        res=subprocess.Popen(cmd.decode('gbk'),shell=True,
                         stdout=subprocess.PIPE,
                         stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE)
 
        stderr=res.stderr.read()
        stdout=res.stdout.read()
        conn.send(stderr)
        conn.send(stdout)

    client端代碼示例:

import socket
ip_port = ('127.0.0.1',8080)
size = 1024
tcp_sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res = tcp_sk.connect(ip_port)
while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break
 
    tcp_sk.send(msg.encode('utf-8'))
    act_res=tcp_sk.recv(size)
    print('接收的返回結果長度爲>',len(act_res))
    print('std>>>',act_res.decode('gbk')) #windows返回的內容須要用gbk來解碼,由於windows系統的默認編碼爲gbk

   tcp粘包演示(二):發送數據時間間隔很短,數據也很小,會合到一塊兒,產生粘包

   server端代碼示例:(若是兩次發送有必定的時間間隔,那麼就不會出現這種粘包狀況,試着在兩次發送的中間加一個time.sleep(1))

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()

    client端代碼示例:

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)
res=s.connect(ip_port)
s.send('hi'.encode('utf-8'))
s.send('meinv'.encode('utf-8'))

    示例二的結果:所有被第一個recv接收了

udp粘包演示:注意:udp是面向包的,因此udp是不存在粘包的
      server端代碼示例:
import socket
from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF
sk = socket.socket(type=socket.SOCK_DGRAM)
# sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024)
sk.bind(('127.0.0.1',8090))
msg,addr = sk.recvfrom(1024)
while True:
    cmd = input('>>>>')
    if cmd == 'q':
        break
    sk.sendto(cmd.encode('utf-8'),addr)
    msg,addr = sk.recvfrom(1032)
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_SNDBUF))
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_RCVBUF))
    print(len(msg))
    print(msg.decode('utf-8'))
 
sk.close()

     client端代碼示例:

import socket
from socket import SOL_SOCKET,SO_REUSEADDR,SO_SNDBUF,SO_RCVBUF
sk = socket.socket(type=socket.SOCK_DGRAM)
# sk.setsockopt(SOL_SOCKET,SO_RCVBUF,80*1024)
sk.bind(('127.0.0.1',8090))
msg,addr = sk.recvfrom(1024)
while True:
    cmd = input('>>>>')
    if cmd == 'q':
        break
    sk.sendto(cmd.encode('utf-8'),addr)
    msg,addr = sk.recvfrom(1024)
    # msg,addr = sk.recvfrom(1218)
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_SNDBUF))
    # print('>>>>', sk.getsockopt(SOL_SOCKET, SO_RCVBUF))
    print(len(msg))
    print(msg.decode('utf-8'))
 
sk.close()

   在udp的代碼中,咱們在server端接收返回消息的時候,咱們設置的recvfrom(1024),那麼當我輸入的執行指令爲‘dir’的時候,dir在我當前文件夾下輸出的內容大於1024,而後就報錯了,報的錯誤也是下面這個:

  解釋緣由:是由於udp是面向報文的,意思就是每一個消息是一個包,你接收端設置接收大小的時候,必需要比你發的這個包要大,否則一次接收不了就會報這個錯誤,而tcp不會報錯,這也是爲何ucp會丟包的緣由之一,這個和咱們上面緩衝區那個錯誤的報錯緣由是不同的。  

  補充兩個問題:

補充問題一:爲什麼tcp是可靠傳輸,udp是不可靠傳輸
 
    tcp在數據傳輸時,發送端先把數據發送到本身的緩存中,而後協議控制將緩存中的數據發往對端,對端返回一個ack=1,發送端則清理緩存中的數據,對端返回ack=0,則從新發送數據,因此tcp是可靠的。
    而udp發送數據,對端是不會返回確認信息的,所以不可靠
 
補充問題二:send(字節流)和sendall
 
    send的字節流是先放入己端緩存,而後由協議控制將緩存內容發往對端,若是待發送的字節流大小大於緩存剩餘空間,那麼數據丟失,用sendall就會循環調用send,數據不會丟失,通常的小數據就用send,由於小數據也用sendall的話有些影響代碼性能,簡單來說就是還多while循環這個代碼呢。
  
用UDP協議發送時,用sendto函數最大能發送數據的長度爲:65535- IP頭(20) – UDP頭(8)=65507字節。用sendto函數發送數據時,若是發送數據長度大於該值,則函數會返回錯誤。(丟棄這個包,不進行發送)
 
用TCP協議發送時,因爲TCP是數據流協議,所以不存在包大小的限制(暫不考慮緩衝區的大小),這是指在用send函數時,數據長度參數不受限制。而實際上,所指定的這段數據並不必定會一次性發送出去,若是這段數據比較長,會被分段發送,若是比較短,可能會等待和下一次數據一塊兒發送。

 

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

粘包的解決方案

  解決方案(一):

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

 看代碼示例:

      server端代碼
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()

    client端代碼示例

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'))

 解決方案(二):

    經過struck模塊將須要發送的內容的長度進行打包,打包成一個4字節長度的數據發送到對端,對端只要取出前4個字節,而後對這四個字節的數據進行解包,拿到你要發送的內容的長度,而後經過這個長度來繼續接收咱們實際要發送的內容。不是很好理解是吧?哈哈,不要緊,看下面的解釋~~
       爲何要說一下這個模塊呢,由於解決方案(一)裏面你發現,我每次要先發送一個個人內容的長度,須要接收端接收,並切須要接收端返回一個確認消息,我發送端才能發後面真實的內容,這樣是爲了保證數據可靠性,也就是接收雙方能順利溝通,可是多了一次發送接收的過程,爲了減小這個過程,咱們就要使struck來發送你須要發送的數據的長度,來解決上面咱們所說的經過發送內容長度來 解決粘包的問題

    struck模塊的使用:struct模塊中最重要的兩個函數是pack()打包, unpack()解包。

    pack():#我在這裏只介紹一下'i'這個int類型

import struct
a=12
# 將a變爲二進制
bytes=struct.pack('i',a)
-------------------------------------------------------------------------------
struct.pack('i',1111111111111) 若是int類型數據太大會報錯struck.error
struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是範圍

    unpack():

# 注意,unpack返回的是tuple !!
 
a,=struct.unpack('i',bytes) #將bytes類型的數據解包後,拿到int類型數據

 好,到這裏咱們將struck這個模塊將int類型的數據打包成四個字節的方法了,那麼咱們就來使用它解決粘包吧。

  先看一段僞代碼示例:

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,由於bytes只能將字符串類型的數據轉換爲bytes類型的,全部須要先序列化一下這個字典,字典不能直接轉化爲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)

下面看正式的代碼:

  server端代碼示例:報頭:就是消息的頭部信息,咱們要發送的真實內容爲報頭後面的內容。

import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #忘了這是幹什麼的了吧,地址重用?想起來了嗎~
 
phone.bind(('127.0.0.1',8080))
phone.listen(5)
while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)
        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()
        conn.send(struct.pack('i',len(back_msg))) #先發back_msg的長度
        conn.sendall(back_msg) #在發真實的內容
        #其實就是連續的將長度和內容一塊兒發出去,那麼整個內容的前4個字節就是咱們打包的後面內容的長度,對吧
         
    conn.close()

    client端代碼示例:

import socket,time,struct
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'))  #發送給一個指令
    l=s.recv(4)     #先接收4個字節的數據,由於咱們將要發送過來的內容打包成了4個字節,因此先取出4個字節
    x=struct.unpack('i',l)[0]  #解包,是一個元祖,第一個元素就是咱們的內容的長度
    print(type(x),x)
    # print(struct.unpack('I',l))
    r_s=0
    data=b''
    while r_s < x:    #根據內容的長度來繼續接收4個字節後面的內容。
        r_d=s.recv(1024)
        data+=r_d
        r_s+=len(r_d)
    # print(data.decode('utf-8'))
    print(data.decode('gbk')) #windows默認gbk編碼
相關文章
相關標籤/搜索