wifiルーターを用いたクローズドネットワークでのソケット通信

## 目的 
　本記事では`デバイス間`（PCとラズパイ）でのソケット通信を、`wifiルーター`を用いた`クローズドネットワーク内`で実装してみたいと思います。

　ゆくゆくはクローズドネットワーク内で複数のドローンと通信を確立して、制御を行いたいと思っています。

## 使用したハード・ソフト
- MacStudio　(2022)　←メイン
- MacBookPro(2016)　←サブ
- RaspberryPi 4B
- VisualStudioCode
- WiFiルーター（BUFFALO WSR-2533DHP）

## アジェンダ
- WiFiルーターのクローズドネットワーク設定
- ソケット通信の実装


## WiFiルーターのクローズドネットワーク設定
　今回はソケット通信を用いますが、ソケット通信というのはネットワークにおいて、ソケットという「`出入り口`」を作ることで、通信を行うものです。（超簡単な説明）
　
　これを世界中と繋がる`オープン`なネットワークで行うこともできますが、セキュリティの面や自分のやりたいことは手元のデバイスを接続できれば良いため、インターネットには接続していないネットワーク（`クローズドネットワーク`）を作成して使用していきたいと思います。
　当初私はルーターをデフォルトの状態で起動し、接続を試みましたが、設定されているIPアドレスが自動取得になっていたりすることでうまく接続が確立できない経験があるため以下の手順を踏む必要がありました。

https://www.buffalo.jp/support/faq/detail/15920.html

　上記のサイトにある方法でWiFiルーターをクローズ状態にできました。
行ったことは
- ルーター機能の使用
- マニュアル動作に変更
- IPアドレス、サブネットマスクの固定

になります。

## ソケット通信の実装
 ソケット通信の実装をするにあたって私は`Mac`と`Raspberry Pi`を使用します。
`Raspberry Pi`を使用するために私はMacから`SSH接続`によって`RaspberryPi`を扱いたいと思います。
SSH接続自体は`ターミナル`から簡単に行うことができますが、本開発では`VSCode`を用いたいと思います。

`Mac`から`SSH接続`をするために`Mac`、`RaspberryPi`の両方を先ほど設定したクローズドネットワークに接続します。

※Macをお使いの方で、ターミナルからのSSH接続はできるのにVSCodeで接続しようとした際にうまくいかない（XHRエラーなど）場合には以下の記事を参考にしてみてください。（私も以前陥ってしまい、参考にさせていただきました。）

https://hitori-sekai.com/tool/error-remotessh/

VSCodeでSSH接続を行なって開発を行う際に役立つのが「Remote SSH」という拡張機能です。

![スクリーンショット 2023-08-08 13.59.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3403549/f1d87839-5785-bc0c-f77a-84c5937d9bb7.png)

　私はこちらをインストールして利用しています。以下の記事でわかりやすい説明をされていたので、導入を行う際は参考にしてみてください。

https://makky12.hatenablog.com/entry/2022/09/20/120500

私は`新規SSH接続`を行う際は下の写真のように`リモートメニュー`からSSHの「`＋`」ボタンを選択し、

![スクリーンショット 2023-08-08 14.05.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3403549/fec8e320-882d-e512-b408-fe46c5e3fdb0.png)


`ssh接続`のコマンドを入力しましょう。
（ssh ユーザ名＠IPアドレス　　　　　　　　のようなものです。）

![スクリーンショット 2023-08-08 14.05.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3403549/532c18c4-8841-03e4-26d2-757a9b4f9431.png)

その後接続先の`OS`や`パスワード`の入力画面が出ることがありますが、提示された通りに入力します。

これにて左下の表示が接続先の名前に変更されれば接続が完了しています。

![スクリーンショット 2023-08-08 14.15.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3403549/a5ea03e7-4e25-6058-1d41-eed7db2c1e39.png)

　さて、やっと`ソケット通信`による通信を行うところまで来ました。
本来ならば`ソケット通信`とは？いつ使うの？などと言った解説を行うべきかとは思うのですが、他の知識を持った方が詳しく解説されている記事がたくさんあるのでそちらを参考にしてください…

https://envader.plus/article/27

↑こちらが個人的にはわかりやすかったです。

### （やっと）コードを書いていきましょう。

今回は`RaspberryPi`を使用して`ソケット通信`を行うため、（私の場合は）`python`にてプログラムを書いていきます。
仕様としては
- MacStudio　   （サーバ）
- RaspberryPi　（クライアント）

にて行います。

### サーバ側
　それではコードを書いていきましょう。
今回のシステムでは`ターミナル`からのプログラム起動時にオプションとして`IP`や`ポート`の指定ができるようになっていますが、本記事ではファイルを通常実行し、宣言されている`IP`と`ポート`に接続します。
それらの`設定`を行なっているのが以下の部分です。
~~~python3
import sys
import time

SERVER = 'localhost'
WAITING_PORT = 8765
LOOP_WAIT = 3
~~~
　この箇所ではインタプリタや実行環境を扱うために`sys`、待機時間を作るために`time`をインポートしています。

　また、パブリックな変数としてサーバとして使用するIPを格納する`SERVER`、受信を待機するポート番号を格納する`WAITING_PORT`、ループする際の待機時間を格納する`LOOP_WAIT`を宣言しています。
　
　今回はIPアドレスに`localhost`を使用していますが、有線で直結をしていない限りは`IP`を指定してあげた方が良いです。（できるならIPを調べることもやってみましょう。）


`　IPアドレス`の調べ方はいろいろありますが、`ターミナル等`で検索できると思いますので`windows`の方は`ipconfig`、`Mac`や`Linux`の方は`ifconfig`と入力して検索してあげましょう。（どこを見れば良いかわからない方は以下の記事を参考にしてみると良さそうです。）

https://www.pc-master.jp/trouble/ipconfig.html

https://qiita.com/yoriblog/items/6bd310cbe919eba9504b


　上記のように`CUI`から操作（というほどかっこいいものでもありませんが）する必要なく設定画面などで簡単に確認できますが、せっかくなのでコマンドも打ってみましょう〜

さて話を戻しまして、続きに参ります。

~~~python3
def server_test(server_v1=SERVER, waiting_port_v1=WAITING_PORT):
    import socket
    
    # socoket for waiting of the requests.
    # AF_INET     : IPv4
    # SOCK_STREAM : TCP
    socket_w = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    socket_w.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    node_s = server_v1
    port_s = waiting_port_v1
    socket_w.bind((node_s, port_s)) 

    BACKLOG = 5
    socket_w.listen(BACKLOG)

    print('Waiting for the connection from the client(s). '
        + 'node: ' + node_s + '  '
        + 'port: ' + str(port_s))

    try:
        while True:
            socket_s_r, client_address = socket_w.accept()
            print('Connection from ' 
                + str(client_address) 
                + " has been established.")

            data_r = socket_s_r.recv(1024)
            data_r_str = data_r.decode('utf-8')
            print('I (the server) have just received the data __'
                + data_r_str + '__ from the client. '
                + str(client_address))

            time.sleep(LOOP_WAIT)

            print("Now, closing the data socket.")
            socket_s_r.close()

    except KeyboardInterrupt:
        print("Ctrl-C is hit!")
        print("Now, closing the data socket.")
        socket_s_r.close()
        print("Now, closing the waiting socket.")
        socket_w.close()
~~~

　いきなり長いコードに見えますが、ここが`ソケット通信`を行なっている`関数`です。難しいことをいろいろ行なっているようですが、ソケット通信を学んだみなさんならわかるところが多いはずです。
- ソケットのインポート
- ソケットの設定
- クライアントからの受信待ち許可
- クライアントからの受信待ち開始
- クライアントから受信
- ソケット通信送信受信終了

やっていることはこれくらいですね。
ここまできたらあとはソケット通信関数を扱えるメインプログラムを書くだけです。

~~~python3
if __name__ == '__main__':
    print("Start if __name__ == '__main__'")

    sys_argc = len(sys.argv)
    count = 1
    hostname_v = SERVER
    waiting_port_v = WAITING_PORT

    while True:
            print(count, "/", sys_argc)
            if(count >= sys_argc):
                break

            option_key = sys.argv[count]
#            print(option_key)
            if ("-h" == option_key):
                count = count + 1
                hostname_v = sys.argv[count]
#                print(option_key, hostname_v)

            if ("-p" == option_key):
                count = count + 1
                waiting_port_v = int(sys.argv[count])
#               print(option_key, waiting_port_v)

            count = count + 1

    print(hostname_v)
    print(waiting_port_v)
    
    server_test(hostname_v, waiting_port_v)
~~~

　上の方で少し言及していましたが、コマンドラインからプログラムを起動した際に付け加えられるオプション項目の部分を取得するための部分が大半になっていますね。（Whileのあたり）

　実際にソケット通信の関数を呼び出しているのは一番下の`server_test()`の部分だけです。なのでコマンドラインからの処理オプションをオミットすれば
~~~python3
if __name__ == '__main__':
    print("Start if __name__ == '__main__'")

    sys_argc = len(sys.argv)
    count = 1
    hostname_v = SERVER
    waiting_port_v = WAITING_PORT

    print(hostname_v)
    print(waiting_port_v)
    
    server_test(hostname_v, waiting_port_v)
~~~

たったこれだけってことになります。

サーバ側のコードは以上になります。（以下サーバコード）

~~~python3

import sys
import time

SERVER = 'localhost'
WAITING_PORT = 8765

LOOP_WAIT = 3

def server_test(server_v1=SERVER, waiting_port_v1=WAITING_PORT):
    import socket
    
    # socoket for waiting of the requests.
    # AF_INET     : IPv4
    # SOCK_STREAM : TCP
    socket_w = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    socket_w.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

    node_s = server_v1
    port_s = waiting_port_v1
    socket_w.bind((node_s, port_s)) 

    BACKLOG = 5
    socket_w.listen(BACKLOG)

    print('Waiting for the connection from the client(s). '
        + 'node: ' + node_s + '  '
        + 'port: ' + str(port_s))

    try:
        while True:
            socket_s_r, client_address = socket_w.accept()
            print('Connection from ' 
                + str(client_address) 
                + " has been established.")

            data_r = socket_s_r.recv(1024)
            data_r_str = data_r.decode('utf-8')
            print('I (the server) have just received the data __'
                + data_r_str + '__ from the client. '
                + str(client_address))

            time.sleep(LOOP_WAIT)

            print("Now, closing the data socket.")
            socket_s_r.close()

    except KeyboardInterrupt:
        print("Ctrl-C is hit!")
        print("Now, closing the data socket.")
        socket_s_r.close()
        print("Now, closing the waiting socket.")
        socket_w.close()


~~~

### クライアント側
　こちらのコードで行いたいことはサーバ側よりもシンプルです。
クライアント側では通信先の`サーバIP`と`ポート番号`を指定します。また、今回のプログラムでは送信内容もグローバルで宣言してしまいます。
~~~python3
import sys

SERVER = 'localhost'
WAITING_PORT = 8765
MESSAGE_FROM_CLIENT = "Hello, I am a client."
~~~
サーバ側のコードを乗り越えた方々なら容易に突破できたことでしょう。

　続いてはこちらもソケット通信の関数を作成していきます。
~~~python3 
def socket_client_single(hostname_v1 = SERVER, waiting_port_v1 = WAITING_PORT, message1 = MESSAGE_FROM_CLIENT):
    import socket
    import time

    node_s = hostname_v1
    port_s = waiting_port_v1

    # socoket for receiving and sending data
    # AF_INET     : IPv4
    # SOCK_STREAM : TCP
    socket_r_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket_r_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    print("node_s:", node_s,  " port_s:", str(port_s))
    socket_r_s.connect((node_s, port_s))
    print('Connecting to the server. '
        + 'node: ' + node_s + '  '
        + 'port: ' + str(port_s))

    data_s_str = message1
    #data_s = bytes(data_s_str, encoding = 'utf-8')
    data_s = data_s_str.encode('utf-8')
    socket_r_s.send(data_s)
    print('I (a client) have just sent data __' 
        + data_s_str 
        + '__ to the server ' + node_s + ' .')

    socket_r_s.close()

~~~
　こちらでもサーバ側同様に
- ソケット通信の設定
- サーバへの接続リクエスト
- データの送信
- ソケット通信の終了

というプロセスを踏んでいます。

　そして最後にこちらもソケット通信関数を扱うメインプログラムです。

~~~python3
if __name__ == '__main__':
    print("Start if __name__ == '__main__'")
    
    sys_argc = len(sys.argv)
    count = 1
    hostname_v = SERVER
    waiting_port_v = WAITING_PORT
    message_v = MESSAGE_FROM_CLIENT

    while True:
        #print(count, "/", sys_argc)
        if(count >= sys_argc):
            break

        option_key = sys.argv[count]
        #print(option_key)
        if ("-h" == option_key):
            count = count + 1
            hostname_v = sys.argv[count]
            #print(option_key, hostname_v)
        if ("-p" == option_key):
            count = count + 1
            waiting_port_v = int(sys.argv[count])
            #print(option_key, port_v)
        if ("-m" == option_key):
            count = count + 1
            message_v = sys.argv[count]
            #print(option_key, message_v)

        count = count + 1

    print(hostname_v)
    print(waiting_port_v)
    print(message_v)

    socket_client_single(hostname_v, waiting_port_v, message_v)
~~~

こちらのプログラムもコマンドラインからのオプションをオミットすれば

~~~python3
if __name__ == '__main__':
    print("Start if __name__ == '__main__'")
    
    sys_argc = len(sys.argv)
    count = 1
    hostname_v = SERVER
    waiting_port_v = WAITING_PORT
    message_v = MESSAGE_FROM_CLIENT

  

    print(hostname_v)
    print(waiting_port_v)
    print(message_v)

    socket_client_single(hostname_v, waiting_port_v, message_v)
~~~

こんなもんですね。（以下クライアント側全コード）

~~~python3

import sys

SERVER = 'localhost'
WAITING_PORT = 8765
MESSAGE_FROM_CLIENT = "Hello, I am a client."

def socket_client_single(hostname_v1 = SERVER, waiting_port_v1 = WAITING_PORT, message1 = MESSAGE_FROM_CLIENT):
    import socket
    import time

    node_s = hostname_v1
    port_s = waiting_port_v1

    # socoket for receiving and sending data
    # AF_INET     : IPv4
    # SOCK_STREAM : TCP
    socket_r_s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    socket_r_s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    print("node_s:", node_s,  " port_s:", str(port_s))
    socket_r_s.connect((node_s, port_s))
    print('Connecting to the server. '
        + 'node: ' + node_s + '  '
        + 'port: ' + str(port_s))

    data_s_str = message1
    #data_s = bytes(data_s_str, encoding = 'utf-8')
    data_s = data_s_str.encode('utf-8')
    socket_r_s.send(data_s)
    print('I (a client) have just sent data __' 
        + data_s_str 
        + '__ to the server ' + node_s + ' .')

    socket_r_s.close()


if __name__ == '__main__':
    print("Start if __name__ == '__main__'")
    
    sys_argc = len(sys.argv)
    count = 1
    hostname_v = SERVER
    waiting_port_v = WAITING_PORT
    message_v = MESSAGE_FROM_CLIENT

    while True:
        #print(count, "/", sys_argc)
        if(count >= sys_argc):
            break

        option_key = sys.argv[count]
        #print(option_key)
        if ("-h" == option_key):
            count = count + 1
            hostname_v = sys.argv[count]
            #print(option_key, hostname_v)
        if ("-p" == option_key):
            count = count + 1
            waiting_port_v = int(sys.argv[count])
            #print(option_key, port_v)
        if ("-m" == option_key):
            count = count + 1
            message_v = sys.argv[count]
            #print(option_key, message_v)

        count = count + 1

    print(hostname_v)
    print(waiting_port_v)
    print(message_v)

    socket_client_single(hostname_v, waiting_port_v, message_v)
    
~~~



## ソケット通信をしてみる
　それではプログラムを起動してみましょう。

お分かりかとは思いますが、`サーバ側`を起動してから`クライアント側`を起動しましょう。

![スクリーンショット 2023-08-08 23.31.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3403549/8e7066b2-a597-86e9-f9be-6304990cda5d.png)

（いやぁ隠すところが多すぎる…（だったら表示させなければいいのにとは思います。））



と、いうことで計3回クライアントからのアクセスおよびデータの受信ができたことがわかりますね。
これにてクローズドネットワーク内でのソケット通信は完了になります。
お疲れ様でした。