スポンサーリンク

【Java】DatagramSocketによるUDP通信

スポンサーリンク

DatagramSocket

java.net.DatagramSocket, DatagramPacketを利用することで簡単にUDP通信を行うことが可能です。まずは例から示します。

UdpSender

public class UdpSender {
    public void send(byte[] data, SocketAddress address){
        try (DatagramSocket socket = new DatagramSocket()) {
            DatagramPacket packet = new DatagramPacket(data, data.length, address);
            socket.send(packet);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

UdpReceiver

public class UdpReceiver {
    public void receive(int port){
        try (DatagramSocket socket = new DatagramSocket(port)) {
            byte[] buf = new byte[10];
            DatagramPacket packet = new DatagramPacket(buf, buf.length);
            socket.receive(packet);

            System.out.printf("packet %s (%d byte) was received from {%s} at %s %n", Arrays.toString(packet.getData()), packet.getLength(), packet.getSocketAddress(), port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Main

public class Main {
    public static void main(String... args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                UdpReceiver receiver = new UdpReceiver();
                receiver.receive(10005);
            }
        }).start();

        UdpSender sender = new UdpSender();
        sender.send("test".getBytes(StandardCharsets.UTF_8), new InetSocketAddress("localhost", 10005));
    }
}

UdpReceiverは10005 portで待機し、UdpSenderはlocalhostの10005 portにUdpPacketを送信します。実行すると以下のような結果が得られます。(ちなみにstartするタイミングがスレッド依存のためたまにsendが先に走って何も表示されません)

packet [116, 101, 115, 116, 0, 0, 0, 0, 0, 0] (4 byte) was received from {/127.0.0.1:59735} at 10005

UTF_8は半角英数字1文字1byteなので4byteとなります。

buf超過のデータを受信した場合

DatagramSocket#receive()に以下のJavadoc文面があります。ただ”truncated”と言っている意味がpacketを一切受け取らないのか、切り捨てなのか分からなかったので調査しました。

If the message is longer than the packet’s length, the message is truncated.

javadoc

結論としては、bufを超過したデータを受信した場合には、bufサイズ以下まではbufに格納され、buf超過のデータは破棄されます

public static void main(String... args) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            UdpReceiver receiver = new UdpReceiver();
            receiver.receive(10005);
        }
    }).start();

    UdpSender sender = new UdpSender();
    sender.send("012345678912".getBytes(StandardCharsets.UTF_8), new InetSocketAddress("localhost", 10005));
}

これを実行すると以下です。

packet [48, 49, 50, 51, 52, 53, 54, 55, 56, 57] (10 byte) was received from {/127.0.0.1:60523} at 10005

“1”, “2”が格納されていないことが分かります。dropしたかは判別できないため、最大サイズになっている場合は超過したとみなしてエラー処理すべきです。

Portの再利用性

TCP通信の場合にはCLOSEまでの待機時間があり、bindしたアドレスは一定時間TIME_WAIT状態となり直後に通常は再利用することはできません。そのため、ServerSocketをエラーから即座に再構成したい場合にはsetReuseAddress(true)として、明示的に再利用する旨をシステムに伝える必要があります。

UDPの場合はそのような必要はないようです。

public class Main {
    public static void main(String... args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    UdpReceiver receiver = new UdpReceiver();
                    receiver.receive(10005);
                }
            }
        }).start();

        while(true){
            UdpSender sender = new UdpSender();
            sender.send("012345678912".getBytes(StandardCharsets.UTF_8), new InetSocketAddress("localhost", 10005));

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

上記Mainの場合、毎回DatagramSocketを作成しなおし、setReuseAddress(true)は設定していませんが、毎回SocketException (Already Used)なしに10005ポートを再利用できています。

packet [48, 49, 50, 51, 52, 53, 54, 55, 56, 57] (10 byte) was received from {/127.0.0.1:65285} at 10005 
 packet [48, 49, 50, 51, 52, 53, 54, 55, 56, 57] (10 byte) was received from {/127.0.0.1:65286} at 10005 
 packet [48, 49, 50, 51, 52, 53, 54, 55, 56, 57] (10 byte) was received from {/127.0.0.1:65287} at 10005 
 packet [48, 49, 50, 51, 52, 53, 54, 55, 56, 57] (10 byte) was received from {/127.0.0.1:65288} at 10005 

Datagram#getData()

この関数はbufをそのまま返しますので、そのまま表示するとデータが入っていない不必要な部分まで取得してしまいます。そのため、Datagram#getLength()で得られる実データサイズをもとにtrimするのが普通です。

UdpReceiver

public class UdpReceiver {
    public void receive(int port){
        try (DatagramSocket socket = new DatagramSocket(new InetSocketAddress("localhost", port))) {
            byte[] buf = new byte[10];
            DatagramPacket packet = new DatagramPacket(buf, buf.length);
            socket.receive(packet);

            System.out.printf("packet %s (%d byte) was received from {%s} at %s %n", Arrays.toString(Arrays.copyOf(packet.getData(), packet.getLength())), packet.getLength(), packet.getSocketAddress(), port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

タイトルとURLをコピーしました