一、背景說明
昨天一個同事讓幫忙寫個服務,用於接收並返回他那邊提交過來的數據,以便其查看提交的數據及格式是否正確。
開始想用django寫個接口,但寫接口接口名稱就得是定死的,他那邊只能向這接口提交數據;接收一下就返回這種事情不如直接寫個socket監聽然后返回去。
以前也沒怎么寫正經的socket編程,基本是能收發點數據應差不多了,此次收發的數據一多就出了問題。
一是沒接收完客戶端要發送的數據就給客戶端回RST,二是沒發送完要給客戶端發送的數據就又直接給客戶端發送RST。
二、問題處理
2.1 tcp收發處理
使用s.recv()接收數據時,s.recv()只管從操作系統緩沖區中讀取數據,阻塞模式下只要讀到數據、非阻塞模式下不管讀到讀不到或者讀到多少,函數都算執行完了程序會繼續往后執行。因此接收大量數據時我們需要不斷使用s.recv()進行讀取然后設定一個終止標志。
使用s.send()發送數據時,s.send()只管通知操作系統發送數據,操作系統每次只是盡力發送數據然后把本次發送的數據作為返回值並不保證數據發送完成。因此在發送大量數據時我們需要不斷使用s.send()發送然后設定一個終止標志。(不過如果單純說python那可以使用snedall()函數來實現一次發送完,sendall()的實現方法和我們這里說的意思一樣)
另外注意如果測試自己使用requests等作為客戶端時,服務端的返回要加上http的響應頭部,不然數據原樣返回requests等進行解析會因不是一個正確的http響應而出錯。
2.2 有問題程序
import socket # 獲取本地主機名 host = socket.gethostname() port = 9999 # 創建socket對象 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 綁定端口號 server_socket.bind((host, port)) # 設置最大連接數,超過后排隊 server_socket.listen(5) # 這里實現的是接收客戶端發來的數據、打印、然后再原樣返回給客戶端 while True: client_socket, addr = server_socket.accept() print(f"連接地址: {str(addr)}") # 錯誤二、當發來的數據很長時tcp不會等接收完成再執行下一條語句,這里沒處理這個問題 result = client_socket.recv(1024 * 1024) # 問題一、decode默認使用utf-8編碼,但當發來的數據有utf-8不可解碼內容時會報異常,這里沒捕獲異常 print(f"{result.decode()}") # 錯誤三、發送時tcp不會等發送完再執行下一條語句,這里沒處理這個問題 client_socket.send(result) # 注意四、如果客戶端中的接收代碼是和上邊錯誤二一樣的,那么沒發完也會被客戶端reset client_socket.close()
2.3 修正后程序
import socket # 獲取本地主機名 host = socket.gethostname() port = 9999 # 創建socket對象 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 綁定端口號 server_socket.bind((host, port)) # 設置最大連接數,超過后排隊 server_socket.listen(5) print("服務器程序已啟動") while True: # 建立客戶端連接 client_socket, addr = server_socket.accept() # 阻塞模式,設置接收超時時長為1秒,1秒內沒有新數據視為數據已傳送完成 client_socket.settimeout(1) # client_socket.setblocking(0) print(f"連接地址: {str(addr)}") # 這里result不能賦值為None,否則下邊的result += tmp_result會因類型不一致報錯 # 這里result不能賦值為字符串"",否則下邊的result += tmp_result會因類型不一致報錯 result = b"" tmp_result = "" # 錯誤二修正:此處while循環用於確保接收完所有數據再執行后續指令 while True: # 每次最多讀取2048字節 try: tmp_result = client_socket.recv(2048) # 1秒內無數據,觸發超時異常,此時我們判定為數據已接收完成break退出 # 不能使用獲取數據為空作為退出標志,因為阻塞模式除非是已建立的網絡連接被拆除不然讀不到數據是不會返回的 except socket.timeout as e: print(f"{e}") break # 將本次讀取到的內容拼接到result中 result += tmp_result # 問題一修正:對解碼異常進行捕獲,直接以byte形式輸出 try: print(f"{result.decode()}") except: print(f"{result}") total_lenght = result.__len__() print(f"\r\ntotal_length :{total_lenght}") flag = 0 # 錯誤三修正:確保數據發送完才執行后續代碼 # python其實可以使用sendall()來完成,但sendall本身也是類似以下形式,為了通用性我們這里暫時不用 while True: # 每次從已發送數據位置發送 # 每次返回的是本次發送數據長度 tmp_flag = client_socket.send(result[flag:]) flag += tmp_flag # 如果已發送完則退出 if flag == total_lenght: break # 至於問題四,客戶端未完成接收即返回reset,那就只能由客戶端去處理了 client_socket.close()
參考: