まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:http

前回(「WAS Liberty Core を 80/443 番ポートで動かす」)の続きです。

今回は軽量版ではなく、いわゆる「フルプロファイル版」などと呼ばれている、従来の WAS(Websphere Application Server)を対象に起動ポート番号を変更する手順を紹介します。また今回も具体的には IBM Bluemix 内の WAS on Bluemix サービスの BASE プランインスタンスを使って紹介します。


では実際に起動ポートを変更します。ウェブブラウザで https://(ホスト名):9433/ibm/console/ にアクセスして管理コンソールを開き、正しいユーザー名とパスワードを入力してログインします:
2017031601


WAS の管理コンソールにアクセスしました:
2017031602


WAS の場合、起動ポート番号はアプリケーションサーバー内の Web コンテナのトランスポートチェーンで管理しています。というわけで、まずは左ペインから サーバー - サーバータイプ - WebSphere Application Server を選択し、アプリケーションサーバーとして起動ポートを変更したいサーバーの名称(デフォルトでは server1)を選択します:
2017031603


選択したアプリケーションサーバー内から Web コンテナー設定 - Web コンテナー・トランスポート・チェーン を選択します:
2017031604


現在設定されている Web トランスポートチェーンの一覧が表示されます。この中に 80 番(http)と 443 番(https)を追加します。まずは 80 番ポートを追加するために「新規作成」ボタンをクリックします:
2017031604


作成するトランスポートチェーンの名称(下図では WCInboundDefault80)を入力し、テンプレートに WebContainer が選択されていることを確認して「次へ」をクリックします:
2017031605


利用するポート番号をここで指定します。「新規ポートの作成」が選択されていることを確認して、ポート名は "80"、ホストは "*" 、ポートは "80" をそれぞれ入力して「次へ」:
2017031606


確認の画面で「終了」を選択します:
2017031607


トランスポートチェーン一覧の画面に戻るので、今回の変更をマスター構成として「保存」します:
2017031608


追加したトランスポートチェーンが一覧に含まれていることを確認します:
2017031609


同様にしてもう1つ、443 番ポート(https)のトランスポートチェーンも新規に作成します。その場合、テンプレートには "WebContainer-Secure" を選択することに注意してください:
2017031601

また、ポート名とポートには "443" を入力します:
2017031602


こうして 80 番ポート(http)と 443 番ポート(https)のトランスポートチェーンが追加されたことを確認します:
2017031603


ここまでの作業ができれば後はサーバーを再起動して、変更を反映するだけです。が、ここも前回紹介した理由により root ユーザー権限でサーバーを起動する必要があります。具体的には ssh 等でログイン後、root ユーザーに su/sudo して、root ユーザー権限でサーバーを止めて、再び起動する、という手順を実行します:

$ sudo /bin/bash

# /opt/IBM/WebSphere/AppServer/bin/stopServer.sh server1

# /opt/IBM/WebSphere/AppServer/bin/startServer.sh server1

# exit

$

これで WAS Liberty Core の時と同様に WAS も 80/443 番ポートで利用できるようになっているはずです。

IBM の Java アプリケーションサーバーである WAS(Websphere Application Server) は標準設定のまま導入して使い始めると、9080 番ポート(http)や 9443 番ポート(https)でサーバーが起動します。これを一般的な 80 番や 443 番で起動させるための設定を紹介します。方法自体はいくつかあるのですが、ここで紹介するのは「とりあえずてっとり早くできる方法」です。 また今回は軽量版である WAS Liberty Core を対象として紹介します(フル機能版は次回)。具体的にはパブリッククラウドであるIBM Bluemix 内の WAS on Bluemix サービスの Liberty Core インスタンスを使って紹介します:


では実際に起動ポートを変更します。WAS Liberty Core の場合はアプリケーションサーバーの server.xml を編集することで変更できるので、まずはこのファイルを探します。

既にアプリケーションサーバーが起動している場合はウェブブラウザからも変更できます。 https://(ホスト名):9080/ にアクセスして、"Open Admin Console" をクリックします:
2017031601


認証が有効に設定されている場合は認証画面になります。正しいユーザー名とパスワードを入力して「送信」します:
2017031602


管理コンソールにアクセスできました。server.xml を編集するには "Server Config" を選択します:
2017031601


構成ファイルとして server.xml が表示されている(これしか表示されてない??)ので、server.xml をクリック:
2017031602


すると server.xml の編集画面に移動します。「ソース」タブで表示すると、XML テキストを直接編集することも可能です:
2017031603


なお、SSH 等でアプリケーションサーバーシステムに直接ログインできる場合であれば、server.xml は以下に存在しているので、このファイルを直接テキストエディタで編集しても構いません:
/opt/IBM/WebSphere/Profiles/Liberty/servers/server1/server.xml


以下の赤字部分4箇所を変更します。変更が完了したら保存(管理コンソールであれば右上のボタン)します:
<server description="Default Hypervisor Server">
  <!-- Simple application server, supporting servlets and jsps -->
  <featureManager>
    <feature>jsp-2.2</feature>
    <feature>adminCenter-1.0</feature>
  </featureManager>
<remoteFileAccess>
<writeDir>${server.config.dir}</writeDir>
</remoteFileAccess>
<virtualHost id="default_host" allowFromEndpointRef="defaultHttpEndpoint">
 <hostAlias>*:80</hostAlias>
 <hostAlias>*:443</hostAlias>
</virtualHost>
<!-- virtualHost id="external_host">
 <hostAlias>*:80</hostAlias>
 <hostAlias>*:443</hostAlias>
</virtualHost -->

  <quickStartSecurity userName="wsadmin" userPassword="{xor}am07ZzlubWg=" />
  <keyStore id="defaultKeyStore" password="{xor}am07ZzlubWg=" />
  <!-- disable automatic configuration and application updates, but leave mbean support enabled -->
  <config updateTrigger="mbean"/>
  <applicationMonitor updateTrigger="mbean" dropinsEnabled="true"/>
  <ssl id="defaultSSLConfig"
     sslProtocol="SSL_TLSv2"
     keyStoreRef="defaultKeyStore"
     clientAuthenticationSupported="true"/>

  <!-- open port 9080 for incoming http connections -->
  <httpEndpoint id="defaultHttpEndpoint"
                host="*"
                httpPort="80"
                httpsPort="443">
      <tcpOptions soReuseAddr="true"/>
  </httpEndpoint>
  <!-- httpEndpoint id="publicHttpEndpoint"
              host="*"
              httpPort="80"
              httpsPort="443">
      <tcpOptions soReuseAddr="true"/>
  </httpEndpoint -->
</server>


設定の変更そのものはこれだけです。後はアプリケーションサーバーを再起動・・・なのですが、OS が Linux の場合はもう1点注意が必要です。

Linux の場合、1024 番未満のポートはデフォルトでは root 権限がないと listen できません
。つまり上記の設定変更をしても再起動の際に root 以外のユーザー権限で再起動するとポートを listen できないのです。

特に IBM Bluemix 環境での場合、OS は RedHat で、その一般ユーザーである virtuser の権限で WAS は起動します。つまり上記の制約をまともに受けてしまうのでした。というわけで、WAS 再起動の際には注意が必要です。具体的にはまず root ユーザーに su(または sudo)し、root ユーザー権限でサーバーを止めて、再び起動、という手順が必要です:
$ sudo /bin/bash

# /opt/IBM/WebSphere/Liberty/bin/server stop server1

# /opt/IBM/WebSphere/Liberty/bin/server start server1

# exit

$


これで WAS Liberty Core が 80/443 番ポートで利用できるようになっているはずです:
2017031603


 

Java で(Web)アプリケーションから REST API を実行する時など、HTTP のクライアント機能を java.net.* から作るのは面倒です。現実的にはなんらかのライブラリを使うことになると思います。

そんな場合によく使われるのが Apache HTTP Client だと思ってます。2017/Mar/16 現在の最新バージョンは 4.5.3 でした。モジュールはこちらからダウンロードできます:
https://hc.apache.org/downloads.cgi

2017031501


上記サイトの HttpClient カテゴリから 4.5.3.zip と書かれたリンクをクリックすると 4.5.3 のバイナリが zip アーカイブとして取得できます(ファイル名は httpcomponents-client-4.5.3-bin.zip)。ダウンロードした zip ファイルを展開し、lib フォルダから jar ファイル群を取り出します(今回のサンプルで最低限必要なのは以下の6ファイルです):
  • commons-codec-1.4.jar
  • commons-logging-1.2.jar
  • httpclient-4.5.3.jar
  • httpclient-cache-4.5.3.jar
  • httpcore-4.4.6.jar
  • httpmime-4.5.3.jar

Eclipse 等で Java のウェブアプリケーションプロジェクトを作成し、lib フォルダ(Webcontent/WEB-INF/lib など)に上記作業で取り出した jar ファイル群をまとめてコピーしておきます。これで準備完了:
2017031502


では実際にこれらのモジュールを使って HTTP アクセスを実現するプログラムを書いて実行してみます。今回はスタンドアロンに HTTP GET を実行する、こんなプログラムにしてみます:
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpClient1 {
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    String url = "https://www.ibm.com/developerworks/jp/"; //. HTTP GET する URL

    try{
      CloseableHttpClient client = HttpClients.createDefault();
      HttpGet get = new HttpGet( url );
      CloseableHttpResponse response = client.execute( get );
      int sc = response.getStatusLine().getStatusCode(); //. 200 の想定
      HttpEntity entity = response.getEntity();
      String html = EntityUtils.toString( entity, "UTF-8" );
      System.out.println( html ); //. 取得結果をコンソールへ
      client.close();
    }catch( Exception e ){
      e.printStackTrace();
    }
  }
}

指定した URL(上記の場合は https://www.ibm.com/developerworks/jp/")に HTTP でアクセスして、GET した結果をコンソールに出力する、というものです。この内容を記述したファイル(HttpClient1.java)を Eclipse から実行します:
2017031503


で、指定した URL  の HTML が取得できることを確認します。HTTP GET は呼び出すだけなのでシンプルですね:
2017031504


アクセス先として HTML のようなテキストではなく、画像のようなバイナリデータの場合は以下のように byte 配列として結果を取得します(HTTP リクエストヘッダを設定する例も加えています):
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpClient2 {
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    String url = "https://dw1.s81c.com/developerworks/i/f-ii-ibmbluemix.png"; //. HTTP GET する URL

    try{
      CloseableHttpClient client = HttpClients.createDefault();
      HttpGet get = new HttpGet( url );
      get.addHeader( "User-Agent", "MyBot/1.0" );  //. HTTP リクエストヘッダの設定
      CloseableHttpResponse response = client.execute( get );
      int sc = response.getStatusLine().getStatusCode(); //. 200 の想定
      HttpEntity entity = response.getEntity();
      byte[] img = EntityUtils.toByteArray( entity );
      System.out.println( "" + img.length ); //. 取得結果をコンソールへ
      client.close();
    }catch( Exception e ){
      e.printStackTrace();
    }
  }
}

一方、HTTP POST の場合も同様ですが、GET の時との違いとしてポストデータを送信する必要もあります。以下はテキスト情報とファイルのアップロードを同時に(Multipart で)送信する場合の例です:
import java.io.File;
import java.io.FileInputStream;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

public class HttpClient3 {
  public static void main(String[] args) {
    // TODO Auto-generated method stub
    String url = "https://xxx.com/posturl"; //. HTTP POST する URL

    try{
      CloseableHttpClient client = HttpClients.createDefault();
      HttpPost post = new HttpPost( url );

//. 文字情報2つとファイル1つをポスト MultipartEntityBuilder builder = MultipartEntityBuilder.create(); builder.addTextBody( "name", "K.Kimura", ContentType.TEXT_PLAIN ); builder.addTextBody( "email", "dotnsf@jp.ibm.com", ContentType.TEXT_PLAIN ); File f = new File( "./logo.png" ); builder.addBinaryBody( "image_file", new FileInputStream( f ), ContentType.APPLICATION_OCTET_STREAM, f.getName() ); HttpEntity multipart = builder.build(); post.setEntity( multipart ); CloseableHttpResponse response = client.execute( post ); int sc = response.getStatusLine().getStatusCode(); //. 200 の想定 HttpEntity entity = response.getEntity(); String html = EntityUtils.toString( entity, "UTF-8" ); System.out.println( html ); //. 取得結果をコンソールへ client.close(); }catch( Exception e ){ e.printStackTrace(); } } }

PUT や DELETE の場合も同様に。

とある REST API を使っていて気付いたこと/考えさせられたことをまとめてみました。明快な結論や提案があるわけではなく、グダグダに感じられる内容かもしれないので、あらかじめご了承ください。


そのとある REST API を使ったウェブアプリケーションを作って運用している中で、おかしな挙動に気付くことがありました。以前は問題なく動いていたのに、あるタイミングで実行すると期待通りに動かない、という「まあまあよくある」ケースです。自分のケースでは動かないというよりも、タイムアウトを起こすような挙動になっていました。ただ REST API そのものが止まっている様子はない、というケースでした。

自分のソースコードでは一応エラーハンドリングはしていたつもりでしたが、REST API がエラーを起こしている様子もなく、原因究明に時間を要するものでした。結論としては自分のアプリケーションコードを見ていてもよく分からず、REST API のユニットテストのようなツールを動かした結果気付くことがありました。

それがこちら:
20170207


・・・わかるでしょうか? REST API を実行したレスポンス本文が Response Body に、ステータスコードが Response Code に記述されています。これによるとレスポンス本文は
{
  "status": "ERROR",
  "statusInfo": "daily-transaction-limit-exceeded"
}

となっていて、"daily-transaction-limit-exceeded" が原因のエラーが発生している、という内容でした。この API には Daily Transaction Limit(1日で使える回数の上限)が決められていて、その上限に達したのでもう実行できない、という内容です。これに関してはそういう条件で使っている API なので、なるほど、エラーの原因はわかりました。

問題はこの REST API を実行したステータスコードが 200 になっている点です。HTTP ステータスコードの分類とその意味についてはウィキペディアなどを参照していただきたいのですが、簡単にいうとこんな感じで分類されています:
コード意味考えられる原因など
2xx(200番台)成功 -
4xx(400番台)クライアントエラー認証が必要、アクセス権がない、URLが間違っている、タイムアウト、、、
5xx(500番台)サーバーエラーアプリケーションエラー、不正なゲートウェイが利用されている、、、


200 番台は成功。400 番台と 500 番台がエラーで、それぞれクライアント側のエラーなのか、サーバー側のエラーなのかを分類しています。

で、今回のケースですが、実行結果はエラーなのに 200 番のステータスコードが返されているのでした。自分のプログラムコードの中では「200 が返ってきたら成功」と決めつけて実装していたため、このようなケースに対処できていなかったのでした。


ここまでが実際に目の当たりにしたエラーとその原因でした。以下はこの件に関して自分が考えたことです。


自分を弁護する意味で「えー、でもそれっておかしくない? エラーなんだからスタータスコードは 400 番台なり 500 番台で返ってくるべきでは?」という考えもないわけではありません。ただ今回のケースではこれも難しいような、つまり 200 番のステータスコードがあながち間違ってはいないような気もしています。

その理由として、まず「これはクライアントエラーなのか?それともサーバーエラーなのか?どちらかに分類できるのか?」という問題です。これに関してはどちらとも言えるし、どちらとも言えないと思っています。

実は 402 番のステータスコードは "Payment Required" 、つまり(お金を払わないといけないんだけど)払ってないエラー、と定義されています。が、実際には定義だけで実装されていない、つまり将来のための定義とされているのでした。また厳密には支払い契約を結んで使っているわけではなく、「この条件で1日○○回使える」というルールの下で利用しているだけなので、402 番に(クライアントエラーに)該当するエラーであるとは考えにくいのです。

※本来の意味とは違いますが、「権限がない」に該当するのではないかと言われると、まあ・・・ という考え方もあるとは思います。ただそれをクライアントエラーとして返してもクライアント側はどうにもできないので、やはり 400 番台エラーには該当しないと思います。

ではサーバーエラーなのか?というと、これも該当しないと思います。プログラミングにミスがあったわけではなく、「実行したら、『実行できない』という結果が返ってきた」のは、正しく実行された(結果が期待通りではなかった)とも言えます。そう考えると、そもそも今回の件は REST API レベルではエラーですらないとも言えます。


そう考えると、今のように API を組み合わせてアプリケーションを作ることが珍しくない環境においては、200 番のステータスコードが返ってきてもエラーの可能性を疑ってコーディングする必要があるのかも、と思うようになりました。このケースであれば実装側が工夫すれば(というか、そもそもちゃんと色んなケースを想定してエラーハンドリングしていれば)防げるものです。

そういう意味でもいい反省の機会でした。 でも今後は同様のケースを想定した「成功でもエラーでもないステータス」が出て来る可能性もありますよね。。


前回の続きです:
CentOS に Go をインストールする


Go の実行環境が導入できたので、テンプレートエンジンを使ってみます。まずは HTML テンプレートを作成しておきます。base.html というファイル名で以下の内容を作成/保存します:
{{define "base"}}<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>{{.Title}}</title>
</head>
<body>
<div class="content">
<h1>{{.Title}}</h1>
<hr/>
<div>
{{.Body|safehtml}}
</div>
</div>
</body>
</html>{{end}}

↑ほぼ HTML ですが、赤字で書いた部分が変数になります。変数名としては Title と Body の2つがあり、その中身はこの後の Go アプリの中で定義して渡します。また全体を "base" という名前で定義しています(青字部分)。


次にこのテンプレートを使って画面を表示する Go のウェブアプリを作成します(http2.go というファイル名で、base.html と同じディレクトリに作成します):
package main

import(
  "net/http"
  "html/template"
)

func viewHandler( w http.ResponseWriter, req *http.Request ){
  funcMap := template.FuncMap{
    "safehtml": func(text string) template.HTML { return template.HTML(text) },
  }
  templates := template.Must(template.New("").Funcs(funcMap).ParseFiles("base.html"))
  dat := struct {
    Title string
    Body string
  }{
    Title: req.FormValue( "title" ),
    Body: req.FormValue( "body" ),
  }
  err := templates.ExecuteTemplate(w, "base", dat)
  if err != nil {
    http.Error( w, err.Error(), http.StatusInternalServerError )
  }
}

func main(){
  http.HandleFunc( "/", viewHandler )
  http.ListenAndServe( ":8080", nil )
}

基本形は前回の Go ウェブアプリとほぼ同じですが、まず html/template を使う宣言をして、テンプレートに上記で作成した(同じディレクトリ上にある) base.html を使う宣言をしています。

またテンプレート内の変数 Title, Body を、それぞれ title, body という URL パラメータを受け取って定義するようにしています。

この状態で http2.go を実行します:
# go run http2.go


そしてウェブブラウザでこのサーバーの 8080 番ポートのルートパスに、パラメータ title と body を指定してアクセスすると、指定した内容がウェブページの一部になって表示されるはずです:
2016042602


これで Go でもテンプレートエンジンっぽいこともできました。

このページのトップヘ