本記事では、Java を使って任意の情報を、C#のクライアントが要求し、それをJavaサーバーが送信する、基本的な通信アプリの実装を示します。
これまで Java には触れてこなかったのですが…仕方なく使わなければならない状況となったため、このたび勉強することになりました。(トホホ…)
初めて Java を触った人間がどういう風に理解しながら通信アプリを作ったのかメモします。(Java に親しんでいる人は暖かい目で見守ってください…)
Java アプリで Hello World
まずは最もシンプルな Java アプリ実行を試してみました。
Java アプリを開発するには、Java の SDK なる 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"
Maven で Java プロジェクトをビルドするにはフォルダ構成に気を使う必要があります。
まずはプロジェクトフォルダを決めて、次のようにフォルダを切ります。
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
と正常に動作するところまで確認できます。
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>
こういう maven の dependency の表記やバージョン番号は次の 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); } } } }
クライアントアプリを実行して何かを打つと…すぐに同じ内容が受信された旨が示されます。
サーバー側ではこのように、クライアントから送られてきたメッセージが何だったのか示されています。
正しくサーバー、クライアントが機能していることを確認できました。
まとめ
Java を使って任意の情報を、C#のクライアントが要求し、それをJavaサーバーが送信する、基本的な通信アプリの実装を示しました。
本来なら、ここからさらに通信量を抑える努力をしていくところですかね。
今なら dependency の設定と java の一部コードを教えてもらえれば、実行可能 jar が作れる気がします。