ろうとるがPythonを扱う、、（その10：ソケットによる通信）

# ネットワークソケット
ここでは、低レベルネットワークインターフェースであるソケットを使ったネットワーク通信を取り上げる。具体的には、ポート番号を指定した、UDPおよびTCP通信である。また、tkinterによりGUI化している。

## 参考URL
- [Pythonでネットワークプログラミング](https://memo.saitodev.com/home/python_network_programing/#id1)
- [How to Work with TCP Sockets in Python (with Select Example)](https://steelkiwi.com/blog/working-tcp-sockets/)
- [Pythonのソケットrecvメソッドでタイムアウトを設定するにはどうすればよいですか？](https://qastack.jp/programming/2719017/how-to-set-timeout-on-pythons-socket-recv-method)

他にも参照したサイトがあったが、記録が残っておらず、、。

## ソースコード
### import、タブ名、ボタン名、変数定義
```python:
from socket import socket, AF_INET, SOCK_DGRAM, SOCK_STREAM
import select
import threading
import time
import tkinter as tk
import tkinter.ttk as ttk

# List
tab_list = [' UdpClient ', ' UdpServer ', ' TcpClient ', ' TcpServer ']
btn1_list = [' Send ', ' Start ']   # Initial Button for Client and Server
btn2_list = [' Send ', ' Sending '] # For Client
btn3_list = [' Start ', ' Stop ']   # For Server

# Flag
Sending = [0, 0]   # UDP, TCP
Receiving = [0, 0] # UDP, TCP
```
最後の変数は、送信中か否か、受信中か否かのフラグである。

### 送信側処理
```python:
def ClientThread(frm, arg, ip, num, txt):
    global Sending
    #print('Start of Thread')
    frm.Btn['text'] = btn2_list[1]
    frm.Btn['state'] = 'disabled'
    if arg == 0:
        sock = socket(AF_INET, SOCK_DGRAM)
    else:
        sock = socket(AF_INET, SOCK_STREAM)
    txt = txt + '\n'
    try:
        if arg == 0:
            sock.sendto(txt.encode('utf-8'), (ip, int(num)))
        else:
            sock.settimeout(10) # ToDo: Timeout Value
            sock.connect((ip, int(num)))
            sock.settimeout(None)
            sock.send(txt.encode('utf-8'))
        # Receive from Server
        out = ''
        fds = set()
        fds.add(sock)
        r, w, x = select.select(list(fds), [], [], 10) # ToDo: Timeout Value
        #print('After Select')
        for s in r:
            if s is sock:
                if arg == 0:
                    msg, srvaddr = sock.recvfrom(64)
                else:
                    msg = sock.recv(64)
                    srvaddr = ip
                msg = msg.decode(encoding='utf-8')
                out = 'From: ' + str(srvaddr) + '     Replied Text: ' + msg
        if len(out) == 0:
            out = 'No data received (Timeout).' + '\n'
    except:
        out = 'Cannot connect to server.' + '\n'
    sock.close()
    Sending[int(arg/2)] = 0
    frm.text.config(state='normal')
    frm.text.insert(tk.END, out)
    frm.text.config(state='disabled')
    frm.text.see('end')
    frm.Btn['text'] = btn2_list[0]
    frm.Btn['state'] = 'normal'
    #print('End of Thread')
    return
```
後述する関数から呼び出されるスレッドである。
- ボタン表示を「Sending」に変更し、クリック不可状態にする。
- UDP or TCPに合わせてソケット作成（SOCK_DGRAM or SOCK_STREAM）。
- UDP or TCPに合わせて、メッセージ送信（sendto or connect+send）。TCP時、connectタイムアウトを10秒とする。
- サーバからのリプライ（データ受信）を10秒間待つ（select）。
- データ受信時、UDP or TCPに合わせて、受信（recvfrom or recv）データを表示用Bufferに入れる。IPアドレスも取得。
- ソケットクローズ。
- 表示用Bufferの内容をテキストボックスに書き出し。
- ボタン表示を「Send」に変更し、クリック可能な状態にする。

### 受信側処理
```python:
def ServThread(frm, arg, num, txt):
    global Receiving
    if arg == 1:
        sock = socket(AF_INET, SOCK_DGRAM)
    else:
        sock = socket(AF_INET, SOCK_STREAM)
    try:
        sock.bind(('0.0.0.0', int(num))) # for all I/Fs
    except:
        frm.text.config(state='normal')   # editable
        frm.text.insert(tk.END, 'Bind Error.' + '\n')
        frm.text.config(state='disabled') # not editable
        frm.text.see('end')
        Receiving[int((arg-1)/2)] = 0
        frm.Btn['text'] = btn3_list[Receiving[int((arg-1)/2)]]
        sock.close()
        return
    if arg == 3:
        sock.listen(1)
    fds = set()
    fds.add(sock)
    txt = txt + '\n'
    while 1:
        out = ''
        try:
            r, w, x = select.select(list(fds), [], [], 1)
            for s in r:
                if s is sock:
                    #print('Received')
                    if arg == 1:
                        msg, cliaddr = s.recvfrom(64)
                    else:
                        clisock, cliaddr = s.accept()
                        msg = clisock.recv(64)
                    msg = msg.decode(encoding='utf-8')
                    out = 'From: ' + str(cliaddr) + '     Received Text: ' + msg
                    # Reply to Client
                    time.sleep(0.5)
                    if arg == 1:
                        s.sendto(txt.encode('utf-8'), cliaddr)
                    else:
                        clisock.send(txt.encode('utf-8'))
                        clisock.close()
        except:
            out = 'Some Errors.' + '\n'
            Receiving[int((arg-1)/2)] = 0

        if len(out):
            frm.text.config(state='normal')   # editable
            frm.text.insert(tk.END, out)
            frm.text.config(state='disabled') # not editable
            frm.text.see('end')

        if Receiving[int((arg-1)/2)] == 0:
            break

    sock.close()
    return
```
こちらも、後述する関数から呼び出されるスレッドである。
- UDP or TCPに合わせてソケット作成（SOCK_DGRAM or SOCK_STREAM）。
- すべてのIPアドレス（”0.0.0.0”）に対してバインディング（bind）。NG時は、Stop状態にして、エラーメッセージを表示。
- TCP時はListen。
- １秒毎にデータ受信のチェック（select）。
- UDP or TCPに合わせて、クライアントからの受信（recvfrom or accept+recv）データを表示用Bufferに入れる。IPアドレスも取得。
- UDP or TCPに合わせて、メッセージをリプライ（sendto or send）。
- 表示用Bufferの内容をテキストボックスに書き出し。
- 「Stop」ボタンがクリックされていれば（Receiving[int((arg-1)/2)] == 0）、ループを抜け、ソケットをクローズして、スレッド終了。

### 送信側クリック時に呼び出される関数
```python:
def ExecSend(frm, arg, ip, num, txt):
    global Sending
    if Sending[int(arg/2)] == 0:
        Sending[int(arg/2)] = 1
        th = threading.Thread(target=ClientThread, args=(frm, arg, ip, num, txt), daemon=True)
        th.start()
    else:
        Sending[int(arg/2)] = 0
    return
```
- 送信中（リプライ待ち）の２度押し（クリック）を避けるため、実際の送信処理はスレッド化（前述の「ClientThread」）する。

### 受信側クリック時に呼び出される関数
```python:
def ExecRecv(frm, arg, num, txt):
    global Receiving
    if Receiving[int((arg-1)/2)] == 0:
        Receiving[int((arg-1)/2)] = 1
        th = threading.Thread(target=ServThread, args=(frm, arg, num, txt), daemon=True)
        th.start()
    else:
        Receiving[int((arg-1)/2)] = 0
    frm.Btn['text'] = btn3_list[Receiving[int((arg-1)/2)]]
    return
```
- 待ち受け中でもサーバーをStopできるよう、実際の待ち受け処理はスレッド化（前述の「ServThread」）する。
- 状態（フラグ）に合わせて、ボタンの表示を変える。（Stop/Start）

### ボタンクリック時の動作
```python:
def btn_clk(frm, arg):
    if arg%2 == 0:
        ip = frm.entryIP.get()
    num = frm.entryPort.get()
    txt = frm.entryText.get()
    if arg%2 == 0:
        ExecSend(frm, arg, ip, num, txt)
    else:
        ExecRecv(frm, arg, num, txt)
    return
```
エントリーボックスから、IPアドレス（送信側のみ）、ポート番号、テキストを取得し、前述した送信関数（ExecSend()）および受信側関数（ExecRecv()）を呼び出す。

### tkinter(GUI)
```python:
def create_content(frm, arg):
    frm1 = tk.Frame(frm)
    frm2 = tk.Frame(frm)
    frm1.pack()
    frm2.pack()

    if arg%2 == 0:
       label0 = ttk.Label(frm1, text=' IP address ')
    else:
       label0 = ttk.Label(frm1, text='')
    label0.grid(row=0, column=0)
    if arg%2 == 0:
       frm.entryIP = ttk.Entry(frm1, width=15)
       frm.entryIP.grid(row=0, column=1)
    else:
       label05 = ttk.Label(frm1, text='')
       label05.grid(row=0, column=1)
    label1 = ttk.Label(frm1, text=' Port ')
    label1.grid(row=0, column=2)
    frm.entryPort = ttk.Entry(frm1, width=7)
    frm.entryPort.grid(row=0, column=3)
    label2 = ttk.Label(frm1, text=' Text ')
    label2.grid(row=0, column=4)
    frm.entryText = ttk.Entry(frm1, width=20)
    frm.entryText.grid(row=0, column=5)
    frm.Btn = ttk.Button(frm1, text=btn1_list[arg%2], command=lambda:btn_clk(frm, arg))
    frm.Btn.grid(row=0, column=6)

    frm.text = tk.Text(frm2, width=80, height=40)
    ysc = tk.Scrollbar(frm2, orient=tk.VERTICAL, command=frm.text.yview)
    frm.text["yscrollcommand"] = ysc.set
    ysc.pack(side=tk.RIGHT, fill="y")
    frm.text.config(state='disabled')
    frm.text.pack()
    return
```
ここは、[その５](https://qiita.com/infinite1oop/items/f798699c7058314b308c)あたりを参照されたし。送信側処理と受信側処理とをできるだけ共通化できるよう、IPアドレス不要の受信側処理には、空テキストを利用している。

### メインプログラム
```python:
# Start of main program
# root main window
root = tk.Tk()
root.title("Send/Receive at UDP and TCP")
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 = tk.Frame(note)
tab1 = tk.Frame(note)
tab2 = tk.Frame(note)
tab3 = tk.Frame(note)

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

# Create content of tab
create_content(tab0, 0)
create_content(tab1, 1)
create_content(tab2, 2)
create_content(tab3, 3)

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

# main loop
root.mainloop()
```
こちらも、[その５](https://qiita.com/infinite1oop/items/f798699c7058314b308c)あたりを参照されたし。

### 全体
```python:ConnTest.py
from socket import socket, AF_INET, SOCK_DGRAM, SOCK_STREAM
import select
import threading
import time
import tkinter as tk
import tkinter.ttk as ttk

# List
tab_list = [' UdpClient ', ' UdpServer ', ' TcpClient ', ' TcpServer ']
btn1_list = [' Send ', ' Start ']   # Initial Button for Client and Server
btn2_list = [' Send ', ' Sending '] # For Client
btn3_list = [' Start ', ' Stop ']   # For Server

# Flag
Sending = [0, 0]   # UDP, TCP
Receiving = [0, 0] # UDP, TCP

def ClientThread(frm, arg, ip, num, txt):
    global Sending
    #print('Start of Thread')
    frm.Btn['text'] = btn2_list[1]
    frm.Btn['state'] = 'disabled'
    if arg == 0:
        sock = socket(AF_INET, SOCK_DGRAM)
    else:
        sock = socket(AF_INET, SOCK_STREAM)
    txt = txt + '\n'
    try:
        if arg == 0:
            sock.sendto(txt.encode('utf-8'), (ip, int(num)))
        else:
            sock.settimeout(10) # ToDo: Timeout Value
            sock.connect((ip, int(num)))
            sock.settimeout(None)
            sock.send(txt.encode('utf-8'))
        # Receive from Server
        out = ''
        fds = set()
        fds.add(sock)
        r, w, x = select.select(list(fds), [], [], 10) # ToDo: Timeout Value
        #print('After Select')
        for s in r:
            if s is sock:
                if arg == 0:
                    msg, srvaddr = sock.recvfrom(64)
                else:
                    msg = sock.recv(64)
                    srvaddr = ip
                msg = msg.decode(encoding='utf-8')
                out = 'From: ' + str(srvaddr) + '     Replied Text: ' + msg
        if len(out) == 0:
            out = 'No data received (Timeout).' + '\n'
    except:
        out = 'Cannot connect to server.' + '\n'
    sock.close()
    Sending[int(arg/2)] = 0
    frm.text.config(state='normal')
    frm.text.insert(tk.END, out)
    frm.text.config(state='disabled')
    frm.text.see('end')
    frm.Btn['text'] = btn2_list[0]
    frm.Btn['state'] = 'normal'
    #print('End of Thread')
    return

def ServThread(frm, arg, num, txt):
    global Receiving
    if arg == 1:
        sock = socket(AF_INET, SOCK_DGRAM)
    else:
        sock = socket(AF_INET, SOCK_STREAM)
    try:
        sock.bind(('0.0.0.0', int(num))) # for all I/Fs
    except:
        frm.text.config(state='normal')   # editable
        frm.text.insert(tk.END, 'Bind Error.' + '\n')
        frm.text.config(state='disabled') # not editable
        frm.text.see('end')
        Receiving[int((arg-1)/2)] = 0
        frm.Btn['text'] = btn3_list[Receiving[int((arg-1)/2)]]
        sock.close()
        return
    if arg == 3:
        sock.listen(1)
    fds = set()
    fds.add(sock)
    txt = txt + '\n'
    while 1:
        out = ''
        try:
            r, w, x = select.select(list(fds), [], [], 1)
            for s in r:
                if s is sock:
                    #print('Received')
                    if arg == 1:
                        msg, cliaddr = s.recvfrom(64)
                    else:
                        clisock, cliaddr = s.accept()
                        msg = clisock.recv(64)
                    msg = msg.decode(encoding='utf-8')
                    out = 'From: ' + str(cliaddr) + '     Received Text: ' + msg
                    # Reply to Client
                    time.sleep(0.5)
                    if arg == 1:
                        s.sendto(txt.encode('utf-8'), cliaddr)
                    else:
                        clisock.send(txt.encode('utf-8'))
                        clisock.close()
        except:
            out = 'Some Errors.' + '\n'
            Receiving[int((arg-1)/2)] = 0

        if len(out):
            frm.text.config(state='normal')   # editable
            frm.text.insert(tk.END, out)
            frm.text.config(state='disabled') # not editable
            frm.text.see('end')

        if Receiving[int((arg-1)/2)] == 0:
            break

    sock.close()
    return

def ExecSend(frm, arg, ip, num, txt):
    global Sending
    if Sending[int(arg/2)] == 0:
        Sending[int(arg/2)] = 1
        th = threading.Thread(target=ClientThread, args=(frm, arg, ip, num, txt), daemon=True)
        th.start()
    else:
        Sending[int(arg/2)] = 0
    return

def ExecRecv(frm, arg, num, txt):
    global Receiving
    if Receiving[int((arg-1)/2)] == 0:
        Receiving[int((arg-1)/2)] = 1
        th = threading.Thread(target=ServThread, args=(frm, arg, num, txt), daemon=True)
        th.start()
    else:
        Receiving[int((arg-1)/2)] = 0
    frm.Btn['text'] = btn3_list[Receiving[int((arg-1)/2)]]
    return

def btn_clk(frm, arg):
    if arg%2 == 0:
        ip = frm.entryIP.get()
    num = frm.entryPort.get()
    txt = frm.entryText.get()
    if arg%2 == 0:
        ExecSend(frm, arg, ip, num, txt)
    else:
        ExecRecv(frm, arg, num, txt)
    return

def create_content(frm, arg):
    frm1 = tk.Frame(frm)
    frm2 = tk.Frame(frm)
    frm1.pack()
    frm2.pack()

    if arg%2 == 0:
       label0 = ttk.Label(frm1, text=' IP address ')
    else:
       label0 = ttk.Label(frm1, text='')
    label0.grid(row=0, column=0)
    if arg%2 == 0:
       frm.entryIP = ttk.Entry(frm1, width=15)
       frm.entryIP.grid(row=0, column=1)
    else:
       label05 = ttk.Label(frm1, text='')
       label05.grid(row=0, column=1)
    label1 = ttk.Label(frm1, text=' Port ')
    label1.grid(row=0, column=2)
    frm.entryPort = ttk.Entry(frm1, width=7)
    frm.entryPort.grid(row=0, column=3)
    label2 = ttk.Label(frm1, text=' Text ')
    label2.grid(row=0, column=4)
    frm.entryText = ttk.Entry(frm1, width=20)
    frm.entryText.grid(row=0, column=5)
    frm.Btn = ttk.Button(frm1, text=btn1_list[arg%2], command=lambda:btn_clk(frm, arg))
    frm.Btn.grid(row=0, column=6)

    frm.text = tk.Text(frm2, width=80, height=40)
    ysc = tk.Scrollbar(frm2, orient=tk.VERTICAL, command=frm.text.yview)
    frm.text["yscrollcommand"] = ysc.set
    ysc.pack(side=tk.RIGHT, fill="y")
    frm.text.config(state='disabled')
    frm.text.pack()
    return

# Start of main program
# root main window
root = tk.Tk()
root.title("Send/Receive at UDP and TCP")
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 = tk.Frame(note)
tab1 = tk.Frame(note)
tab2 = tk.Frame(note)
tab3 = tk.Frame(note)

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

# Create content of tab
create_content(tab0, 0)
create_content(tab1, 1)
create_content(tab2, 2)
create_content(tab3, 3)

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

# main loop
root.mainloop()
```


## 検証状況
以降、上側のキャプチャが送信側（クライアント）、下側のキャプチャが受信側（サーバー）である。

### UDP
#### ループバック送受信
![UDPSendToLoopback.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/45c08363-708f-e211-1ef7-d41ecb0ac49e.png)

#### 指定IPアドレス送受信
![UDPSendtoMyIP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/9a3a4486-abd2-a504-d788-657a4dc2c390.png)
ループバック送受信と本結果から、複数のIPアドレスがバインディングされた状況にも対応できていることがわかる。

#### ポートクローズ
![UDPnotOpened.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/e33ccd96-35cb-58fe-36cd-af28b4e1fc06.png)
ポートがクローズしていると、サーバーにアクセス不可となる。

#### 送信中のボタン
![UDPSending.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/3b34d2ac-fa27-2f60-50c1-94ae1678d337.png)
送信中（受信待ち）は、ボタンが「Sending」となる。

#### タイムアウト
![UDPTimeout.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/021f5f5a-66ea-cf17-fefc-928958d3f40f.png)
タイムアウト発生時の状況。

### TCP
#### ループバック送受信
![TCPSendtoLoopback.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/92cbee36-0c9b-4058-a158-0fb1c5c85f4a.png)

#### 指定IPアドレス送受信
![TCPSendtoMyIP.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/255fda73-0746-d812-142f-08d8772d30b1.png)
ループバック送受信と本結果から、複数のIPアドレスがバインディングされた状況にも対応できていることがわかる。

#### ポートクローズ
![TCPNotOpend.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/634816/a439fac2-1488-f865-dcef-083c4a57cbb8.png)
ポートがクローズしていると、サーバーにアクセス不可。


EOF