※半年ほど前に書いた記事を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つ並べています。
左のターミナルでHello!と入力すると、他のターミナルにもメッセージが表示されました!左のターミナルで実行しているコンテナのプライベートIPアドレスは172.29.0.3
だということも分かりますね!
真ん中のターミナルでHello!Hello!と入力しても他のターミナルに表示されます。真ん中は172.29.0.2
ですね。
右のターミナルも同じく動きます。
感想
今回はJavaのMulticastSocketを使って、UDPのマルチキャストを送受信するチャットクライアントを作り、Dockerコンテナ・ネットワーク上で動かしてみました。UDPについて調べる過程でかなり勉強できたので良かったです。また、Dockerの動かし方についても学べたので一石二鳥でした!
参考