粘包現象
一、基於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),該函數返回一個元組。
粘包現象