banner
肥皂的小屋

肥皂的小屋

github
steam
bilibili
douban
tg_channel

Python - ソケットに基づくFTP基本機能の実装

起因#

学期末のコンピュータネットワークのコース設計に由来し、元の題目は DELPHI に基づく FTP プロトコルの関連機能の実装です。

言語を自由に選べるので、私は世界で最も優れたPythonを使用します(反論は受け付けません)#(赤面)

この記事では、参考文献に基づいてsocket通信に基づく交流ソフトウェアを再現します。

** ソケット通信に基づく部分はそれほど難しくないので、この記事ではあまり説明しません **

参考文献:

最も簡単なソケット通信#

server側のコードは以下の通りです:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@function: 簡単なソケット通信(サーバー側)
@time: 2019-07-07
'''

import socket
host = ''
port = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # ソケットTCP通信インスタンスを生成
s.bind((host, port))  # IPとポートをバインド、bindは1つの引数しか受け付けないので、(host,port)をタプルにして渡す
s.listen(5)  # リスニングを開始、内部の数字はサーバーが新しい接続を拒否する前に最大でどれだけの接続を待機できるかを示すが、実験した結果あまり意味がないので1にしておく

while True:
    conn, addr = s.accept()  # 接続を受け入れ、2つの変数を返す。connは新しい接続ごとにサーバーが生成する新しいインスタンスを表し、後でこのインスタンスを使用して送受信できる。addrは接続してきたクライアントのアドレスで、accept()メソッドは新しい接続が入るとconn,addrの2つの変数を返すが、接続がない場合はこのメソッドはブロックされ、新しい接続が来るまで待機する。

    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
@function: 簡単なソケット通信(クライアント側)
@time: 2019-07-07
'''

import socket
host = '192.168.2.1'  # リモートソケットサーバーのIP
port = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # ソケットをインスタンス化
s.connect((host, port))  # ソケットサーバーに接続

while True:
    msg = input("入力してください:\n").strip().encode('ascii')
    s.sendall(msg)  # サーバーにメッセージを送信
    data = s.recv(1024)  # サーバーからのメッセージを受信

    print("受信した:", data)
s.close()

デモの効果は以下の通りです:

image

SocketServer マルチスレッド版#

2 つのクライアントを同時に起動すると、1 つのクライアントだけがサーバーと継続的に通信でき、もう 1 つのクライアントは常に待機状態になります。

通信可能なクライアントを切断すると、2 番目のクライアントがサーバーと通信できるようになります。

サーバーが複数のクライアントと同時に通信できるようにするために、SocketServer というモジュールを呼び出します。

server側のコード:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@function: 簡単なソケット通信(サーバー側)
@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クラスに引数として渡すことで、以下のコードでマルチスレッドソケットサーバーを作成します
    server = socketserver.ThreadingTCPServer((host, port), MyTCPHandler)

    # このサーバーを起動します。このサーバーはctrl+cで停止するまでずっと動作します
    server.serve_forever()

client側のコード:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@function: 簡単なソケット通信(クライアント側)
@time: 2019-07-07
'''

import socket
host = 'localhost'  # リモートソケットサーバーのIP
port = 50007
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # ソケットをインスタンス化
s.connect((host, port))  # ソケットサーバーに接続

while True:
    msg = input("入力してください:\n").strip().encode('ascii')
    s.sendall(msg)  # サーバーにメッセージを送信
    data = s.recv(1024)  # サーバーからのメッセージを受信

    print("受信した:", data)
s.close()

デモの効果は以下の通りです:

image

FTP サーバーのシミュレーション実装#

server側のコードは以下の通りです:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@function: 簡単なソケット通信による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("クライアントがダウンロードしたいファイル:", msg[2])
            if os.path.isfile(msg[2]):  # クライアントが送ったファイル名が存在し、ファイルであるかを確認
                file_size = os.path.getsize(msg[2])  # ファイルサイズを取得
                res = "ready|{}".format(file_size)  # ファイルサイズをクライアントに知らせる
            else:
                res = "ファイルが存在しません"  # ファイルが存在しない可能性もある
            send_confirmation = "FileTransfer|get|{}".format(
                res).encode("ascii")
            self.request.send(send_confirmation)  # クライアントに確認メッセージを送信
            # クライアントの確認を待つ。ここでクライアントの確認を待たずにすぐにファイル内容を送信すると、IO操作を減らすために、ソケットの送受信にはバッファがあり、バッファが満杯になるまで送信されないため、前のメッセージとファイル内容の一部が1つのメッセージとしてクライアントに送信されることがあり、これが粘包を形成する。したがって、ここでクライアントの確認メッセージを待つことで、2回の送信を分け、粘包を防ぐ
            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("--ファイル送信完了:{}".format(msg[2]))


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

client側:

# -*- coding: utf-8 -*-
'''
@author: soapffz
@function: 簡単なソケット通信による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("コマンドの使用法が間違っています")

    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("--受信したファイル:{}".format(file_name))
            else:
                print(feedback)
        else:
            print("\033[31;1mコマンドの使用法が間違っています\033[0m")


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

効果は以下の通りです:

image

ここまでで、socketの部分は大体理解できました。次は GUI を設計し、同時にロジックを設計することです。

課題設計#

socketserverライブラリを使用する際、サーバーを動作させるためにserver.serve_forever()文を使用する必要があります。

また、GUIの実行中にもsys_exit(app.exec_())文を使用してインターフェースを維持する必要があります。

これら 2 つの文はどちらもWhile Trueに似ており、内部で常にループしているため、最初にどちらかのループを使用すると、そのループ内で実行され続け、後続のコードが実行されなくなります。

直接的な欠陥は、サーバー側とGUIが同時に実行できないことです。したがって、最終的にサーバー側のGUIインターフェースを放棄しました。

PyQt5のチュートリアルは、ローカルIPプロキシプールの構築(完結)という記事の UI 設計部分で紹介されています。

ここでいくつか補足します。

まず、UIデザイン図を見てみましょう:

image

私がローカルプロキシプールを構築する記事で言ったように、各部分は強くFrameフレームで包むことをお勧めします。

もちろん、一番上のログインエリアは整列の際に便利にするために、2 つのFrameを使用しました。

区分を分けて部品を配置したら、必要なボタンの名前を変更し、プロキシプールの記事で紹介したUIとコードロジックの分離操作に従ってコードを書き始めることができます。

以下は、今回PyQt5で遭遇したいくつかの小さな問題を紹介します。

プログレスバー#

プログレスバーに初めて触れ、効果は悪くないと思いましたが、設定方法があまり見つからず、1 つだけありました。

setRangeで範囲を設定し、setValueを使用しますが、これら 2 つで十分です。私の使用法は以下の通りです:

# プログレスバーの範囲を設定
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>
  1. py ファイルを生成します。この py ファイルは画像をバイナリとして保存します:
pyrcc5 -o image.py image.qrc
  1. モジュールをインポートし、アイコンを設定します。
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です

後の 2 つのパラメータは省略可能で、直接使用できます。

server側の復号化にはcheck_password_hashを使用します。

check_password_hash(pwhash, password)

pwhashは暗号文
passwordは平文

同じであればTrueを返し、異なればFalseを返します。

最終的な効果のデモ#

  1. ログインとログアウト

image

  1. ディレクトリ操作

image

  1. ダウンロード

image

  1. アップロード

image

  1. 削除

image

この記事はここまでです。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。