網絡編程(三)——通信循環、鏈接循環、粘包問題
通信循環、鏈接循環、粘包問題
一、通信循環
服務端和客戶端可以進行連續的信息交流
from socket import * ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8886)) ser_socket.listen(5) conn, addr = ser_socket.accept() while True: try: # 拋出異常,若不拋出處理,一旦客戶端強行退出,服務端就會報錯 data = conn.recv(1024)通信循環服務端print(data.decode(‘utf-8‘)) conn.send(data.upper()) except ConnectionResetError: break conn.close() ser_socket.close()
from socket import * cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8886)) #通信循環,可以多次輸入 while True: msg = input(‘>>>>:通信循環客戶端‘).strip() if len(msg) == 0: # 如果輸入為空,給服務端發送信息之後,服務端什麽都沒接受,一直處於阻塞狀態 continue cli_socket.send(msg.encode(‘utf-8‘)) data = cli_socket.recv(1024) print(data.decode(‘utf-8‘)) cli_socket.close()
tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制
二、鏈接循環
可以啟動多個客戶端,但是只有一個客戶端是處於連接狀態,其余部分在半連接池等待連接,等待的數量不能超過半連接池的最大監聽數量
from socket import * ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8886)) ser_socket.listen(5) #鏈接循環,可以同時啟動最多6個客戶端,但是只有一個處於連接狀態,其余最多5個在半連接池等待。只有當連接狀態的客戶端斷開連接,下一個客戶端才進入連接 while True: conn, addr = ser_socket.accept() # 通信循環 while True: try: data = conn.recv(1024) print(data.decode(‘utf-8‘)) conn.send(data.upper()) except ConnectionResetError: break conn.close() ser_socket.close()鏈接循環服務端
from socket import * cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8886)) while True: msg = input(‘>>>>:‘).strip() if len(msg) == 0: continue cli_socket.send(msg.encode(‘utf-8‘)) data = cli_socket.recv(1024) print(data.decode(‘utf-8‘)) cli_socket.close()鏈接循環客戶端
三、粘包問題
1、模擬ssh遠程執行命令
from socket import socket, AF_INET, SOCK_STREAM import subprocess ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8882)) ser_socket.listen(5) while True: conn, addr = ser_socket.accept() while True: try: data = conn.recv(1024) obj = subprocess.Popen(data.decode(‘utf-8‘), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() conn.send(stdout + stderr) except ConnectionResetError: break conn.close() ser_socket.close()服務端
from socket import socket, AF_INET, SOCK_STREAM cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8882)) while True: msg = input(‘>>>‘).strip() if len(msg) == 0: continue cli_socket.send(msg.encode(‘utf-8‘)) data = cli_socket.recv(1024) print(data.decode(‘gbk‘)) # Windows系統,默認編碼gbk,所以用gbk解碼 cli_socket.close()客戶端
2、產生粘包原因
(1)所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
(2)此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。
在上面的例子中,如果執行命令tasklist,那麽就會存在粘包問題。由於TCP協議是流式協議,所以數據都以流的形式傳輸。假如數據大小是123456,可是已經設定了接收的大小 是1024,所以只接受了數據中的一小部分,但是,剩余部分數據並不會消失,會一直存在於操作系統中,所以下一次接收數據的時候是優先從剩余數據中接收。這樣所有數據就亂套了,這就是粘包問題。
3、發生粘包的兩種情況
(1)發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據了很小,會合到一起,產生粘包)
from socket import * ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8886)) ser_socket.listen(5) conn, addr = ser_socket.accept() data = conn.recv(1024) print(‘第一次接收:‘, data.decode(‘utf-8‘)) data1 = conn.recv(5) print(‘第二次接收:‘, data1.decode(‘utf-8‘)) data2 = conn.recv(1024) print(‘第三次接收:‘, data2.decode(‘utf-8‘)) conn.send(data.upper()) conn.close() ser_socket.close()服務端
from socket import * cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8886)) cli_socket.send(‘hello‘.encode(‘utf-8‘)) cli_socket.send(‘world‘.encode(‘utf-8‘)) cli_socket.send(‘object‘.encode(‘utf-8‘)) # data = cli_socket.recv(1024) # print(data.decode(‘utf-8‘)) cli_socket.close()客戶端
(2)接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
例如:模擬ssh遠程執行命令,若執行tasklist命令,在客戶端,無法幾次性全部接受執行結果,所以剩余結果會在下一次執行命令式優先接收
from socket import socket, AF_INET, SOCK_STREAM import subprocess ser_socket = socket(AF_INET, SOCK_STREAM) ser_socket.bind((‘127.0.0.1‘, 8882)) ser_socket.listen(5) while True: conn, addr = ser_socket.accept() while True: try: data = conn.recv(1024) obj = subprocess.Popen(data.decode(‘utf-8‘), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout = obj.stdout.read() stderr = obj.stderr.read() conn.send(stdout + stderr) except ConnectionResetError: break conn.close() ser_socket.close()服務端
from socket import socket, AF_INET, SOCK_STREAM cli_socket = socket(AF_INET, SOCK_STREAM) cli_socket.connect((‘127.0.0.1‘, 8882)) while True: msg = input(‘>>>‘).strip() if len(msg) == 0: continue cli_socket.send(msg.encode(‘utf-8‘)) data = cli_socket.recv(1024) print(data.decode(‘gbk‘)) cli_socket.close()客戶端
4、解決粘包問題的方法
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據。為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然後再取真實數據。
(1)簡單版本
# 服務端必須滿足至少三點: # 1. 綁定一個固定的ip和port # 2. 一直對外提供服務,穩定運行 # 3. 能夠支持並發 from socket import * import subprocess import struct server = socket(AF_INET, SOCK_STREAM) server.bind((‘127.0.0.1‘, 8081)) server.listen(5) # 鏈接循環 while True: conn, client_addr = server.accept() print(client_addr) # 通信循環 while True: try: cmd = conn.recv(1024) #cmd=b‘dir‘ # if len(cmd) == 0: break # 針對linux系統 obj=subprocess.Popen(cmd.decode(‘utf-8‘), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout=obj.stdout.read() stderr=obj.stderr.read() # 1. 先制作固定長度的報頭 header=struct.pack(‘i‘,len(stdout) + len(stderr)) # 2. 再發送報頭 conn.send(header) # 3. 最後發送真實的數據 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()服務端
from socket import * import struct client = socket(AF_INET, SOCK_STREAM) client.connect((‘127.0.0.1‘, 8081)) # 通信循環 while True: cmd=input(‘>>: ‘).strip() if len(cmd) == 0:continue client.send(cmd.encode(‘utf-8‘)) #1. 先收報頭,從報頭裏解出數據的長度 header=client.recv(4) total_size=struct.unpack(‘i‘,header)[0] #2. 接收真正的數據 cmd_res=b‘‘ recv_size=0 while recv_size < total_size: data=client.recv(1024) recv_size+=len(data) cmd_res+=data print(cmd_res.decode(‘gbk‘)) client.close()客戶端
(2)終極版本
# 服務端必須滿足至少三點: # 1. 綁定一個固定的ip和port # 2. 一直對外提供服務,穩定運行 # 3. 能夠支持並發 from socket import * import subprocess import struct import json server = socket(AF_INET, SOCK_STREAM) server.bind((‘127.0.0.1‘, 8081)) server.listen(5) # 鏈接循環 while True: conn, client_addr = server.accept() print(client_addr) # 通信循環 while True: try: cmd = conn.recv(1024) # cmd=b‘dir‘ if len(cmd) == 0: break # 針對linux系統 obj = subprocess.Popen(cmd.decode(‘utf-8‘), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) stdout = obj.stdout.read() stderr = obj.stderr.read() # 1. 先制作報頭 header_dic = { ‘filename‘: ‘a.txt‘, ‘md5‘: ‘asdfasdf123123x1‘, ‘total_size‘: len(stdout) + len(stderr) } header_json = json.dumps(header_dic) header_bytes = header_json.encode(‘utf-8‘) # 2. 先發送4個bytes(包含報頭的長度) conn.send(struct.pack(‘i‘, len(header_bytes))) # 3 再發送報頭 conn.send(header_bytes) # 4. 最後發送真實的數據 conn.send(stdout) conn.send(stderr) except ConnectionResetError: break conn.close() server.close()服務端
from socket import * import struct import json client = socket(AF_INET, SOCK_STREAM) client.connect((‘127.0.0.1‘, 8081)) # 通信循環 while True: cmd=input(‘>>: ‘).strip() if len(cmd) == 0:continue client.send(cmd.encode(‘utf-8‘)) #1. 先收4bytes,解出報頭的長度 header_size=struct.unpack(‘i‘,client.recv(4))[0] #2. 再接收報頭,拿到header_dic header_bytes=client.recv(header_size) header_json=header_bytes.decode(‘utf-8‘) header_dic=json.loads(header_json) print(header_dic) total_size=header_dic[‘total_size‘] #3. 接收真正的數據 cmd_res=b‘‘ recv_size=0 while recv_size < total_size: data=client.recv(1024) recv_size+=len(data) cmd_res+=data print(cmd_res.decode(‘gbk‘)) client.close()客戶端
網絡編程(三)——通信循環、鏈接循環、粘包問題