Java
HTTP
websocket
JavaEE

僕がJavaでサーバーを建てるときに勉強したこと

※これは ICT Advent Calendar 2018 13日目 の記事です。

飛び込み勢なので認知されていないでしょう

本文はこちらから始まるので、ONCTのICT委員会の方以外は読み飛ばしてください。

言い訳ですが、未だにサーバーやらネットワークについて よく分かっていない(勉強しろ) 詳しくないので、プロの方々ご指摘宜しくお願いします。


余談


なぜこの記事を書いたのか

・丁度同じ内容について記事を書くよう依頼されていた

・長期休暇での時間内の講義では後輩に対してここまで教えられなかった

・委員長に勧められた

から。

特に部内では功績もなければ教育もあまりしていないため、ここらで少しは仕事をしなければと思いましたね。

ICTでは特に「先輩に聞け」の一言でいきなり(AWSどころかIPアドレスすら知らないうちに)SNSのサーバーを建てさせられたりするので、知識の共有は必要かなと思いました。

(共有されてるのを知らないだけならすみません)

あとは、Javaをもっともっと布教したいので、初めてのサーバーで使って貰えたらなと思います。

僕自身ほぼ独学で、かつ、基礎的、理論的、体系的な知識は教えられませんが、少なくとも簡単なSNSとDCGを動かせるぐらいのコードを見せられたらなと思います。

というか設計もコードも正直役に立たないですが、初心者がそもそもググるための用語を知らない・最低限の構文を知らないという状態を脱出するために使えればなと思います。


本文

最初は意気込み強く書いていたのですが、余談を書き終えたあたりから終わりが見えなくなったのと、モチベが減少したので雑に書きました。

文句はコメントで受け付けます。

あと、SQLについては今回は 書くの面倒だったので省略 割愛しました。


環境設定を始める前に


そもそもサーバー開発とは何をすればいいのか

基本的には、クライアント(動作する側(HTMLとかAndroidアプリとか))から文字列を受け取り、処理結果を文字列で返すプログラムを書きます。


Javaのプログラムなんて、コンソール用しか書いたことないけど?

JavaEEという、サーバー開発用のAPIがあります。

TomcatなどのWebコンテナを使えば、それを公開して他のプログラム(やコンピュータ)とやりとりが出来るようになります。


通信方式が色々あるって聞いた

今回はHTTPとWebSocket(以下WS)について説明します。

HTTPは、基本的にはメッセージを受信した際に処理を返します。(通信が途切れる)

WSは、一度繋がった接続を保ち、相互にメッセージを送り合います。(通信が途切れない)


具体的にはどんなプログラムを書いたの?

僕の経験した2案件では、以下のような感じです。


SNS (HTTPサーバー)

・クライアント(Androidアプリ)で投稿ボタンを押すと、その内容(ID、投稿内容、位置情報など)がサーバーに送られるので、それをSQLに保存します。

・クライアントで読み込みボタンを押すと、読み込みを表すメッセージが送られます。

・サーバーはSQLからそのユーザーが閲覧可能な投稿を読み出し、送り返します。


DCG (WebSocketサーバー)

・クライアントが起動すると、まずサーバーに接続します。

・クライアントで行われた操作は、逐一サーバに送られます。

・サーバーは、ゲームプログラムを持っており送られてきたデータを基にゲームを操作します。

・操作が完了したら、クライアントに処理結果を送り、クライアントはそれに基づいて描画を行います。


環境設定

まず、環境は以下のようにします。

ソフトウェア(など)

開発言語
Java (8を推奨)

サーブレットAPI
JavaEE

IDE
IntelliJ Community

Webコンテナ
Tomcat

ビルドツール
Gradle

これらのセットアップについては別記事に書いたので、そちらに飛んでください。(すみません)

IntelliJ Community で Tomcat の実行環境を整備する


プログラムファイルを作成する

src -> main -> java を右クリックして、新規 -> パッケージをクリックし、パッケージを作成します。

更にパッケージ最下段を右クリックして、新規 -> Javaファイル で.javaファイルを作成します。


試しに実行してみる

環境設定記事でも触れましたが、IntelliJ Communityにはサーバー起動関連の機能はないため、実行はGradleのタスクで代用します。

画面右端のGradleをクリックし、タスクの一覧を表示します。

grettyの中に、Tomcat Run という項目があると思います。

それをダブルクリックでサーバーを立ち上げることが出来ます。

※たまに以下のようなエラーが出て再起動を繰り返すことがありますが、多分別窓でTomcatか他のプログラムがポートを占有しています。

切ってあげましょう。

org.apache.catalina.LifecycleException: Protocol handler initialization failed

at org.apache.catalina.connector.Connector.initInternal(Connector.java:935)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:136)
at org.apache.catalina.core.StandardService.initInternal(StandardService.java:530)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:136)
at org.apache.catalina.core.StandardServer.initInternal(StandardServer.java:852)
at org.apache.catalina.util.LifecycleBase.init(LifecycleBase.java:136)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:173)
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:371)
at org.apache.catalina.startup.Tomcat$start$0.call(Unknown Source)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:120)
at org.akhikhl.gretty.TomcatServerManager.startServer(TomcatServerManager.groovy:49)
at org.akhikhl.gretty.ServerManager$startServer$0.call(Unknown Source)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:128)
at org.akhikhl.gretty.Runner.run(Runner.groovy:117)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite$PogoCachedMethodSiteNoUnwrapNoCoerce.invoke(PogoMetaMethodSite.java:210)
at org.codehaus.groovy.runtime.callsite.PogoMetaMethodSite.call(PogoMetaMethodSite.java:71)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:47)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:120)
at org.akhikhl.gretty.Runner.main(Runner.groovy:44)
Caused by: java.net.BindException: Address already in use: bind
at sun.nio.ch.Net.bind0(Native Method)
at sun.nio.ch.Net.bind(Net.java:433)
at sun.nio.ch.Net.bind(Net.java:425)
at sun.nio.ch.ServerSocketChannelImpl.bind(ServerSocketChannelImpl.java:223)
at sun.nio.ch.ServerSocketAdaptor.bind(ServerSocketAdaptor.java:74)
at org.apache.tomcat.util.net.NioEndpoint.initServerSocket(NioEndpoint.java:227)
at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:202)
at org.apache.tomcat.util.net.AbstractEndpoint.init(AbstractEndpoint.java:1043)
at org.apache.coyote.AbstractProtocol.init(AbstractProtocol.java:540)
at org.apache.coyote.http11.AbstractHttp11Protocol.init(AbstractHttp11Protocol.java:74)
at org.apache.catalina.connector.Connector.initInternal(Connector.java:932)
... 27 more

http://localhost:8080/

そしてここにアクセスしましょう

スクリーンショット (75).png

こんな感じでNot Foundが表示されれば成功です。

まだ何もプログラムを書いていないので、当然ですね。

ちなみに実行していないと、ページ読み込みエラーなどが表示されます。


まずはHttpのサーブレットを作る

サーブレットってのはサーバーのメインプログラムみたいなものです(適当)

ここでは、まずWSより先にHttpを紹介していきます。

Httpにはメソッドというものがあり、通信の内容などによって使い分けますが、ここでは簡単化のためにGETだけ利用します。(ごめんなさい)

GETは、サーバーに対してリソースなどを要求するメソッドになります。

URLとパラメータを使って処理とデータを渡し、実行結果を受け取るという形で行きます。

では、まずHttpServlet.javaとでもしたサーブレットのプログラムを作ってみましょう。(クラス名は自由です)

Tomcatではweb.xmlという設定ファイルを書くのが作法ですが、アノテーションでも代用できるようなので、今回はそうします。

内容は以下にコードで示します。

import java.io.File;

import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.annotation.MultipartConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.annotation.WebServlet;

//必須
//nameにサーブレット名を、urlPatternsにヒットするURLのリストを渡す。
//ワイルドカードを指定することもできる。
@WebServlet(
name = "HttpServlet",
urlPatterns = {"/*"})
@MultipartConfig (
fileSizeThreshold= 32768 ,
maxFileSize= 5242880 ,
maxRequestSize= 27262976
)
// javax.servlet.http.HttpServletを継承することでサーブレットとして利用できる
public class HttpServlet extends javax.servlet.http.HttpServlet {

// doGetメソッドがGETリクエストを受け取ったときに呼び出されるメソッド
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response){
PrintWriter out = null;
try {
// この辺はとりあえずおまじない。
//文字コードとか色々
response.setContentType("text/html; charset=UTF-8");
response.setHeader("Access-Control-Allow-Origin", "*");
request.setCharacterEncoding("utf-8");

// PrintWiterをgetしておくことで、System.out.println()のように文字列を返却できる。
out = response.getWriter();

// request.getServletPath();でこのプロジェクトのルートから下でどのようなパスが入力されたかを受け取る。
//今回は適当にswitchで分岐する。
String path = request.getServletPath();
String str = null;
switch(path){
case "/hoge":
// request.getParameter()でパラメータを受け取る。
// http://localhost:8080/Test/hoge?str=hage&str2=hige
// のようなURLの?str=hage&str2=higeの部分
str = request.getParameter("str");
break;
case "/huga":
str = "huga";
break;
}
// strの中身を送り返す。
out.println(str);
// flushもちゃんと呼ぶこと。
out.flush();

} catch (IOException e){
e.printStackTrace();
}
}

}

これを実行して、以下のようなURLを踏むと、aaaと帰ってきます。

http://localhost:8080/<プロジェクト名>/hoge?str=aaa

これを利用して、実行するプログラムを分岐、データを受け取って処理を行います。

僕は使いましたがリクエストパラメータはセキュリティ的に問題しかないらしいので実務ではあまり使わないようにしましょう。

あと、ここではケース内に直接処理を書きましたが、関数にでも切り分けた方がいいと思います。


次はWebSocketのサーブレットを書いてみる

こんどはHttpではなく、WebSocketを利用するサーブレットを書いていきます。

Httpは(基本)クライアントからの通信に返事をするような(毎回新しく接続を行う)方式でしたが、

WebSocketでは、クライアントとの通信を保持し、双方向に通信を行います。

package scptcg.server;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.*;

// これを付けるとWebSocketサーバーになる
// URLはワイルドカード可
@ServerEndpoint("/ws")
public final class WSEndPoint {

// Session(通信)を保存しておくためのMap
private static Map<String, Session> session = new HashMap<>();

// 接続時に呼ばれるメソッド
@OnOpen
public void onOpen(final Session client, final EndpointConfig config) {
String log = client.getId() + " was connected.";
System.out.println(log);
}

// 切断時に呼ばれるメソッド
@OnClose
public void onClose(final Session client, final CloseReason reason) throws IOException {
String log = client.getId() + " was closed by "
+ reason.getCloseCode() + "[" + reason.getCloseCode().getCode() + "]";
System.out.println(log);
}

// エラー時に呼ばれるメソッド
@OnError
public void onError(final Session client, final Throwable error) {
String log = client.getId() + " was error. [" + error.getMessage() + "]";
error.printStackTrace();
}

// メッセージが送られたときに呼ばれるメソッド
@OnMessage
public void onMessage(final String text, final Session client) throws IOException {
// メッセージの内容は、改行区切りで操作・id・データが記述されているものとする。
String[] t = text.split("\n");
String event = t[0];
String id = t[1];
//eventの内容毎に分岐
switch (event){
case "login":
//HashMapにSessionを保存しておく。
session.put(id, client);
//idで保存したセッションに文字列を送信。
session.get(id).getBasicRemote().sendText(id);
System.out.println(id);
break;
case "commit":
// ブロードキャスト
for (Session s :session.values()){
s.getBasicRemote().sendText(t[2]);
}
System.out.println(t[2]);
break;
case "close":
// セッション一覧から削除
session.remove(t[1]);
break;
}

}

}

こちらは、OnMessage()メソッド内で処理を行います。

処理内容の分岐は元々はないっぽいので通信内容の頭で渡すといいと思います。

ちなみにこれはサンプルなので改行区切りのクソデータですが、普段はGsonを使ってJSONをやりとりしています。

@OnMessageなどのアノテーションによって、呼び出されるメソッドが認識されるので、特別なクラスを継承する必要がありません。

面倒なのでGsonについても割愛します。

一応テスト用のHTMLも置いておきます。

が、動作テストはしていないので、自分で書いた方がマシだと思います。

<!DOCTYPE html>

<html>
<head>
<title>WebSocket Sample</title>
<script type="text/javascript">
var uri = "ws://localhost:8080/Project/";

// WebSocketオブジェクト
var webSocket = null;

// 初期処理
function init() {
if (webSocket == null) {
// WebSocket の初期化
webSocket = new WebSocket(uri);
// イベントハンドラの設定
webSocket.onopen = onOpen;
webSocket.onmessage = onMessage;
webSocket.onclose = onClose;
webSocket.onerror = onError;
webSocket.send("login\n" + document.getElementById('id').innerText);
}
}

function onOpen(event) {
alert('open');
}

// メッセージ受信イベント
function onMessage(event) {
document.getElementById('box').innerText += event;
}

// エラーイベント
function onError(event) {
alert('error');
}

// 切断イベント
function onClose(event) {
alert('close');
webSocket = null;
}

function send(){
webSocket.send("commit\n" + document.getElementById('txt').innerText);
}

</script>
</head>
<body>
<input type="id" name="name" size="30" maxlength="20" />
<input type="txt" name="name" size="30" maxlength="20" />
<hr />
<input id="box" type="text" data-name="message" size="100" />
<hr />
<button id="con" onclick="init()">init</button>
<button id="send" onclick="send()">send</button>
</body>
</html>

ちなみに、URLは ws://localhost:8080/<プロジェクト名>/ です。


おわりに

雑になってしまってすみません。

SNSとDCGのコードは僕の GitHub に置いておくので見てみて下さい。

(そのコードのクソさに驚きます)

マジで雑に書いたので、訂正依頼・追加説明依頼があれば直接かSNSで言ってコメントに書いてください。

皆さんよいクリスマスを!