網絡編程【三】tcp協議的粘包問題

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協議特色的黏包現象成因 

複製代碼
發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據。
也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。
而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。
怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,
TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。
複製代碼

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

此外,發送方引發的粘包是由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根本不會粘包,可是會丟數據,不可靠。
複製代碼

補充說明:json

  udp和tcp一次發送數據長度的限制

發生粘包的兩種狀況

狀況一發送方的機制緩存

發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)緩存

#_*_coding:utf-8_*_
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()

server
server
#_*_coding:utf-8_*_
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)


s.send('hello'.encode('utf-8'))
s.send('egg'.encode('utf-8'))

client
client

狀況二接收方的緩存機制

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

#_*_coding:utf-8_*_
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(2) #一次沒有收完整
data2=conn.recv(10)#下次收的時候,會先取舊的數據,而後取新的

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()

server
server
#_*_coding:utf-8_*_
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)


s.send('hello egg'.encode('utf-8'))

client
client

總結:網絡

黏包現象只發生在tcp協議中:數據結構

1.從表面上看,黏包問題主要是由於發送方和接收方的緩存機制、tcp協議面向流通訊的特色。dom

2.實際上,主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的socket

粘包的解決方案

解決方案

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

struct模塊

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

複製代碼
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)
複製代碼

藉助struct模塊,咱們知道長度數字能夠被轉換成一個標準大小的4字節數字。所以能夠利用這個特色來預先發送數據長度。

發送時 接收時
先發送struct轉換好的數據長度4字節 先接受4個字節使用struct轉換成數字來獲取要接收的數據長度
再發送數據 再按照長度接收數據
import socket
import struct

sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
conn,addr = sk.accept()
l = conn.recv(4)    # 先接收客戶端發送來的4字節
t = struct.unpack('i',l)[0]  # 將4字節解碼成數據長度
print(t)
msg = conn.recv(t).decode('utf-8')  #在根據字節長度接收數據
d = conn.recv(1024).decode('utf-8')
print(msg)
print(d)
conn.close()
sk.close()

server
server
import socket
import struct
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
msg = '你好'.encode('utf-8')  # 將要發送的數據編碼成二進制格式
li = struct.pack('i',len(msg)) # 將編碼後的二進制的長度轉換成固定的4字節
print(li)   # b'\x06\x00\x00\x00'
sk.send(li)   # 先發送固定的四字節
sk.send(msg)
sk.send(''.encode('utf-8'))
sk.close()

client
client

咱們還能夠把報頭作成字典,字典裏包含將要發送的真實數據的詳細信息,而後json序列化,而後用struck將序列化後的數據長度打包成4個字節(4個本身足夠用了)

 

發送時 接收時

先發報頭長度

先收報頭長度,用struct取出來
再編碼報頭內容而後發送 根據取出的長度收取報頭內容,而後解碼,反序列化
最後發真實內容 從反序列化的結果中取出待取數據的詳細信息,而後去取真實的數據內容

sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
conn,addr = sk.accept()
num = conn.recv(4)
num = struct.unpack('i',num)[0]
str_dic = conn.recv(num).decode('utf-8')
dic = json.loads(str_dic)
with open(dic['filename'],'wb') as f:
    content = conn.recv(dic['filesize'])
    f.write(content)
conn.close()
sk.close()

server
server
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
filepath = input('請輸入文件路徑')
filename = os.path.basename(filepath)
filesize = os.path.getsize(filepath)
dic = {'filename':filename,'filepath':filepath}
str_dic = json.dumps(dic)
bytes_dic = str_dic.encode('utf-8')
len_dic = len(bytes_dic)
bytes_len = struct.pack('i',len_dic)
sk.send(bytes_len)
sk.send(bytes_dic)
with open(filepath,'rb') as f:
    content = f.read()
    sk.send(content)
sk.close()

client
client
import socket
import hashlib
import json
import sys
def md5_func(user,pwd):
    md5 = hashlib.md5(user.encode('utf-8'))
    md5.update(pwd.encode('utf-8'))
    return md5.hexdigest()

def login(ret):
    with open('file_info',encoding='utf-8') as f:
        for i in f:
            user,pwd = i.strip().split('|')
            if user == ret['username'] and pwd == md5_func(ret['username'],ret['password']):
                return {'opt': 'login', 'result': True}
            else:
                return {'opt': 'login', 'result': False}

sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
while True:
    conn,addr = sk.accept()
    msg = conn.recv(1024).decode('utf-8')
    ret = json.loads(msg)
    if hasattr(sys.modules[__name__],ret['opt']):
        res = getattr(sys.modules[__name__],ret['opt'])(ret)
        content = json.dumps(res).encode('utf-8')
        conn.send(content)
    conn.close()
sk.close()

tcp驗證登陸server
tcp驗證登陸server
import socket
import hashlib
import json
def md5_func(user,pwd):
    md5 = hashlib.md5(user.encode('utf-8'))
    md5.update(pwd.encode('utf-8'))
    return md5.hexdigest()

username = input('用戶名')
password = input('密 碼')
sk = socket.socket()
sk.connect(('127.0.0.1',9000))
res = {'opt':'login','username':username,'password':md5_func(username,password)}
ret = json.dumps(res)
sk.send(ret.encode('utf-8'))
msg = sk.recv(1024).decode('utf-8')  # {"opt": "login", "result": false}
a = json.loads(msg)
if a['result']:
    print('登錄成功')
else:
    print('登錄失敗')
sk.close()

tcp驗證client
tcp驗證登陸client

驗證客戶端連接的合法性hmac

若是你想在分佈式系統中實現一個簡單的客戶端連接認證功能,又不像SSL那麼複雜,那麼利用hmac+加鹽的方式來實現

import os
import hmac
import socket
sk = socket.socket()
sk.bind(('127.0.0.1',9000))
sk.listen()
conn,addr = sk.accept()
def get_hmac(secret_key,rand):
    hmac_t = hmac.new(secret_key, rand)
    res = hmac_t.digest()
    return res

secret_key = '宋治學'.encode('utf-8')
rand = os.urandom(32)  # 轉換後就是bytes
conn.send(rand)
res = get_hmac(secret_key,rand)
msg = conn.recv(1024)
if msg == res:
    print('合法')
else:
    print('非法客戶端')
    conn.close()
# 利用os.urandom隨機生成32位數發送給客戶端,服務端進行加密,客戶端那頭拿着這32位數字與本身的secret_key進行加密發送給服務端,由服務端進行驗證,若是加密後的結果與服務端的加密結果不匹配就斷開與客戶端的鏈接

server
server
import socket
import hmac

sk = socket.socket()

def get_hmac(secret_key, rand):
    hmac_t = hmac.new(secret_key, rand)
    res = hmac_t.digest()
    return res

def auth():
    sk.connect(('127.0.0.1', 9000))
    secret_key = '治學'.encode('utf-8')
    rand = sk.recv(1024)
    res = get_hmac(secret_key, rand)
    sk.send(res)
auth()

client
client
相關文章
相關標籤/搜索