Python Day 28 網絡編程 (socket遠程命令執行, tcp黏包現象,以及struck模塊的使用 )

Python Day 28 網絡編程 (socket遠程命令執行, tcp黏包現象,以及struck模塊的使用 )

subprocess模塊

res=subprocess.Popen(cmd, #字符串命令
shell=True, #使用系統命令
stderr=subprocess.PIPE, #錯誤輸出
stdout=subprocess.PIPE) #標準輸出

的結果的編碼是以當前所在的系統爲準的,若是是windows,那麼res.stdout.read()讀出的就是GBK編碼的,在接收端須要用GBK解碼
且只能從管道里讀一次結果,且tcp是不能傳輸空內容的,pycharm或cmd有處理過程,沒法看到現象,直接使用解釋器執行會報錯.
View Code

 

  使用subprocess遠程執行命令算法

import subprocess
import socket


sk = socket.socket()
sk.bind(('127.0.0.1',8091))
sk.listen(5)

while True:
    conn,addr = sk.accept()
    while True:
        cmd = conn.recv(1024).decode('utf-8')
        r = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        stdout = r.stdout.read()
        stderr = r.stderr.read()  
    if stdout: #windows gbk編碼,client直接使用gbk解碼
        conn.send(stdout)
    elif stderr:   
        conn.send(stderr)

    conn.close()

sk.close()
server

 

import socket

sk = socket.socket()
sk.connect(('127.0.0.1',8091))

while True:
    cmd =input('請輸入命令:')
    sk.send(cmd.encode('utf-8'))
    # ret = sk.recv(1024).decode('utf-8')
    ret = sk.recv(1024).decode('gbk')
    print(ret)

sk.close()
client

 

黏包

同時執行多條命令以後,獲得的結果極可能只有一部分,在執行其餘命令的時候又接收到以前執行的另一部分結果,這種顯現就是黏包。shell

注意:只有TCP有粘包現象,UDP永遠不會粘包編程

 

黏包成因

TCP協議中的數據傳遞

  tcp協議的拆包機制json

當發送端緩衝區的長度大於網卡的MTU時,tcp會將此次發送的數據拆成幾個數據包發送出去。 
MTU是Maximum Transmission Unit的縮寫。意思是網絡上傳送的最大數據包。MTU的單位是字節。 大部分網絡設備的MTU都是1500。若是本機的MTU比網關的MTU大,大的數據包就會被拆開來傳送,這樣會產生不少數據包碎片,增長丟包率,下降網絡速度。
View Code

面向流的通訊特色和Nagle算法

TCP(transport control protocol,傳輸控制協議)是面向鏈接的,面向流的,提供高可靠性服務。
收發兩端(客戶端和服務器端)都要有一一成對的socket,所以,發送端爲了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將屢次間隔較小且數據量小的數據,合併成一個大的數據塊,而後進行封包。
這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通訊是無消息保護邊界的。 
對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。 
可靠黏包的tcp協議:tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端老是在收到ack時纔會清除緩衝區內容。數據是可靠的,可是會粘包。
View Code

 

 

 

發送端能夠是一K一K地發送數據,而接收端的應用程序能夠兩K兩K地提走數據,固然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據。
也就是說,應用程序所看到的數據是一個總體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,所以TCP協議是面向流的協議,這也是容易出現粘包問題的緣由。
而UDP是面向消息的協議,每一個UDP段都是一條消息,應用程序必須以消息爲單位提取數據,不能一次提取任意字節的數據,這一點和TCP是很不一樣的。
怎樣定義消息呢?能夠認爲對方一次性write/send的數據爲一個消息,須要明白的是當對方send一條信息的時候,不管底層怎樣分段分片,TCP協議層會把構成整條消息的數據段排序完成後才呈如今內核緩衝區。
View Code

例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束windows

此外,發送方引發的粘包是由TCP協議自己形成的,TCP爲提升傳輸效率,發送方每每要收集到足夠多的數據後才發送一個TCP段。若連續幾回須要send的數據都不多,一般TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。緩存

UDP不會發生黏包

UDP(user datagram protocol,用戶數據報協議)是無鏈接的,面向消息的,提供高效率服務。 
不會使用塊的合併優化算法,, 因爲UDP支持的是一對多的模式,因此接收端的skbuff(套接字緩衝區)採用了鏈式結構來記錄每個到達的UDP包,在每一個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來講,就容易進行區分處理了。 即面向消息的通訊是有消息保護邊界的。 
對於空消息:tcp是基於數據流的,因而收發的消息不能爲空,這就須要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即使是你輸入的是空內容(直接回車),也能夠被髮送,udp協議會幫你封裝上消息頭髮送過去。 
不可靠不黏包的udp協議:udp的recvfrom是阻塞的,一個recvfrom(x)必須對惟一一個sendinto(y),收完了x個字節的數據就算完成,如果y;x數據就丟失,這意味着udp根本不會粘包,可是會丟數據,不可靠。
View Code

用UDP協議發送時,用sendto函數最大能發送數據的長度爲:65535- IP頭(20) – UDP頭(8)=65507字節。用sendto函數發送數據時,若是發送數據長度大於該值,則函數會返回錯誤。(丟棄這個包,不進行發送)服務器

用TCP協議發送時,因爲TCP是數據流協議,所以不存在包大小的限制(暫不考慮緩衝區的大小),這是指在用send函數時,數據長度參數不受限制。而實際上,所指定的這段數據並不必定會一次性發送出去,若是這段數據比較長,會被分段發送,若是比較短,可能會等待和下一次數據一塊兒發送。網絡

 

會發生黏包的兩種狀況

  狀況一 發送方的緩存機制數據結構

發送端須要等緩衝區滿才發送出去,形成粘包(發送數據時間間隔很短,數據了很小,會合到一塊兒,產生粘包)socket

 

#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(10)
data2=conn.recv(10)

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()

服務端
View Code
#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello'.encode('utf-8'))
s.send('egg'.encode('utf-8'))

客戶端
View Code

  狀況二 接收方的緩存機制

接收方不及時接收緩衝區的包,形成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候仍是從緩衝區拿上次遺留的數據,產生粘包) 

#_*_coding:utf-8_*_
from socket import *
ip_port=('127.0.0.1',8080)

tcp_socket_server=socket(AF_INET,SOCK_STREAM)
tcp_socket_server.bind(ip_port)
tcp_socket_server.listen(5)


conn,addr=tcp_socket_server.accept()


data1=conn.recv(2) #一次沒有收完整
data2=conn.recv(10)#下次收的時候,會先取舊的數據,而後取新的

print('----->',data1.decode('utf-8'))
print('----->',data2.decode('utf-8'))

conn.close()

服務端
View Code
#_*_coding:utf-8_*_
import socket
BUFSIZE=1024
ip_port=('127.0.0.1',8080)

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(ip_port)


s.send('hello egg'.encode('utf-8'))

客戶端
View Code

總結

黏包現象只發生在tcp協議中:

1.從表面上看,黏包問題主要是由於發送方和接收方的緩存機制、tcp協議面向流通訊的特色。

2.實際上,主要仍是由於接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所形成的

黏包的解決方案

解決方案一

問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,因此解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把本身將要發送的字節流總大小讓接收端知曉,而後接收端來一個死循環接收完全部數據。

 

FTP進化史: 

 思路 : https://www.processon.com/diagraming/5b17c677e4b00490ac8b3a01

小文件版:   upload舉例: 經過success控制,防止黏包

#小文件版
import socket
import json
import os

sk = socket.socket()
sk.bind(('127.0.0.1',8091))
sk.listen(5)

while True:
    sock,addr = sk.accept()
    while True:
        msg_key_dict_str = sock.recv(1024).decode('utf-8')
        msg_key_dict = json.loads(msg_key_dict_str)  #{'file_opt':file_opt,'file_name':file_name}
        file_opt = msg_key_dict['file_opt']
        if file_opt == 'upload':
            file_name = msg_key_dict['file_name']
            file_path = os.path.join(r'D:\Learn\Python全棧開發\Day28\\upload',file_name)
            sock.send('上傳通道創建成功...'.encode('utf-8'))
            with open(file_path,'wb') as f:
                file_conn = sock.recv(1024)
                f.write(file_conn)
            sock.send('{}上傳成功'.format(file_name).encode('utf-8'))
        elif  file_opt == 'download':
            pass
server
#小文件版
import socket
import os
import json

sk = socket.socket()
sk.connect(('127.0.0.1',8091))

options = {'1':'upload','2':'download'}

while True:
    for id,option in options.items():
        print(id,':',option)
    user_choice = input('請您選擇操做:')

    if user_choice == '1': #上傳
        file_opt = 'upload'
        file_path = input('請輸入要上傳的文件路徑:')
        file_name = os.path.basename(file_path)
        upload_dict = {'file_opt':file_opt,'file_name':file_name}
        upload_dict_str = json.dumps(upload_dict)
        sk.send(upload_dict_str.encode('utf-8'))
        upload_connect_ret = sk.recv(1024).decode('utf-8')
        print(upload_connect_ret)
        with open(file_path, 'rb') as f:
            file_conn = f.read()
            sk.send(file_conn)
        upload_ret = sk.recv(1024).decode('utf-8')
        print(upload_ret)
    elif user_choice == '2': #下載
        pass
client

存在問題:  recv只能接收1024, 超過大小接收不到. 

 

支持大文件: upload舉例:經過success控制,防止黏包,經過對文件讀取大小控制,接收完整的大文件.

#支持大文件經過success控制,防止黏包
import socket
import json
import os

sk = socket.socket()
sk.bind(('127.0.0.1',8091))
sk.listen(5)

while True:
    sock,addr = sk.accept()
    while True:
        msg_key_dict_str = sock.recv(1024).decode('utf-8')
        msg_key_dict = json.loads(msg_key_dict_str)  #{'file_opt':file_opt,'file_name':file_name,'file_size':fiel_size}
        file_opt = msg_key_dict['file_opt']
        if file_opt == 'upload':
            file_name = msg_key_dict['file_name']
            file_path = os.path.join(r'D:\Learn\Python全棧開發\Day28\upload',file_name)
            file_size = msg_key_dict['file_size']
            sock.send('上傳通道創建成功...'.encode('utf-8'))
            with open(file_path,'wb') as f:
                while file_size:
                    file_conn = sock.recv(1024)
                    f.write(file_conn)
                    file_size-=len(file_conn)
            sock.send('{}上傳成功'.format(file_name).encode('utf-8'))
        elif  file_opt == 'download':
            pass
server
#小文件版
import socket
import os
import json

sk = socket.socket()
sk.connect(('127.0.0.1',8091))

options = {'1':'upload','2':'download'}

while True:
    for id,option in options.items():
        print(id,':',option)
    user_choice = input('請您選擇操做:')

    if user_choice == '1': #上傳
        file_opt = 'upload'
        file_path = input('請輸入要上傳的文件路徑:')
        file_name = os.path.basename(file_path)
        file_size = os.path.getsize(file_path)
        upload_dict = {'file_opt':file_opt,'file_name':file_name,'file_size':file_size}
        upload_dict_str = json.dumps(upload_dict)
        sk.send(upload_dict_str.encode('utf-8'))
        upload_connect_ret = sk.recv(1024).decode('utf-8')
        print(upload_connect_ret)
        with open(file_path, 'rb') as f:
            while file_size:
                file_conn = f.read(1024)
                sk.send(file_conn)
                file_size-=len(file_conn)
        upload_ret = sk.recv(1024).decode('utf-8')
        print(upload_ret)
    elif user_choice == '2': #下載
        pass
client

在網絡傳輸過程當中send是很快的,使用recv會下降效率.

解決方案進階struct

#struct防止黏包
import socket
import json
import os
import struct

sk = socket.socket()
sk.bind(('127.0.0.1',8091))
sk.listen(5)

while True:
    sock,addr = sk.accept()
    while True:
        msg_key_len_struct = sock.recv(4) #bytes直接解包
        msg_key_len = struct.unpack('i',msg_key_len_struct)[0]
        msg_key_dict_str = sock.recv(msg_key_len).decode('utf-8')
        msg_key_dict = json.loads(msg_key_dict_str)  #{'file_opt':file_opt,'file_name':file_name,'file_size':fiel_size}
        file_opt = msg_key_dict['file_opt']
        if file_opt == 'upload':
            file_name = msg_key_dict['file_name']
            file_path = os.path.join(r'D:\Learn\Python全棧開發\Day28\\upload',file_name)
            file_size = msg_key_dict['file_size']
            with open(file_path,'wb') as f:
                while file_size:
                    file_conn = sock.recv(1024)
                    f.write(file_conn)
                    file_size-=len(file_conn)
            sock.send('{}上傳成功'.format(file_name).encode('utf-8'))
        elif  file_opt == 'download':
            pass
server
#struct防止黏包
import socket
import os
import json
import struct

sk = socket.socket()
sk.connect(('127.0.0.1',8091))

options = {'1':'upload','2':'download'}

while True:
    for id,option in options.items():
        print(id,':',option)
    user_choice = input('請您選擇操做:')

    if user_choice == '1': #上傳
        file_opt = 'upload'
        file_path = input('請輸入要上傳的文件路徑:')
        file_name = os.path.basename(file_path)
        file_size = os.path.getsize(file_path)
        upload_dict = {'file_opt':file_opt,'file_name':file_name,'file_size':file_size}
        upload_dict_str = json.dumps(upload_dict)
        upload_dict_struct = struct.pack('i',len(upload_dict_str)) #bytes類型
        sk.send(upload_dict_struct)
        sk.send(upload_dict_str.encode('utf-8'))
        with open(file_path, 'rb') as f:
            while file_size:
                file_conn = f.read(1024)
                sk.send(file_conn)
                file_size-=len(file_conn)
        upload_ret = sk.recv(1024).decode('utf-8')
        print(upload_ret)
    elif user_choice == '2': #下載
        pass
client

 

剛剛的方法,問題在於咱們在發送字典後client經過進行一次接收進行下一次內容傳輸,防止黏包

咱們能夠藉助一個模塊,這個模塊能夠把要發送的數據長度轉換成固定長度的字節。這樣客戶端每次接收消息以前只要先接受這個固定長度字節的內容看一看接下來要接收的信息大小,那麼最終接受的數據只要達到這個值就中止,就能恰好很少很多的接收完整的數據了。

struct模塊

 該模塊能夠把一個類型,如數字,轉成固定長度的bytes

head_len_bytes = struct.pack('i',999999999) #數字最大爲九位數能夠轉換爲4個字節的bytes
x=struct.unpack('i',head_len_bytes)[0]   #提取報頭的長度, unpack後獲得是元組 (999999999,)

 

使用struct解決黏包 

藉助struct模塊,咱們知道長度數字能夠被轉換成一個標準大小的4字節數字。所以能夠利用這個特色來預先發送數據長度。

 

 

完整版 ftp上傳下載功能

#特別low的完整版本
import socket
import hashlib
import os
import json
import struct

def md5file(file_path):
    md5_obj = hashlib.md5()
    with open(file_path, 'rb') as f:
        for i in f:
            md5_obj.update(i)
    return md5_obj.hexdigest()

sk = socket.socket()
sk.bind(('127.0.0.1',8091))
sk.listen(5)

while True:
    conn,addr = sk.accept()
    while True:
        msg_len0 = conn.recv(4)
        msg_len1 = struct.unpack('i',msg_len0)[0]
        msg_key_str_r = conn.recv(msg_len1).decode('utf-8')
        msg_key_dict_r = json.loads(msg_key_str_r)
        func = msg_key_dict_r['opt']

        if func == 'upload':
            filename = msg_key_dict_r['filename']
            newfile = os.path.join(r'D:\Learn\Python全棧開發\Day28\\upload', filename)
            file_size = msg_key_dict_r['file_size']
            file_md5 = msg_key_dict_r['file_md5']
            with open(newfile, mode='wb') as f:
                while file_size:
                    msg_conn = conn.recv(1024)
                    file_size-= len(msg_conn)
                    f.write(msg_conn)

            if md5file(newfile) == file_md5:
                conn.send('{}上傳成功!'.format(newfile).encode('utf-8'))
                print('{}上傳成功!'.format(newfile))
            else:
                conn.send('{}上傳md5驗證失敗!'.format(newfile).encode('utf-8'))
                print('{}上傳md5驗證失敗!'.format(newfile))

        elif func == 'download':
            share_path = r'D:\Learn\Python全棧開發\Day28'
            share_dict = {}
            sharelist = {}
            id = 1
            for root, dirnames, filenames in os.walk(share_path):
                for sharefile in filenames:
                    abspath = os.path.join(root, sharefile)
                    share_dict[id] = [sharefile, abspath]
                    sharelist[id] = sharefile
                    id += 1
            sharelist_str = json.dumps(sharelist)
            conn.send(sharelist_str.encode('utf-8'))

            share_id = int(conn.recv(1024).decode('utf-8'))
            file_path = share_dict[share_id][1]

            file_name = os.path.basename(file_path)
            file_size = os.path.getsize(file_path)
            file_md5 = md5file(file_path)
            msg_key_s = {'opt': 'upload', 'filename': file_name, 'file_size': file_size, 'file_md5': file_md5}
            msg_key_str_s = json.dumps(msg_key_s)
            msg_size = struct.pack('i', len(msg_key_str_s))
            conn.send(msg_size)
            conn.send(msg_key_str_s.encode('utf-8'))
            with open(file_path, mode='rb') as f:
                while file_size:
                    file_conn = f.read(1024)
                    file_size -= len(file_conn)
                    conn.send(file_conn)
            print('{}被{}下載'.format(file_path,addr))
#特別low的完整版本 server
#特別low的版本
import socket
import os
import hashlib
import json
import struct

def md5file(file_path):
    md5_obj = hashlib.md5()
    with open(file_path, 'rb') as f:
        for i in f:
            md5_obj.update(i)
    return md5_obj.hexdigest()

sk = socket.socket()
sk.connect(('127.0.0.1',8091))

opt_dict = {'1':'upload','2':'download'}

while True:
    for k,v in opt_dict.items():
        print(k,":",v)
    user_choice = input('請輸入你的選擇:')

    if user_choice == '1':
        file_path = input('請輸入文件路徑:')
        file_name = os.path.basename(file_path)
        file_size = os.path.getsize(file_path)
        file_md5 = md5file(file_path)
        msg_key = {'opt':'upload','filename':file_name,'file_size':file_size,'file_md5':file_md5}
        msg_key_str = json.dumps(msg_key)
        msg_size = struct.pack('i',len(msg_key_str))
        sk.send(msg_size)
        sk.send(msg_key_str.encode('utf-8'))
        with open(file_path,mode='rb') as f:
            while file_size:
                conn = f.read(1024)
                file_size -= len(conn)
                sk.send(conn)
        ret = sk.recv(1024).decode('utf-8')
        print(ret)
    elif user_choice == '2':
        msg_key = {'opt':'download',}
        msg_key_str = json.dumps(msg_key)
        msg_size = struct.pack('i', len(msg_key_str))
        sk.send(msg_size)
        sk.send(msg_key_str.encode('utf-8'))
        downlist_str = sk.recv(1024).decode('utf-8')
        downlist_dict = json.loads(downlist_str)
        for downid,downname in downlist_dict.items():
            print(downid,':',downname)
        user_choice = input('請輸入要下載的文件id:')
        if user_choice in downlist_dict:
            sk.send(user_choice.encode('utf-8'))
            # 開始創建接收
            msg_len0 = sk.recv(4)
            msg_len1 = struct.unpack('i', msg_len0)[0]
            msg_key_str = sk.recv(msg_len1).decode('utf-8')
            msg_key_dict = json.loads(msg_key_str)
            print(msg_key_dict)
            filename = msg_key_dict['filename']
            newfile = os.path.join(r'D:\Learn\Python全棧開發\Day28\download', filename)
            file_size = msg_key_dict['file_size']
            file_md5 = msg_key_dict['file_md5']
            with open(newfile, mode='wb') as f:
                while file_size:
                    msg_conn = sk.recv(1024)
                    file_size-= len(msg_conn)
                    f.write(msg_conn)

            if md5file(newfile) == file_md5:
                print('{}下載成功!'.format(newfile))
            else:
                print('{}下載文件md5驗證失敗!'.format(newfile))
        else:
            print('輸入有誤,從新選擇功能')
    else:
        print('輸入有誤,從新選擇功能')
#特別low的版本 client

 

優化版一:

相關文章
相關標籤/搜索