1. 程式人生 > >粘包現象

粘包現象

put exception 不知道 原因 pipe 結果 blog 關閉 pen

一、基於udp的套接字

udp是無鏈接的,先啟動哪一端都不會報錯

udp服務端:

ss = socket()   #創建一個服務器的套接字
ss.bind()       #綁定服務器套接字
while True :       #服務器無限循環
    cs = ss.recvfrom()/ss.sendto() # 對話(接收與發送)
ss.close()                         # 關閉服務器套接字

udp客戶端:

cs = socket()   # 創建客戶套接字
while True :      # 通訊循環
    cs.sendto()/cs.recvfrom()   # 對話(發送/接收)
cs.close()                      # 關閉客戶套接字

1、udp套接字簡單實例

服務端:

from socket import *

udp_ss=socket(AF_INET,SOCK_DGRAM)
udp_ss.bind((127.0.0.1,8080))

while True:
    msg,addr=udp_ss.recvfrom(1024)
    print(msg,addr)
    udp_ss.sendto(msg.upper(),addr)

客戶端:

from socket import *

udp_cs=socket(AF_INET,SOCK_DGRAM)

while True:
    msg=input(>>: 
).strip() if not msg:continue udp_cs.sendto(msg.encode(utf-8),(127.0.0.1,8080)) msg,addr=udp_cs.recvfrom(1024) print(msg.decode(utf-8),addr)

2、模擬聊天(由於udp無連接,所以可以同時多個客戶端去跟服務端通信)

服務端:

from socket import *

udp_ss=socket(AF_INET,SOCK_DGRAM)
udp_ss.bind((127.0.0.1,8081))

while True:
    msg,addr
=udp_ss.recvfrom(1024) print(來自[%s]的一條消息:%s %(addr,msg.decode(utf-8))) msg_b=input(回復消息: ).strip() udp_ss.sendto(msg_b.encode(utf-8),addr)

客戶端1:

from socket import *

udp_cs = socket(AF_INET,SOCK_DGRAM)

while True :
    msg = input(‘請輸入消息,回車發送: ‘).strip()
    if msg == ‘quit‘ : break
    if not msg : continue
    udp_cs.sendto(msg.encode(‘utf-8‘),(‘127.0.0.1‘,8081))

    back_msg,addr = udp_cs.recvfrom(1024)
    print(‘來自[%s]的一條消息:%s‘ %(addr,back_msg.decode(‘utf-8‘)))

udp_cs.close()

客戶端2:

from socket import *

udp_cs = socket(AF_INET,SOCK_DGRAM)

while True :
    msg = input(‘請輸入消息,回車發送: ‘).strip()
    if msg == ‘quit‘ : break
    if not msg : continue
    udp_cs.sendto(msg.encode(‘utf-8‘),(‘127.0.0.1‘,8081))

    back_msg,addr = udp_cs.recvfrom(1024)
    print(‘來自[%s]的一條消息:%s‘ %(addr,back_msg.decode(‘utf-8‘)))

udp_cs.close()

二、粘包現象

先做粘包現象:

服務端:

from socket import *
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
phone.bind((‘127.0.0.1‘,8080))
phone.listen(5)
conn,client_addr=phone.accept()

data1=conn.recv(1024)
print(‘data1: ‘,data1)
data2=conn.recv(1024)
print(‘data2:‘,data2)

客戶端:

from socket import *
phone=socket(AF_INET,SOCK_STREAM)
phone.connect((‘127.0.0.1‘,8080))

phone.send(‘hello‘.encode(‘utf-8‘))
phone.send(‘world‘.encode(‘utf-8‘))

我們再將上個隨筆裏的ssh例子拿出來(先執行 ipconfig /all 再執行 dir 看結果)

客戶端:

from socket import *
import subprocess
cs=socket(AF_INET,SOCK_STREAM)
cs.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
cs.bind((‘127.0.0.1‘,8082))
cs.listen(5)

print(‘starting...‘)
while True:
    conn,addr=cs.accept()
    print(‘-------->‘,conn,addr)

    while True:
        try:
            cmd=conn.recv(1024)
            res = subprocess.Popen(cmd.decode(‘utf-8‘), shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout=res.stdout.read()
            stderr=res.stderr.read()

            #發送命令的結果
            conn.send(stdout+stderr)
        except Exception:
            break
    conn.close() #掛電話
cs.close() #關機

服務端:

from socket import *
ss=socket(AF_INET,SOCK_STREAM) #買手機
ss.connect((‘127.0.0.1‘,8082)) #綁定手機卡

#發,收消息
while True:
    cmd=input(‘>>: ‘).strip()
    if not cmd:continue
    ss.send(cmd.encode(‘utf-8‘))
    cmd_res=ss.recv(1024)
    print(cmd_res.decode(‘gbk‘))
ss.close()

註意:

subprocess模塊的結果的編碼是以當前所在的系統為準的,如果是windows,那麽res.stdout.read()讀出的就是GBK編碼的,在接收端需要用GBK解碼


三、粘包

註意:只有TCP有粘包現象,UDP永遠不會粘包,首先需要掌握一個socket收發消息的原理

技術分享

應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。

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

所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。

此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。

兩種情況會粘包:

1、發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)

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

拆包的發生情況:

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

四、解決粘包方法

粘包現象中第一個現象解決:

解決一:(需要知道每次發過來的數據大小 不現實)

技術分享
from socket import *
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
phone.bind((127.0.0.1,8080))
phone.listen(5)
conn,client_addr=phone.accept()

data1=conn.recv(10)
print(data1: ,data1)
data2=conn.recv(4)
print(data2:,data2)
服務端 技術分享
from socket import *
phone=socket(AF_INET,SOCK_STREAM)
phone.connect((127.0.0.1,8080))

phone.send(helloworld.encode(utf-8))
phone.send(egon.encode(utf-8))
客戶端

解決二:

技術分享 服務端 技術分享 客戶端

ssh例子問題解決:

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

技術分享
from socket import *
phone=socket(AF_INET,SOCK_STREAM)
phone.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
phone.bind((127.0.0.1,8080))
phone.listen(5)
conn,client_addr=phone.accept()

data1=conn.recv(1024)
print(data1: ,data1)
data2=conn.recv(1024)
print(data2:,data2)
服務端 技術分享
from socket import *
import time
phone=socket(AF_INET,SOCK_STREAM)
phone.connect((127.0.0.1,8080))

phone.send(hello.encode(utf-8))
time.sleep(5)
phone.send(world.encode(utf-8))
客戶端

五、struct模塊(了解)

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

struct.pack(‘i‘,11111111)
#struct.error: ‘i‘ format requires -2147483648 <= number <= 2147483647 #這個是範圍

技術分享

struct.pack用於將Python的值根據格式符,轉換為字符串(因為Python中沒有字節(Byte)類型)。它的函數原型為:struct.unpack(fmt, string)。

struct.unpack做的工作剛好與struct.pack相反,用於將字節流轉換成python數據類型。它的函數原型為:struct.unpack(fmt, string),該函數返回一個元組

粘包現象