1. 程式人生 > >python網路程式設計2-黏包問題

python網路程式設計2-黏包問題

一、複習

# ip地址:一臺機器在網路上的位置
# 公網ip 私網ip
# TCP協議:可靠,面向連線的,耗時長
            #三次握手
            #四次揮手
# UDP協議:不可靠,無連線,效率高
# ARP協議:通過ip找mac的過程
# ip協議屬於網路osi中的網路層
# TCP協議和UDP協議屬於傳輸層
# arp協議屬於資料鏈路層

二、黏包(第一條和第二條資料合併傳送)

tcp:不會丟包會黏包

udp:會丟包不會黏包

tcp黏包案例:

server_tcp端:

#server端
import socket
sk=socket.socket()
sk.bind(('127.0.0.1',8090))
sk.listen()

conn,addr=sk.accept()
while True:
    cmd=input('>>>')
    conn.send(cmd.encode('utf-8'))
    ret1=conn.recv(1024).decode('utf-8')
    ret2 = conn.recv(1024).decode('utf-8')
    print(ret1)
    print(ret2)

conn.close()
sk.close()

client_tcp端:

#client端
import socket
import subprocess
sk=socket.socket()

sk.connect(('127.0.0.1',8090))
while True:
    cmd=sk.recv(1024).decode('utf-8')
    ret=subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    std_out='stdout:'+(ret.stdout.read()).decode('gbk')
    std_err='stderr:'+(ret.stderr.read()).decode('gbk')
    sk.send(std_out.encode('utf-8'))
    sk.send(std_err.encode('utf-8'))
sk.close()

執行結果:

udp丟包案例:(udp起server和client)

server_udp端:


import socket
sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1',8090)
sk.bind(ip_port)
msg, addr = sk.recvfrom(10240)
while True:
    cmd = input('>>>')
    if cmd == 'q':
        break
    sk.sendto(cmd.encode('utf-8'),addr)
    msg1,addr = sk.recvfrom(20480)
    print(msg1.decode('utf-8'))
    msg2, addr = sk.recvfrom(20480)
    print(msg2.decode('utf-8'))
sk.close()

client_udp端:

import socket
import subprocess
sk = socket.socket(type=socket.SOCK_DGRAM)
ip_port = ('127.0.0.1',8090)
sk.sendto(b'hi',ip_port)
while True:
    cmd,addr = sk.recvfrom(1024)
    cmd = cmd.decode('utf-8')
    ret = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
    std_out = 'stdout:'+(ret.stdout.read()).decode('gbk')
    std_err = 'stderr:'+(ret.stderr.read()).decode('gbk')
    print(std_out)
    print(std_err)
    sk.sendto(std_out.encode('utf-8'),addr)
    sk.sendto(std_err.encode('utf-8'),addr)
sk.close() #不會黏包,會丟包

執行結果:

執行結果顯示,如果udp的接收資料量大小滿足傳送資料量大小,那麼就不會丟包,若是不滿足傳送資料量大小,則就會報錯。而不是丟包

三 、黏包的觸發

情況一:傳送方的快取機制:傳送端要等緩衝區滿才傳送出去。造成黏包(傳送資料時間間隔很短,資料很小,會合並一起造成粘包)

如:

#server端
import socket
sk=socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()
conn,addr=sk.accept()
ret=conn.recv(12)
ret2=conn.recv(12)
print(ret)
print(ret2)
conn.close()
sk.close()

#關閉時會發空訊息
# 多個send小的資料連在一起,可能會發生黏包現象,是tcp內部的優化演算法引起的
#client端
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',8080))
sk.send(b'hello')
import time
# time.sleep(0.01)
sk.send(b'world')
sk.send(b'dd')
sk.close()

執行結果:(有時會黏包有時不會)

情況二:接收方的緩衝機制

接收方不及時接收緩衝區的包,造成多個包接收(客戶端傳送了一段資料,服務端只接收了一小部分,伺服器下次再接收時還是從緩衝區拿上次遺留的資料,產生粘包)

如:

# server端
import socket
sk=socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()

conn,addr=sk.accept()
ret=conn.recv(2)
ret2=conn.recv(10)
print(ret)
print(ret2)
conn.close()
sk.close()

# 黏包的本職問題:不知道傳送資料的長度
#  連續的小資料包會被合併
# client端
import socket
sk=socket.socket()
sk.connect(('127.0.0.1',8080))

sk.send(b'hello,egg')
sk.close()

執行結果:

總結

黏包現象只發生在tcp協議中:

1.從表面上看,黏包問題主要是因為傳送方和接收方的快取機制、tcp協議面向流通訊的特點。

2.實際上,主要還是因為接收方不知道訊息之間的界限,不知道一次性提取多少位元組的資料所造成的

四、黏包的解決方案

解決方案一:

問題的根源在於,接收端不知道傳送端將要傳送的位元組流的長度,所以解決粘包的方法就是圍繞,如何讓傳送端在傳送資料前,把自己將要傳送的位元組流總大小讓接收端知曉,然後接收端來一個死迴圈接收完所有資料。

# server
# server 下發命令 給client
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')
    conn.send(b'ok')
    res=conn.recv(int(num)).decode('gbk')
    print(res)
conn.close()
sk.close()
# client
# 接收server端的命令後在自己的機器上執行
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'))
    sk.recv(1024)  #ok
    sk.send(std_out)
    sk.send(std_err)
sk.close()


# 好處:確定我到底要接收多大的資料
# recv的大小一般不超過4096
# 要在檔案中配置一個配置項:就是每次recv的大小 buffer=4096
# 當我們傳送大資料量的時候,要明確的告訴接收方要傳送多大資料以便接收方能準確接收到所有資料
# 多用在檔案傳輸過程中
    #大檔案的傳輸一定是按照位元組讀 每次讀固定的位元組
    # 傳輸的過程中 一邊讀一邊傳 接收端:一邊收一邊寫
    # send大檔案之前,告知大小。大小-4096-4096....-->0  檔案傳輸完
    # recv大檔案,先接受大小。再recv2048.不會丟 大小-2048-2048  -->0  檔案接收完
# 不好的地方:多了一次互動
# 5個g資料
# send 和sendto在超過一定範圍後,都會報錯

執行結果:

存在的問題: 程式的執行速度遠快於網路傳輸速度,所以在傳送一段位元組前,先用send去傳送該位元組流長度,這種方式會放大網路延遲帶來的效能損耗

解決方案進階

剛剛的方法,問題在於我們我們在傳送

我們可以藉助一個模組,這個模組可以把要傳送的資料長度轉換成固定長度的位元組。這樣客戶端每次接收訊息之前只要先接受這個固定長度位元組的內容看一看接下來要接收的資訊大小,那麼最終接受的資料只要達到這個值就停止,就能剛好不多不少的接收完整的資料了。

struct模組

該模組可以把一個型別,如數字,轉成固定長度的bytes

>>> struct.pack('i',1111111111111)

struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是範圍

# server 下發命令 給client
import socket
import struct
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(4) #4  2048
    num = struct.unpack('i', num)[0]
    res=conn.recv(int(num)).decode('gbk')  #2048
    print(res)
conn.close()
sk.close()
# 連續send兩個小資料
# 兩個recv,第一個recv特別小
# 遠端執行命令的程式:ipconfig--> 2000,只接收1024.就會快取,下次繼續接收上次未接收完的資料

#連續send兩個小資料2+8=10
# 2
# 8
#兩個recv,第一個recv特別小
# recv(資料的長度)
# 接收server端的命令後在自己的機器上執行
import socket
import subprocess
import struct

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()
    len_num=len(std_out)+len(std_err)
    num_by=struct.pack('i',len_num)
    sk.send(num_by)  # 4  2048
    sk.send(std_out)  # 1024
    sk.send(std_err)   #1024
sk.close()

執行結果:

五、ftp傳送視訊

sever:

import socket
import struct
import json
buffer=1024
# ip地址和埠號需要寫在配置檔案中
sk=socket.socket()
sk.bind(('127.0.0.1',8080))
sk.listen()

conn,addr=sk.accept()
# 接收
head_len=conn.recv(4)   # 報頭長度
head_len=struct.unpack('i',head_len)[0]
head_json=conn.recv(head_len).decode('utf-8')
head=json.loads(head_json)
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)
            filesize=0
            break
f.close()
conn.close()
sk.close()
#傳送端
import socket
import os
import json
import struct
sk=socket.socket()
sk.connect(('127.0.0.1',8080))
# 傳送檔案
buffer=2046
# 改成4096檔案大小會變:傳送和接收時間不匹配
# 讀操作快,寫速度慢。緩衝資料多

head={'filepath':r'E:\test',
      'filename': '[反貪風暴3]BD國語.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) #字典轉成了字串
byte_head=json_head.encode('utf-8')  # 字串轉成了bytes
head_len=len(byte_head)   # 報頭的長度
pack_len=struct.pack('i',head_len)
sk.send(pack_len)  # 先發報頭的長度
sk.send(byte_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)
            filesize=0
            break
f.close()
sk.close()

參考自https://www.cnblogs.com/Eva-J/articles/8244551.html#_label5