simplestarの技術ブログ

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

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

目的達成!

Go って何?

■前置き1

プログラミング言語は最終的にマシン語に翻訳されて、コンピュータの加算器で足し算を繰り返しますので、私から見ればどんな言語も同じに見えます。
Go は何なのかという問いは、どういう思想で作られているのかという点
違いはどれだけ人間に認識しやすく、また、どれだけ面倒なことをきっちりと管理して、思想が目指す処理を実現するかという部分に答えを得たい。

■調査1 どういう思想で作られた?

ネットワークをマルチコアで効率的にさばくのに最適な言語となるように Google が作った

今まで大規模オンラインゲーム開発の現場では、パフォーマンスの面で C++ しか選択肢がなかった状況の中
Java やその他の低速な言語を差し置いて、 Go はコンパイルしてマシン語にした実行ファイルを使って計算するようにして解決した。

まとめると、速度が C++ に迫るネットワーク向け軽量言語

Go公式ページ
The Go Programming Language

Go を使ってネットワークプログラムを動かしてみたくなりました。

環境準備

■前置き1

オンラインゲーム開発には UNIX ベースの OS からなるマシンが必要です。
一般の人が触れるようになった UNIX 仕様を満たすOSで有名なものが Linux で、そのLinuxディストリビューションLinux の種類を表すもの?)には Ubuntu, Cent OS などがあります。
ディストリビューションって Windows でいうところの 7 や 10 といった感じですかね? でも Cent OS 6 とか 7 とかあるし、やっぱり Windows = Cent OS というイメージ?
なんでいっぱい Linux があるんだろう?

http://nomado.uzusionet.com/index.php?Linux%E3%81%AE%E7%A8%AE%E9%A1%9E%E3%81%AF%E3%81%AA%E3%81%9C%E6%B2%A2%E5%B1%B1%E3%81%82%E3%82%8B%E3%81%AE%E3%81%8B%EF%BC%9Fnomado.uzusionet.com

Linux」とは正確に言うとOSの中核になる「カーネル」と呼ばれる部分だけを指し
カーネル以外の組み込み作業は、一般のユーザーが簡単に行えるものではない
各種の企業や非営利団体が独自に組み込み作業を行い、一般のユーザーでも簡単にインストールできるようにパッケージ化
このパッケージのことをディストリビューションと呼んでいる

なるほど、そもそも Linux の認識から間違っていたのか、カーネル部分が Linux で、その他もろもろが入ってディストリビューションなのね
ディストリビューションの中に Linuxカーネルがあるとイメージを変えることで理解できた。(無知な人の間違った説明を受けて、今まで混乱していたのか)

■前置き2

マシンを用意するという面倒な作業があるので参入障壁が高いと思っていましたが
最近の Windows には for Linux システムがあるので、これを使えば極めて高速に Linux のアプリケーション開発と実行ができます。
調べてみると、ネットワーク対応はしないとのことでサーバーとして運用することはできません。
(じゃ、困るんですけど)

Cent OS をインストールするというキーワードで調べると、VirtualBox (Windows) に Cent OS が入るらしいので、こちらでなんとかなるかもしれません。
qiita.com
でも、つながるだけの部分で色々と気をもむし、勝手に接続できなくなるらしいので、作業のモチベーションが上がらない。

そこで、お金をかけてでもクラウドサービスを始めてみて、ネットワーク上に Ubuntu や Cent OS を用意して、ローカル環境じゃなくてインターネットでつないでテストしていきたいと思います。

■前置き3

全然知識ないのですが C++ でサーバーコード書くとハンザツになるそうです。
最近は軽量言語でもパフォーマンスが出る Go とか Rust を使って簡単にサーバープログラムを書けるらしいので、私はそこから学んでみたい。
ですが、そもそもクラウドサービスもLinuxディストリビューションについても、何にもわからない。

学習作業のゴールを決めてみます。
1.pingを打ち合う適当なプロトコル定義したらコード自動生成にて Go か Rust でサーバーコードが出力されて、C# Unity クライアントも出力されることを確認する
2.出力されたサーバーコードをクラウドサービスの複数のディストリビューションで実行し、Unity クライアントからそれぞれのクラウドマシンに ping を飛ばしたら、往復時間を算出するところまで確認する
3.データベースにKVSとRDBを用意して、それぞれに値を書き込んで、読み出すプロトコルを定義して動作確認する。

要は世にある大規模オンラインゲームの基本的な要素を用意できるようになりたいわけです。

■作業1

Go って何?
Hello World はどうやって確認?
ローカルで通信確認できる?
サーバーで実行するには?

■作業2

クラウドサービスには何がある?どれ使うべき?
クラウドで何個かディストリビューション用意して常駐させるには?
さっき作った Go のサーバーってどうやって配置?
作ったディストリビューションにつながる?

■作業3

データベースっていっぱいあるの?どれがいいの?
ローカルで試せる?
サーバで試してみる

ここまでくれば、最初の学習作業はゴールしてるよね