python socket編程和黏包問題


一、基於TCP的socket

tcp是基於鏈接的,必須先啟動服務端,然后再啟動客戶端去鏈接服務端,有順序,不重復,可靠。不會被加上數據邊界。

server端

import socket
sk = socket.socket()
sk.bind(('127.0.0.1',8898))  #把地址綁定到套接字
sk.listen()          #監聽鏈接
conn,addr = sk.accept() #接受客戶端鏈接
ret = conn.recv(1024)  #接收客戶端信息
print(ret)       #打印客戶端信息
conn.send(b'hi')        #向客戶端發送信息
conn.close()       #關閉客戶端套接字
sk.close()        #關閉服務器套接字(可選)

client

import socket
sk = socket.socket()           # 創建客戶套接字
sk.connect(('127.0.0.1',8898))    # 嘗試連接服務器
sk.send(b'hello!')
ret = sk.recv(1024)         # 對話(發送/接收)
print(ret)
sk.close()            # 關閉客戶套接字

二、基於UDP的socket

udp是無鏈接的,啟動服務之后可以直接接受消息不需要提前建立鏈接

server

import socket
udp_sk = socket.socket(type=socket.SOCK_DGRAM)   #創建一個服務器的套接字
udp_sk.bind(('127.0.0.1',9000))        #綁定服務器套接字
msg,addr = udp_sk.recvfrom(1024)
print(msg)
udp_sk.sendto(b'hi',addr)                 # 對話(接收與發送)
udp_sk.close()                         # 關閉服務器套接字

client

import socket
ip_port=('127.0.0.1',9000)
udp_sk=socket.socket(type=socket.SOCK_DGRAM)
udp_sk.sendto(b'hello',ip_port)
back_msg,addr=udp_sk.recvfrom(1024)
print(back_msg.decode('utf-8'),addr)

socket參數詳解

socket.socket(family=AF_INET,type=SOCK_STREAM,proto=0,fileno=None)

family:地址族:AF_INET(默認值,基於網絡),AF_INET6;AF_UNIX(基於文件)
type:
    SOCK_STREAM 是基於TCP的,有保障的(即能保證數據正確傳送到對方)面向連接的SOCKET,多用於資料傳送。
    SOCK_DGRAM 是基於UDP的,無保障的面向消息的socket,多用於在網絡上發廣播信息。

proto:0

fileno:
    如果指定了fileno,則其他參數將被忽略,導致帶有指定文件描述符的套接字返回。
    與socket.fromfd()不同,fileno將返回相同的套接字,而不是重復的。
    這可能有助於使用socket.close()關閉一個獨立的插座。

 

三、黏包問題

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

server端: 只會接收到一個client send來的一條數據

# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 15:15
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    :  server端發送命令
'''
import socket

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen()
conn,addr = server.accept()

while True:
    # 發送指令
    cmd = input('輸入指令:')
    conn.send(cmd.encode('utf-8'))

    # 接收執行后的結果
    stdout = conn.recv(1024)  # 接收正確的結果
    print(stdout.decode('gbk'))
    stderr = conn.recv(1204)
    print(stderr.decode('gbk'))

conn.close()
server.close()
server端

client端:

# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 15:16
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    : 根據server端發送的命令,進行執行,返回執行結果
'''
import socket
import subprocess

client = socket.socket()
client.connect(('127.0.0.1',8080))


while True:
    # 接收指令
    cmd = client.recv(1024)

    # 執行指令
    ret = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                           stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)

    stdout = ret.stdout.read()  # 正確的信息
    stderr = ret.stderr.read()  # 錯誤信息

    client.send(stdout)
    client.send(stderr)


client.close()
client端

注意只有TCP有黏包問題,UDP沒有

四、黏包成因

4.1 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用戶狀態說明

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

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

4.2 UDP不會發生黏包

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

udp最大能發送數據的長度

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

4.3 會發生黏包的兩種情況

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

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

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

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

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

五、黏包的解決方案

5.1 解決方案一:使用time.sleep

解決前:

import socket

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen()
conn,addr = server.accept()

msg1 = conn.recv(1024)
msg2 = conn.recv(1024)

print('msg1:',msg1.decode('utf-8'))
print('nsg2:',msg2.decode('utf-8'))



輸出結果:
msg1: hellohi
nsg2: 

server端

# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 17:33
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    : 
'''
import socket

client = socket.socket()

client.connect(('127.0.0.1',8080))

client.send(b'hello')
client.send(b'hi')
client端

解決后:

# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 17:33
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    : 
'''
import socket

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen()
conn,addr = server.accept()

msg1 = conn.recv(1024)
msg2 = conn.recv(1024)

print('msg1:',msg1.decode('utf-8'))
print('nsg2:',msg2.decode('utf-8'))
server端

 

# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 17:33
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    : 
'''
import socket
import time

client = socket.socket()

client.connect(('127.0.0.1',8080))

client.send(b'hello')
time.sleep(0.1)     
client.send(b'hi')
client

 

5.2 解決方案二:recv

# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 17:33
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    : 
'''
import socket

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen()
conn,addr = server.accept()

msg1 = conn.recv(1024)
conn.send(b'1121')
msg2 = conn.recv(1024)

print('msg1:',msg1.decode('utf-8'))
print('nsg2:',msg2.decode('utf-8'))
server端
# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 17:33
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    : 
'''
import socket
import time

client = socket.socket()

client.connect(('127.0.0.1',8080))

client.send(b'hello')
client.recv(1024)#只是用於防止黏包,沒有其他作用
client.send(b'hi')
client端

 

5.3 解決方案三:使用struct模塊

# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 18:33
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    : 
'''
import socket
import json
import struct

server = socket.socket()
server.bind(('127.0.0.1',8080))
server.listen()
conn,addr = server.accept()

msg1 = conn.recv(4) #固定大小的struc
msg1 = struct.unpack('i',msg1)
msg2 = conn.recv(msg1[0]) #字典
msg2 = json.loads(msg2.decode('utf-8'))
print(msg2)
msg3 = conn.recv(1024)
print(msg3)
conn.close()
server.close()
server
# -*- coding: utf-8 -*-
'''
@Time    : 2018/3/21 18:33
@Author  : Weiheng
@Email   : weicunheng@gmail.com
@Info    : 
'''
import socket
import struct
import json

client = socket.socket()
client.connect(('127.0.0.1',8080))


msg = 'hello'
dic = {'msg_len':len(msg)}


byte_dic = json.dumps(dic).encode('utf-8')
ret = struct.pack('i',len(byte_dic))

# print(struct.unpack('i',ret))

client.send(ret + byte_dic)

client.send(b'hello')

client.close()
client

該模塊可以把一個類型,如數字,轉成固定長度的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)
struc用法

借助struct模塊,我們知道長度數字可以被轉換成一個標准大小的4字節數字。因此可以利用這個特點來預先發送數據長度。

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

我們還可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化,然后用struck將序列化后的數據長度打包成4個字節(4個自己足夠用了)

發送時    接收時
先發報頭長度

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM