粘包問題
- 須知:只有TCP有粘包現象,UDP永遠不會粘包
一、什麼是粘包問題
- 什麼時候會發生粘包問題?
- 當TCP傳輸和接收的資料並非我們規定的最大資料量時,就會發生粘包
- 我們日常傳輸的資料幾乎不可能等於我們規定的資料量,所以我們必須要解決這個問題
- 為什麼只有TCP會發生粘包問題?
- TCP是面向流的協議,就是TCP為了提高傳輸效率傳送資料時往往都是收集到足夠多的資料後才傳送一個段
- 大白話講就是TCP會收集一定的資料然後再傳送,但是接收方並沒有辦法知道發來的資料要按怎麼樣的方式切分,而無法讀取資料
- UDP則不會使用合併演算法,且接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了訊息頭(訊息來源地址,埠等資訊)
- 這樣,對於接收端來說,就容易進行區分處理了。 即面向訊息的通訊是有訊息保護邊界的。
[小結]什麼是粘包問題
- 客戶端傳送需要執行的程式碼
- 服務端接收到客戶端傳過來的程式碼
- 服務端呼叫方法執行程式碼並拿到執行後的結果
- 服務端將執行後的結果進行返回
- 客戶端接收到服務端返回的結果並做列印輸出
二、粘包問題的演示
-
我們來使用客戶端傳送一段cmd命令給服務端,在服務端執行並返回資料給客戶端
-
客戶端接收資料並列印
-
由於返回的資料長度大於1024所以無法全部顯示
-
服務端
import socket
import subprocess
# 建立服務物件
server = socket.socket(type=socket.SOCK_STREAM)
# 設定預設的IP和埠號
IP = '127.0.0.1'
PORT = 8081
server.bind((IP, PORT))
server.listen(5)
while True:
conn, addr = server.accept()
while True:
# 檢測可能出現的異常並進行處理
try:
# 取到 客戶端發來的cmd命令
cmd_from_client = conn.recv(1024)
# 不允許傳入的命令為空
if not cmd_from_client:
break
# 對接收的命令進行解碼
cmd_from_client = cmd_from_client.decode('gbk')
# 執行客戶端傳過來的cmd命令並獲取結果
msg_server = subprocess.Popen(cmd_from_client,
shell=True, # 執行shell命令
stdout=subprocess.PIPE, # 管道一
stderr=subprocess.PIPE, # 管道二
)
true_res = msg_server.stdout.read() # 讀取正確結果
false_res = msg_server.stderr.read() # 讀取錯誤結果
conn.send(true_res)
conn.send(false_res)
except ConnectionResetError as e:
break
conn.close()
- 客戶端
import socket
while True:
client = socket.socket(type=socket.SOCK_STREAM)
# 設定預設的IP和埠號
IP = '127.0.0.1'
PORT = 8081
client.connect((IP, PORT))
cmd_send_to_server = input('請輸入想要實現的cmd命令:').strip()
if not cmd_send_to_server:
break
cmd_send_to_server = cmd_send_to_server.encode('utf-8')
client.send(cmd_send_to_server)
msg_from_server = client.recv(1024)
msg_from_server = msg_from_server.decode('gbk')
print(msg_from_server)
client.close()
- 客戶端收到的返回的不完整的結果
- cmd中的結果
- 可以看出客戶端接收的結果並不完全
三、解決方案
[1]解決問題的思路
- 拿到資料的總大小
recv_total_size
recv_size = 0
,迴圈接收,每接收一次,recv_size += 接收的長度
- 直到
recv_size = recv_total_size
表示接受資訊完畢,結束迴圈
[2]解決方案
(1)基礎版
-
直接使用
struct
模組打包資料的長度,傳輸到客戶端,方便對資料的處理 -
服務端
import socket
import subprocess
import struct
server = socket.socket(type=socket.SOCK_STREAM)
IP = '127.0.0.1'
PORT = 8083
server.bind((IP, PORT))
server.listen(5)
while True:
conn, addr = server.accept()
cmd_from_client = conn.recv(1024)
cmd_from_client = cmd_from_client.decode('utf-8')
msg_server = subprocess.Popen(cmd_from_client,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
true_msg = msg_server.stdout.read()
false_msg = msg_server.stderr.read()
total_len = len(true_msg) + len(false_msg)
total_len_pack = struct.pack('i', total_len)
conn.send(total_len_pack)
conn.send(true_msg)
conn.send(false_msg)
conn.close()
break
server.close()
- 客戶端
import socket
import struct
while True:
client = socket.socket(type=socket.SOCK_STREAM)
IP = '127.0.0.1'
PORT = 8083
client.connect((IP, PORT))
cmd_send_to_server = input('輸入你想要實現的cmd命令:')
cmd_send_to_server = cmd_send_to_server.encode('utf8')
client.send(cmd_send_to_server)
cmd_from_server_pack = client.recv(4)
cmd_from_server_unpack = struct.unpack('i', cmd_from_server_pack)
total_len = cmd_from_server_unpack[0]
txt = b''
while total_len > 0:
msg = client.recv(1024)
txt += msg
total_len -= 1024
print(txt.decode('gbk'))
break
(2)進階版
-
思路
- 將資料的名字,長度等資訊放入字典中
- 透過
json
模組將字典轉換成json
字串並編碼成二進位制資料(需要傳輸的資料:json
字串) - 使用
struct
模組將json
字串的長度打包成四位二級制資料(需要傳輸的資料:struct包) - 將原始資料(不是二進位制格式的轉換成二進位制)進行傳輸(需要傳輸的資料:原始資料)
- 客戶端可以透過解struct包和
json
字串的二進位制資料得到之後每一部分資料的長度,並透過迴圈取出每個部分
-
服務端
import socket
import struct
import subprocess
import json
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
conn, addr = server.accept()
msg_from_client = conn.recv(1024)
msg_from_client = msg_from_client.decode('utf-8')
if msg_from_client == 'q':
conn.send(b'q')
break
msg_server = subprocess.Popen(msg_from_client,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
true_msg = msg_server.stdout.read()
false_msg = msg_server.stderr.read()
total_len = len(true_msg) + len(false_msg)
msg_dict = {
'name': 'cmd',
'total_len': total_len
}
msg_dict_json = json.dumps(msg_dict)
msg_dict_json_b = msg_dict_json.encode('utf-8')
struct_pack = struct.pack('i', len(msg_dict_json_b))
conn.send(struct_pack)
conn.send(msg_dict_json_b)
conn.send(true_msg)
conn.send(false_msg)
- 客戶端
import json
import socket
import struct
while True:
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
cmd_send_to_server = input('請輸入想要實現的cmd命令:')
if not cmd_send_to_server:
print('命令不能是空白!')
continue
cmd_send_to_server = cmd_send_to_server.encode('utf-8')
break
client.send(cmd_send_to_server)
struct_pack = client.recv(4)
if struct_pack == b'q':
break
struct_unpack = struct.unpack('i', struct_pack)
dict_json_b = client.recv(struct_unpack[0])
dict_json = dict_json_b.decode('utf-8')
msg_dict = json.loads(dict_json)
total_len = msg_dict['total_len']
txt = b''
while total_len > 0:
msg = client.recv(1024)
txt += msg
total_len -= 1024
print(txt.decode('gbk'))
client.close()
四、案例
-
一個資料夾中的將一個影片檔案放入另一個資料夾中
-
服務端
conn, addr = server.accept()
# conn.send(json_file_list_b)
msg_from_client_b = conn.recv(1024)
if msg_from_client_b == b'q':
conn.send(b'q')
break
msg_from_client = msg_from_client_b.decode('utf-8')
if msg_from_client not in file_list:
conn.send('f'.encode('utf-8'))
continue
file_path = os.path.join(os.getcwd(), msg_from_client)
with open(file_path, 'rb') as fp:
video_data = fp.read()
video_len = len(video_data)
headers = {
'file_name': msg_from_client,
'video_len': video_len
}
headers_json = json.dumps(headers)
headers_json_b = headers_json.encode('utf-8')
struct_package = struct.pack('i', len(headers_json_b))
conn.send(struct_package)
conn.send(headers_json_b)
conn.send(video_data)
- 客戶端
print('以下是檔案列表:\n')
# 將操作的目錄轉到目標資料夾
os.chdir(r'D:\Users\21277\Desktop\home')
# 將資料夾中的所有檔名放入列表
file_list = os.listdir(os.getcwd())
# 將檔名列印出來
for file_name in file_list:
print(file_name)
print('\n')
client = socket.socket()
client.connect(('127.0.0.1', 8080))
# json_file_list_b = client.recv(1024)
# file_list = json.loads(json_file_list_b.decode('utf-8'))
#
# for file_name in file_list:
# print(file_name)
while True:
msg_send_to_server = input('請輸入想要傳輸的檔案:')
if not msg_send_to_server:
print('不能是空白!')
continue
msg_send_to_server = msg_send_to_server.encode('utf-8')
break
client.send(msg_send_to_server)
struct_package = client.recv(4)
if struct_package == b'q':
break
if struct_package == b'f':
print('未找到該檔案!請重新輸入檔名。')
continue
headers_json_len = struct.unpack('i', struct_package)[0]
headers_json_b = client.recv(headers_json_len)
headers = json.loads(headers_json_b.decode('utf-8'))
video_len = headers['video_len']
video_data = b''
while video_len > 0:
video_data += client.recv(1024)
video_len -= 1024
new_file_name = input('請重新命名檔案(不重新命名則跳過):')
if not new_file_name:
new_file_name = msg_send_to_server.decode('utf-8')
else:
new_file_name = new_file_name + '.mp4'
with open(os.path.join(r'D:\Users\21277\Desktop\new', new_file_name), 'wb') as fp:
fp.write(video_data)
client.close()