python網路編程-socket套接字通訊循環-粘包問題-struct模組-02
- 2019 年 10 月 7 日
- 筆記
前置知識
不同電腦程式之間數據的傳輸

應用程式中的數據都是從程式所在電腦記憶體中讀取的。 記憶體中的數據是從硬碟讀取或者網路傳輸過來的
不同電腦程式數據傳輸需要經過七層協議物理連接介質才能到達目標程式
socket (套接字)
json.dump/dumps 只是把數據類型序列化成字元串 要想用來文件傳輸,還需要encode 給它編碼成二進位數據才能傳輸 不用pickle是因為要和其他語言交互(你給頁面就是js來處理,能不能支援是問題),而pickle只能是在python中用
程式設計師不需要七層一層一層地去操作硬體寫網路傳輸程式,直接使用python解釋器提供的socket 模組即可
大多數注意點都在程式碼後面的注釋里,要仔細看哦~

初略版的雙端(C/S)通訊
程式運行時先啟動服務端再啟動客戶端(程式碼設置一下可以一份程式碼跑好幾個(客戶端可能會啟好幾個))
server服務端
socket.scoket()不傳參數默認就是TCP協議
import socket server = socket.socket() # 有一個參數 type=SOCK_STREAM,即不傳參數,默認就是TCP協議 # socket.socket() # socket模組中有個socket類,加() 實例化成一個對象(ctrl + 單擊 可以看到) # 不要形成固有思想, 模組.名字() 就以為是模組里的方法,點進去,可能還是類(看他這個類的名字還是全小寫的...) server.bind(('127.0.0.1', 8080)) # 127.0.0.1 本機迴環地址只能本機訪問,其他電腦訪問不了(識別到了就不用走七層協議這些了) # address: Union[tuple, str, bytes]) ---> address 參數是一個元組,綁定ip 和 埠 server.listen(5) # 半連接池 print("waitting....") # waitting.... conn, addr = server.accept() # 阻塞,等待客戶端連接,沒有收到資訊會停在這裡 print("hi") # 在連通之前並沒有反應 # hi # -------------------------------------- # send 與 recv 要對應 # 不要兩邊都 recv,不然就都等著接收了 # -------------------------------------- data = conn.recv(1024) # 阻塞,等待客戶端發送數據,接收1024個位元組的數據 print(data) # b'halo baby' conn.send(b'ok') # 發送數據(必須是二進位數據) conn.close() # 關閉連接 server.close() # 關閉服務
client客戶端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) # 去連接伺服器上的程式(伺服器的IP + port) client.send(b'halo baby') data = client.recv(1024) print(data) client.close()
點進去發現socket這個類有實現 __enter_ 、 __exit__
方法,__exit__
方法中有關閉連接的方法,故可以用with
上下文來操作(暫不舉例了,面向對象這兩個函數的知識點提一嘴)

在重啟伺服器的時候可能會遇到的BUG(mac居多)

解決方法
# 加入一條socket配置,重用ip和埠 import socket from socket import SOL_SOCKET, SO_REUSEADDR server = socket.socket() # ------------------------------------- # 加上他就可以防止重啟報錯了(注意位置) # ------------------------------------- server.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) server.bind(('127.0.0.1', 8080)) # 把地址綁定到套接字 server.listen(5) # 半連接池 conn, addr = server.accept() # 接受客戶端鏈接 ret = conn.recv(1024) # 接收客戶端資訊 print(ret) # 列印客戶端資訊 conn.send(b'hi') # 向客戶端發送資訊 conn.close() # 關閉客戶端套接字 server.close() # 關閉伺服器套接字(可選)
服務端需要具備的條件
- 固定的ip和port 讓客戶端可以連接你(試想如果百度一天一個域名/ip?咋上百度))
- 要能24小時不間斷提供服務 伺服器不在線的話,客戶端連啥?(雙重循環 server.accpet() 來連接建立連接)
- 暫時不知道
半連接池,允許等待的最大個數

server.listen(5)
指定5個等待席位
通訊循環
雙方都處於收的等待狀態
直接回車沒有發出數據,自身程式碼往下走進入了等待接收狀態, 而另一端也沒有收到消息,依然處於等待接收狀態圖,雙方就都處於等待接收的狀態了

linux、mac斷開鏈接時不會報錯,會一直返回空(b『』)
窮,買不起mac…沒圖
解決方案
服務端
import socket server = socket.socket() server.bind(('127.0.0.1', 8080)) # 本地迴環地址 server.listen(5) conn, addr = server.accept() # 阻塞 for i in range(1, 5): try: data = conn.recv(1024) # 阻塞 print(data.decode('utf-8')) msg = f"收到 {i} ga ga ga~" # 發的時候要判斷非空,空的自己send出去處於接收狀態,對方依舊是接收狀態,那就都等待了 conn.send(msg.encode('utf-8')) # ***** send 直接傳回車會導致兩遍都處於接收狀態 except ConnectionResetError: # ***** 當服務端被強制關閉時彙報異常,這裡捕獲並做處理 # mac或者linux 會一直輸空,不會自動結束 break conn.close() server.close()
客戶端
import socket client = socket.socket() client.connect(('127.0.0.1', 8080)) hi = input(">>>:").strip() for i in range(1, 5): msg = f'-{i} hi 咯~' client.send(msg.encode('utf-8')) data = client.recv(1024) if len(data) == 0: # ***** mac或者linux 需要加,避免客戶端突然斷開,他不會報錯,會一直列印空 break print(f"收到 {i} {data.decode('utf-8')}") client.close()
實現服務端可以接收多個客戶端通訊(一個結束還可以接收下一個) — 利用好server.listen(5) 半連接池
以及conn, addr = server.accept()
把接收的程式碼用循環包起來
粘包問題
多次發送被並為一次
根據最上面的前置知識可以知道,數據是從記憶體中讀取過來的

產生問題的原因

黏包現象只發生在tcp協議中
1.從表面上看,黏包問題主要是因為發送方和接收方的快取機制、tcp協議面向流通訊的特點
2.實際上,主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少位元組的數據所造成的
粘包是接收長度沒對上導致的
控制recv接收的位元組數與之對應(你發多少位元組我收多少位元組)
在很多情況下並不知道數據的長度,服務端不能寫死
思路一如果在不知道數據有多長的情況下就會出現意外,那麼我們可以先傳一個固定長度的數據過去告訴他真實數據有多長,就可以對應著收了
struct模組
該模組可以把一個類型,如數字,轉成固定長度的bytes
這裡利用struct模組模組的struct.pack() struct.unpack()
方法來實現打包(將真實數據長度變為固定長度的數字)解包(將該數字解壓出打包前真實數據的長度)
pack unpack
模式參數對照表(standard size 轉換後的長度)

i 模式的範圍:-2147483648 <= number <= 2147483647
在傳真實數據之前還想要傳一些描述性資訊
如果在傳輸數據之前還想要傳一些描述性資訊,那麼就得在中間再加一步了(傳個電影,我告訴你電影名,大小,大致情節,演員等資訊,你再選擇要不要),前面的方法就不適用了
粘包問題解決思路
伺服器端
- 先製作一個發送給客戶端的字典
- 製作字典的報頭
- 發送字典的報頭
- 發送字典
- 再發真實數據
客戶端
- 先接收字典的報頭
- 解析拿到字典的數據長度
- 接收字典
- 從字典中獲取真實數據的長度
- 循環獲取真實數據
ps:為什麼要多加一個字典
- pack打包的數據長度(的長度)有限,字典再打包會很小(長度值也會變很小)(120左右)
- 可以攜帶更多的描述資訊
粘包問題解決最終版模板
伺服器端
import socket import subprocess import struct import json server = socket.socket() server.bind(('127.0.0.1',8080)) server.listen(5) while True: conn, addr = server.accept() while True: try: cmd = conn.recv(1024) if len(cmd) == 0:break cmd = cmd.decode('utf-8') obj = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) res = obj.stdout.read() + obj.stderr.read() d = {'name':'jason','file_size':len(res),'info':'asdhjkshasdad'} json_d = json.dumps(d) # 1.先製作一個字典的報頭 header = struct.pack('i',len(json_d)) # 2.發送字典報頭 conn.send(header) # 3.發送字典 conn.send(json_d.encode('utf-8')) # 4.再發真實數據 conn.send(res) # conn.send(obj.stdout.read()) # conn.send(obj.stderr.read()) except ConnectionResetError: break conn.close()
客戶端
import socket import struct import json client = socket.socket() client.connect(('127.0.0.1',8080)) while True: msg = input('>>>:').encode('utf-8') if len(msg) == 0:continue client.send(msg) # 1.先接受字典報頭 header_dict = client.recv(4) # 2.解析報頭 獲取字典的長度 dict_size = struct.unpack('i',header_dict)[0] # 解包的時候一定要加上索引0 # 3.接收字典數據 dict_bytes = client.recv(dict_size) dict_json = json.loads(dict_bytes.decode('utf-8')) # 4.從字典中獲取資訊 print(dict_json) recv_size = 0 real_data = b'' while recv_size < dict_json.get('file_size'): # real_size = 102400 data = client.recv(1024) real_data += data recv_size += len(data) print(real_data.decode('gbk'))
案例-客戶端向服務端傳輸文件
需求
# 寫一個上傳電影功能 1.循環列印某一個文件夾下面的所有文件 2.用戶選取想要上傳的文件 3.將用戶選擇的文件上傳到服務端 4.服務端保存該文件
服務端(沒有處理斷開連接的報錯以及空輸入的報錯,linux、mac的兼容)
import os import sys import socket import struct import json server = socket.socket() server.bind(('192.168.13.34', 8080)) server.listen(5) conn, addr = server.accept() ''' 伺服器端將文件都放在同一個目錄 ''' BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) sys.path.append(BASE_DIR) dir_path = os.path.join(BASE_DIR, 'datas', 're_movies') if not os.path.exists(dir_path): os.makedirs(dir_path) import time from functools import wraps # 統計運行時間裝飾器 def count_time(func): @wraps(func) def inner(*args, **kwargs): start_time = time.time() res = func(*args, **kwargs) end_time = time.time() print(f"耗時{end_time - start_time}s") return res return inner @count_time def save_file(file_path, file_size): with open(file_path, 'ab') as f: # 一行一行地收文件,同時寫入文件 recv_size = 0 while recv_size < file_size: data = conn.recv(1024) # 存文件 # json.dump(data.decode('utf-8'), f) # -------------可能報錯,不傳文件對象 f.write(data) f.flush() recv_size += len(data) msg = f'已收到{file_name},{file_size/1024/1024}MB,over~' print('