AndroidでUDPを使ってJavaオブジェクトを送受信する

Javaにはオブジェクトをバイト配列へ、またはバイト配列からオブジェクトへと変換する機能があります。この機能はシリアライズ・直列化などと呼ばれています。参考：[Wikipedia](https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%AA%E3%82%A2%E3%83%A9%E3%82%A4%E3%82%BA#Java.E3.81.AB.E3.81.8A.E3.81.91.E3.82.8B.E3.82.B7.E3.83.AA.E3.82.A2.E3.83.A9.E3.82.A4.E3.82.BA.EF.BC.88.E7.9B.B4.E5.88.97.E5.8C.96.EF.BC.89), [今まで知らなかった 5 つの事項: Java オブジェクトをシリアライズする場合](https://www.ibm.com/developerworks/jp/java/library/j-5things1/)

この機能を使用して、UDPによるソケット通信でアプリケーション間でオブジェクトデータがやりとりできたらいいなと思い、やってみました。タイトルには「Androidで」と書きましたが、Androidに依存した方法ではないのでJavaアプリケーションなら同じように使えます。

```java:UDPObjectTransfer.java
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class UDPObjectTransfer {

  /**
   * オブジェクトを送信する。
   * 
   * @param obj     オブジェクト
   * @param address 宛先アドレス。192.168.1.255のようにネットワークアドレスを指定するとブロードキャスト送信。
   * @param port    宛先ポート。受信側と揃える。
   * @throws IOException
   */
  public static void send(Object obj, String address, int port) throws IOException {
    try (DatagramSocket clientSocket = new DatagramSocket()) {
      InetAddress IPAddress = InetAddress.getByName(address);
      byte[] sendData = convertToBytes(obj);
      DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
      clientSocket.send(sendPacket);
    }
  }

  /**
   * 指定ポートでUDPソケットを開いてオブジェクトの受信を待機する。
   * 
   * @param port       受信待機ポート。送信側と揃える。
   * @param bufferSize 受信時のバッファーサイズ。小さすぎると受信に失敗するので、適当に大きめな値(1024～8192くらい？)を指定する。
   * @return オブジェクト
   * @throws IOException
   * @throws ClassNotFoundException
   */
  public static Object receive(int port, int bufferSize)
      throws IOException, ClassNotFoundException {
    try (DatagramSocket clientSocket = new DatagramSocket(port)) {
      return receive(clientSocket, bufferSize);
    }
  }

  /**
   * 指定UDPソケットを使ってオブジェクトの受信を待機する。
   * 
   * @param clientSocket 事前作成したUDPソケット
   * @param bufferSize   受信時のバッファーサイズ。小さすぎると受信に失敗するので、適当に大きめな値(1024～8192くらい？)を指定する。
   * @return
   * @throws IOException
   * @throws ClassNotFoundException
   */
  public static Object receive(DatagramSocket clientSocket, int bufferSize)
      throws IOException, ClassNotFoundException {
    byte[] buffer = new byte[bufferSize];
    DatagramPacket packet = new DatagramPacket(buffer, bufferSize);
    clientSocket.receive(packet);
    return convertFromBytes(buffer, 0, packet.getLength());
  }

  /**
   * オブジェクトをバイト配列に変換する。
   * 
   * @param  object Serializableを実装していなければいけない。
   * @return バイト配列
   * @throws IOException シリアライズに失敗した時に発生する
   */
  private static byte[] convertToBytes(Object object) throws IOException {
    try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutput out = new ObjectOutputStream(bos)) {
      out.writeObject(object);
      return bos.toByteArray();
    }
  }

  /**
   * バイト配列をオブジェクトに変換する。
   * 
   * @param bytes  バイト配列
   * @param offset 読み込み開始位置
   * @param length 読み込むデータの長さ
   * @return 復元されたオブジェクト
   * @throws IOException デシリアライズに失敗した時に発生する
   * @throws ClassNotFoundException デシリアライズに失敗した時に発生する
   */
  private static Object convertFromBytes(byte[] bytes, int offset, int length)
      throws IOException, ClassNotFoundException {
    try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes, offset, length);
        ObjectInput in = new ObjectInputStream(bis)) {
      return in.readObject();
    }
  }
}
```

## 使い方

### 送信する

```java
Object obj = Arrays.asList("Hello", "World", 123, 4567.89, true); // 適当なデータを用意
String address = "192.168.1.10"; // 受信側端末の実際のアドレスに書き換える
int port = 12345;                // 受信側と揃える
UDPObjectTransfer.send(obj, address, port); 
```

### 受信する

```java
int port = 12345;      // 送信側と揃える
int bufferSize = 1024; // 適当なサイズで
Object obj = UDPObjectTransfer.receive(port, bufferSize); // 受信するまで待機
System.out.println(obj); // 受信結果を表示
```

受信側の端末で先に処理を実行しておき、送信側の端末で送信処理を行います。正しく通信ができれば、受信側の端末の標準出力に受信結果が表示されます。

UDPではブロードキャスト送信を行うことができるため、一台のコントローラーアプリから複数のアプリを制御するというような処理を簡単に実装できます。ブロードキャスト送信を行う場合は、送信先アドレスのところにネットワークアドレス(`192.168.1.255`等)を指定します。

## 注意

UDP通信のしかたを覚えることを第一目標としたため、この実装はあまり効率的ではありません。たくさんのデータをやりとりしたい場合は、`InetAddress`や`DatagramSocket`や`byte[]`などのインスタンスはなるべく最初に一度だけ作って使い回すようにした方が良いと思います。

UDP通信では送信側と受信側でネゴシエーションしたり正常に送信・受信できたかどうかをチェックしないため、宛先アドレスが間違っていたりしてもエラーになりません。上手く動かない時にどこがいけないのかを調べるのが大変かもしれません。

Javaアプリケーション間での通信ならこの方法が簡単ですが、AndroidとiOSなどクロスプラットフォームで通信したい場合はシリアライズする部分をJavaのシリアライズ機能ではなくJSON等の方法にした方が良いでしょう。

Androidではマルチキャストパケットを受信するために[WifiManager.createMulticastLock](https://developer.android.com/reference/android/net/wifi/WifiManager.html#createMulticastLock(java.lang.String))を使ってロックを作成し、[WifiManager.MulticastLock.acquire()](https://developer.android.com/reference/android/net/wifi/WifiManager.MulticastLock.html#acquire())を呼ぶ必要があります。