banner
肥皂的小屋

肥皂的小屋

github
steam
bilibili
douban
tg_channel

Python-基於socket實現FTP基本功能

起因#

來源於學期結束的計算機網絡課程設計,原題目是基於 DELPHI 實現 FTP 協議的相關功能

既然可以隨意選擇語言,那我就使用世界上最好的Python了 (不接受反駁)#(臉紅)

本篇文章將根據參考文章復現一下基於socket通信的交流軟件

** 由於基於 socket 通信的部分不是太難,本篇將不做過多解釋 **

參考文章:

最簡單的 socket 通信#

server端代碼如下:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@fucntion: 簡單的socket通信(服務端)
@time: 2019-07-07
'''

import socket
host = ''
port = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 生成socket tcp通信實例,
s.bind((host, port))  # 綁定ip和端口,注意bind只接受一個參數,(host,port)做成一個元組傳進去
s.listen(5)  # 開始監聽,裡面的數字是代表服務端在拒絕新連接之前最多可以掛起多少連接,不過實驗過了沒啥用,所以寫個1就好了

while True:
    conn, addr = s.accept()  # 接受連接,並返回兩個變量,conn代表每個新連接進入後服務端都會為生成一個新實例,後面可以用這個實例進行發送和接收,addr是連接進來的客戶端的地址,accept()方法在有新連接進入時就會返回conn,addr這兩個變量,但如果沒有連接時,此方法就會阻塞直至有新連接過來。

    print('Connected by', addr)
    while True:
        data = conn.recv(1024)  # 接收1024字節數據
        if not data:
            break  # 如果收不到客戶端數據了(代表客戶端斷開了),就斷開
        print("收到消息:", data)
        conn.sendall(data.upper())  # 將收到的數據全變成大寫再發給客戶端
    conn.close()  # 關閉連接

client端:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@fucntion: 簡單的socket通信(客戶端)
@time: 2019-07-07
'''

import socket
host = '192.168.2.1'  # 遠程socket伺服器ip
port = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 實例化socket
s.connect((host, port))  # 連接socket伺服器

while True:
    msg = input("Please input:\n").strip().encode('ascii')
    s.sendall(msg)  # 向伺服器發送消息
    data = s.recv(1024)  # 接受伺服器的消息

    print("Recevied:", data)
s.close()

演示效果如下:

image

SocketServer 多線程版#

當我們同時啟動 2 個客戶端,發現只能有一個客戶端跟服務端不斷的通信,另一個客戶端會一直處在掛起狀態

當把可以通信的客戶端斷開後,第 2 個客戶端就可以跟服務端進行通信了。

為了讓服務端口可以同時為與多個客戶端進行通信,我們調用一個叫 SocketServer 的模塊

server端代碼:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@fucntion: 簡單的socket通信(服務端)
@time: 2019-07-07
'''

import socketserver


class MyTCPHandler(socketserver.BaseRequestHandler):
    # 繼承BaseRequestHandler基類,然後必須重寫handle方法,並且在handle方法裡實現與客戶端的所有交互
    def handle(self):
        while True:
            data = self.request.recv(1024)  # 接收1024字節數據
            if not data:
                break
            print("收到消息:", data)
            self.request.sendall(data.upper())


if __name__ == "__main__":
    host, port = "localhost", 50007

    # 把剛才寫的類當作一個參數傳給ThreadingTCPServer這個類,下面的代碼就創建了一個多線程socket server
    server = socketserver.ThreadingTCPServer((host, port), MyTCPHandler)

    # 啟動這個server,這個server會一直運行,除非按ctrl+c停止
    server.serve_forever()

client代碼:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@fucntion: 簡單的socket通信(客戶端)
@time: 2019-07-07
'''

import socket
host = 'localhost'  # 遠程socket伺服器ip
port = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # 實例化socket
s.connect((host, port))  # 連接socket伺服器

while True:
    msg = input("Please input:\n").strip().encode('ascii')
    s.sendall(msg)  # 向伺服器發送消息
    data = s.recv(1024)  # 接受伺服器的消息

    print("Recevied:", data)
s.close()

演示效果如下:

image

模擬實現 ftpserver#

server端代碼如下:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@fucntion: 簡單的socket通信實現ftp傳輸(服務端)
@time: 2019-07-07
'''

import socketserver
import os


class MYTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        instruction = self.request.recv(
            1024).strip().decode("ascii")  # 接收客戶端命令
        if not instruction:
            exit(0)
        # 將客戶端發過來消息拆分,消息類似這種格式""FileTransfer|get|file_name"
        instruction = instruction.split("|")
        if hasattr(self, instruction[0]):  # 判斷類中是否有這個方法
            func = getattr(self, instruction[0])  # 獲取這個方法的內存對象
            func(instruction)  # 調用此方法

    def FileTransfer(self, msg):  # 負責文件的發送和接收
        print("--filetransfer--", msg)
        if msg[1] == "get":
            print("client wants to download file:", msg[2])
            if os.path.isfile(msg[2]):  # 判斷客戶端發的文件名是否存在並是個文件
                file_size = os.path.getsize(msg[2])  # 獲取文件大小
                res = "ready|{}".format(file_size)  # 把文件大小告訴客戶端
            else:
                res = "file not exist"  # 文件也有可能不存在
            send_confirmation = "FileTransfer|get|{}".format(
                res).encode("ascii")
            self.request.send(send_confirmation)  # 發送確認消息給客戶端
            # 等待客戶端確認,如果這時不等客戶端確認就立刻給客戶端發文件內容,因為為了減少IO操作,socket發送和接收是有緩衝區的,緩衝區滿了才會發送,那上一條消息很有可能會和文件內容的一部分被合併成一條消息發送給客戶端,這就形成了粘包,所以這裡等待客戶端的一個確認消息,就把兩次發送分開了,不會再有粘包
            feedback = self.request.recv(100)
            if feedback == "FileTransfer|get|recv_ready":  # 如果客戶端說準備好接收了
                with open("{}".format(msg[2], 'rb')) as f:
                    size_left = file_size
                    while size_left > 0:
                        if size_left < 1024:
                            # 剩下的部分小於1024個字節就直接傳輸
                            self.request.sendall(f.read(size_left))
                            size_left = 0
                        else:
                            # 剩下的部分大於1024個字節就一次傳輸1024個字節
                            self.request.sendall(f.read(1024))
                            size_left -= 1024
                    print("--send file:{}done".format(msg[2]))


if __name__ == "__main__":
    host, port = "", 9002
    server = socketserver.ThreadingTCPServer((host, port), MYTCPHandler)
    server.serve_forever()

client端:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@fucntion: 簡單的socket通信實現ftp傳輸(客戶端)
@time: 2019-07-07
'''

import socket
import os


class FTPClient(object):
    def __init__(self, host, port):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.sock.connect((host, port))  # 連接伺服器

    def start(self):  # 實例化客戶端類後,需要調用此方法啟動客戶端
        self.interactive()

    def interactive(self):
        while True:
            user_input = input(">>.").strip()
            if len(user_input) == 0:
                continue
            user_input = user_input.split()  # 用戶輸入的指令進行拆分,第一個參數是指要進行什麼動作,比如get remote_filename
            if hasattr(self, user_input[0]):  # 判斷類中是否有get或其他輸入的方法
                func = getattr(self, user_input[0])  # 通過字符串獲取類中對應方法的內存對象
                func(user_input)  # 調用此內存對象
            else:
                print("Wrong cmd usage")

    def get(self, msg):  # 從伺服器端下載文件
        print("---get func---", msg)
        if len(msg) == 2:
            file_name = msg[1]
            instruction = "FileTransfer|get|{}".format(
                file_name).encode("ascii")  # 告訴伺服器端要下載什麼文件
            self.sock.send(instruction)
            feedback = self.sock.recv(100).decode("ascii")  # 等待伺服器端的消息確認
            print("-->", feedback)
            # 代表伺服器上文件存在,並且伺服器已經準備好了發送此文件到客戶端
            if feedback.startswith("FileTransfer|get|ready"):
                # 伺服器端發回來的確認消息中,最後面一個值是文件大小,必須知道文件大小才知道 一共要收多少內容
                file_size = int(feedback.split("|")[-1])
                self.sock.send("FileTransfer|get|recv_ready".encode(
                    "ascii"))  # 告訴伺服器端已經準備好了接收
                recv_size = 0  # 因為 文件可能比較大,一次收不完,所以要循環收,每收到一次,就計個數
                # 在本地創建一個新文件來存這個要下載的文件內容
                with open("client_recv/{}".format(os.path.basename(file_name)), 'wb') as f:
                    print("__>", file_name)
                    recv_size = 0
                    while recv_size != file_size:
                        if file_size - recv_size > 1024:
                            data = self.sock.recv(1024)
                        else:
                            data = self.sock.recv(file_size - recv_size)
                        recv_size += len(data)
                        f.write(data)
                    print("--recv file:{}".format(file_name))
            else:
                print(feedback)
        else:
            print("\033[31;1mWrong cmd usage\033[0m")


if __name__ == "__main__":
    f = FTPClient("localhost", 9002)
    f.start()

效果如下:

image

到這裡為止,socket的部分我們已經大概了解的差不多了,接下來就是設計好 GUI 並同時設計邏輯了

課設設計#

由於使用socketserver庫時需要使用server.serve_forever()語句來保持伺服器的運行

而在GUI的運行中,也需要使用sys_exit(app.exec_())語句來保持界面的運行

這兩條語句都類似於While True,是一直在內部循環的,所以先使用了哪個循環

就會一直在這個循環中運行,不能運行後面的代碼

直接導致的缺陷就是,伺服器端和GUI不能同時運行,所以最後我放棄了server端的GUI界面

PyQt5的教程在搭建本地 ip 代理池 (完結)這篇文章的 UI 設計部分已經介紹過

我們這裡來補充一些

先來一張UI設計圖:

image

如同我在搭建本地搭建代理池的文章中說的一樣,每一個部分都強烈建議用Frame框架包起來

當然,最上面登錄區為了對齊的時候方便,我使用了兩個Frame

分完區放置完部件,就把需要用到的按鈕修改常用名,然後按照代理池文章中介紹的UI與代碼邏輯分離操作就可以開始寫代碼了

下面介紹這次使用PyQt5中遇到的幾個小問題

進度條#

第一次接觸到進度條,覺得效果還可以,不過沒找到太多設置的方法,只有一個

setRange設置範圍和setValue,不過這兩個也足夠了,我的用法如下:

# 設置進度條的範圍
self.progressBar.setRange(0, 1)

# 在用的地方插入
self.progressBar.setValue(int(recv_size / file_size))

socket在本地傳輸文件超級快,完全看不出效果,後面效果演示的時候大家可以看下速度

Qt5Core.dll 缺失#

這次在打包的時候遇到的問題,全新的Python安裝環境

按順序pip installpipreqs,PyQt5,pyinstaller等庫之後

打包的時候非說Qt5Core.dll文件缺失,而且在系統文件中是搜不到的

於是在網上找到了個老哥提供的下載,安全性未知,反正我可以用

點我去下載,下載後放到系統環境變量有的位置即可,比如:

C:\Windows\System32
C:\
C:\Anaconda

即可。


更新,我在csdn下載的時候這個文件還不需要積分,現在需要了,我把我下載下來的傳一份

打包後的圖標不顯示的問題#

本來這個我在寫本地代理池的時候就遇到了,但是當時給忘了,這裡補上

1. 創建 qrc 文件,寫入以下內容 (注意代碼格式過了就會直接報錯):

<RCC>
  <qresource prefix="/">
    <file>favicon.ico</file>
  </qresource>
</RCC>

2. 生成 py 文件,這個 py 文件把圖片保存成二進制:

pyrcc5 -o image.py image.qrc

3. 導入模塊,設置圖標

import image
MainWindow.setWindowIcon(QtGui.QIcon(':/favicon.ico'))

即在原來的圖標名字前加上;/ 即可

加鹽的哈希庫#

為了能在老師驗收的時候加一個噱頭,client端加密時使用加密哈希加密傳輸

server端收到用戶名和哈希值後先判斷用戶名是否在數據庫中,然後使用解密算法

將傳輸的哈希與數據庫中的明文密碼作比較

參考文章:python 中使用加鹽哈希函數加密密碼

安裝:pip install werkzeug

client端加密使用generate_password_hash函數

generate_password_hash(password, method='pbkdf2:sha256', salt_length=8)

password 為明文密碼
method 哈希的方式,格式為 pbpdf2:<method> 主要有sha1,sha256,md5
salt_length 鹽值的長度,默認為8

一般後面兩個參數都可以缺省,直接使用就好

server端解密使用check_password_hash

check_password_hash(pwhash, password)

pwhash 為密文
password 為明文

相同則返回True,不同返回 False

最終效果演示#

1. 登錄註銷

image

2. 目錄操作

image

3. 下載

image

4. 上傳

image

5. 刪除

image

本文完。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。