31_網路編程-struct
- 2020 年 1 月 19 日
- 筆記
一、struct
1、簡述
我們可以藉助一個模組,這個模組可以把要發送的數據長度轉換成固定長度的位元組。這樣客戶端每次接收消息之前只要先接受這個固定長度位元組的內容看一看接下來要接收的資訊大小,那麼最終接受的數據只要達到這個值就停止,就能剛好不多不少的接收完整的數據了。
該模組可以把一個類型,如數字,轉成固定長度的bytes
1 >>> struct.pack('i',1111111111111) 2 struct.error: 'i' format requires -2147483648 <= number <= 2147483647 #這個是範圍



1 import json,struct 2 #假設通過客戶端上傳1T:1073741824000的文件a.txt 3 4 #為避免粘包,必須自訂製報頭 5 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T數據,文件路徑和md5值 6 7 #為了該報頭能傳送,需要序列化並且轉為bytes 8 head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化並轉成bytes,用於傳輸 9 10 #為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個位元組 11 head_len_bytes=struct.pack('i',len(head_bytes)) #這4個位元組里只包含了一個數字,該數字是報頭的長度 12 13 #客戶端開始發送 14 conn.send(head_len_bytes) #先發報頭的長度,4個bytes 15 conn.send(head_bytes) #再發報頭的位元組格式 16 conn.sendall(文件內容) #然後發真實內容的位元組格式 17 18 #服務端開始接收 19 head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的位元組格式 20 x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度,解包出來是元組 21 22 head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式 23 header=json.loads(json.dumps(header)) #提取報頭 24 25 #最後根據報頭的內容提取真實的數據,比如 26 real_data_len=s.recv(header['file_size']) 27 s.recv(real_data_len)
View Code
2、struct解決黏包問題
藉助struct模組,我們知道長度數字可以被轉換成一個標準大小的4位元組數字。因此可以利用這個特點來預先發送數據長度。
發送時 |
接收時 |
---|---|
先發送struct轉換好的數據長度4位元組 |
先接受4個位元組使用struct轉換成數字來獲取要接收的數據長度 |
再發送數據 |
再按照長度接收數據 |
服務端


1 import socket 2 import subprocess 3 import struct 4 5 server = socket.socket() 6 ip_port = ('192.168.15.113',8001) 7 server.bind(ip_port) 8 server.listen() 9 conn,addr = server.accept() 10 while 1: 11 #來自客戶端的指令 12 print('等待接受資訊。。。') 13 from_client_cmd = conn.recv(1024).decode('utf-8') 14 print(from_client_cmd) 15 #通過subprocess模組執行服務端的系統指令,並且拿到指令執行結果 16 sub_obj = subprocess.Popen( 17 from_client_cmd, #客戶端的指令 18 shell=True, 19 stdout=subprocess.PIPE, #標準輸出:正確指令的執行結果在這裡 20 stderr=subprocess.PIPE, #標準錯誤輸出:錯誤指令的執行結果在這裡 21 ) 22 #接受到的返回資訊是bytes類型的,並且windows系統的默認編碼為gbk 23 server_cmd_msg = sub_obj.stdout.read() 24 # server_cmd_err = sub_obj.stderr.read().decode('gbk') 25 #首先計算出你將要發送的數據的長度 26 cmd_msg_len = len(server_cmd_msg) 27 #先對數據長度進行打包,打包成4個位元組的數據,目的是為了和你將要發送的數據拼在一起,就好我們自訂製了一個消息頭 28 msg_len_stru = struct.pack('i',cmd_msg_len) 29 conn.send(msg_len_stru) #首先發送打包成功後的那4個位元組的數據 30 conn.sendall(server_cmd_msg) #循環send數據,直到數據全部發送成功
server
客戶端


1 import socket 2 import struct 3 client = socket.socket() 4 server_ip_port = ('192.168.15.113',8001) 5 client.connect(server_ip_port) 6 while 1: 7 msg = input('請輸入要執行的指令>>>') 8 client.send(msg.encode('utf-8')) 9 #先接收服務端要發送給我的資訊的長度,前4個位元組,固定的 10 from_server_msglen = client.recv(4) 11 unpack_len_msg = struct.unpack('i',from_server_msglen)[0] 12 #接收數據長度統計,和服務端發給我的數據長度作比較,來確定跳出循環的條件 13 recv_msg_len = 0 14 #統計拼接接收到的數據,注意:這個不是統計長度 15 all_msg = b'' 16 while recv_msg_len < unpack_len_msg: 17 every_recv_data = client.recv(1024) 18 #將每次接收的數據進行拼接和統計 19 all_msg += every_recv_data 20 #對每次接收到的數據的長度進行累加 21 recv_msg_len += len(every_recv_data) 22 23 print(all_msg.decode('gbk'))
client
複雜的服務端(自定義報頭)


1 import socket,struct,json 2 import subprocess 3 phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM) 4 phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) 5 6 phone.bind(('127.0.0.1',8080)) 7 phone.listen(5) 8 9 while True: 10 conn,addr=phone.accept() 11 while True: 12 cmd=conn.recv(1024) 13 if not cmd:break 14 print('cmd: %s' %cmd) 15 16 res=subprocess.Popen(cmd.decode('utf-8'), 17 shell=True, 18 stdout=subprocess.PIPE, 19 stderr=subprocess.PIPE) 20 err=res.stderr.read() 21 print(err) 22 if err: 23 back_msg=err 24 else: 25 back_msg=res.stdout.read() 26 27 headers={'data_size':len(back_msg)} 28 head_json=json.dumps(headers) 29 head_json_bytes=bytes(head_json,encoding='utf-8') 30 31 conn.send(struct.pack('i',len(head_json_bytes))) #先發報頭的長度 32 conn.send(head_json_bytes) #再發報頭 33 conn.sendall(back_msg) #在發真實的內容 34 35 conn.close()
View Code


1 from socket import * 2 import struct,json 3 4 ip_port=('127.0.0.1',8080) 5 client=socket(AF_INET,SOCK_STREAM) 6 client.connect(ip_port) 7 8 while True: 9 cmd=input('>>: ') 10 if not cmd:continue 11 client.send(bytes(cmd,encoding='utf-8')) 12 13 head=client.recv(4) 14 head_json_len=struct.unpack('i',head)[0] 15 head_json=json.loads(client.recv(head_json_len).decode('utf-8')) 16 data_len=head_json['data_size'] 17 18 recv_size=0 19 recv_data=b'' 20 while recv_size < data_len: 21 recv_data+=client.recv(1024) 22 recv_size+=len(recv_data) 23 24 #print(recv_data.decode('utf-8')) 25 print(recv_data.decode('gbk')) #windows默認gbk編碼 26 27 tcp_client.py
View Code
head = {'file name': 'test', 'filesize': 8192, 'filetype': 'txt', 'filepath': r'userbin'}
報頭長度 ——> 先接收4位元組
send(head) 報頭 ——> 根據這4個位元組獲取報頭
send(file) 報文 ——> 從報頭中獲取filesize,然後根據filesize接收文件
整個流程的大致解釋:
我們可以把報頭做成字典,字典里包含將要發送的真實數據的描述資訊(大小啊之類的),然後json序列化,然後用struck將序列化後的數據長度打包成4個位元組。
我們在網路上傳輸的所有數據 都叫做數據包,數據包里的所有數據都叫做報文,報文裡面不止有你的數據,還有ip地址、mac地址、埠號等等,其實所有的報文都有報頭,這個報頭是協議規定的,看一下
發送時:
先發報頭長度
再編碼報頭內容然後發送
最後發真實內容
接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,然後解碼,反序列化
從反序列化的結果中取出待取數據的描述資訊,然後去取真實的數據內容