起因#
学期末のコンピュータネットワークのコース設計に由来し、元の題目は 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()
デモの効果は以下の通りです:
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()
デモの効果は以下の通りです:
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()
効果は以下の通りです:
ここまでで、socket
の部分は大体理解できました。次は GUI を設計し、同時にロジックを設計することです。
課題設計#
socketserver
ライブラリを使用する際、サーバーを動作させるためにserver.serve_forever()
文を使用する必要があります。
また、GUI
の実行中にもsys_exit(app.exec_())
文を使用してインターフェースを維持する必要があります。
これら 2 つの文はどちらもWhile True
に似ており、内部で常にループしているため、最初にどちらかのループを使用すると、そのループ内で実行され続け、後続のコードが実行されなくなります。
直接的な欠陥は、サーバー側とGUI
が同時に実行できないことです。したがって、最終的にサーバー側のGUI
インターフェースを放棄しました。
PyQt5
のチュートリアルは、ローカルIPプロキシプールの構築(完結)
という記事の UI 設計部分で紹介されています。
ここでいくつか補足します。
まず、UI
デザイン図を見てみましょう:
私がローカルプロキシプールを構築する記事で言ったように、各部分は強く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 install
でpipreqs,PyQt5,pyinstaller
などのライブラリをインストールした後、パッケージング時にQt5Core.dll
ファイルが欠如していると言われ、システムファイル内には見つかりませんでした。
そこで、ネットで見つけた古い兄弟が提供したダウンロードを見つけましたが、安全性は不明です。とにかく、私は使えました。
ここをクリックしてダウンロード、ダウンロード後、システム環境変数のある場所に配置します。例えば:
C:\Windows\System32
C:\
C:\Anaconda
で大丈夫です。
更新:csdn
でダウンロードした時、このファイルはポイントが必要ありませんでしたが、今は必要です。私がダウンロードしたものを一つアップロードします。
パッケージ後のアイコンが表示されない問題#
これは、ローカルプロキシプールを作成する際に遭遇した問題ですが、その時に忘れてしまったので、ここで補足します。
- qrc ファイルを作成し、以下の内容を書き込みます(コード形式に注意、過ぎると直接エラーになります):
<RCC>
<qresource prefix="/">
<file>favicon.ico</file>
</qresource>
</RCC>
- py ファイルを生成します。この py ファイルは画像をバイナリとして保存します:
pyrcc5 -o image.py image.qrc
- モジュールをインポートし、アイコンを設定します。
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を返します。
最終的な効果のデモ#
- ログインとログアウト
- ディレクトリ操作
- ダウンロード
- アップロード
- 削除
この記事はここまでです。