simplestarの技術ブログ

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

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人以上から接続できないとのこと…

Go言語の通信スケルトンコード例

■前置き

Goってのはマルチコア処理で効率的に同時アクセスをさばく、現時点で最強?のネットワークプログラミング言語ですので
その機能を最大限に発揮するスケルトンコードをチュートリアルで公開しているものと思っていました。
が、あまりにプリミティブな(基礎的な)ことしか頭に入ってこなかったので、仕方なく自作することにしました。

ゼロから作るのは非効率でしたので、参考コードとして前々回紹介した以下の二つのコードを使わせてもらいました。
golang socket server & client ping-pong demo
go server-clientアプリケーションの実装

■本題

複数クライアントからの同時アクセスを受け付けるサーバーとクライアントのコードを書いてみました。
足りない部分は公式ドキュメントを頼りに実装し、動作確認まで済ませました。

1.クライアントを起動するとサーバーに接続、クライアントで適当な文字列を打ち込むと、これをサーバーに送ります。
2.サーバーは複数のクライアントのセッションを張り続け、一つのクライアントから文字列が送られてきたら、同じ内容をそのクライアントだけに返します。
3.クライアントはサーバーから文字列を受け取ったら表示し、次の文字列の入力を待ちます。
4.サーバーはセッションが切れたら、クライアントからの送信待ちループを抜けてコネクションを閉じます(サーバーは終了せずに、次の接続を待機します)

server.go

package main

import (
	"bufio"
	"io"
	"log"
	"net"
	"strconv"
	"strings"
)

func main() {
	port := 7777
	SocketServer(port)
}

// const meesages
const (
	StopCharacter = "\r\n\r\n"
)

// SocketServer start a server
func SocketServer(port int) {
	listen, err := net.Listen("tcp4", ":"+strconv.Itoa(port))
	defer listen.Close()
	if err != nil {
		log.Fatalf("Socket listen port %d failed,%s", port, err)
	}
	log.Printf("Begin listen port: %d", port)
	for {
		conn, err := listen.Accept()
		if err != nil {
			log.Fatalln(err)
			continue
		}
		go handler(conn)
	}
}

func handler(conn net.Conn) {
	defer conn.Close()
	var (
		buf = make([]byte, 1024)
		r   = bufio.NewReader(conn)
		w   = bufio.NewWriter(conn)
	)
CLOOP:
	for {
		var received []string
	ILOOP:
		for {
			n, err := r.Read(buf)
			data := string(buf[:n])
			switch err {
			case io.EOF:
				break CLOOP
			case nil:
				hasStopCharacter := false
				if strings.HasSuffix(data, StopCharacter) {
					hasStopCharacter = true
					data = strings.TrimSuffix(data, StopCharacter)
				}
				log.Println("Receive:", data)
				received = append(received, data)
				if hasStopCharacter {
					break ILOOP
				}
			default:
				log.Fatalf("Receive data failed:%s", err)
				return
			}
		}
		echo := strings.Join(received, "")
		w.Write([]byte(echo))
		w.Flush()
		log.Printf("Send: %s", echo)
	}
}

client.go

package main

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"os"
	"strconv"
	"strings"
)

func main() {
	var (
		ip   = "127.0.0.1"
		port = 7777
	)
	SocketClient(ip, port)
}

// const messages
const (
	Exit          = "exit\r\n"
	StopCharacter = "\r\n\r\n"
)

// SocketClient start client
func SocketClient(ip string, port int) {
	addr := strings.Join([]string{ip, strconv.Itoa(port)}, ":")
	conn, err := net.Dial("tcp", addr)
	defer conn.Close()
	if err != nil {
		log.Fatalln(err)
	}
	for {
		inputln := WaitInputln()
		log.Printf("inputln: %s", inputln)
		if 0 == strings.Compare(inputln, Exit) {
			break
		}
		SendMessage(conn, inputln)
		WaitServerMessage(conn)
	}
}

// WaitInputln wait inupt line
func WaitInputln() string {
	for {
		reader := bufio.NewReader(os.Stdin)
		fmt.Print("Input text: ")
		text, _ := reader.ReadString('\n')

		if len(text) > 1 {
			return text
		}
	}
}

// SendMessage sand message to server
func SendMessage(conn net.Conn, message string) {
	jointed := strings.Join([]string{message, StopCharacter}, "")
	conn.Write([]byte(jointed))
	log.Printf("Send: %s", message)
}

// WaitServerMessage wait server message
func WaitServerMessage(conn net.Conn) {
	buff := make([]byte, 1024)
	n, _ := conn.Read(buff)
	log.Printf("Receive: %s", buff[:n])
}

Go の GC について気になったので調べた
かなりシンプルなアルゴリズムを使って、Java などと比べるとかなり停止時間が小さく、スループットも意識して改善が続けられているとのこと
5年前にはいくつかのケースでメモリリークが見つかっていたが、最近はそれらの問題は解決されているとのこと
postd.cc

GoのIDE試し

■前置き1
Go言語学習を、ずっとテキストファイルで進めてきましたけど、そろそろ限界
C#のようにVisual Studio のインテリセンスがあるなら使いたい

Visual Studio Code で、Go サポートがあるので、設定して試してみます。

■本題

Visual Studio Code

VSCode 入れたら Extensions for the Visual Studio family of products のページに飛んだので go って調べたら Microsoft が FREE で用意してくれていた。
これを選択してインストールしました。
なんか bin フォルダにいっぱいインストールされた。
とにかく環境が整った模様。

Installing 10 tools at C:\Users\yourname\go\bin
gocode
gopkgs
go-outline
go-symbols
guru
gorename
godef
goreturns
golint
dlv

Installing github.com/nsf/gocode SUCCEEDED
Installing github.com/uudashr/gopkgs/cmd/gopkgs SUCCEEDED
Installing github.com/ramya-rao-a/go-outline SUCCEEDED
Installing github.com/acroca/go-symbols SUCCEEDED
Installing golang.org/x/tools/cmd/guru SUCCEEDED
Installing golang.org/x/tools/cmd/gorename SUCCEEDED
Installing github.com/rogpeppe/godef SUCCEEDED
Installing github.com/sqs/goreturns SUCCEEDED
Installing github.com/golang/lint/golint SUCCEEDED
Installing github.com/derekparker/delve/cmd/dlv SUCCEEDED

All tools successfully installed. You're ready to Go :).

デバッグ実行する場合は node 入れます。(あれ、いらない?)
node をインストールします。
Node.js

lauch.json にて構成の追加ボタンが現れたので go debug を選び、node の設定を消したらデバッグできました。
ステップイン、ステップアウトも可能です。
注意点として、コンソールの標準入力をテストできないという問題がありました。

調べたところ、みんな困っています。対処方法にLunch.jsonをいじるとあるが
go の場合の Lunch.json の編集方法がわからん…

追記 2019/11/04

それが普通に Visual Studio Code で次のプラグイン入れたらデバッグできたよ
f:id:simplestar_tech:20191104120718p:plain

Goはローカルで通信確認できる?

■前置き1

Go 環境整って Hello World 動きます。
言語の目的がマルチコアのネットワークブログラムなんだから、数行でサーバーとして機能するんだよね?
どういう書式か確認させてもらおう

■前置き2

先に書式を学んで行け、馬鹿野郎
とドキュメントに怒られたので、A Tour of Go を体験することにしました。

関数定義、変数定義は型を変数名の後ろに持ってくることでいけます。(変数名から入れるって直感的でいいですね)
初期化時に型が確定している場合は、型を宣言しなくてよくなります。(ビルドに時間かかりそうな書式だな…)
未初期化変数は0
明示的な型変換が必要 float64 a = int 0 みたいなことができない
const が使える(やった)

書式を習っていて驚いたこと

  • 関数の戻り値は一つじゃなくてもいい(柔軟ですね!)
  • naked return という新しい概念が使える split(sum int) (x, y int) という、引数の後ろに引数のようなものを記述してコンパイルが通り、そしてこの二つ目の定義が戻り値として戻ってくる(読めねぇよ!)
  • 標準の型で複素数の complex がある(やるな!)
  • defer というキーワードにより、関数を抜ける時に実行させられるLIFO(last-in-first-out)
  • ポインタが使えるがポインタ演算はない(ん?)
  • 構造体のポインタからメンバアクセスが . でできる(これでポインタと参照切り替え時にコード書き換えなくていいね)
  • 配列がスライス可能、スライスは参照
  • null は nil らしい
  • foreach に当たる構文は range で優しいことに要素のインデックスも渡してくれる
  • while に相当するのは for {}
  • for {} にラベルを付けると swich の中からでも for {} を抜けることができる(この書式を待ってました!)
  • map のキー確認に contentskey なる関数が不要、複数戻り値の2番目に true, false が返ってくる(これはうれしい)
  • 関数に関数オブジェクトを渡せる
  • クロージャー?という概念で、関数の戻り値が内部生成関数のようでいて、その内部生成関数の戻り値、おおもとの関数のローカル変数を内部関数が利用できる点で、そのローカル変数にバインドできるというもの

ここでいう関数内の内部関数がクロージャだとか(アニメーションは頭の中でできるけど、学習コスト高そうなのであんま使いたくないな)

クロージャーのエクササイズで私が書いた答え(なんか汚いな)

package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
	p := 0
	s := p
	return func() int {
		d := p
		if p == 0 {
			p = 1
		}
		p = p + s
		s = d
		return d
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}

Slice の Exercise では次のコードを答えに書きました

package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
	img := make([][]uint8, dy)
	for y := 0; y < dy; y++ {
		img[y] = make([]uint8, dx)
		for x := 0; x < dx; x++ {
			img[y][x] = uint8(x*y)
		}
	}
	return img
}

func main() {
	pic.Show(Pic)
}
||< 

Exercise: Maps の私の答えは次の通り

>|go|
package main

import (
	"golang.org/x/tour/wc"
	"strings"
)

func WordCount(s string) map[string]int {
	m := make(map[string]int)
	for _, v := range strings.Fields(s) {
		if e, ok := m[v]; ok {
			m[v] = e + 1
		} else {
			m[v] = 1
		}
	}
	return m
}

func main() {
	wc.Test(WordCount)
}

■前置き3

前置きの A Tour of Go による書式習得時間が長い…
サーバーとして起動するには?
net パッケージを import して port listen すればよいとのこと

参考にした記事
golang socket server & client ping-pong demo
go server-clientアプリケーションの実装

同時接続のところで、さっそく goroutine (ゴルーチン) 使うコード出てきちゃった、また tour of Go やり直します。
go って書くと軽量マルチコアスレッド処理が起動して チャネル( Channel )型 で計算が完了するのをブロックして待つとのことです。

あ、フィボナッチはこう書くのか、そしてバッファーチャネルやクローズを学ぶ

package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

Go 言語はプログラマが何年も、何十年も悔しい思いをして
無駄に煩雑に書いてきたロジックを、構文を使ってすっきりと書けるようにしてくれる小技が多く感じられる
超好きになった!

■本題

先ほど紹介した記事
golang socket server & client ping-pong demo

こちらを go build して試すことでローカルでの通信確認できました。
長らくコードの書式を頭に叩き込みましたので、コードの流れが追えるようになっています。

これにて目的達成!

Go の Hello World はどうやって確認?

■あ

Go 言語のトップページで Hello World を確認できる
The Go Programming Language

コードをオンラインで解析して、結果を返してくれるみたい

java っぽく、package 名をファイル頭で定義して
import で出力関数を利用できるようにし
main 関数(プログラムのエントリポイント)で、文字列を出力するよう命令

実行すると、文字列が出力される

というもの

■前置き1

オンラインもいいけど、ローカルでコンパイルして、ローカルで実行することをやりたいです。
確実に動く Hello World のコードは見たので、ローカルでコンパイルするときに必要なものと手順をここで明らかにさせてください。

ドキュメントを読むと、Get Started にまとめたので、必ず読めよってありました。
Documentation - The Go Programming Language

読みました。

手順は

■本題

1.あなたのマシンに適した Go をダウンロードしろ
2.ワークスペースを作る(インストールすると作られる)
3.ワークスペースの src フォルダに Hello World プログラムを置いてビルド
4.成果物を実行

◆手順1.
Windows版、Mac版、Linux カーネルを使ったディストリビューション用が用意されています。
私はWindows 環境なので Windows 版 go1.10.2 をインストール C:\Go フォルダに入った

◆手順2.
ワークスペース
どうやって設定する?
すでにインストール時に %USERPROFILE%\go として設定されています。
go フォルダは作っていなかったので作りました。

◆手順3.
go\src\hello フォルダを作り、そこに tes.go テキストファイルを作成し

package main

import "fmt"

func main() {
    fmt.Printf("hello, Go\n")
}

を記入して保存
コマンドラインを go\src\hello フォルダをカレントにして開きビルドコマンドとして go build を叩きます。
しばらくして hello.exe が作られます。(あ、ここフォルダ名なんだ…)

◆手順4.
exe を叩く
f:id:simplestar_tech:20180502134748j:plain

目的達成!