1. 程式人生 > >第三十三天 遠端執行命令與粘包問題

第三十三天 遠端執行命令與粘包問題

上週回顧:

1.三層結構

  一種程式的框架

  使用者檢視   與使用者互動 接受和輸出資料

  業務邏輯   複雜對資料進行  判斷  驗證  組裝

  資料訪問層 複雜處理資料的存取

 

2.異常處理

  異常處理的目的是為了 提高程式的健壯性(不容易奔潰)

  異常是? 發生錯誤前的一種訊號,如果沒有正確處理這種訊號  程式終止執行  並丟擲資訊

  異常是哪個組成部分

    1.異常的追蹤資訊

    2.異常的型別

    3.異常的詳細資訊

    排除錯誤是 先檢視異常型別和詳細資訊  最後根據追蹤資訊找到發生的位置進行修改!

 

  主動丟擲異常

    raise 異常型別(訊息)型別必須是Exception的子類

  

  斷言
    assert 一個條件表示式 結果必須是bool
    當後續的邏輯執行前必須要保證某個條件成立時 使用
    也可以使用if 來手動丟擲異常
    所以assert 就是為了簡化程式碼

  語法: *****
  try:
    except 異常型別 | (多個型別,...) | 萬能型別 as 別名:
  else:
  finally:
  使用場景:
    無法預知錯誤發生的原因時 或者 知道為什麼錯 但是程式無法解決


  網路程式設計:
  為了使程式能夠分佈在不同計算機上 本質問題是通訊:
  通訊的兩個條件
  1.物理連線介質
  2.通訊協議 ******

  OSI 七層協議:
    應用層
    表示
    會話
    傳輸
    網路層
    資料鏈路
    物理層

  表示層和會話都屬於應用層
    應用層 沒有固定的協議 需要雙方程式設計師商量
    傳輸 TCP/UDP port埠號 用於唯一標識一個應用程式
    網路層 ip協議 找到一個唯一的區域網 在找到區域網內的一臺計算機
    資料鏈路層 乙太網協議 傳送二進位制的格式 mac標識唯一一臺計算機
    物理層 傳送二進位制

    我們需要關注的是傳輸層的協議 然而像TCP這種協議原理還是很複雜
    為了簡化這些複雜操作 在傳輸層之上封裝了socket抽象層
    這樣一來 要完成網路通訊 就不需要在與底層的TCP/UDP協議打交道
    直接使用socket封裝好的功能

二.今日內容

  1.TCP協議詳解

  2.粘包問題

  3.粘包的解決方案

 

1.TCP的模板程式碼

  手法訊息的迴圈  通訊迴圈

  不斷的連線客戶端迴圈  連線迴圈

  判斷  用於判斷客戶端異常退出(拋異常)或close(死迴圈)

#客戶端
import socket
c = socket.socket()
#連線伺服器
c.connect("127.0.01",65535))
while True:
    #傳送資料
    msg = input(">>:")
    if not msg:continue
    c.send(msg.encode("utf-8"))
    print("send!")
    #收資料
    data = c.recv(1024).decode("utf-8")
    print("receiver!")
    print(data)
c.close()


#伺服器
import socket
#使用TCP 可以直接預設
server = socket.socket()
#指定埠 和ip埠 0 - 1023 是系統保留的
server.bind(("127.0.0.1",65535))

server.listen(5)
while True:
    
    c,addr = server.accept()
    while True:
        try:
            data = c.recv(1024).decode("utf-8")
            #如果客戶端斷開連線 結束迴圈
            if not data:
                print("client closed!")
                c.close()
                break
            print(data)
            c.send(data.upper().encode("utf-8")
        except ConnectionResetError:
            print("客戶端異常關閉!!")
            c.close()
            break
server.close()

2.遠端CMD
粘包問題
一方傳送空資料 導致程式卡死 今後會通過多執行緒處理

# 伺服器
#  1.伺服器先啟動 -> 客戶端傳送指令 -> 伺服器接收後使用subprocess執行命令->將執行結果返回給客戶端

import socket,subprocess
# 使用TCP 可以直接預設
server = socket.socket()

# 指定埠 和 ip     埠 0 - 1023是系統保留的
server.bind(("127.0.0.1",65535))

# 監聽請求  引數為最大半連線數(三次握手未完成的請求  可能是伺服器來不及 客戶端惡意攻擊)
server.listen(5)
# 為了可以不斷的接受客戶端連線請求
while True:
    # 接受連線請求
    c,addr = server.accept()
    # 為了可以重複收發資料
    while True:
        try:
            # 1024  程式的最大緩衝區容量   返回值型別為bytes型別
            cmd = c.recv(1024).decode("utf-8")
            # 如果客戶端斷開連線(客戶端呼叫了close) recv 返回值為kong 此時應該結束迴圈
            if not cmd:# 在linux中 客戶端異常關閉 伺服器也會收空
                print("client closed!")
                c.close()
                break
            #解碼
            print(cmd)
            # 執行命令
            p = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            # 將錯誤資訊和正確資訊拼接到一起
            res = p.stdout.read() + p.stderr.read()
            print("執行結果長",len(res))
            # 將執行結果傳送給客戶端
            c.send(res)
        except ConnectionResetError:
            print("客戶端異常關閉!!")
            c.close()
            break
# 關閉資源
server.close()


# TCP斷開連線的正確姿勢
# 客戶端呼叫close
# 伺服器判斷如果接收資料為空則相應的呼叫close

#客戶端1
import socket

c = socket.socket()

# 連線伺服器
c.connect(("127.0.0.1",65535))

while True:
    # 傳送資料
    msg = input(">>>:")
    if not msg:continue
    c.send(msg.encode("utf-8"))
    # while True:
        #     # 收資料
    data = c.recv(1024).decode("gbk")
    print(data)

# 關閉資源
c.close()

# 問題?  伺服器傳送的資料超過了接收端緩衝區大小 可直接修改大小來滿足伺服器傳輸的大小 但是不長遠
# 上述問題 稱之為粘包
# 思考: 迴圈每次讀取一小部分 直到取完為止
# 什麼時候可以結束迴圈  前提是讓客戶端直知道你的資料到底有多長
# 正確思路:
"""
    傳送方
    1.先告訴對方你要發的資料的長度
    2.在傳送真實資料
    
    接收方
    1.先接收資料的長度資訊
    2.根據長度資訊迴圈獲取直到以獲取的長度等於總長度
    
"""

#客戶端2
import socket,time

c = socket.socket()

# 連線伺服器
c.connect(("127.0.0.1",65535))

while True:
    # 傳送資料

    c.send("dir".encode("utf-8"))
    time.sleep(1)
    c.send("dir".encode("utf-8"))

    data = c.recv(1024).decode("gbk")
    print(data)

# 關閉資源
c.close()

# 問題2 當客戶端連續兩行程式碼都發送一個dir時  伺服器收到了一個dirdir
# 兩個命令黏在一起
# TCP協議內的一個nagle演算法  如果資料量小 並且時間間隔短會將資料合併一個包

 

3.解決粘包的方案 自定義報頭
1.先用報頭傳輸資料的長度
對於我們遠端CMD程式來說 只要先傳輸長度就能解決粘包的問題
但是如果做得是一個檔案上傳下載 除了資料的長度 還需要傳輸檔案的名字 md5等等資訊
又該如何?

# 伺服器
#  1.伺服器先啟動 -> 客戶端傳送指令 -> 伺服器接收後使用subprocess執行命令->將執行結果返回給客戶端

import socket,subprocess,struct
# 使用TCP 可以直接預設
server = socket.socket()

# 指定埠 和 ip     埠 0 - 1023是系統保留的
server.bind(("127.0.0.1",65535))

# 監聽請求  引數為最大半連線數(三次握手未完成的請求  可能是伺服器來不及 客戶端惡意攻擊)
server.listen(5)
# 為了可以不斷的接受客戶端連線請求
while True:
    # 接受連線請求
    c,addr = server.accept()
    # 為了可以重複收發資料
    while True:
        try:
            # 1024  程式的最大緩衝區容量   返回值型別為bytes型別
            cmd = c.recv(1024).decode("utf-8")
            # 如果客戶端斷開連線(客戶端呼叫了close) recv 返回值為kong 此時應該結束迴圈
            if not cmd:# 在linux中 客戶端異常關閉 伺服器也會收空
                print("client closed!")
                c.close()
                break
            #解碼
            print(cmd)
            # 執行命令
            p = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            # 將錯誤資訊和正確資訊拼接到一起
            res = p.stdout.read() + p.stderr.read()
            print("執行結果長",len(res))


            # 1.先發送資料的長度
            data_len = len(res)
            # 長度是一個整型 需要轉為位元組  1000 b'\x001'  2000 b'\x001\x002'
            # 另外 需要保證 長度資訊轉換後的結果長度是固定的 否則客戶端也會粘包(不知道取多少位元組)
            # struct 模組負責將python中的資料型別 轉為c語言中結構體
            # 整型轉位元組
            bytes_len = struct.pack("i",data_len)
            c.send(bytes_len)
            # 2.傳送真實資料
            c.send(res)
        except ConnectionResetError:
            print("客戶端異常關閉!!")
            c.close()
            break
# 關閉資源
server.close()


# TCP斷開連線的正確姿勢
# 客戶端呼叫close
# 伺服器判斷如果接收資料為空則相應的呼叫close


#客戶端
import socket,struct

c = socket.socket()

# 連線伺服器
c.connect(("127.0.0.1",65535))

while True:
    # 傳送資料
    msg = input(">>>:")
    if not msg:continue
    c.send(msg.encode("utf-8"))

    # 1.先獲取長度
    bytes_len = c.recv(4) #對方是i格式 固定4位元組
    # 2.轉回整型
    total_len = struct.unpack("i",bytes_len)[0]
    # 已經接收的長度
    recv_len = 0
    # 一個表示最終資料的bytes
    finally_data = b''
    # 3.收到的長度小於總長度就繼續
    while recv_len < total_len:
        # 迴圈收資料
        data = c.recv(1024)
        recv_len += len(data)
        finally_data += data
    # 整體解碼
    print(finally_data.decode("gbk"))

# 關閉資源
c.close()

# 問題?  伺服器傳送的資料超過了接收端緩衝區大小 可直接修改大小來滿足伺服器傳輸的大小 但是不長遠
# 上述問題 稱之為粘包
# 思考: 迴圈每次讀取一小部分 直到取完為止
# 什麼時候可以結束迴圈  前提是讓客戶端直知道你的資料到底有多長
# 正確思路:
"""
    傳送方
    1.先告訴對方你要發的資料的長度
    2.在傳送真實資料
    
    接收方
    1.先接收資料的長度資訊
    2.根據長度資訊迴圈獲取直到以獲取的長度等於總長度
    
    自定義報頭未講
"""

 

2.自定義複雜報頭 完成傳送

#伺服器
#  1.伺服器先啟動 -> 客戶端傳送指令 -> 伺服器接收後使用subprocess執行命令->將執行結果返回給客戶端

import socket,subprocess,struct,json
# 使用TCP 可以直接預設
server = socket.socket()

# 指定埠 和 ip     埠 0 - 1023是系統保留的
server.bind(("127.0.0.1",65535))

# 監聽請求  引數為最大半連線數(三次握手未完成的請求  可能是伺服器來不及 客戶端惡意攻擊)
server.listen(5)
# 為了可以不斷的接受客戶端連線請求
while True:
    # 接受連線請求
    c,addr = server.accept()
    # 為了可以重複收發資料
    while True:
        try:
            # 1024  程式的最大緩衝區容量   返回值型別為bytes型別
            cmd = c.recv(1024).decode("utf-8")
            # 如果客戶端斷開連線(客戶端呼叫了close) recv 返回值為kong 此時應該結束迴圈
            if not cmd:# 在linux中 客戶端異常關閉 伺服器也會收空
                print("client closed!")
                c.close()
                break
            #解碼
            print(cmd)
            # 執行命令
            p = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            # 將錯誤資訊和正確資訊拼接到一起
            res = p.stdout.read() + p.stderr.read()
            print("執行結果長",len(res))

            # 1.組裝一個報頭資訊
            head_dic = {
                "name":"倉老師視訊教學 如何做炸雞!",
                "md5":"asasasasaas",
                "total_size":len(res),
                "type":"video"
            }
            # 2.轉json字串
            head_str = json.dumps(head_dic)
            # 3.轉位元組
            head_bytes = head_str.encode("utf-8")
            # 4.傳送報頭長度
            bytes_len = struct.pack("i",len(head_bytes))
            c.send(bytes_len)
            # 5.傳送報頭
            c.send(head_bytes)
            # 6.傳送真實資料
            c.send(res)

        except ConnectionResetError:
            print("客戶端異常關閉!!")
            c.close()
            break
# 關閉資源
server.close()


# TCP斷開連線的正確姿勢
# 客戶端呼叫close
# 伺服器判斷如果接收資料為空則相應的呼叫close

#客戶端


#  1.伺服器先啟動 -> 客戶端傳送指令 -> 伺服器接收後使用subprocess執行命令->將執行結果返回給客戶端

import socket,subprocess,struct,json
# 使用TCP 可以直接預設
server = socket.socket()

# 指定埠 和 ip     埠 0 - 1023是系統保留的
server.bind(("127.0.0.1",65535))

# 監聽請求  引數為最大半連線數(三次握手未完成的請求  可能是伺服器來不及 客戶端惡意攻擊)
server.listen(5)
# 為了可以不斷的接受客戶端連線請求
while True:
    # 接受連線請求
    c,addr = server.accept()
    # 為了可以重複收發資料
    while True:
        try:
            # 1024  程式的最大緩衝區容量   返回值型別為bytes型別
            cmd = c.recv(1024).decode("utf-8")
            # 如果客戶端斷開連線(客戶端呼叫了close) recv 返回值為kong 此時應該結束迴圈
            if not cmd:# 在linux中 客戶端異常關閉 伺服器也會收空
                print("client closed!")
                c.close()
                break
            #解碼
            print(cmd)
            # 執行命令
            p = subprocess.Popen(cmd,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
            # 將錯誤資訊和正確資訊拼接到一起
            res = p.stdout.read() + p.stderr.read()
            print("執行結果長",len(res))

            # 1.組裝一個報頭資訊
            head_dic = {
                "name":"倉老師視訊教學 如何做炸雞!",
                "md5":"asasasasaas",
                "total_size":len(res),
                "type":"video"
            }
            # 2.轉json字串
            head_str = json.dumps(head_dic)
            # 3.轉位元組
            head_bytes = head_str.encode("utf-8")
            # 4.傳送報頭長度
            bytes_len = struct.pack("i",len(head_bytes))
            c.send(bytes_len)
            # 5.傳送報頭
            c.send(head_bytes)
            # 6.傳送真實資料
            c.send(res)

        except ConnectionResetError:
            print("客戶端異常關閉!!")
            c.close()
            break
# 關閉資源
server.close()


# TCP斷開連線的正確姿勢
# 客戶端呼叫close
# 伺服器判斷如果接收資料為空則相應的呼叫close

 

額外的資訊 例如檔名
1.將要傳送的額外資料打包成一個字典
2.將字典轉為bytes型別
3.計算字典的bytes長度 並先傳送
4.傳送字典資料
5.傳送真實資料

 

伺服器端示例:
# 為了方便存取 可以把需要的資訊打包為一個字典
dic{
"filename":"倉老師視訊教學 如何做炸雞!",
"md5":"xzxbzxkbsa1212121",
"total_size":2121221
}
# 字典轉字串? json
head_dic = str(dict)
bytes = head_dic.encode("utf-8")
# 先發送這個字典字串的長度
dic_len = len(head_dic)
#將長度轉為了 位元組
bytes_len = struct.pack("i",dic_len)
# 傳送報頭的長度
c.send(bytes_len)

# 傳送真實資料
c.send(xxx.mp4.bytes)
TCP能傳的只有位元組