simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

先週までJava知らなかった人が、初めてJava通信アプリ作るとこうなる

本記事では、Java を使って任意の情報を、C#のクライアントが要求し、それをJavaサーバーが送信する、基本的な通信アプリの実装を示します。

これまで Java には触れてこなかったのですが…仕方なく使わなければならない状況となったため、このたび勉強することになりました。(トホホ…)
初めて Java を触った人間がどういう風に理解しながら通信アプリを作ったのかメモします。(Java に親しんでいる人は暖かい目で見守ってください…)

Java アプリで Hello World

まずは最もシンプルな Java アプリ実行を試してみました。

Java アプリを開発するには、JavaSDK なる JDK を開発環境にインストールしなければなりません。
ダウンロードページはこちら
Java SE - Downloads | Oracle Technology Network | Oracle

取得したインストーラを叩いて PC を再起動すれば Java 開発できるようになります。
インストール直後に案内される公式ドキュメントページはこちら
docs.oracle.com

上記ドキュメントを読んで問題なく進められるならそれでよいです。
手っ取り早く Hello World プログラムを試すならばまず、次のコードを HelloWorld.java テキストファイルに書き保存します。

public class HelloWorld {
    public static void main (String[] args) {
        System.out.println("Hello World !!");
    }
}

ファイルを置いたディレクトリをカレントにして java プログラムのコンパイルをします。
javac .\HelloWorld.java
コマンドを実行し同フォルダにコンパイルの成果物として HelloWorld.class を出力します。

それでは main 関数が書かれたクラス名を指定して、.class ファイルが置いてあるディレクトリをカレントに以下のコマンドを実行してみます。
java HelloWorld

コンソール出力に
Hello World !!
と表示されました。
最初に動作確認するのはこんな感じのコードですね。

複数の実装ファイルから構成されるアプリケーションを実行する

Java はファイルに1クラスしか記述できないようなので
複数のファイルと連携するプログラムを書くのが普通のアプリケーション開発です。

では別のクラスを用意してみましょう。
HelloWorld.java に次のコードを書き

public class HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Hello World!!");
    }
}

Main.java に次のコードを書きます

public class Main {
    public static void main (String[] args) {
    	HelloWorld helloWorld = new HelloWorld();
    	helloWorld.printHelloWorld();
    }
}

ビルドコマンドは
javac .\Main.java
初心者として驚いたことに、勝手に依存関係を見てくれたのか一つのコマンドから HelloWorld.calss, Main.class ファイルが二つ同時に出力されました。
実行コマンドに
java Main
を叩けば
Hello World!!
と出力することを確認できます。

メインクラスから別のクラスの機能を呼び出すことができるようになりました。
さて、ここで HelloWorld.class ファイルを削除して
実行コマンド
java Main
を叩くと次のエラーメッセージが表示されます。
Exception in thread "main" java.lang.NoClassDefFoundError: HelloWorld
at Main.main(Main.java:3)
Caused by: java.lang.ClassNotFoundException: HelloWorld
このような NoClassDefFoundError が出る時は .class ファイルが配置されていないケースととらえることができます。
実行時エラーが発生した時の手掛かりとして覚えておきましょう。

さて、続いてサブフォルダを切って
ファイルを管理していきましょう。

先ほどの
HelloWorld.java に package の行を書き加えます。

package sub;

public class HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Hello World!!");
    }
}

そして HelloWorld.java ファイルを sub フォルダを作成してその中に入れます。

Main.java に次の import 行を書き加えます

import sub.HelloWorld;

public class Main {
    public static void main (String[] args) {
    	HelloWorld helloWorld = new HelloWorld();
    	helloWorld.printHelloWorld();
    }
}

もう一度ビルドコマンド
javac .\Main.java
を叩けば、各フォルダに .class ファイルが作られ
実行コマンド
java Main
を叩くと正常に Hello World!! が表示されます。

.class ファイルがあちこちに散在するのが厄介だなと思います。

ビルド成果物を一つの jar ファイルに集約して実行する

そろそろコーディングが記憶を圧迫してくるのでインテリセンスを使います。
Java のエディタなんてのも初めて触りますが、IntelliJ IDEA で作ったJavaコードをレビューしてくれと頼まれたことが過去にあったので
これを機に IntelliJ IDEA を Java のエディタに利用してみようと思います。(その時は構造レビューなので書式詳細は見なくて良かったよ)

インストーラのダウンロードページはこちら
www.jetbrains.com

さてさて、インストーラの指示に従ってインストールしますが、ビルドツールとして Maven と Gradle があるよって示されます。
今回は Maven をビルドツールとして使っていきたいと思います。

Maven を使えるようにするにはまずツールをダウンロードして環境で利用できるようにする必要があります。
Maven – Download Apache Maven

どのファイルを落とすかは、次の記事を参考にするとよいです。
qiita.com

自分はこんな感じのパスに配置しました
C:\Program Files\Maven\apache-maven-3.5.2

bin フォルダに環境変数のPathを通してmvn コマンドが有効になるように PC を再起動します。

次の mvn コマンドを打ってバージョン情報が表示されれば準備 OK です。
mvn -v

Apache Maven 3.5.2 (138edd61fd100ec658bfa2d307c43b76940a5d7d; 2017-10-18T16:58:13+09:00)
Maven home: C:\Program Files\Maven\apache-maven-3.5.2\bin\..
Java version: 9.0.1, vendor: Oracle Corporation
Java home: C:\Program Files\Java\jdk-9.0.1
Default locale: ja_JP, platform encoding: MS932
OS name: "windows 10", version: "10.0", arch: "amd64", family: "windows"

MavenJava プロジェクトをビルドするにはフォルダ構成に気を使う必要があります。
まずはプロジェクトフォルダを決めて、次のようにフォルダを切ります。

HelloWorld
├─src
├─src
│ └─main
│   └─java
│     ├─ Main.java
│     └─sub
│       └─HelloWorld.java
└─pom.xml

pom.xml はテキストファイルで、Maven のビルド設定が書かれたファイルです。
POM の書き方は
Maven – Introduction to the POM
を参考にします。

とりあえず以下の
Apache Maven Compiler Plugin – Usage
Usage を参考に pom.xml に次のように書き込んで

<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-module</artifactId>
  <version>1</version>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <properties>
  </properties>
  <dependencies>
  </dependencies>
</project>

プロジェクトフォルダにカレントを持ってきてコマンドプロンプトより
mvn package
コマンドを実行します。

すると、maven が必要なライブラリをダウンロードして、しばらくすると Build Success の表示が出ます。
target フォルダが自動的に作成され、その中に .jar ファイルが見つかります。

jar ファイルはビルド成果物群の zip 圧縮形式のファイルです。

これを実行したいので、jar を実行するコマンドを叩くのですが
java -jar .\my-module-1.jar

.\my-module-1.jarにメイン・マニフェスト属性がありません

とそのままでは実行できません。

複数のクラスの中で、どれが Main Class なのか指定してあげる必要があるとのことで
それを記述したメイン・マニフェストを jar に埋め込むため pom を次のように書き換えます。
この書き換えに関する参考ドキュメントはこちら
Apache Maven Assembly Plugin – Usage

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>jp.simplestar.app</groupId>
  <artifactId>my-module</artifactId>
  <version>1</version>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.7.0</version>
        <configuration>
          <source>1.6</source>
          <target>1.6</target>
        </configuration>
      </plugin>
      <plugin>
        <artifactId>maven-assembly-plugin</artifactId>
        <version>3.1.0</version>
        <configuration>
          <descriptorRefs>
            <descriptorRef>jar-with-dependencies</descriptorRef>
          </descriptorRefs>
          <archive>
            <manifest>
              <mainClass>Main</mainClass>
            </manifest>
          </archive>
        </configuration>
        <executions>
          <execution>
            <id>make-assembly</id> <!-- this is used for inheritance merges -->
            <phase>package</phase> <!-- bind to the packaging phase -->
            <goals>
              <goal>single</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
  <properties>
  </properties>
  <dependencies>
    <!-- https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/javax-websocket-server-impl -->
    <dependency>
      <groupId>org.eclipse.jetty.websocket</groupId>
      <artifactId>javax-websocket-server-impl</artifactId>
      <version>9.4.8.v20171121</version>
    </dependency>
  </dependencies>
</project>

mvn package
コマンドを実行すると、ここで追加した jar-with-dependencies.jar ファイルも target フォルダに作成されるようになります。

これで先ほどマニフェストが無いと怒られたコマンドを再度叩くと
java -jar .\my-module-1-jar-with-dependencies.jar

Hello World!!

と正常に動作するところまで確認できます。

WebSocket サーバーを書く

ここまで来ると、あとは pom の記述方法と java のコードの書き方に気を配るだけです。
次の内容をpom の dependencies の中に書き加えます。

<!-- https://mvnrepository.com/artifact/org.eclipse.jetty.websocket/javax-websocket-server-impl -->
<dependency>
    <groupId>org.eclipse.jetty.websocket</groupId>
    <artifactId>javax-websocket-server-impl</artifactId>
    <version>9.4.8.v20171121</version>
</dependency>

こういう mavendependency の表記やバージョン番号は次の Maven Repository サイトの Maven タブから取得してきます。
Maven Repository: org.eclipse.jetty.websocket » javax-websocket-server-impl » 9.4.8.v20171121

WebSocket の使い方は
Using WebSocket Annotations
を参考に次のように書きました。

Main.java

package jp.simplestar.app;

import jp.simplestar.app.sub.HelloWorld;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.websocket.server.WebSocketHandler;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;

public class Main {
    public static void main (String[] args) {
        Server server = new Server(8000);
        WebSocketHandler wsHandler = new WebSocketHandler()
        {
            @Override
            public void configure(WebSocketServletFactory factory)
            {
                factory.register(HelloWorld.class);
            }
        };
        server.setHandler(wsHandler);
        try {
            server.start();
            server.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

HelloWorld.java

package jp.simplestar.app.sub;

import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.*;

@WebSocket
public class HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Hello World!!");
    }

    @OnWebSocketConnect
    public void onWebSocketConnect(Session session)
    {
    }

    @OnWebSocketClose
    public void onWebSocketClose(Session session, int closeCode, String closeReason) {
    }

    @OnWebSocketMessage
    public void onWebSocketMessage(Session session, String message)
    {
        if (session.isOpen())
        {
            System.out.printf("Echoing back message [%s]%n",message);
            // echo the message back
            session.getRemote().sendString(message,null);
        }
    }

    @OnWebSocketError
    public void onWebSocketError(Session session, Throwable cause) {
    }
}

これを mvn package でパッケージ化した jar ファイルを実行します。(MainClass パスを変えたのでpomのメインクラスの指定を変える点に注意)
実行したときのコンソールの様子がこちら

java -jar .\my-module-1-jar-with-dependencies.jar
2017-12-10 15:40:46.337:INFO::main: Logging initialized @2558ms to org.eclipse.jetty.util.log.StdErrLog
2017-12-10 15:40:46.405:INFO:oejs.Server:main: jetty-9.4.z-SNAPSHOT, build timestamp: 2017-11-22T06:27:37+09:00, git hash: 82b8fb23f757335bb3329d540ce37a2a2615f0a8
2017-12-10 15:40:47.122:INFO:oejs.AbstractConnector:main: Started ServerConnector@7c417213{HTTP/1.1,[http/1.1]}{0.0.0.0:8000}
2017-12-10 15:40:47.122:INFO:oejs.Server:main: Started @3350ms

サーバーは機能しているみたいです。
では、確認用クライアントの方を書いていきましょう。

C# WebSocket クラアイント

次の stackOverflow の記事を参考に作ってみました。
stackoverflow.com

典型的な C# のコンソールアプリケーションです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace WebSocketClient
{
    class Program
    {
        static void Main(string[] args)
        {
            Task clientTask1 = Client();
            clientTask1.Wait();
        }

        static async Task Client()
        {
            ClientWebSocket ws = new ClientWebSocket();
            var uri = new Uri("ws://localhost:8000/");

            await ws.ConnectAsync(uri, CancellationToken.None);
            var buffer = new byte[1024];
            while (true)
            {
                Console.Write("Input message ('exit' to exit): ");
                string msg = Console.ReadLine();

                ArraySegment<byte> bytesToSend = new ArraySegment<byte>(Encoding.UTF8.GetBytes(msg));
                await ws.SendAsync(bytesToSend, WebSocketMessageType.Text, true, CancellationToken.None);

                var segment = new ArraySegment<byte>(buffer);

                var result = await ws.ReceiveAsync(segment, CancellationToken.None);

                if (result.MessageType == WebSocketMessageType.Close)
                {
                    await ws.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "I don't do binary", CancellationToken.None);
                    return;
                }

                int count = result.Count;
                while (!result.EndOfMessage)
                {
                    if (count >= buffer.Length)
                    {
                        await ws.CloseAsync(WebSocketCloseStatus.InvalidPayloadData, "That's too long", CancellationToken.None);
                        return;
                    }

                    segment = new ArraySegment<byte>(buffer, count, buffer.Length - count);
                    result = await ws.ReceiveAsync(segment, CancellationToken.None);
                    count += result.Count;
                }

                var message = Encoding.UTF8.GetString(buffer, 0, count);
                Console.WriteLine(">" + message);
            }

        }
    }
}

クライアントアプリを実行して何かを打つと…すぐに同じ内容が受信された旨が示されます。

f:id:simplestar_tech:20171210174146j:plain


サーバー側ではこのように、クライアントから送られてきたメッセージが何だったのか示されています。

f:id:simplestar_tech:20171210174215j:plain

正しくサーバー、クライアントが機能していることを確認できました。

まとめ

Java を使って任意の情報を、C#のクライアントが要求し、それをJavaサーバーが送信する、基本的な通信アプリの実装を示しました。
本来なら、ここからさらに通信量を抑える努力をしていくところですかね。

今なら dependency の設定と java の一部コードを教えてもらえれば、実行可能 jar が作れる気がします。