黏包
黏包現象
讓咱們基於tcp先製做一個遠程執行命令的程序(命令ls -l ; lllllll ; pwd)html
res=subprocess.Popen(cmd.decode('utf-8'),
shell=True,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE)算法
#的結果的編碼是以當前所在的系統爲準的,若是是windows,那麼res.stdout.read()shell
讀出的#就是GBK編碼的,在接收端須要用GBK解碼json
#且只能從管道里讀一次結果windows
同時執行多條命令以後,獲得的結果極可能只有一部分,在執行其餘命令的時候又接緩存
收到以前執行的另一部分結果,這種顯現就是黏包。服務器
基於tcp協議實現的黏包網絡
1 tcp-server:socket
#_*_coding:utf-8_*_
from socket import *#引入套接字的全部模塊
import subprocess#引入subprocess模塊tcp
ip_port=('127.0.0.1',8888)#設置ip和端口號
BUFSIZE=1024#設置字節大小爲1024
tcp_socket_server=socket(AF_INET,SOCK_STREAM)#tcp的服務器爲網絡傳輸模式
tcp_socket_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)#設置套接字
tcp_socket_server.bind(ip_port)#綁定地址和端口
tcp_socket_server.listen(5)#服務器監聽5個字節
while True:#循環爲真
conn,addr=tcp_socket_server.accept()#連接和接收地址
print('客戶端',addr)#打印客戶端地址
while True:#循環爲真
cmd=conn.recv(BUFSIZE)#接收信息爲5個字節
if len(cmd) == 0:break#若是 命令長度等於0,則退出
res=subprocess.Popen(cmd.decode('utf-8'),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)#連接發送
2 tcp client:
#_*_coding:utf-8_*_
import socket#引入套接字模塊
BUFSIZE=1024#設置大小爲1024個字節
ip_port=('127.0.0.1',8888)#設置IP地址及端口號,必須和服務器端同樣
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#初始化一個套接字對象,設
置爲網絡傳輸
res=s.connect_ex(ip_port)#鏈接服務器的ip地址及端口
while True:#循環爲真
msg=input('>>: ').strip()#請輸入你的信息(去兩邊的空格)
if len(msg) == 0:continue#若是信息的長度爲0則跳過
if msg == 'quit':break#若是信息='quit'則退出
s.send(msg.encode('utf-8'))#發送文件utf-8編碼的信息
act_res=s.recv(BUFSIZE)#s接收信息以1024字節爲限制
print(act_res.decode('utf-8'),end='')#打印utf-8的接收信息,end=''一行顯
示全部信息
基於udp協議實現的黏包:
1 udp-server:
#_*_coding:utf-8_*_
from socket import *#引入socket包中的全部模塊
import subprocess#引入subprocess模塊
ip_port=('127.0.0.1',9000)#設置一個ip和端口
bufsize=1024#設置大小爲1024
udp_server=socket(AF_INET,SOCK_DGRAM)#獲得一個socket的對象,用網絡鏈接
udp_server.setsockopt(SOL_SOCKET,SO_REUSEADDR,1)
udp_server.bind(ip_port)#綁定ip和端口
while True:#循環爲真
#收消息
cmd,addr=udp_server.recvfrom(bufsize)#接收消息設置1024字節
print('用戶命令----->',cmd)#打印接收的命令
#邏輯處理
res=subprocess.Popen(cmd.decode('utf-
8'),shell=True,stderr=subprocess.PIPE,stdin=subprocess.PIPE,stdout=subproce
ss.PIPE)
stderr=res.stderr.read()
stdout=res.stdout.read()
#發消息
udp_server.sendto(stderr,addr)#發送消息到對端地址
udp_server.sendto(stdout,addr)#發送消息到對端地址
udp_server.close()#關閉這個服務器
2 udp—client:
from socket import *#引入全部的套接字模塊
ip_port=('127.0.0.1',9000)#ip和端口
bufsize=1024#字節大小爲1024
udp_client=socket(AF_INET,SOCK_DGRAM)#udp的客戶端用網絡鏈接
while True:#循環爲真
msg=input('>>: ').strip()#輸入信息
udp_client.sendto(msg.encode('utf-8'),ip_port)#客戶端發送信息,到服務器
err,addr=udp_client.recvfrom(bufsize)#接收字節大小爲1024的字節
out,addr=udp_client.recvfrom(bufsize)#接收字節大小爲1024的字節
if err:#若是有err
print('error : %s'%err.decode('utf-8'),end='')#打印錯誤:信息內容。
用一行表示
if out:#若是有out
print(out.decode('utf-8'), end='')#打印信息,用一行表示
注意:只有TCP有粘包現象,UDP永遠不會粘包
黏包成因
TCP協議中的數據傳遞
tcp協議的拆包機制
#當發送端緩衝區的長度大於網卡的MTU時,tcp會將此次發送的數據拆成幾個數據包發
送出去。
#MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的
單位是字節。 大部分網絡設備的MTU都是1500。若是本機的MTU比網關的MTU大,大的
數據包就會被拆開來傳送,這樣會產生不少數據包碎片,增長丟包率,下降網絡速度
。
面向流的通訊特色和Nagle算法
#TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提
供高可靠性服務。
#收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個
發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較
小且數據量小的數據,合併成一個大的數據塊,而後進行封包。
#這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是
無消息保護邊界的。
#對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和
服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸
入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。
#可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼
續接收,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。
基於tcp協議特色的黏包現象成因 :
例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字
節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束
此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每
要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般
TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了
粘包數據。
UDP不會發生黏包
#UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供
高效率服務。
#不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的
skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就
有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區
分處理了。 即面向消息的通訊是有消息保護邊界的。
#對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和
服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸
入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。
#不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一
個sendinto(y),收完了x個字節的數據就算完成,如果y;x數據就丟失,這意味着udp根
本不會粘包,可是會丟數據,不可靠。
補充說明:
#用UDP協議發送時,用sendto函數最大能發送數據的長度爲:65535- IP頭(20) –
UDP頭(8)=65507字節。用sendto函數發送數據時,若是發送數據長度大於該值,則函
數會返回錯誤。(丟棄這個包,不進行發送)
#用TCP協議發送時,因爲TCP是數據流協議,所以不存在包大小的限制(暫不考慮緩衝
區的大小),這是指在用send函數時,數據長度參數不受限制。而實際上,所指定的
這段數據並不必定會一次性發送出去,若是這段數據比較長,會被分段發送,若是比
較短,可能會等待和下一次數據一塊兒發送。
會發生黏包的兩種狀況
狀況一 發送方的緩存機制
發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小
,會合到一塊兒,產生粘包)
#服務器端
#_*_coding:utf-8_*_
from socket import *#引入套接字的全部模塊
ip_port=('127.0.0.1',8080)#設置ip地址的端口號
tcp_socket_server=socket(AF_INET,SOCK_STREAM)#甚至套接字以網絡方式傳送
tcp_socket_server.bind(ip_port)#綁定IP地址和端口
tcp_socket_server.listen(5)#設置監聽5個字節
conn,addr=tcp_socket_server.accept()#接收鏈接和地址
data1=conn.recv(10)#數據1接收10個字節
data2=conn.recv(10)#數據2接收10個字節
print('----->',data1.decode('utf-8'))#打印數據1的內容
print('----->',data2.decode('utf-8'))#打印數據2的內容
conn.close()#關閉鏈接
#客戶端
#_*_coding:utf-8_*_
import socket#引入套接字模塊
BUFSIZE=1024#設置文件大小爲1024
ip_port=('127.0.0.1',8080)#設置ip和端口
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#實例化一個套接字以網絡的
形式傳輸
res=s.connect_ex(ip_port)#鏈接ip和端口
s.send('hello'.encode('utf-8'))#發送信息
s.send('egg'.encode('utf-8'))#發送信息
狀況二 接收方的緩存機制
接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只
收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包)
#服務器端
#_*_coding:utf-8_*_
from socket import *引入 套接字模塊
ip_port=('127.0.0.1',8080)#設置ip地址
tcp_socket_server=socket(AF_INET,SOCK_STREAM)#實例化一個服套接字對象
tcp_socket_server.bind(ip_port)#綁定ip和端口
tcp_socket_server.listen(5)#監聽大小爲5個字節
conn,addr=tcp_socket_server.accept()#接收鏈接、地址
data1=conn.recv(2) #一次沒有收完整#數據1爲接收2個字節
data2=conn.recv(10)#下次收的時候,會先取舊的數據,而後取新的#數據二接收爲10個
字節
print('----->',data1.decode('utf-8'))#打印數據1內容
print('----->',data2.decode('utf-8'))#打印數據2內容
conn.close()#關閉鏈接
#客戶端
#_*_coding:utf-8_*_
import socket#引入套接字模塊
BUFSIZE=1024#設置大小爲1024
ip_port=('127.0.0.1',8080)#IP地址和端口
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#實例化一個ftp套接字對象
以網絡傳輸
res=s.connect_ex(ip_port)#綁定ip
s.send('hello egg'.encode('utf-8'))#發送信息內容
總結
黏包現象只發生在tcp協議中:
1.從表面上看,黏包問題主要是由於發送方和接收方的緩存機制、tcp協議面向流通訊
的特色。
2.實際上,主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節
的數據所形成的
黏包的解決方案
解決方案一
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方
法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端
知曉,而後接收端來一個死循環接收完全部數據。
#服務端
#_*_coding:utf-8_*_
import socket,subprocess#引入套接字模塊和遠程控制模塊
ip_port=('127.0.0.1',8080)#設置一個ip地址和端口
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#實例化一個對象
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)設置套接字
s.bind(ip_port)#綁定ip和端口
s.listen(5)#設置監聽的字節位5
while True:#循環爲真
conn,addr=s.accept()#鏈接和獲得地址
print('客戶端',addr)#打印客戶端和地址
while True:#循環爲真
msg=conn.recv(1024)#設置接收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')#數據接收,設置大小爲1024個字節
if data == 'recv_ready':#若是數據等於
conn.sendall(ret)
conn.close()#關閉鏈接
#客戶端
#_*_coding:utf-8_*_
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#若是信息爲0則打斷
if msg == 'quit':break#若是信息輸入'quit'則打斷
s.send(msg.encode('utf-8'))#發送信息
length=int(s.recv(1024).decode('utf-8'))#接收一個數字的信息
s.send('recv_ready'.encode('utf-8'))#發送接收的信息
send_size=0#發送 大小爲0
recv_size=0#接收 大小爲0
data=b''#數據爲bytes類型
while recv_size < length:#循環若是接收的長度小於,內容長度
data+=s.recv(1024)#data+=接收的數據
recv_size+=len(data)#接收的大小+=數據長度
print(data.decode('utf-8'))#打印這個數據
存在的問題:
程序的運行速度遠快於網絡傳輸速度,因此在發送一段字節前,先用send去發送該字
節流長度,這種方式會放大網絡延遲帶來的性能損耗.
解決方案進階
剛剛的方法,問題在於咱們咱們在發送
咱們能夠藉助一個模塊,這個模塊能夠把要發送的數據長度轉換成固定長度的字節。
這樣客戶端每次接收消息以前只要先接受這個固定長度字節的內容看一看接下來要接
收的信息大小,那麼最終接受的數據只要達到這個值就中止,就能恰好很少很多的接
收完整的數據了。
struct模塊
該模塊能夠把一個類型,如數字,轉成固定長度的bytes
#>>> struct.pack('i',1111111111111)
#struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是範圍
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
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)
#_*_coding:utf-8_*_
#http://www.cnblogs.com/coser/archive/2011/12/17/2291160.html
__author__ = 'Linhaifeng'
import struct
import binascii
import ctypes
values1 = (1, 'abc'.encode('utf-8'), 2.7)
values2 = ('defg'.encode('utf-8'),101)
s1 = struct.Struct('I3sf')
s2 = struct.Struct('4sI')
print(s1.size,s2.size)
prebuffer=ctypes.create_string_buffer(s1.size+s2.size)
print('Before : ',binascii.hexlify(prebuffer))
# t=binascii.hexlify('asdfaf'.encode('utf-8'))
# print(t)
s1.pack_into(prebuffer,0,*values1)
s2.pack_into(prebuffer,s1.size,*values2)
print('After pack',binascii.hexlify(prebuffer))
print(s1.unpack_from(prebuffer,0))
print(s2.unpack_from(prebuffer,s1.size))
s3=struct.Struct('ii')
s3.pack_into(prebuffer,0,123,123)
print('After pack',binascii.hexlify(prebuffer))
print(s3.unpack_from(prebuffer,0))
使用struct解決黏包
藉助struct模塊,咱們知道長度數字能夠被轉換成一個標準大小的4字節數字。所以能夠利用這個特色來預先發送數據長度。
發送時 接收時
1先發送struct轉換好的數據長度4字節 2先接受4個字節使用struct轉換成數字來獲取要接收的數據長度
1再發送數據 2再按照長度接收數據
#服務器端(自定製報頭)
import socket,struct,json#引入三個模塊
import subprocess#引入遠程控制模塊
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#實例化一個對象
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))#綁定一個ip和端口
phone.listen(5)#設置監聽大小爲5個字節
while True:#循環爲真
conn,addr=phone.accept()#接受鏈接和地址
while True:#循環爲真
cmd=conn.recv(1024)#接收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()#讀取錯誤信息
print(err)#打印這個信息
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) #在發真實的內容
conn.close()#關閉鏈接
#客戶端(自定製報頭)
#_*_coding:utf-8_*_
import socket,time,struct#引入三個模塊
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#實例化一個套接字對象
res=s.connect_ex(('127.0.0.1',8080))#設置一個IP地址和端口
while True:#循環爲真
msg=input('>>: ').strip()#輸入信息
if len(msg) == 0:continue#若是信息長度爲0則跳過
if msg == 'quit':break#若是輸入'quit'則打斷
s.send(msg.encode('utf-8'))#發送信息
l=s.recv(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:
r_d=s.recv(1024)
data+=r_d
r_s+=len(r_d)
# print(data.decode('utf-8'))#打印這個數據
print(data.decode('gbk')) #windows默認gbk編碼
咱們還能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)
發送時
先發報頭長度,再編碼報頭內容而後發送,最後發真實內容
接收時:
先收報頭長度,用struct取出來,根據取出的長度收取報頭內容,而後解碼,反序列化,從反序列化的結果中取出待取數據的詳細信息,而後去取真實的數據內容
#服務器端定製複雜的頭
import socket,struct,json#引入三個模塊
import subprocess#引入遠程控制命令
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)#實例化一個對象
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加
phone.bind(('127.0.0.1',8080))#綁定一個ip和端口
phone.listen(5)#監聽字節設置爲5
while True:#循環 爲真
conn,addr=phone.accept()#鏈接,接受一個地址
while True:#循環爲真
cmd=conn.recv(1024)#接收消息,大小爲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()#讀取這個錯誤
print(err)#打印這個錯誤
if err:#若是有錯誤
back_msg=err
else:#不然讀取內容
back_msg=res.stdout.read()
headers={'data_size':len(back_msg)}#獲得一個頭的字典大小:對應成都
head_json=json.dumps(headers)#序列化這個頭
head_json_bytes=bytes(head_json,encoding='utf-8')#獲得bytes結構的頭
conn.send(struct.pack('i',len(head_json_bytes))) #先發報頭的長度
conn.send(head_json_bytes) #再發報頭
conn.sendall(back_msg) #在發真實的內容
conn.close()#關閉鏈接
#客戶端
from socket import *#引入套接字的全部模塊
import struct,json#引入兩個模塊
ip_port=('127.0.0.1',8080)#ip和端口
client=socket(AF_INET,SOCK_STREAM)#實例化一個套接字
client.connect(ip_port)#創建鏈接
while True:#循環爲真
cmd=input('>>: ')#輸入內容
if not cmd:continue#若是沒有信息則跳過
client.send(bytes(cmd,encoding='utf-8'))#發送一個bytes類型的信息
head=client.recv(4)#接受一個頭部
head_json_len=struct.unpack('i',head)[0]#查看頭部的長度
head_json=json.loads(client.recv(head_json_len).decode('utf-8'))#加載接收頭部的長度
data_len=head_json['data_size']#數據長度的大小
recv_size=0設置接收爲0
recv_data=b''#設置接收的數據爲bytes類型
while recv_size < data_len:#循環若是接收的字節大小<數據的長度
recv_data+=client.recv(1024)#接收數據+=接收的長度(1024)字節
recv_size+=len(recv_data)#接受大小+=接收的數據長度
print(recv_data.decode('utf-8'))#打印這個接收的數據 #print(recv_data.decode('gbk')) #windows默認gbk編碼