UDP通信でパケットロスを判別する方法

# UDP通信でパケットロスを判別する方法

今回は，UDP通信でパケットロスを判別する方法について説明します．

そもそもUDPとは，User Data Protocolの略語であり，コネクションレス型の通信プロトコルです．

コネクションレス型であるがゆえに起こるメリットとデメリット両方あります．

・メリットは高速通信が可能になること
・デメリットはパケットロス(データの欠損)が起こること

しかし，クライアント(データを受け取る側)はパケットロスが起こったことすら分かりません．そのため，何が足りなかったんだろう？ってクライアントは分からずじまいになってしまいます．
そこで解決するのがシーケンス番号です．
シーケンス番号とは連続した番号でそのままの意味です．しかし，これが役に立ちます．
それを下記で説明します．
![シーケンス番号説明図](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3562110%2F2f437c33-de27-34c3-3259-c808f558b640.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=02db62c128c2205ecbdf2d5429a322d3)
クライアントはシーケンス番号1番のパケットを受信した後に，シーケンス番号3番のパケットを受信しました．この時，シーケンス番号2番のパケットが欠番していることがクライアントで分かります．このようにしてパケットロスを判別します．
ですが，UDPにはシーケンス番号の実装がされていません．

実装されていない？なら実装しましょう

出来上がったものが下記になります．

## 今回の実験環境について
今回，クライアントはESP32，サーバはESXiに仮想マシンを建てました．
クライアントはMicroPythonで実装し，サーバはPythonで実装しています．

## クライアント(ESP32)
~~~
import network
import usocket as socket
import uasyncio as asyncio
import json
import machine

# Global variables
sta_if = network.WLAN(network.STA_IF)
received_sequence_number = ""
last_seq = -1

def inet_aton(ip):
    parts = [int(part) for part in ip.split('.')]
    return bytes(parts)

def connect_wifi(ssid, password):
    global sta_if
    if not sta_if.isconnected():
        print('Connecting to WiFi...')
        sta_if.active(True)
        sta_if.connect(ssid, password)
        while not sta_if.isconnected():
            pass
        machine.Pin(2, machine.Pin.OUT).value(1)  # Assuming the blue LED is connected to GPIO pin 2

    print('Network configuration:', sta_if.ifconfig())
    return sta_if

async def receive_multicast(mcast_sock):
    global sta_if, received_sequence_number, last_seq

    while True:
        try:
            data, addr = mcast_sock.recvfrom(2048)
            if data:
                received_message = json.loads(data.decode('utf-8'))
                sequence_number = received_message["sequence_number"]
                print(f"received_sequence_number:{sequence_number}")
                if "sequence_number" in received_message:
                    received_sequence_number = received_message["sequence_number"]
                                                        
                    if last_seq != -1 and received_sequence_number != last_seq + 1:
                        for i in range(last_seq+1, received_sequence_number):
                            print(f"loss_sequence_number:{i}")

                    last_seq = received_sequence_number

        except Exception as e:
            print(f"Error receiving multicast message: {type(e).__name__}, {str(e)}")
            


async def main():
    ssid = "ssid"
    password = "password"
    MULTICAST_IP = "239.0.0.1"
    MULTI_PORT = 12345
    sta_if = connect_wifi(ssid, password)

    if sta_if.isconnected():
        mcast_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        mcast_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        mreq = inet_aton(MULTICAST_IP) + inet_aton('0.0.0.0')
        mcast_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
        mcast_sock.bind(('0.0.0.0', MULTI_PORT))

        print("Starting multicast reception...")
        await receive_multicast(mcast_sock)
    else:
        print("Failed to connect to Wi-Fi")

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except Exception as e:
        print(f"Error running main: {e}")
    machine.Pin(2, machine.Pin.OUT).value(0)
~~~

## サーバ(仮想マシン)
~~~
import time
import socket
import json
import logging

# Global variables
LOCALHOST = socket.gethostbyname(socket.gethostname())
MULTICAST_IP = "239.0.0.1"
MULTI_PORT = 12345
PACKET_SIZE = 115

## UDP socket creation
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sock.bind(('0.0.0.0', MULTI_PORT))  # Bind for multicast communication

## Logging configuration
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

## IP settings
hostname = socket.gethostname()
hostname, alias_list, ipaddr_list = socket.gethostbyname_ex(hostname)
logging.info(f"Host IP addresses: {ipaddr_list}")


def send_packets(chunks):
    for i, chunk in enumerate(chunks):
        chunk = chunk.ljust(PACKET_SIZE, '\0')
        msg = {
            "sequence_number": i,
            "datas": chunk
        }
        send_json_message = json.dumps(msg)
        try:
            udp_sock.sendto(send_json_message.encode(), (MULTICAST_IP, MULTI_PORT))
            if i % 100 == 0:
                logging.info(f"Sent packet {i}")
        except Exception as e:
            logging.error(f"Send error: {e}")
        time.sleep(0.1)


def main():
    chunks = []
    FILE_NAME = 'sendingFile_100KB.txt'
    with open(FILE_NAME, 'r') as f:
        for line in f:
            line = line.strip()
            chunks.append(line[:PACKET_SIZE])
    
    send_packets(chunks)

if __name__ == "__main__":
    main()
~~~

## 実行手順
client.pyをクライアントに，server.pyをサーバに記述します．
※client.pyのssidとpasswordは適切なものに設定してください
まず，クライアントを起動し，Wi-Fi接続をします．
その後，仮想マシン上でserver.pyを実行します．

## 実行結果
### クライアントの実行結果
![クライアントの実行結果](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3562110%2F113eefc5-fb6f-30a7-d653-ad408266b680.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=cd16d73426f53bd3ee6c8e2f8812b75c)

### サーバの実行結果
![サーバの実行結果](https://qiita-user-contents.imgix.net/https%3A%2F%2Fqiita-image-store.s3.ap-northeast-1.amazonaws.com%2F0%2F3562110%2Fa31b435d-da49-69ec-a6e4-44c0f306f57f.png?ixlib=rb-4.0.0&auto=format&gif-q=60&q=75&w=1400&fit=max&s=01790fc56b03de8c8daca485c9bbb2fb)

## 参考にしたサイト

https://qiita.com/FScom_Japan/items/3abec24a7d059785bfdd