【入門】ソケット通信

# はじめに
普段何気にWebサイトを見たりする際に、実際にどのような通信が行われているのか意識したことはない、、、ブラックボックスとして意識の外に追いやっている方が多いのではないかと思います、

ネットワークエンジニアは、プロトコルの通信の仕組みはある程度理解している。
しかし、実際に作ってみようとしても、「あれ、、そんなのできるのか、、」となりました。

そこで、ネットワークの基礎的な通信であるソケット通信についてまとめて、実際に再現してみました。

# この記事の対象者
- ネットワーク初心者で、通信の基礎を学習したい
- 自分でソケット通信を再現してみたい
- ネットワークエンジニアなりたての初学者

# 環境
- Visual Studio Code
- Python 3.9.7

環境が無い方は以下を参考にして、準備してください

https://docs.microsoft.com/ja-jp/training/modules/python-install-vscode/

# 通信情報の取得
socketをimportすることで、通信の情報が取得することができます。
```python:test.py
# coding: utf-8
import socket
import requests # クローバルIPを取得のために使用

# 情報の取得
def info():
    # ホスト名取得
    host = socket.gethostname()
    print('hostname: ' + host)

    # ローカルIPアドレスを取得
    pip = socket.gethostbyname(host)
    print('pip: ' + pip)  # 192.168.○○○.○○○

    # グローバルIPアドレスを取得
    gip = requests.get('https://ifconfig.me').text
    print('gip: ' + gip)  # グローバルIPアドレス

if __name__ == "__main__":
    info()
```
```terminal:実行例
> python .\test.py
hostname: DESKTOP-*******
pip: 192.168.1.2
gip: 11*.***.***.***
```

## ホスト名の取得
`socket.gethostname()`を使用することで、実行している環境のホスト名を取得することが可能です。

## ローカルIPアドレスを取得
`socket.gethostbyname(ホスト名)`

`socket.gethostbyname(socket.gethostname())`で、インターネットに出るためのルータに接続しているインターフェースのローカルIPアドレスを取得することができる。
wifiルータに接続されているローカルIPは基本的に`192.168.○○○.○○○`である

`socket.gethostbyname("localhost")`とすると、ループバックアドレスである`127.0.0.1`と表示されます。
※`127.0.0.1`のホスト名 ＝ localhost

## グローバルIPアドレスを取得 
`requests.get('URL').text`

グローバルIPアドレスは、socketではなくrequestsをimportします。
インターネットにアクセスする際に使用する、送信元のグローバルIPが取得できます。

つまり、wifiルータのグローバルIPアドレスです。


# サービス名の取得　
`socket.getservbyport(ポート番号, プロトコル)`
```python:test.py
# coding: utf-8
import socket

# ポート番号とプロトコル名からサービス名を取得
def service_name(port, protocol_name):
    print('Port:', port, '\nprotocolname:', protocol_name)
    print('Service name:', socket.getservbyport(port, protocol_name))
    print('--------------------')

if __name__ == "__main__":
    service_name(port=80, protocol_name='tcp')
    service_name(port=23, protocol_name='tcp')
    service_name(port=25, protocol_name='tcp')
```
```terminal:実行例
> python .\test.py
Port: 80     
protocolname: tcp   
Service name: http  
--------------------
Port: 23
protocolname: tcp   
Service name: telnet
--------------------
Port: 25
protocolname: tcp   
Service name: smtp  
--------------------
```

# Ping
```python:ping.py
# coding: utf-8
import subprocess

# ping試験
class Ping(object):
    def __init__(self, hosts):
        for host in hosts:
            # -n:回数, -w:タイムアウト時間
            commands = ["ping", "-n", "5", "-w", "300", host]

            proc = subprocess.run(
                commands,
                stdout=subprocess.PIPE,     # 標準出力は保存
                stderr=subprocess.DEVNULL   # 標準エラーは捨てる
            )
            result = proc.stdout.decode("cp932")  # 出力結果をdecode

            # 以下結果出力
            print("=" * 60)
            print(" ".join(commands))  # コマンドの表示
            print(f"return code : {proc.returncode}")  # 戻り値 成功:0 失敗:1
            print("=" * 60)
            print(result)

if __name__ == "__main__":
    # ping試験するホスト
    hosts = ['www.google.com', '192.168.1.2',
             'sgasgae.gdsag', '192.168.100.110']
    # ping試験
    Ping(hosts)
```
```terminal:実行例
> python .\ping.py
============================================================
ping -n 5 -w 300 www.google.com
return code : 0
============================================================

www.google.com [2404:6800:4004:822::2004]に ping を送信しています 32 バイトのデータ:
2404:6800:4004:822::2004 からの応答: 時間 =3ms
2404:6800:4004:822::2004 からの応答: 時間 =4ms
2404:6800:4004:822::2004 からの応答: 時間 =4ms
2404:6800:4004:822::2004 からの応答: 時間 =4ms
2404:6800:4004:822::2004 からの応答: 時間 =4ms

2404:6800:4004:822::2004 の ping 統計:
    パケット数: 送信 = 5、受信 = 5、損失 = 0 (0% の損失)、
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 3ms、最大 = 4ms、平均 = 3ms

============================================================
ping -n 5 -w 300 127.0.0.1
return code : 0
============================================================

127.0.0.1 に ping を送信しています 32 バイトのデータ:       
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128        
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128        
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128        
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128        
127.0.0.1 からの応答: バイト数 =32 時間 <1ms TTL=128        

127.0.0.1 の ping 統計:
    パケット数: 送信 = 5、受信 = 5、損失 = 0 (0% の損失)、  
ラウンド トリップの概算時間 (ミリ秒):
    最小 = 0ms、最大 = 0ms、平均 = 0ms

============================================================
ping -n 5 -w 300 sgasgae.gdsag
return code : 1
============================================================
ping 要求ではホスト sgasgae.gdsag が見つかりませんでした。ホスト名を確認してもう一度実行してください。

============================================================
ping -n 5 -w 300 192.168.100.110
return code : 1
============================================================

192.168.100.110 に ping を送信しています 32 バイトのデータ:
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。
要求がタイムアウトしました。

192.168.100.110 の ping 統計:
    パケット数: 送信 = 5、受信 = 0、損失 = 5 (100% の損失)、
```
# ソケット通信（TCP/IP）
本記事のメインであるソケット通信の実装です。


今回実装する仕様は以下である。

クライアントとサーバーの接続後、
　①クライアントからサーバーへのデータ送信
　②サーバーからクライアントへのデータ送信
①と②を繰り返して、qが押されたら終了するソケット通信を実装する。

```python:server.py
# coding: utf-8
import socket
import sys

def main():
    # 1.ソケット（ファイルディスクリプタ）作成（socket）
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 2.アドレスとソケットをバインド（bind）
    s.bind(("localhost", 10000))    # 指定したホスト(IP)とポートをソケットに設定

    # 3.ソケットに接続を待ち受けるように命令（listen）
    # 引数にキューの数を指定します。ここで指定した数だけ並列的にリクエストを処理することができます。
    s.listen(1)                     # 1つの接続要求を待つ

    # 4.接続の受け付けを行う（accept）
    soc, addr = s.accept()          # 要求が来るまでブロック
    print("Connected by" + str(addr))  # サーバ側の合図

    # 7.接続のクローズ＋ファイルディスクリプタ削除（close）
    def close(data):
        if data == "q":   # qが押されたら終了
            soc.close()
            sys.exit()

    while (1):  # qが押されるまで永遠に繰り返す
        # 5.データの受信（receive）
        # データを受信（1024バイトまで）（bytes型を文字列に変換）
        data = soc.recv(1024).decode('utf-8')
        print("Client>", data)       # サーバー側の書き込みを表示

        # 受信データがqだったらクローズする
        close(data)

        # 6.データの送信（send）
        data = input("Server>")  # 入力待機(サーバー側)
        soc.send(data.encode('utf-8'))  # ソケットにデータ(bytes型)を送信

        # データの送信後にqだったらクローズする
        close(data)

main()
```
```python:client.py
# coding: utf-8
import socket
import sys

def main():
    # 1.ソケット（ファイルディスクリプタ）作成（socket）
    soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 2.接続要求（connect）、リモートのソケットに接続する
    soc.connect(("localhost", 10000))

    # 5.接続のクローズ＋ファイルディスクリプタ削除（close）
    def close(data):
        if data == "q":   # qが押されたら終了
            soc.close()
            sys.exit()

    while(1):  # qが押されるまで永遠に繰り返す
        # 3.データの送信（send）
        data = input("Client>")  # 入力待機
        soc.send(data.encode('utf-8'))  # ソケットに入力したデータ(bytes型)を送信

        # データの送信後にqだったらクローズする
        close(data)

        # 4.データの受信（receive）
        data = soc.recv(1024).decode('utf-8')  # サーバからのデータ(bytes型)受信して文字列変換
        print("Server>", data)      # サーバー側の書き込みを表示

        # 受信データがqだったらクローズする
        close(data)

main()
```
```terminal:実行結果
=================================

> python .\server.py   
Connected by('127.0.0.1', 65466)
Client> Hello world!               (2.receive)
Server>Client Ni Okuruyo!          (3.send)
Client> q                          (6.receive + 7.close)

=================================

> python .\client.py
Client>Hello world!                (1.send)
Server> Client Ni Okuruyo!         (4.receive)
Client>q                           (5.send + 7.close)

=================================
```