1. 程式人生 > >socket 黏包現象

socket 黏包現象

utf href true 等待 源地址 緩存機制 .com 文件 優化算法

什麽是黏包:

  同事執行多條命令之後,得到的結果很可能只有一部分,在執行其他命令的時候又接收到之前執行的另外一部分結果,這種顯現就是黏包

  註意:只有TCP有粘包現象,UDP永遠不會粘包

UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務.
不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,
所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,
在每個UDP包中就有了消息頭(消息來源地址,端口等信息),
這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。 對於空消息:tcp是基於數據流的,於是收發的消息不能為空,
這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,
即便是你輸入的是空內容(直接回車),也可以被發送,udp協議會幫你封裝上消息頭發送過去。 不可靠不黏包的udp協議:udp的recvfrom是阻塞的,
一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,
若是y;x數據就丟失,這意味著udp根本不會粘包,但是會丟數據,不可靠。

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

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

2.實際上,主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的

基於tcp協議實現的黏包

client端

import socket
sk=socket.socket()
sk.connect((192.168.16.35,9000))
s1=床前明月光
s2=疑是地上霜
sk.send(s1.encode(utf-8))
sk.send(s2.encode(utf-8))

server端

import socket
sk=socket.socket()
sk.bind((192.168.16.35,9000))
sk.listen()
while 1:
    conn,addr=sk.accept()
    s1
=conn.recv(1024) print(s1: +s1.decode(utf-8)) #s1: 床前明月光疑是地上霜 s2=conn.recv(1024) print(s2: +s2.decode(utf-8)) #s2:

黏包原因

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的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束

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

補充說明:
用UDP協議發送時,用sendto函數最大能發送數據的長度為:
65535- IP頭(20) – UDP頭(8)=65507字節。
用sendto函數發送數據時,如果發送數據長度大於該值,則函數會返回錯誤。
(丟棄這個包,不進行發送) 用TCP協議發送時,由於TCP是數據流協議,因此不存在包大小的限制(暫不考慮緩沖區的大小),
這是指在用send函數時,數據長度參數不受限制。
而實際上,所指定的這段數據並不一定會一次性發送出去,如果這段數據比較長,會被分段發送,
如果比較短,可能會等待和下一次數據一起發送。

會發生黏包的兩種情況

情況一 發送方的緩存機制

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

技術分享圖片
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(1024)
data2=conn.recv(1024)
print(data1: ,data1.decode(utf-8))
print(data2: ,data2.decode(utf-8))
conn.close()
server 技術分享圖片
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

情況二 接收方的緩存機制

技術分享圖片
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端 技術分享圖片
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

黏包解決方案

解決方案一

問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據。

技術分享圖片

server
import
struct import socket sk = socket.socket() sk.bind((127.0.0.1,9090)) sk.listen() conn,addr = sk.accept() while True: s = input(內容:).encode(utf-8) pack_num = struct.pack(i,len(s)) #將文件大小轉換成一個4字節的值 conn.send(pack_num) #發送這個值 conn.send(s) #發送文件 conn.close() sk.close()
client
import
socket import struct sk = socket.socket() sk.connect((127.0.0.1,9090)) while True: pack_num = sk.recv(4) #第一次只接受4個字節 這個值代表要接受的文件的大小 num = struct.unpack(i,pack_num)[0] #將這個值轉成十進制 ret = sk.recv(num) #按大小來接受文件 print(ret.decode(utf-8)) sk.close()

socket 黏包現象