1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

UDPのマルチキャストをDockerコンテナ・ネットワーク上で動かしてみた

Last updated at Posted at 2020-12-01

※半年ほど前に書いた記事をQiitaに持ってきました

JavaのMulticastSocketを使って、UDPのマルチキャストを送受信するクライアントを作り、Dockerコンテナ・ネットワーク上で動かしてみようと思います。

今回書いたコードはこちらのリポジトリに置いてあります。

UDPのマルチキャストとは

コネクションの確立をせず、パケットを一方的に送りつけることが特徴なUDPですが、以下のような送信方式があります。

  • ユニキャスト
  • ブロードキャスト
  • マルチキャスト

ユニキャストは、特定の一台のホストに対して送信します。ブロードキャストは、あるネットワークに属する全てのホストに対して送信します。マルチキャストは、あるネットワークに属する特定のホスト(一台でも複数台でもよい)に対して送信します。

マルチキャストにおいて、送信側はマルチキャストアドレスを宛先アドレスとして指定してデータを送信します。マルチキャストアドレスはIPアドレスクラスのうち、クラスDのIPアドレスです。受信側はこのマルチキャストアドレスにJoinすることでデータを受け取ることができるようになります。

また、ユニキャストを複数台に行うことに対して、マルチキャストを行うことのメリットは以下のようになります。

  • 宛先のIPアドレスを知らなくても、マルチキャストアドレスだけ分かっていれば、Joinしているホスト全員に対して送信できる
  • 一つのパケットで複数台に向けて送信できるので通信効率が良く、送信元の負荷が低い

Dockerコンテナ・ネットワークとは

Dockerコンテナ・ネットワークとは、Dockerコンテナが属するネットワークのことです。docker network lsコマンドを実行すると一覧が表示されます。今回はマルチキャストを検証するために、同一ネットワーク内にコンテナを立てなければなりません。しかし、実は難しいことをする必要はなく、docker-composeを使えば自動でネットワークを作成してくれるます。なので、今回はdocker-composeを使って複数のクライアントを動かそうと思います。

簡単なチャットアプリを作ってみる

それではUDPのマルチキャストを送受信するクライアントとして、今回は簡単なチャットアプリを作ってみます。一つのクライアントで送信と受信の両方を行うようにしてみます。

ソースコード

MulticastClient.javaというファイルに以下を記述します。

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.Scanner;

class Sender extends Thread {
 private final int toPort;
 private final String mcastAddrName;

 public Sender(int toPort, String mcastAddress) {
   this.toPort = toPort;
   this.mcastAddrName = mcastAddress;
 }

 public void run() {
   Scanner scan = null;
   MulticastSocket socket = null;
   try {
     InetAddress mcastAddress = InetAddress.getByName(mcastAddrName);
     scan = new Scanner(System.in);
     socket = new MulticastSocket();

     String message;
     while ( (message = scan.nextLine()).length() > 0 ) {
       byte[] bytes = message.getBytes();
       DatagramPacket packet = new DatagramPacket(bytes, bytes.length, mcastAddress, toPort);
       socket.send(packet);
     }
   } catch (IOException e) {
     e.printStackTrace();
   } finally {
     if (scan != null) scan.close();
     if (socket != null) socket.close();
   }
 }
}

class Receiver extends Thread {
 private final int fromPort;
 private final String mcastAddrName;
 private static final int PACKET_SIZE = 1024;

 public Receiver(int fromPort, String mcastAddrName) {
   this.fromPort = fromPort;
   this.mcastAddrName = mcastAddrName;
 }

 public void run() {
   MulticastSocket socket = null;
   byte[] buf = new byte[PACKET_SIZE];
   DatagramPacket packet = new DatagramPacket(buf, buf.length);

   try {
     socket = new MulticastSocket(fromPort);
     InetAddress mcastAddress = InetAddress.getByName(mcastAddrName);
     socket.joinGroup(mcastAddress);

     while (true) {
       socket.receive(packet);
       String message = new String(buf, 0, packet.getLength());
       System.out.println(packet.getSocketAddress() + " : " + message);
     }
   } catch (IOException e) {
     e.printStackTrace();
   } finally {
     if (socket != null) {
       socket.close();
     }
   }
 }
}

public class MulticastClient {
 public static final int PORT = 3000;
 public static final String MCAST_ADDR = "224.0.1.1";

 public static void main(String args[]) {
   Receiver receiver = new Receiver(PORT,  MCAST_ADDR);
   Sender sender = new Sender(PORT,  MCAST_ADDR);
   receiver.start();
   sender.start();
 }
}

ソースコードの説明

上記のコードには以下の3つのクラスが含まれています。

  • Senderクラス
  • Receiverクラス
  • MulticastClientクラス

それぞれについて説明します。

Senderクラス

Senderクラスは、データを送信するためのクラスです。受信と並列して動かすためにThreadクラスを継承して、runメソッドをオーバーライドしています。

フィールド

 private final int toPort;
 private final String mcastAddrName;

toPortは送信先のポート番号(受信側が受信するポート番号)です。mcastAddrNameは送信先のマルチキャストアドレスの名前です。

コンストラクタ


 public Sender(int toPort, String mcastAddress) {
   this.toPort = toPort;
   this.mcastAddrName = mcastAddress;
 }

変数を初期化します。

runメソッド

 public void run() {
   Scanner scan = null;
   MulticastSocket socket = null;
   try {
     InetAddress mcastAddress = InetAddress.getByName(mcastAddrName);
     scan = new Scanner(System.in);
     socket = new MulticastSocket();

     String message;
     while ( (message = scan.nextLine()).length() > 0 ) {
       byte[] bytes = message.getBytes();
       DatagramPacket packet = new DatagramPacket(bytes, bytes.length, mcastAddress, toPort);
       socket.send(packet);
     }
   } catch (IOException e) {
     e.printStackTrace();
   } finally {
     if (scan != null) scan.close();
     if (socket != null) socket.close();
   }
 }

whileループ内だけ説明します。message = scan.nextLine()では標準入力から一行読み取って、String messageに入れます。byte[] bytes = message.getBytes()では、messageをバイト配列に変換します。DatagramPacket packet = new DatagramPacket(bytes, bytes.length, mcastAddress, toPort)では、上記のバイト配列、宛先のマルチキャストアドレス、宛先のポート番号を指定し、DatagramPacketを生成します。socket.send(packet)でパケットを送信します。

Receiverクラス

Receiverクラスは、データを受信するためのクラスです。送信と並列で動かすためにThreadクラスを継承して、runメソッドをオーバーライドしています。

フィールド

 private final int fromPort;
 private final String mcastAddrName;
 private static final int PACKET_SIZE = 1024;

fromPortはパケットを受け取るポート番号です。mcastAddrNameはJoinするマルチキャストアドレスです。PACKET_SIZEは受け取るパケットのサイズです。

コンストラクタ

 public Receiver(int fromPort, String mcastAddrName) {
   this.fromPort = fromPort;
   this.mcastAddrName = mcastAddrName;
 }

変数を初期化します。

runメソッド

 public void run() {
   MulticastSocket socket = null;
   byte[] buf = new byte[PACKET_SIZE];
   DatagramPacket packet = new DatagramPacket(buf, buf.length);

   try {
     socket = new MulticastSocket(fromPort);
     InetAddress mcastAddress = InetAddress.getByName(mcastAddrName);
     socket.joinGroup(mcastAddress);

     while (true) {
       socket.receive(packet);
       String message = new String(buf, 0, packet.getLength());
       System.out.println(packet.getSocketAddress() + " : " + message);
     }
   } catch (IOException e) {
     e.printStackTrace();
   } finally {
     if (socket != null) {
       socket.close();
     }
   }
 }

whileループの中だけ説明します。socket.receive(packet)でパケットの受信を待機します。パケットを受信したらString message = new String(buf, 0, packet.getLength())によりバイト配列をStringに変換します。そしてSystem.out.println(packet.getSocketAddress() + " : " + message)により、標準出力に出力します。

MulticastClientクラス

MulticastClientクラスでは、mainメソッドを定義しています。

フィールド

 public static final int PORT = 3000;
 public static final String MCAST_ADDR = "224.0.1.1";

SenderクラスとReceiverクラスに渡すポート番号とマルチキャストアドレスを定義しています。

mainメソッド

 public static void main(String args[]) {
   Receiver receiver = new Receiver(PORT,  MCAST_ADDR);
   Sender sender = new Sender(PORT,  MCAST_ADDR);
   receiver.start();
   sender.start();
 }

ReceiverクラスとSenderクラスをインスタンス化し、Threadをstartさせています。

検証環境を準備する

上記のチャットアプリを動かす環境をDockerで構築します。同一ネットワーク内に複数のクライアントを立ち上げるためにdocker-composeファイルを定義します。

Dockerfile

Dockerfileは以下のようになります。

FROM openjdk:14
COPY . /usr/src/myapp
WORKDIR /usr/src/myapp
RUN javac MulticastClient.java
CMD ["/bin/bash"]

Imageはopenjdk:14を使います。javac MulticastClient.javaでコンパイルした後、/bin/bashでbashを開きます。実行するときはdocker container exec -it [CONTAINER ID] bashで中に入ってから、java MulticastClientを叩きます。

本当はCMD ["/bin/bash"]のところをCMD ["java", "MulticastClient"]として実行しておき、containerの中に入りたかったのですが、やり方がわかりませんでした。

docker-compose

docker-compose.ymlは以下のようになります。


version: '3'
services:
 client1:
   build:
     context: .
     dockerfile: Dockerfile
   tty: true
 client2:
   build:
     context: .
     dockerfile: Dockerfile
   tty: true

先ほどのDockerfileからImageをBuildします。tty: trueを記述しておくとコンテナが起動したままになります。

client1と同じように、client2, client3...とコンテナを作成します。これらのコンテナはdocker-compose upを実行すると自動で作成されるnetworkに属することになります。プライベートIPアドレスは自動で割り振られます。

検証してみる

それではクライアントを複数立ち上げてマルチキャストが動くか検証してみます。

コンテナを起動させるために以下のコマンドを実行します。

docker-compose up --build

コンテナに入るには以下のコマンドを実行します。CONTAINER IDはdocker container lsで調べることができます。

docker container exec -it [CONTAINER ID] bash

アプリケーションを実行するにはコンテナ内で以下のコマンドを実行します。

java MulticastClient

3つコンテナを作って、それぞれクライアントを実行してみたのが以下の画像です。ターミナルを横に3つ並べています。

20200609202314.png

左のターミナルでHello!と入力すると、他のターミナルにもメッセージが表示されました!左のターミナルで実行しているコンテナのプライベートIPアドレスは172.29.0.3だということも分かりますね!

20200609202323.png

真ん中のターミナルでHello!Hello!と入力しても他のターミナルに表示されます。真ん中は172.29.0.2ですね。

20200609203052.png

右のターミナルも同じく動きます。

20200609203100.png

感想

今回はJavaのMulticastSocketを使って、UDPのマルチキャストを送受信するチャットクライアントを作り、Dockerコンテナ・ネットワーク上で動かしてみました。UDPについて調べる過程でかなり勉強できたので良かったです。また、Dockerの動かし方についても学べたので一石二鳥でした!

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?