simplestarの技術ブログ

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

クライアントサイドが学ぶサーバーサイド技術

という題で、20分ほどの発表をしようと思います。

すこしずつ書いていきます。

■前置き1

クライアントサイド、サーバーサイドを語る前に、まずはゲームが関数であることをみなさんに理解してもらいます。
f:id:simplestar_tech:20180519185122j:plain

■クライアントサイド

クライアントサイドは、単一機材で処理が完結する関数を最適化するお仕事です。
f:id:simplestar_tech:20180520103945j:plain

次のステップを踏みながら関数の最適化を行います。

  1. プランナーのアイディアから要求分析
  2. オブジェクト指向設計により不具合の特定が行いやすく、また複数人で分業しやすいスケルトン(骨組み)を作成
  3. アジャイルスクラムテスト駆動開発を例に、プロジェクト全員の進捗意識を統一
  4. 情報技術/数学/物理の知識を駆使し、ゲーム内の4次元の機能を1次元のプログラムコードに展開
  5. シナリオ、イラスト、音声、BGM、効果音など、ユーザーが直接観測するアート部分を量産・管理できるツールを作成
  6. ハードウェアとソフトウェアの知識を駆使し、関数のパフォーマンスを向上

■サーバーサイド

サーバーサイドは、関数同士のアクセス集中における負荷対策をするお仕事です。

f:id:simplestar_tech:20180520210037j:plain

次のステップを踏んで利益率を意識しながら大量アクセス集中における障害を未然に防ぐお仕事です。

  1. プランナーのアイディアから要求分析
  2. 運用コストを計算
  3. ケルトンを作成
  4. 負荷対策
  5. チート対策
  6. サービス運用

■発表しないけど気づいたこと

パッケージ開発なら、クライアントサイドの方が花形
ソーシャルゲーム開発なら、サーバーサイドの方が花形

またクライアントサイドよりもサーバーサイドの方が具体的に金額を提示しながらプランナーの意見を変えやすいことがわかる。
逆を言えば、クライアントサイドは開発工数やパフォーマンスの提示がメインなので、プランナーの意見を変えづらい(自身の頑張りで工数やパフォーマンスが改善するため)

goでjson文字列からオブジェクトを生成

■前置き

Goで書いたWebAPIサーバが今 json 文字列を受け取っています。
json文字列に含まれる情報から必要な値をピックアップして、外部のグラフ表示ソフトが期待しているようなフォーマットでファイル出力しなければなりません。

さて、つまり json 文字列から go 環境にて、元のデータ構造を完全復元(または必要なフィールドのみ対応)して、独自フォーマットでファイル出力する必要があります。

汎用的な操作かつ、非常に需要があるケースなので、対応方法がすぐ見つかると思います。

■調査結果

はい、すぐに見つかりました。
JSONのパース/生成 - はじめてのGo言語

ここを参考にします。

■本題
前回の obj から生成した次の json 文字列をパースしてみましょう。
{"obj2":{"a":1,"b":2,"array":[{"test":"hoge","pageX":101,"pageY":102,"identifier":321},{"test":"hoge","pageX":201,"pageY":202,"identifier":987}]},"obj3":{"c":3,"d":4}}

go 側の実装は次の通り、対応するデータタイプに json タグを付けます。
ここで type struct で定義するフィールドは必ず大文字で始まらなければならない規則があることに注意してください。
必ずフィールド名は頭文字を大文字にする必要があることに注意してください。
フィールド名は頭文字を大文字に( ゚д゚)…もういいか

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

// Animal a
type Animal struct {
	Test       string `json:"test"`
	PageX      int    `json:"pageX"`
	pageY      int    `json:"pageY"`
	Identifier int    `json:"identifier"`
}

// OBJ2 b
type OBJ2 struct {
	A       int      `json:"a"`
	B       int      `json:"b"`
	Animals []Animal `json:"array"`
}

// OBJ3 c
type OBJ3 struct {
	C int `json:"c"`
	D int `json:"d"`
}

// OBJ a
type OBJ struct {
	Obj2 OBJ2 `json:"obj2"`
	Obj3 OBJ3 `json:"obj3"`
}

// handler impl
	r.ParseForm()
	form := r.PostForm
	output := fmt.Sprintf("%s", form["json"][0])
	
	jsonBytes := ([]byte)(output)
	postData := new(OBJ)
	if err := json.Unmarshal(jsonBytes, postData); err != nil {
		log.Fatal(err)
	}

	// access test
	fmt.Println("data a = " + strconv.Itoa(postData.Obj2.A))
	fmt.Println("data b = " + strconv.Itoa(postData.Obj2.B))
	fmt.Println("data c = " + strconv.Itoa(postData.Obj3.C))
	fmt.Println("data d = " + strconv.Itoa(postData.Obj3.D))
	for i, v := range postData.Obj2.Animals {
		fmt.Print(fmt.Sprintf("index:%d,value: %v\n", i, v))
	}

デバッグコンソールの出力は次の通りでした。

data a = 1
data b = 2
data c = 3
data d = 4
index:0,value: {hoge 101 0 321}
index:1,value: {hoge 201 0 987}

goでjson文字列からオブジェクトを生成できました。
過去の一連の記事を読んだ方なら、今の私と一緒で
WebページにおけるユーザアクティビティをWebAPIサーバに記録できる能力を持ったことになりますw。

Webページからオブジェクトをjson文字列に変換してPOST

■前置き
Webページから文字列をPOSTする際、クエリストリングのように連想配列を文字列で送信できることは確認済み
さて、ユーザーアクティビティってのはそう単純なデータ構造じゃないんです。
ユーザ定義のデータ構造の配列の配列なんてこともあって、これをPOSTできるんでしょうか?

普通、こうした複雑なデータ構造は一度形式だった文字列に変換するということが、Webの通信では通例なようで
jsonフォーマットの文字列というものがあります。例えば次のような文字列

{
    "id": "0000",
    "str": "hello",
    "CustomType": [
        {
            "you": "are",
            "c": "t"
        },
        {
            "you": is,
            "c": "hoge"
        }
    ]
}

javascript ではオブジェクトから json 文字列にシリアライズしたり
json文字列からデシリアライズしたりするんじゃないですかね?

やったことないですが、推理です。
json文字列へのシリアライズ、デシリアライズについて javascript の書式を知りたいです。

■本題

あった

JavaScript 値を JSON (JavaScript Object Notation) 文字列に変換
JSON.stringify 関数 (JavaScript)

JSON (JavaScript Object Notation) 文字列をオブジェクトに変換
JSON.parse 関数 (JavaScript)

POSTで送信したい情報があったら、次のようにすることで json 文字列を送信することを確認しました。

            let jsonStr = JSON.stringify(obj);
            xhr.send(jsonStr);

試しに、適当に複雑化したオブジェクトを JSON.stringify に突っ込んでみて、どのような json 文字列が作られるのか見てみました。

        var obj = {
            obj2: {
                a : 1,
                b : 2,
                array : Array
            },
            obj3: {
                c : 3,
                d : 4
            }
        };
        var Animal = (function () {
            function Animal() {
                this.test = "hoge";
            }
            this.test = "test";
            this.pageX = 0;
            this.pageY = 0;
            this.identifier = 0;
            return Animal;
        })();

            let touchList = new Array();
            let touch = new Animal();
            touch.pageX = 101;
            touch.pageY = 102;
            touch.identifier = 321;
            touchList.push(touch);
            let touch2 = new Animal();
            touch2.pageX = 201;
            touch2.pageY = 202;
            touch2.identifier = 987;
            touchList.push(touch2);
            obj.obj2.array = touchList;

            let jsonStr = JSON.stringify(obj);
            console.log(jsonStr);

            xhr.send(jsonStr);

JavaScript 側でコンソール出力を見てみると次の json 文字列を取得し
{"obj2":{"a":1,"b":2,"array":[{"test":"hoge","pageX":101,"pageY":102,"identifier":321},{"test":"hoge","pageX":201,"pageY":202,"identifier":987}]},"obj3":{"c":3,"d":4}}

goで書いたAPIサーバにて受け取った文字列を見てみると次の通りでした。
{"obj2":{"a":1,"b":2,"array":[{"test":"hoge","pageX":101,"pageY":102,"identifier":321},{"test":"hoge","pageX":201,"pageY":202,"identifier":987}]},"obj3":{"c":3,"d":4}}

まったく一緒です。

なお見通しがきくように javascript 側と go サーバ側を次のように書き換えてみました。

            let jsonStr = JSON.stringify(obj);
            var params = "json=" + jsonStr;
            xhr.send(params);
	form := r.PostForm
	output := fmt.Sprintf("%s", form["json"][0])
	file.Write(([]byte)(output))

ファイルには json 文字列が間違いなく出力されていました。

これにてWebページからオブジェクトをjson文字列に変換してPOST成功を確認

Goでローカルファイルにテキストファイルを書き出す

■前置き
WebページをユーザのクライアントマシンからWebブラウザで見てもらい
ユーザアクティビティを短い文字列情報でPOSTしてもらうことで、ユーザ端末で何が起きたのかを収集します。

ログを出すだけじゃ意味がなく、受け取った情報をファイルに保存しなければなりません。
POSTメッセージを捌く go の HTTP サーバを別ポートで起動し、疎通確認を行ったのが前回
今回は、受け取ったメッセージをファイルに保存してみせます。

■調査
go でファイルを操作する時にimport するパッケージは?
osパッケージを利用するのが基本とのことです。
参考
ファイル入出力 - はじめてのGo言語

前回からの差分としては以下のコードを追加しました

import (
	"os"
)

	file, err := os.Create("osfile.txt")
	if err != nil {
		fmt.Printf("os.Open err = %v\n", err)
		return
	}
	defer file.Close()
	output := fmt.Sprintf("%v", form)
	file.Write(([]byte)(output))

HTTPメッセージPOSTを受け取ったらそのままファイル出力するだけのコードは以下の通り(動作確認済み)
送られてくるたびにファイル名が書き換わる便利機能を追加しておきました。

package main

import (
	"fmt"
	"net/http"
	"os"
	"time"
)

func apiname1(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		fmt.Fprintf(w, "this is apiname1")
		return
	}
	w.Header().Set("Content-Type", "text/plain")
	r.ParseForm()
	form := r.PostForm
	// params := r.Form

	layout := "2006_01_02_15_04_05"
	fname := time.Now().Format(layout) + ".txt"
	file, err := os.Create(fname)
	if err != nil {
		fmt.Printf("os.Open err = %v\n", err)
		return
	}
	defer file.Close()
	output := fmt.Sprintf("%v", form)
	file.Write(([]byte)(output))
}

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

日付のフォーマットがなかなか考えられていて、こういう風に書きたいってレイアウトを書くとその通りに出してくれる。
それぞれの数字がユニークで、時間や分を意味している。
一覧はここで確認する。

クライアントのブラウザの操作から Webサーバ側でファイルを書き出せたのでよしとします。
なお、出力フォルダは go プログラムをルートとしたところから指定できました。

GoのAPIサーバの疎通確認

■前置き
何かしらのユーザアクティビティを収集するWebページをjavascriptで書いていて、セキュリティの関係でクライアントのローカルストレージに一切アクセスできないということに気づいたとき
Webサーバ側にクライアントの情報を送信するしかないってことに同時に気付くことになるわけですよね

つい先日Windows 環境で IIS を使ってお手軽にローカルネットワークで Webサーバを構築したわけですけど、実はこの Web サーバ側も javascript で動くわけですから、もちろんローカルストレージに一切アクセスできません。

はぁ( ゚Д゚)?って思いながら解決策を調べると
ポート番号80でWebサーバを起動している傍らで、別ポート番号でWebAPIサーバを起動して、クライアントからのアクセスにより特別な処理を行うそうです。
POSTメッセージを送信する場合は、この別ポートを指定して送ります。

その別ポート番号で HTTP を処理するサーバを node とか go で書くという話で、記述が見つかります。
今回は go で HTTP メッセージをさばく WebAPIサーバとやらをゼロから組んで疎通確認まで行ってみます。

■思想

ポート番号を指定して http サーバスタート!
クエリストリングを見て、実行する関数をスイッチ

という非常に簡素なロジックから説明して、書式を示す優れたエンジニアの記事を探してみたところ…
qiita.com
あった!

以下の go をデバッグ実行すると、ちゃんと localhost:62874 にアクセスしたときに handler 関数にてブレークポイントを張っているとブレークします。

package main

import (
  "fmt"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, World")
}

func main() {
  http.HandleFunc("/", handler)
  http.ListenAndServe(":62874", nil)
}

すごいと思ったのが、次のように apiname1って書いたら、localhost:62874/apiname1にアクセスしたときにだけ反応しました。

  http.HandleFunc("/apiname1", handler)

次に、単純に post するクライアントコードを作ってみてWebAPIサーバが反応するか見てみましょう。

<html xmlns="http://www.w3.org/1999/xhtml">

<head>
    <title></title>
    <script language="javascript" type="text/javascript">
        function Post() {
            var xhr = new XMLHttpRequest();
            var rootURL = window.location.href.split('/').slice(0, 3).join('/');
            var url = rootURL + ":62874/apiname1"; // WebAPI Server port and API name
            var params = "hoge=fuga&test=hello";
            xhr.open("POST", url, true);
            xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
            xhr.onreadystatechange = function () {
                if (xhr.readyState == 4) {
                    target = document.getElementById("output");
                    target.innerHTML = "complete status = " + xhr.status;
                }
            }
            xhr.send(params);
        }
    </script>
</head>

<body>
    <input type="button" value="Post" onclick="Post();" />
    <div id="output"></div>
</body>
</html>

さらにサーバーコードを次のように書き換えます。

package main

import (
	"fmt"
	"net/http"
)

func apiname1(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		fmt.Fprintf(w, "this is apiname1")
		return
	}
	w.Header().Set("Content-Type", "text/plain")
	r.ParseForm()
	form := r.PostForm
	fmt.Printf("form = %v\n", form)
	params := r.Form
	fmt.Printf("params = %v\n", params)
}

func main() {
	http.HandleFunc("/apiname1", apiname1)
	http.ListenAndServe(":62874", nil)
}

クライアントから go のサーバへparams情報が渡ることを確認しました。
疎通確認はとれたことになる

■参考サイト
In Introduction to HTTP Basics

www.yoheim.net

Webサーバーがブラウザから情報を受け取るサンプル

メモ書き、あとで消します。

■前置き
Webサイトを閲覧する iPad で行われる操作ログをファイルとして保存しようにも、保存することができない仕様なので詰む
解決するにはブラウザで処理した内容をサーバーに送るしかない
どうやって?

■前提知識
HTTP というプロトコルで Webサイトを閲覧しているが、基本的に iPad などのクライアントからリクエストメッセージをWebサーバーに送り、その返答として html ファイルの内容を受け取っている
qiita.com

じゃあリクエストメッセージに、操作ログを載せればいいじゃないか

その通り

■前置き2
リクエストメッセージの種類には代表的に GET, POST の2種類があるが、操作ログなどの大量のバイナリ情報を Web サーバーに送る場合は POST を使う。
POST で任意のバイナリを送り、Webサーバーの特定のフォルダにファイルとして、または、サーバーの javascript の処理でバイト配列としてそのバイナリ情報が渡るサンプルコードを確認できればいい。

■本題
この辺かな?
POSTでリクエストを送信してテキストを受信する

第四章 サーバーにデータを送る:POSTメソッドでデータを送信する

前回準備した Webサーバーの仕組み(IIS) は POST を許可していないので、自分のローカル環境でテストできない。

POST を許可する方法はこちら
ameblo.jp

なんか他人と同じ設定で動きませんね、私は以下の設定でやっとPOST を受け入れてくれました。
f:id:simplestar_tech:20180510014836j:plain

で、POST を受け取ったサーバーが、情報をファイル出力するには?
サンプルを探します。

これかな?
qiita.com

まだ自分の言語習熟度が低くて、サーバーだけで処理するために Main を使う?
そうです、javascript じゃなくて java のサーバーコードだった。(´ー`)
ローカルのブラウザでは accept が走らないのは、index.html にこの java の Main 関数の記述がないから
サーバーマシンでは別途 java の accept を実行するプロセスを起動して、POST メッセージを待ち受け、処理します。

WindowsでWeb アプリ開発環境作る

■前置き
とにかく今こうして作業しているマシンがローカルネットワーク環境でWebサーバーとして機能すればいい
index.html を配置したフォルダがルートになって Web サーバーとなる仕組みがほしい

■本題
IIS がキーワード、導入手順は以下のどれも同じ手順を紹介してくれています。
Windows10でIISを(ホームページ)有効化する設定
WINDOWS10にてIIS(WEBサーバー)をインストール・構成する方法
https://creativeweb.jp/personal-site/iis/

ディフォルトだとシステムドライブ
C:\inetpub\wwwroot
がルートになっているので、簡単にファイルを編集できません。
作業が面倒くさくなるので、これを変更します。

Cortanaに「インターネット インフォメーション サービス」と打ち込めば IIS マネージャーが表示されるので
Default Web Site の Web サイトの管理>詳細設定を開いて、物理パスを書き換えます。

f:id:simplestar_tech:20180507235300j:plain
詳細設定画面

あとは、ホームページを編集してきた要領で html や javascript の編集を行ってアプリを作って別マシンでブラウザを開いてチェックしてみてください。
私は問題なく、サブPCから編集した Web サイトを閲覧できました。(お堅いトップ絵を、好きな画像にすり替えてみるとか、とてもわかりやすいですよ。)

Webサーバーと機能を提供した IIS について知りたくなったら、こちらを読みましょう
Web サーバー (IIS) の概要

注意点として、20人以上から接続できないとのこと…