DartでUDP通信の結果を同期的に待つ方法

こんにちは、ラビーの K@zuki. です。
自分を含めた誰かが少しだけ便利になるようなアプリを開発する中でFlutter
を使って開発していることが多いです。
今回はそういった中でもUDP通信の実装を書いている時のTipsです。

はじめに

UDPは通常、データの送受信は非同期で行うものですが、ユースケースによっては通信先からのレスポンスを待ちたいケースがあります。
この記事では、通信先からのレスポンスが同期的に待機し、後続処理に結果を返すための実装についての紹介です。

TL; DR

  • そもそもUDP通信を同期的に行うのは推奨しない

  • DartでUDP通信の結果を同期的に待つために FutureCompleter を組み合わせる

  • 今回のコードでは、RawDatagramSocketを使用してUDPソケットを作成し、メッセージを送信して受信イベントをリッスンする

  • 受信データを処理した後に Completer を使用することで、非同期処理の結果を同期的に待つことができる

前提

今回使う通信先のコードですが、以下のようなRubyのコードを通信先として実行しておきます。

require 'socket'

socket = UDPSocket.new
socket.bind('localhost', 5001)

loop do
  data, addr = socket.recvfrom(1024)
  puts "Received: #{data} from #{addr[2]}:#{addr[1]}"

  sleep 3 # 待機

  response = "Hello, #{data}!"
  socket.send(response, 0, addr[2], addr[1])
  puts "Sent: #{response} to #{addr[2]}:#{addr[1]}"
end

リクエストされたデータを元に Hello, #{data} という形式で返す簡単なサーバで、受信してから送信するまでに3秒間待機(sleep 3)させています。
今回は、このサーバと通信するDartのコードを書いていきます。

UDP通信の結果を同期的に待つ

今回のコードでは、UDP通信を行うために RawDatagramSocket を使用します。
RawDatagramSocketではUDP通信の送受信を非同期で簡単に実装することができますが、今回は同期的に結果を待ち、値を呼び出し元に渡せるようにrequest関数と呼び出し元のmain関数実装します。

以下がそのコードになります。

import 'dart:convert';
import 'dart:async';
import 'dart:io';

Future<String> request(InternetAddress address, int port, String name) async {
  final socket = await RawDatagramSocket.bind(InternetAddress.anyIPv4, 0);
  final completer = Completer<String>();

  try {
    final data = utf8.encode(name);
    socket.send(data, address, port);

    socket.listen((RawSocketEvent event) {
      if (event == RawSocketEvent.read) {
        final datagram = socket.receive();
        if (datagram != null) {
          final message = utf8.decode(datagram.data);
          socket.close();
          completer.complete(message);
        }
      }
    });
  } catch (e) {
    print('Error: $e');
    socket.close();
    completer.completeError(e);
  }

  return completer.future;
}

Future<void> main() async {
  try {
    final result = await request(InternetAddress.loopbackIPv4, 5001, 'Bob');
    print('Received: $result');
  } catch (e) {
    print('Error: $e');
  }
}

上記のコードでは、main関数からrequest関数を呼び出し、通信先からのレスポンスを元にmain関数で標準出力するようにしています。

さて、request関数で同期的に待機するための処理の流れについてですが、

  1. Completer<String>を作成
    非同期処理が完了したときに文字列を返すために使用し、今回の実装のミソです

  2. socket.listenメソッドを使用して、受信イベントをリッスン

  3. 受信したデータをデコードし、completer.completeメソッドに渡すことでデータをFutureの結果として設定

  4. 最後のcompleter.futureでFuture<String>オブジェクトを返す

という流れになります。
そして返ってきたFutureをmain関数側でawaitすれば、completer.completeで渡した文字列データが返ってくるという仕組みになっており、実質的にUDP通信の受信を同期的に行うことができると言える状態になりました。

まとめ

この記事では、DartでUDP通信の結果を同期的に待つ方法について紹介しました。
UDP関係なく他の非同期処理の実装でも書くことが多いので、うまく活用してみてください。

TL;DRにも書きましたが、UDP通信を同期的に待機すること自体は推奨しないので、同期処理を書きたい場合はそもそもTCP通信するなどの別の豊富を検討してください。

この記事が気に入ったらサポートをしてみませんか?