ろうとるがPythonを扱う、、（その16：ファイル転送）

# ソケットによるファイル転送
ソケットレベル（TCP）を用いて、ファイル転送を実現した。

## 画面
最初に結果の紹介。
### Client（ファイル送信側）
後述する「Start」ボタンをクリックすると、転送するファイルを選ぶダイアログボックスが表示される。
![ファイル選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/e9d7d53c-023d-686e-76c7-002000259850.png)
転送中には、ボタンが「Stop」に変わり（クリック可能）、その右に転送の進行状況（％表記）が表示される。
![送信中.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/aa9ca960-a182-23c2-3d10-e202c70c8f51.png)
結果は下記のようなもの。
![Client.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/9afad0f9-ccf9-65a5-24d5-cf250514f2a8.png)
転送時間（msとs）および転送速度（KbpsとMbps）を表示するだけではなく、エラーも表示する。また、転送時間と転送速度とは、CSV化（ファイル化）する。

### Server（ファイル受信側）
「Start」をクリックすると開始（下記は待受中のため「Stop」が表示）。
![Server.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/aaac50f9-fc59-c1c0-760b-0ea6d006f5e0.png)
Client側のIPアドレスや受信したファイルサイズの表示。

### Configuration
![設定画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/1de37f44-cb2e-0d3f-f670-f87d29aa723c.png)
TCP接続タイムアウト値やポート番号の設定である。

## ソースコード
いつものように、キーとなるところを中心に記載（主にコード内）。

### import
```python
### import ###
import os
import time
from datetime import datetime
import threading
from socket import socket, AF_INET, SOCK_STREAM, inet_aton
import select
import logging
import logging.handlers
from tkinter import *
import tkinter.ttk as ttk
import tkinter.filedialog as fd  # ファイル選択ダイアログボックス用
```

### パラメーター関連
```python
### List ###
tab_list = ['  Client  ', '  Server  ', '  Configuration  ']　　# タブリスト
btn_list = ['  Start  ', '  Stop  ']　　           　　　　　　　# ボタンリスト

### Flag ###
Doing = [0, 0]		# Client, Server   進行中かどうかのフラグ

### Timeout & Port Value (Configurable) ###　　タイムアウト値およびポート番号
default_connection_timeout = 10
default_connection_port = 60001
connection_timeout = default_connection_timeout
connection_port = default_connection_port

### Other Parameters ###
log_name = 'Result'
read_size = 4096	# read size from file  一度にファイルから読み込むサイズ
refresh_unit = 5	# refresh by value(%)　５%単位で進行状況の表示
recv_size = 4096	# read size from file　Serverにて一度に受信するサイズ
```
### メインプログラム
```python
##### Start of Main Program #####
# Root Main Window　　ウィンドウの設定
root = Tk()
root.title("File Transfer v0.92")
root.geometry("600x300")

# Create an Instance of ttk style  タブの色付けなど
fgcolor = "lightskyblue2"
bgcolor = "gray80"
style = ttk.Style()
style.theme_create("style1", parent="alt", settings={
        "TNotebook.Tab": {
            "configure": {"background": bgcolor },
            "map":       {"background": [("selected", fgcolor)],
                          } } } )
style.theme_use("style1")

# Create Notebook Widget
note = ttk.Notebook(root)

# Create Tab  フレーム作成
tab0 = Frame(note)	# Client
tab1 = Frame(note)	# Server
tab2 = Frame(note)	# Configuration

# Add Tab　　タブの追加
note.add(tab0, text=tab_list[0])
note.add(tab1, text=tab_list[1])
note.add(tab2, text=tab_list[2])

# Create Content of Tab   タブ及びフレームの作成
create_content(tab0, 0)
create_content(tab1, 1)
create_config(tab2)

# Locate Tab               配置
note.pack(expand=True, fill='both')

# Initial Log Setting    ログ（Clientのみ）の初期化
log = logging.getLogger(log_name)
log.setLevel(logging.DEBUG)
fmt = '{:-%Y%m%d-%H%M%S}.csv'.format(datetime.now())
log_file = log_name + fmt
log_first = True

# Main Loop
root.mainloop()
```

### Configuration画面関連
```python
### Save configufation parameters: save_btn_clk: ###
def save_btn_clk(frm):  # Configuration画面でSaveクリック時の処理
    try:
        connection_timeout = int(frm.connection_timeout.get())
    except:  # エラー時はデフォルト値を設定
        connection_timeout = default_connection_timeout
    try:
        connection_port = int(frm.connection_port.get())
    except:
        connection_port = default_connection_port
    frm.connection_timeout.delete(0, END)
    frm.connection_timeout.insert(0, connection_timeout)  # 入力値をセット
    frm.connection_port.delete(0, END)
    frm.connection_port.insert(0, connection_port)
    return

### Display configuration: create_config ###
def create_config(frm):　　# Configuration画面の作成
    label_Timeout = Label(frm, text='Connect Timeout (sec)')
    label_Timeout.place(x=10, y=10)
    frm.connection_timeout = Entry(frm, width=6)  # 接続タイムアウト用エントリ
    frm.connection_timeout.insert(0, default_connection_timeout)
    frm.connection_timeout.place(x=200, y=10)
    label_Port = Label(frm, text='Connection Port')
    label_Port.place(x=10, y=40)
    frm.connection_port = Entry(frm, width=6)   # ポート番号用エントリ
    frm.connection_port.insert(0, default_connection_port)
    frm.connection_port.place(x=200, y=40)
    frm.btn = Button(frm, text=" Save ", command=lambda:save_btn_clk(frm))  # Saveボタンおよびクリック時の関数定義
    frm.btn.place(x=200, y=70)
    return
```

### ClientおよびServer画面
```python
### Display Client & Server: create_content ###
def create_content(frm, arg):	# arg - 0:Client 1:Server
    frm1 = Frame(frm)  # 入力エントリおよびボタンがあるサブフレーム
    frm2 = Frame(frm)  # 結果表示テキストボックスがあるサブフレーム
    frm1.pack()
    frm2.pack()
    if arg == 0:  # Client画面にはIPアドレスエントリあり
       label_IP = Label(frm1, text=' IP address ')
       label_IP.grid(row=0, column=0)
       frm.entryIP = Entry(frm1, width=18)
       frm.entryIP.grid(row=0, column=1)
    frm.Btn = Button(frm1, text=btn_list[0], command=lambda:btn_clk(frm, arg))　　　# Start/Stopボタン（初期値はStart）およびクリック時の関数定義
    if arg == 0:   # Client画面には転送状況表示用フィールドあり
        frm.Btn.grid(row=0, column=2)
        frm.progress = Label(frm1, text = '')
        frm.progress.grid(row = 0, column = 3)
    else:
        frm.Btn.grid(row=0, column=0)
    frm.text = Text(frm2, width=80, height=40)  結果表示テキストボックス
    ysc = Scrollbar(frm2, orient=VERTICAL, command=frm.text.yview)
    frm.text["yscrollcommand"] = ysc.set
    ysc.pack(side=RIGHT, fill="y")
    frm.text.config(state='disabled')
    frm.text.pack()
    return
```

### ボタンクリック
```python
### Check IP Address Format: is_valid_ip ###
def is_valid_ip(addr):  # IPアドレスフォーマットチェック
    try:
        inet_aton(addr)
        return True
    except:
        return False

### Click button: btn_clk ###　　ボタンクリック時間数
def btn_clk(frm, arg):		# arg - 0:Client 1:Server
    if arg == 0:  # Client時にはIPアドレス取得
        ip = frm.entryIP.get()
        if is_valid_ip(ip) == False:
            ip = '127.0.0.1'	# 入力IPアドレスエラー時はLoopbackセット
    else:
        ip = '' 		# Dummy
    ExecThread(frm, arg, ip)  スレッド呼び出し関数
    return
```

### スレッド呼び出し
```python
### Execute Client&Server Thread: ExecThread ###
def ExecThread(frm, arg, ip):
    if Doing[arg%2] == 0: # フラグが非進行中
        Doing[arg%2] = 1  # フラグを進行中にセット
        if arg == 0:  # Clientスレッド呼び出し
            th = threading.Thread(target=ClientThread, args=(frm, ip), daemon=True)
        else:         # Serverスレッド呼び出し
            th = threading.Thread(target=ServerThread, args=(frm, ), daemon=True)
        th.start()
    else:                 # フラグが進行中
        Doing[arg%2] = 0  # フラグを非進行中にセット
    frm.Btn['text'] = btn_list[Doing[arg%2]]  # ボタンをフラグに合わせて変更
    return
```
### 結果表示
```python
### Show text: add_text ###
def add_text(frm, txt):
    frm.text.config(state='normal')   # テキストボックス編集可状態へ
    frm.text.insert(END, txt)         # テキストボックスの最後に結果を追記
    frm.text.config(state='disabled') # テキストボックス編集不可状態へ
    frm.text.see('end')
    return
```

### Serverスレッド
```python
### Server: ServerThread ###
def ServerThread(frm):
    try:
        sock = socket(AF_INET, SOCK_STREAM)     # TCPソケット
        sock.bind(('0.0.0.0', connection_port))	# for all I/Fs  すべてのNetwork I/Fが対象
    except:  # エラー時処理
        out = 'Socket/Bind Error.' + '\n'
        add_text(frm, out)
        Doing[1] = 0
        frm.Btn['text'] = btn_list[Doing[1]]
        sock.close()
        return
    sock.listen(1)  # リッスン
    fds = set()
    fds.add(sock)   # 待ち受けソケットの追加
    while 1:
        try:
            r, w, x = select.select(list(fds), [], [], 1) # 待受
            for s in r:
                if s is sock:           # ソケットに入力あり
                    clisock, cliaddr = s.accept()
                    out = 'Receiving from ' + str(cliaddr)
                    add_text(frm, out)  # テキストボックスに送信元IPアドレス情報表示
                    total = 0
                    while 1:
                        data = clisock.recv(recv_size)  # データ受信
                        # received data is thrown away
                        if len(data) == 0:
                            out = '\tReceived Size = ' + str(total) + '\n'
                            add_text(frm, out)  # 受信サイズの表示 
                            break
                        elif Doing[1] == 0:  # 受信中にStopクリック
                            out = '\tStop Clicked.' + '\n'
                            add_text(frm, out)
                            break
                        total += len(data)
        except:
            out = 'Some Errors.' + '\n'
            add_text(frm, out)
            Doing[1] = 0
        if Doing[1] == 0:  # Listen中にStopクリック
            break
    sock.close()
    frm.Btn['text'] = btn_list[Doing[1]]  # ボタンをStartに変更
    return
```

### CLientスレッド
```python
### Client: ClientThread ###
def ClientThread(frm, ip):
    global log_first
    if log_first:  # 初回時のみログファイル設定
        fh = logging.FileHandler(log_file)
        log.addHandler(fh)
        log_first = False
        out = 'Time(msec), Speed(Kbps), Time(sec), Speed(Mbps)'  # CSVファイルのタイトル
        log.debug(out)     # ログ化
        out += '\n'
        add_text(frm, out) # テキストボックスへの表示
    try:
        send_file = fd.askopenfilename(title = "Choose a file")  # 転送ファイルの選択
        fdr = open(send_file, 'rb')  # ファイルオープン
    except:  # エラー時処理
        out = 'File Open Error.' + '\n'
        add_text(frm, out)
        Doing[0] = 0
        frm.Btn['text'] = btn_list[Doing[0]]
        return
    frm.progress["text"] = '\t0'            # 転送状況を初期値(0)にセット
    file_size = os.path.getsize(send_file)  # ファイルサイズ取得
    sock = socket(AF_INET, SOCK_STREAM)     # TCPソケット
    try:        
        sock.settimeout(connection_timeout)  # タイムアウト値設定
        sock.connect((ip, connection_port))  # 接続
        total_size = 0
        previous = 0
        start_time = time.perf_counter_ns()  # 開始時間
        while 1:
            send_data = fdr.read(read_size)  # ファイルからデータ読み込み
            sent_length = len(send_data)
            if sent_length:
                sock.send(send_data)          # Serverへ送信
            else:  # ファイルからの読み込み終了
                end_time = time.perf_counter_ns()  # 終了時間
                elapsed_time = (end_time - start_time) / 1000000
                speed = file_size * 8 / elapsed_time
                out = str(elapsed_time) + ', ' + str(speed) + ', '\
                    + str(elapsed_time/1000)+ ', ' + str(speed/1000)  # 転送時間および転送速度
                log.debug(out)     # ログ化
                out += '\n'
                add_text(frm, out) # テキストボックスへの表示
                frm.progress["text"] = '\tDone'  # 進行状況(終了)
                break
            total_size += sent_length
            percent = total_size / file_size * 100
            about = round(percent)
            current = int(about/refresh_unit)
            if current - previous > 0:
                frm.progress["text"] = '\t' + str(about)  # 進行状況をrefresh_unit単位で表示
                previous = current
            if Doing[0] == 0:  # 転送中にStopクリック
                out = 'Stop clicked' + '\n'
                add_text(frm, out)
                break
    except:  # エラー時処理
        out = 'Cannot connect to server.' + '\n'
        add_text(frm, out)
    sock.close()
    fdr.close()
    Doing[0] = 0　　             　　　　　# フラグを落とす
    frm.Btn['text'] = btn_list[Doing[0]]  # ボタンをStartに変更
    return
```
### 全体
```python
### import ###
import os
import time
from datetime import datetime
import threading
from socket import socket, AF_INET, SOCK_STREAM, inet_aton
import select
import logging
import logging.handlers
from tkinter import *
import tkinter.ttk as ttk
import tkinter.filedialog as fd

### List ###
tab_list = ['  Client  ', '  Server  ', '  Configuration  ']
btn_list = ['  Start  ', '  Stop  ']

### Flag ###
Doing = [0, 0]		# Client, Server

### Timeout & Port Value (Configurable) ###
default_connection_timeout = 10
default_connection_port = 60001
connection_timeout = default_connection_timeout
connection_port = default_connection_port

### Other Parameters ###
log_name = 'Result'
read_size = 4096	# read size from file
refresh_unit = 5	# refresh by value(%)
recv_size = 4096	# read size from file

### Show text: add_text ###
def add_text(frm, txt):
    frm.text.config(state='normal')
    frm.text.insert(END, txt)
    frm.text.config(state='disabled')
    frm.text.see('end')
    return

### Client: ClientThread ###
def ClientThread(frm, ip):
    global log_first
    if log_first:
        fh = logging.FileHandler(log_file)
        log.addHandler(fh)
        log_first = False
        out = 'Time(msec), Speed(Kbps), Time(sec), Speed(Mbps)'
        log.debug(out)
        out += '\n'
        add_text(frm, out)
    try:
        send_file = fd.askopenfilename(title = "Choose a file")
        fdr = open(send_file, 'rb')
    except:
        out = 'File Open Error.' + '\n'
        add_text(frm, out)
        Doing[0] = 0
        frm.Btn['text'] = btn_list[Doing[0]]
        return
    frm.progress["text"] = '\t0'
    file_size = os.path.getsize(send_file)
    sock = socket(AF_INET, SOCK_STREAM)
    try:        
        sock.settimeout(connection_timeout)
        sock.connect((ip, connection_port))
        total_size = 0
        previous = 0
        start_time = time.perf_counter_ns()
        while 1:
            send_data = fdr.read(read_size)
            sent_length = len(send_data)
            if sent_length:
                sock.send(send_data)
            else:
                end_time = time.perf_counter_ns()
                elapsed_time = (end_time - start_time) / 1000000
                speed = file_size * 8 / elapsed_time
                out = str(elapsed_time) + ', ' + str(speed) + ', '\
                    + str(elapsed_time/1000)+ ', ' + str(speed/1000)
                log.debug(out)
                out += '\n'
                add_text(frm, out)
                frm.progress["text"] = '\tDone'
                break
            total_size += sent_length
            percent = total_size / file_size * 100
            about = round(percent)
            current = int(about/refresh_unit)
            if current - previous > 0:
                frm.progress["text"] = '\t' + str(about)
                previous = current
            if Doing[0] == 0:
                out = 'Stop clicked' + '\n'
                add_text(frm, out)
                break
    except:
        out = 'Cannot connect to server.' + '\n'
        add_text(frm, out)
    sock.close()
    fdr.close()
    Doing[0] = 0
    frm.Btn['text'] = btn_list[Doing[0]]
    return

### Server: ServerThread ###
def ServerThread(frm):
    try:
        sock = socket(AF_INET, SOCK_STREAM)
        sock.bind(('0.0.0.0', connection_port))	# for all I/Fs
    except:
        out = 'Socket/Bind Error.' + '\n'
        add_text(frm, out)
        Doing[1] = 0
        frm.Btn['text'] = btn_list[Doing[1]]
        sock.close()
        return
    sock.listen(1)
    fds = set()
    fds.add(sock)
    while 1:
        try:
            r, w, x = select.select(list(fds), [], [], 1)
            for s in r:
                if s is sock:
                    clisock, cliaddr = s.accept()
                    out = 'Receiving from ' + str(cliaddr)
                    add_text(frm, out)
                    total = 0
                    while 1:
                        data = clisock.recv(recv_size)
                        # received data is thrown away
                        if len(data) == 0:
                            out = '\tReceived Size = ' + str(total) + '\n'
                            add_text(frm, out)
                            break
                        elif Doing[1] == 0:
                            out = '\tStop Clicked.' + '\n'
                            add_text(frm, out)
                            break
                        total += len(data)
        except:
            out = 'Some Errors.' + '\n'
            add_text(frm, out)
            Doing[1] = 0
        if Doing[1] == 0:
            break
    sock.close()
    frm.Btn['text'] = btn_list[Doing[1]]
    return

### Execute Client&Server Thread: ExecThread ###
def ExecThread(frm, arg, ip):
    if Doing[arg%2] == 0:
        Doing[arg%2] = 1
        if arg == 0:
            th = threading.Thread(target=ClientThread, args=(frm, ip), daemon=True)
        else:
            th = threading.Thread(target=ServerThread, args=(frm, ), daemon=True)
        th.start()
    else:
        Doing[arg%2] = 0
    frm.Btn['text'] = btn_list[Doing[arg%2]]
    return

### Check IP Address Format: is_valid_ip ###
def is_valid_ip(addr):
    try:
        inet_aton(addr)
        return True
    except:
        return False

### Click button: btn_clk ###
def btn_clk(frm, arg):		# arg - 0:Client 1:Server
    if arg == 0:
        ip = frm.entryIP.get()
        if is_valid_ip(ip) == False:
            ip = '127.0.0.1'	# Loopback
    else:
        ip = '' 		# Dummy
    ExecThread(frm, arg, ip)
    return

### Display Client & Server: create_content ###
def create_content(frm, arg):	# arg - 0:Client 1:Server
    frm1 = Frame(frm)
    frm2 = Frame(frm)
    frm1.pack()
    frm2.pack()
    if arg == 0:
       label_IP = Label(frm1, text=' IP address ')
       label_IP.grid(row=0, column=0)
       frm.entryIP = Entry(frm1, width=18)
       frm.entryIP.grid(row=0, column=1)
    frm.Btn = Button(frm1, text=btn_list[0], command=lambda:btn_clk(frm, arg))
    if arg == 0:
        frm.Btn.grid(row=0, column=2)
        frm.progress = Label(frm1, text = '')
        frm.progress.grid(row = 0, column = 3)
    else:
        frm.Btn.grid(row=0, column=0)
    frm.text = Text(frm2, width=80, height=40)
    ysc = Scrollbar(frm2, orient=VERTICAL, command=frm.text.yview)
    frm.text["yscrollcommand"] = ysc.set
    ysc.pack(side=RIGHT, fill="y")
    frm.text.config(state='disabled')
    frm.text.pack()
    return

### Save configufation parameters: save_btn_clk: ###
def save_btn_clk(frm):
    try:
        connection_timeout = int(frm.connection_timeout.get())
    except:
        connection_timeout = default_connection_timeout
    try:
        connection_port = int(frm.connection_port.get())
    except:
        connection_port = default_connection_port
    frm.connection_timeout.delete(0, END)
    frm.connection_timeout.insert(0, connection_timeout)
    frm.connection_port.delete(0, END)
    frm.connection_port.insert(0, connection_port)
    return

### Display configuration: create_config ###
def create_config(frm):
    label_Timeout = Label(frm, text='Connect Timeout (sec)')
    label_Timeout.place(x=10, y=10)
    frm.connection_timeout = Entry(frm, width=6)
    frm.connection_timeout.insert(0, default_connection_timeout)
    frm.connection_timeout.place(x=200, y=10)
    label_Port = Label(frm, text='Connection Port')
    label_Port.place(x=10, y=40)
    frm.connection_port = Entry(frm, width=6)
    frm.connection_port.insert(0, default_connection_port)
    frm.connection_port.place(x=200, y=40)
    frm.btn = Button(frm, text=" Save ", command=lambda:save_btn_clk(frm))
    frm.btn.place(x=200, y=70)
    return

##### Start of Main Program #####
# Root Main Window
root = Tk()
root.title("File Transfer v0.92")
root.geometry("600x300")

# Create an Instance of ttk style
fgcolor = "lightskyblue2"
bgcolor = "gray80"
style = ttk.Style()
style.theme_create("style1", parent="alt", settings={
        "TNotebook.Tab": {
            "configure": {"background": bgcolor },
            "map":       {"background": [("selected", fgcolor)],
                          } } } )
style.theme_use("style1")

# Create Notebook Widget
note = ttk.Notebook(root)

# Create Tab
tab0 = Frame(note)	# Client
tab1 = Frame(note)	# Server
tab2 = Frame(note)	# Configuration

# Add Tab
note.add(tab0, text=tab_list[0])
note.add(tab1, text=tab_list[1])
note.add(tab2, text=tab_list[2])

# Create Content of Tab
create_content(tab0, 0)
create_content(tab1, 1)
create_config(tab2)

# Locate Tab
note.pack(expand=True, fill='both')

# Initial Log Setting
log = logging.getLogger(log_name)
log.setLevel(logging.DEBUG)
fmt = '{:-%Y%m%d-%H%M%S}.csv'.format(datetime.now())
log_file = log_name + fmt
log_first = True

# Main Loop
root.mainloop()
```

EOF