進擊のpython
網絡編程——粘包現象
前面我們提到了套接字的使用方法,以及相關bug的排除
還記得我們提到過一個1024嗎?
我們現在要針對這個來研究一下一個陷阱
在研究這個陷阱之前我要先教你幾條語句
這是windows的命令啊
ipfonfig 查看本地網卡的ip地址
dir 查看某一個文件夾下的子文件名和子文件夾名
tasklist 查看運行的進程
那我這三條命令怎么執行呢??直接敲??
好像沒什么用,所以說我需要打開我的cmd窗口來鍵入這些命令
而cmd也就是一個能把特殊的字母組合執行出來的一個程序而已
當我在cmd里鍵入dir的時候得到的就是這些東西
那我想在編譯器里搞這個東西呢?
哦!第一反應就是os模塊
import os
os.system("dir")
就執行起來了吧
那我這算是拿到結果了嗎?
我覺得不算,為什么?
咱們想要達到的效果是我在客戶端輸入一個dir發送給服務端,服務端給我返回這一堆東西才叫拿到結果了是吧
import os
res = os.system("dir")
print(f"返回的結果是:{res}")
那結果我打印的是什么呢??是0!那為什么是這個呢?
這個0是代表這個命令是不是成功
如果返回的是0,就是成功了,如果是非零,就是失敗了!
所以說他返回的是一個是否成功執行語句的狀態,而不是執行語句的返回結果
那os模塊就被pass掉了,因為他無法返回我們需要的東西
那除了os.還有什么嗎?subprocess
他下面有一個方法
import subprocess
subprocess.Popen()
里面接收兩個參數,第一個參數是字符串的命令
第二個是shell=True,作用是在終端也就是cmd下運行
那我這么寫就沒問題了
import subprocess
subprocess.Popen("dir", shell=True)
但是我不要把這個結果給終端,我要把這個結果給客戶端
那我是不是就要把結果傳進管道然后進行傳輸呢?
好,那這個方法就可以傳遞第三個屬性stdout = subprocess.PIPE
這個管道是用來接收正確的結果的
那錯誤的結果傳在哪呢?第四個屬性! stderr = subprocess.PIPE
好,當我把所有的參數都填進去之后我們再看,是不是在控制台就沒有輸出結果了啊
輸出結果去哪了呢?放到管道里去了!
import subprocess
obj = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
那我想把管道里的東西取出來怎么弄呢?
我們先來看看我打印obj是個啥?很明顯是個對象是吧
那既然是對象就能調用方法
print(obj.stdout)
<_io.BufferedReader name=3>
看到IO第一反應就是文件,用read()方法讀一下
你發現你打印的時候什么???這不就是我們想要的字節類型的數據嘛
然后我們再來看
import subprocess
obj = subprocess.Popen("dir", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(obj.stdout.read().decode("gbk"))
print(obj.stdout.read().decode("gbk"))
打印了幾次?只有一次!為什么?
因為我把數據放到管道之后,第一次打印就把結果拿出來了,第二次再拿就啥也拿不到了
反而錯誤管道里就有信息了
那現在就可以把代碼寫進去吧
# 服務端
import socket
import subprocess
# 買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 綁定手機卡
phone.bind(("127.0.0.1", 8080))
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 開機
phone.listen(5)
# 等電話
connet, client_addr = phone.accept()
# 收發消息
while 1:
try:
k = connet.recv(1024)
obj = subprocess.Popen(k.decode("gbk"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
connet.send(stdout + stderr)
except ConnectionResetError:
break
# 掛電話
connet.close()
# 關機
phone.close()
# 客戶端
import socket
# 買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 撥號
phone.connect(("127.0.0.1", 8080))
# 發收信息
while 1:
msg = input(">>>")
phone.send(msg.encode("gbk"))
k = phone.recv(1024)
k = k.decode("gbk")
print(f"從服務端接收的消息:{k}")
connet.close()
# 關閉
phone.close()
其實到現在,我們其實完成了一個模擬ssh遠程執行命令的操作,就是客戶端獲取主機的信息的操作
但是我們會發現問題,我要是傳的超過1024個字節的數據,怎么辦呢???
還有一個就是 stdout + stderr 這個加號就相當於新開個內存空間
而不是直接在他本身上加,所以這個效率應該是可以優化的
比如說我傳一個超過1024的,會發生什么現象?
我先打印一下D盤的文件
我發現信息沒有完整的顯示出來
然后我再打印一下IP信息
我會發現,打印結果的上面,還有上一次打印沒打印出來的信息!
說明溢出的數據沒有丟,而是等待下一次的調用然后傳過去
其實也很好理解
我們這不是流式傳輸嘛,那水流還能 嘎巴 一下就斷了?
不能吧,當我拿到一點水后關閉通道,本該進來的水就因為容器太小的原因
留在了外面,等到下次調用的時候,在外面的這一部分就先進來,然后再進其他的
這個現象,就叫粘包!
指的是多個包的返回值黏在一起了
那應該怎么解決呢?
我是不是可以在發消息之前,先告訴服務端我要傳多大的文件
然后服務端就對這個信息做出相應的操作
有一種方法是把1024改一個更大的數,但其實
還是治標不治本,因為你不知道返回值有多大,就有可能被超越
那還有一種方法,那我沒接完我就繼續接唄
那我就應該把數據長度發給客戶端,然后再發送數據
然后再說說+的問題,因為他是流數據,那我按順序發,他是不是就自己拼上了啊
connet.send(stdout)
connet.send(stderr)
而服務端首先應該接收到數據長度然后再接收數據是吧
那我客戶端大概應該這么寫
msg = input(">>>")
phone.send(msg.encode("gbk"))
re_len = 1025 # 數據長度
re_size = 0
r = b"" # 我傳過來的是字節模式
if re_size < re_len:
k = phone.recv(1024)
r += k
re_size += len(k)
print(f"從服務端接收的消息:{k}")
那這個數據長度,就應該是服務端傳過來的對吧
所以服務端大概應該這么寫
stdout = obj.stdout.read()
stderr = obj.stderr.read()
r_size = len(stdout)+len(stderr)
connet.send(str(r_size).encode("gbk")) # 數字模式不能傳,只能傳字符串
connet.send(stdout)
connet.send(stderr)
但是問題就出現了!我這三個發送信息也是粘包,那我怎么能讓客戶端進行分辨?
還記得我們在說傳輸數據的時候提到了報頭的概念嘛?
所以其實我們是在寫報頭,而報頭是固定長度的
所以我現在就要學會如何發報頭對吧!
那我們現在開始自定義報頭吧
這時候我們就需要學習一個新的模塊struct
struck.pack()
相當於打包這里面傳的是兩個參數,第一個是數據類型,第二個的是數據
res = struct.pack("i", 1234)
print(res, type(res), len(res))
打印的是:
b'\xd2\x04\x00\x00' <class 'bytes'> 4
所以,我這就算是拿到了報頭的長度
那我這打包怎么解包???
res = struct.unpack("i",res)
(1234,)
我拿到的是元組,所以[0]是不是就拿到了1234了
那長度是不是也就拿到了
那服務端就可以寫了
r_size = len(stdout) + len(stderr)
res = struct.pack("i", r_size)
那客戶端就知道怎么做了
res = phone.recv(4)
re_len = struct.unpack("i", res)[0]
那總的來說,代碼就如下:
# 客戶端
import socket
# 買手機
import struct
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 撥號
phone.connect(("127.0.0.1", 8080))
# 發收信息
while 1:
msg = input(">>>")
phone.send(msg.encode("gbk"))
res = phone.recv(4)
re_len = struct.unpack("i", res)[0]
re_size = 0
r = b"" # 我傳過來的是字節模式
while re_size < re_len:
k = phone.recv(1024)
r += k
re_size += len(k)
print(re_size, re_len)
print(f'從服務端接收的消息:{r.decode("gbk")}')
connet.close()
# 關閉
phone.close()
# 服務端
import socket
import struct
import subprocess
# 買手機
phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 綁定手機卡
phone.bind(("127.0.0.1", 8080))
phone.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 開機
phone.listen(5)
# 等電話
connet, client_addr = phone.accept()
# 收發消息
while 1:
try:
k = connet.recv(1024)
obj = subprocess.Popen(k.decode("gbk"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout = obj.stdout.read()
stderr = obj.stderr.read()
r_size = len(stdout) + len(stderr)
res = struct.pack("i", r_size)
connet.send(res)
connet.send(stdout)
connet.send(stderr)
except ConnectionResetError:
break
# 掛電話
connet.close()
# 關機
phone.close()