- 投稿日:2020-12-18T23:44:29+09:00
Goの準々標準ライブラリの探索
フューチャーアドベントカレンダー21日目のエントリーです。全25話中、神回が25回あったウルトラマンZが終わってしまいましたね。全日本人のうちの1億2000万人ほどがウルトラマンZロスでうちひしがれているころかと思います。
Goでは、Goをインストールしたときに一緒にインストールされるライブラリを「標準ライブラリ」と呼びます。また、golang.org/xなパッケージ群は準標準ライブラリと呼ばれます。準標準ライブラリには多彩なライブラリが含まれます。今ではもう本家に入ってしまって、後方互換性のためだけに残っているパッケージも一部ありますが・・・
これ以外にも https://github.com/golang/ にはたくさんパッケージがあります。一部はgolang/xのものも入っているのですが、それ以外のものをここでは仮に準々標準ライブラリと呼ぶことにします。
golang.org/xのものは、だいたいリポジトリの説明に
[mirror]
と入っているので、それを見れば識別しやすいかと思います。本エントリーではおすすめの準々標準ライブラリを紹介するとともにそのコードを読んでみます。ちなみに、go.chromium.orgも面白そうなライブラリがいっぱいあって、これのerrorsとかloggingとかモック可能なmath/randとか、標準ライブラリをとことん強くしてみた感が興味深いのですが、キリがないのでやめておきます。
あとは、pkg.go.devのコードのリポジトリにも、TeeProxyとか面白げなものがいろいろあったんですが、internalなのでそのままimportできずにコピーせざるを得ないという。やはりinternalは悪い文明!
github.com/golang/groupcache/consistenthash
github.com/golang/groupcacheはmemcachedのリプレースを狙った、分散キャッシュシステムとのこと。単独のアプリケーションのサーバーと、クライアントライブラリの組み合わせです。で、これは、分散キャッシュでよく利用されるアルゴリズムのコンシステントハッシュです。ワクワクしますね。
引数のkeysは一部peersの方が分かりやすいのではないか、というか、入力が全部keyなのは何かの間違い感があります。これを利用しているコードをみる方が分かりやすいですね。
package main import ( "fmt" "github.com/golang/groupcache/consistenthash" ) func main() { h := consistenthash.New(3, nil) h.Add("host1", "host2", "host3", "host4", "host5") self := "host1" // 自分 peer := h.Get("content's key") if self == peer { fmt.Println("このファイルはこのノードにあります。") } else { fmt.Printf("このファイルは%sにあります\n", peer) } }https://play.golang.org/p/GzEtCydb0Ij
これを利用している親パッケージのコードをみるだけだと、ノードが落ちた時の復旧とかどうするのか、そのあたりはよくわからなかったです。1つでも落ちたらイミュータブルにクラスタ丸ごと再起動とかやっているんですかね? デフォルトのレプリカ数50というのが強い。斜め読みしかしてないですが、これを紹介するスライドがありました。
github.com/golang/groupcache/lru
上記のキャッシュサーバー内部で使われているLRUキャッシュです。hashicorpの方が強そうですが、まあシンプルで良いですね。わかりやすいです。コードリーディングにもおすすめ。
package main import ( "fmt" "github.com/golang/groupcache/lru" ) func main() { c := lru.New(10) // 0だと無限 // 計算結果をキャッシュ c.Add(2, 2 * 2) c.Add(3, 3 * 3) c.Add(4, 4 * 4) result, found := c.Get(4) fmt.Println(result, found) }https://play.golang.org/p/QonPQi3UwzJ
github.com/golang/gddo/httputil
Webサービスであるgodoc.orgのリポジトリの中のパッケージです。リアルワールドなアプリケーション向けに作られた生々しいライブラリです。これが紹介したくてこのエントリーを書いたようなものです。サーバーとクライアントの間のコンテンツネゴシエーションを行う関数とか、あるいはバッファリングを行うhttp.ResponseWriterなどがあります。
コンテンツネゴシエーションは、ブラウザが欲しい(理解可能)なフォーマットと、サーバーが提供可能なフォーマットの仲介をする仕組みです。もちろん、みなさん、Real World HTTPですでに学習済みですよね。
きちんとq値を見ての選択もしてくれます。最近は画像フォーマットがいろいろ増えてきていています。お互いにより効率の良い形式で返したい、みたいなことが簡単に実現できます。標準ライブラリのhttputilに入れて欲しい。他にもNegotiateContentEncodingもあります。
package main import ( "fmt" "net/http/httptest" "github.com/golang/gddo/httputil" ) func main() { req := httptest.NewRequest("GET", "http://localhost:8080/item", nil) req.Header.Set("Accept", "image/avif, image/webp;q=0.9, */*;q=0.8") t := httputil.NegotiateContentType(req, []string{"image/avif", "mage/heif", "image/heic", "image/webp", "image/png"}, "image/png") fmt.Printf("acceptable content-type: %s\n", t) }https://play.golang.org/p/g08pPFJUNM5
StaticServerは静的ファイルを返すサーバーです。標準ライブラリにもServeFileあるじゃんと思われた方もいると思いますが、こちらはより積極的なキャッシュ戦略が選択できます。
余談ですが、標準ライブラリのhttp.ServeFileには脆弱性があるんですね。知りませんでした。ServeFileのドキュメントにも、将来的には..はリジェクトされるよ、と書いてありますね。
こちらのStaticServerはディレクトリごと公開もできますし、特定のファイル(単数or複数)のみの公開もできるAPIも別に用意されています。ホワイトリストでできるのは安心ですね。
ResponseBufferは、http.ResponseWriterを満たす構造体です。ミドルウェアでwをこいつに差し替えて、レスポンスの内容をログに出力する、みたいなことが簡単にできますね。
CacheBustersはその名の通り、コンテンツが変わった時にキャッシュを使わせないようにクエリーパラメータを付与するライブラリです。ETagとかLast-Modifiedヘッダーを元に生成します。
package main import ( "fmt" "net/http" "github.com/golang/gddo/httputil" ) func main() { cbs := &httputil.CacheBusters{Handler: http.FileServer(http.Dir("."))} path := cbs.AppendQueryParam("main.go", "cache") fmt.Printf("path: %s\n", path) // path: main.go?cache=Fri-18Dec202022-45-40GMT }github.com/golang/gddo/log
ロガーをcontext.Contextに差し込むミドルウェアです。contextの使用例として無駄がない。
その他の公式サービスが使っている気になったサードパーティライブラリ/ツール
公式が使っているのだからきっと質が高いものも多かろう、ということで。自分が知らなかったものをピックアップ。
github.com/client9/misspell/cmd/misspell
github.com/golang/blogやgithub.com/golang/pkgsiteで使われていたものです。名前からしてスペルチェックですよね。
sourcegraph.com/sourcegraph/go-template-lint
github.com/golang/blogやgithub.com/golang/pkgsiteで使われていたものです。テンプレートのLinterのようです。上のツールと同様、にGo製ウェブサイト用の汎用タスクのスクリプトで利用されていて、こちらも興味深いですね。
github.com/inconshreveable/log15
github.com/golang/gddo/logで使われていたライブラリです。シンプルな構造化ログとのこと。logfmt形式でデフォルトで出力されるが、JSONでも出力できるとのこと。
なお、github.com/golang/gddo/logはcontext.Contextにロガーを格納していました。context.Contextにはロガーなどは入れないみたいな説明が日本では見かけたことがありますが、公式がやっているんで、入れちゃってもいいってことですね。
github.com/yuin/goldmark
これも同じく、github.com/golang/websiteで使われていたMarkdownパーサーですね。
github.com/magiconair/properties
Javaのpropertiesファイルを読み込んで構造体にマッピングするライブラリ。エンタープライズな香りがする!
まとめ
最近、github.com/golangを覗いてみたらいろいろ見つけたのでその紹介でした。brotliをサポートしていたら、内部で持っているbrotli形式のデータをダイレクトに返すアセットバンドラー&http.FileServerもどき(インタフェースは満たしてないので別物)を実装したときに、httputil.NegotiateContentEncoding相当のものを雑ですが自前実装していたので、先に知っておけば!!!!!みたいに思った悔しさをエントリーにしました。
それ以外にも、有名どころだと github.com/golang/snappy とか、 github.com/golang/freetype とかありますが、そのあたりは今更説明するまでもないでしょう。
- 投稿日:2020-12-18T23:39:35+09:00
競技プログラミング用 CLI & テストライブラリ contest.go の紹介
はじめに
この記事は次のアドベントカレンダーの 18 日目の記事です。
当初 Go 3 カレンダーに投稿するつもりだった記事ですが、ふと競技プログラミングのカレンダーを見たらちょうど同じ 18 日目が空いておりましたので、両カレンダーにダブル投稿させていただくことにしました。ダブル投稿できると思っていたら、実はできませんでした。すみません。うーむかっこ悪い…。contest.go
contest.go は、 Go 言語で競技プログラミングコンテストに出場することを支援する目的で作った CLI とテストライブラリです。次のような特徴があります。
- 解答プログラムが標準入出力を使用するコンテストに対応 (AtCoder, Codeforces, Google Code Jam など)
- go test で自分の解答のテストができる (制限あり)
- AtCoder 対応
- 過去のコンテスト、開催中のコンテストの問題のテストケースを自動ダウンロードする機能
- 解答を提出する機能はまだない
実際に contest.go を使ってみたいという方のために、チュートリアルの形で使用法を説明していきたいと思います。
チュートリアル
インストール
contest.go を使うには、まず Go の開発環境が必要になります。 Go で競技プログラミングコンテストに出場するのですから、当然用意していますよね?
ということで、最初に次のコマンドで CLI コマンドである contest-cli をインストールしてください。
$ go get github.com/yaegashi/contest.go/cmd/contest-cli解答テンプレートとテストケースの生成
次に空の Git リポジトリないしフォルダを作り、その中で適当な名前の Go モジュールを作ってください。
$ git init solutions Initialized empty Git repository in /home/yaegashi/solutions/.git/ $ cd solutions/ $ go mod init solutions go: creating new go.mod: module solutionsconetst-cli new により、一般的な解答フォルダを作成できます。 contest.go ではこのように 1 プログラムごとに 1 フォルダを使用します。
$ contest-cli new foo 2020/12/18 20:54:12 I: Created foo/main.go 2020/12/18 20:54:12 I: Created foo/main_test.go 2020/12/18 20:54:12 I: Created foo/sample1.in.txt 2020/12/18 20:54:12 I: Created foo/sample1.out.txtフォルダ内には contest-cli 内蔵のテンプレートから簡単な解答プログラムとテストプログラム、正解になるテストケースが生成されます。 go test を実行して確認してみましょう。
$ go test -v ./foo go: finding module for package github.com/yaegashi/contest.go/tester go: found github.com/yaegashi/contest.go/tester in github.com/yaegashi/contest.go v0.0.4 === RUN TestContest === RUN TestContest/0:sample1.in.txt --- PASS: TestContest (0.00s) --- PASS: TestContest/0:sample1.in.txt (0.00s) PASS ok solutions/foo 0.291sこのように正解の解答プログラムではテストが成功します。
AtCoder テストケースのダウンロード
次に AtCoder の過去のコンテスト問題のテストケースを contest-cli atcoder new で取り寄せてみましょう。
$ contest-cli atcoder new abc069 2020/12/18 20:56:55 I: Fetching AtCoder contest abc069 2020/12/18 20:56:55 I: Created abc069/a/main.go 2020/12/18 20:56:55 I: Created abc069/a/main_test.go 2020/12/18 20:56:55 I: Created abc069/a/sample1.in.txt 2020/12/18 20:56:55 I: Created abc069/a/sample1.out.txt 2020/12/18 20:56:55 I: Created abc069/a/sample2.in.txt 2020/12/18 20:56:55 I: Created abc069/a/sample2.out.txt 2020/12/18 20:56:55 I: Created abc069/b/main.go 2020/12/18 20:56:55 I: Created abc069/b/main_test.go 2020/12/18 20:56:55 I: Created abc069/b/sample1.in.txt 2020/12/18 20:56:55 I: Created abc069/b/sample1.out.txt 2020/12/18 20:56:55 I: Created abc069/b/sample2.in.txt 2020/12/18 20:56:55 I: Created abc069/b/sample2.out.txt 2020/12/18 20:56:55 I: Created abc069/b/sample3.in.txt 2020/12/18 20:56:55 I: Created abc069/b/sample3.out.txt 2020/12/18 20:56:55 I: Created abc069/c/main.go 2020/12/18 20:56:55 I: Created abc069/c/main_test.go 2020/12/18 20:56:55 I: Created abc069/c/sample1.in.txt 2020/12/18 20:56:55 I: Created abc069/c/sample1.out.txt 2020/12/18 20:56:55 I: Created abc069/c/sample2.in.txt 2020/12/18 20:56:55 I: Created abc069/c/sample2.out.txt 2020/12/18 20:56:55 I: Created abc069/c/sample3.in.txt 2020/12/18 20:56:55 I: Created abc069/c/sample3.out.txt 2020/12/18 20:56:55 I: Created abc069/c/sample4.in.txt 2020/12/18 20:56:55 I: Created abc069/c/sample4.out.txt 2020/12/18 20:56:55 I: Created abc069/c/sample5.in.txt 2020/12/18 20:56:55 I: Created abc069/c/sample5.out.txt 2020/12/18 20:56:55 I: Created abc069/d/main.go 2020/12/18 20:56:55 I: Created abc069/d/main_test.go 2020/12/18 20:56:55 I: Created abc069/d/sample1.in.txt 2020/12/18 20:56:55 I: Created abc069/d/sample1.out.txt 2020/12/18 20:56:55 I: Created abc069/d/sample2.in.txt 2020/12/18 20:56:55 I: Created abc069/d/sample2.out.txt 2020/12/18 20:56:55 I: Created abc069/d/sample3.in.txt 2020/12/18 20:56:55 I: Created abc069/d/sample3.out.txt上記の例では AtCoder Beginner Contest 069 に含まれる 4 つの問題の解答フォルダを作成しています。それぞれのフォルダに、解答のテンプレートとテスト実行のコード、そして AtCoder サイトからダウンロードされたテストケースが格納されています。
今回は解答テンプレートコードのままでは当然ながら不正解になるはずです。問題 A でテストしてみましょう。
$ go test -v ./abc069/a === RUN TestContest === RUN TestContest/0:sample1.in.txt tester.go:107: Wrong answer: --- want +++ got @@ -1,1 +0,0 @@ -6 @@ -0,0 +1,1 @@ +hello, 3 === RUN TestContest/1:sample2.in.txt tester.go:107: Wrong answer: --- want +++ got @@ -1,1 +0,0 @@ -1 @@ -0,0 +1,1 @@ +hello, 2 --- FAIL: TestContest (0.00s) --- FAIL: TestContest/0:sample1.in.txt (0.00s) --- FAIL: TestContest/1:sample2.in.txt (0.00s) FAIL FAIL solutions/abc069/a 0.277s FAILこのように、不正解の場合はテストが失敗します。また正答とのプログラムの出力の差分を表示してくれます。差分は、ただのテキストだとよくわかりませんが、 実際の端末ではカラフルに表示されます。
解答プログラムの作成
ABC069 の A 問題の解答プログラムを作成してみましょう。エディタでテンプレート生成された abc069/a/main.go を開きます。解答を記述する場所は、ファイルの最後にある
contest
型のmain()
メソッドです。func (con *contest) main() error { var s string con.Scan(&s) con.Println("hello,", s) return nil }go test を使用する都合上、解答プログラムの入出力は
fmt.Scan()
やfmt.Println()
といった関数の代わりにcon.Scan()
やcon.Println()
を使います。基本的にはfmt
をcon
に置き換えて書けばよいです。AtCoder のようなコンテストでは解答プログラムは 1 ファイルで提出する必要があるため、利用可能なメソッドは main.go の中にすべて定義してあります。
さて、この問題の解答としては次のようなプログラムを書けばよいでしょう。
func (con *contest) main() error { var n, m int con.Scan(&n, &m) con.Println((n - 1) * (m - 1)) return nil }正しいプログラムが書けたら、再度 go test を実行してみましょう。
このように go test が成功したらテストケースでの確認はできたことになりますが、それが正答とは限らないので、提出する前によく見直しましょう。
また contest.go が対応できないタイプの問題もあります。詳しくは後述する制限事項も参照してください。
AtCoder への提出
main.go ファイル全体が解答プログラムとなりますので、これをそのまま AtCoder に提出します。
ただし contest-cli にはまだ解答プログラムを提出する機能がないので、お手数ですが Web ブラウザを使って提出してください。
コンテストの解答プログラムは大した行数にはならないはずなので、エディタで main.go を全選択してからブラウザのフォームへのコピー・ペーストで実用上は問題ないと思います。
AtCoder ログイン
このチュートリアルでは AtCoder の過去コンテンストのテストケースをダウンロードしましたが、開催中のコンテストのテストケースをダウンロードすることもできます。そのためには contest-cli atcoder login でログインしておきます。
$ contest-cli atcoder login AtCoder Login: yaegashi Password:このようにログイン名とパスワードを入力してなにも表示されなければログインは成功しています。ログインのクッキーは ~/.contest-cli/atcoder.json に保存されています。
制限事項
現在の contest.go バージョン v0.0.4 には、次のような制約があります。
- テストの判定では、テストケース出力に対する単純な行単位の文字列マッチングしか行わないため、次のような問題では正しく判定ができません。
- 正答が 1 通りだけではない問題 (例: ABC069 問題 D)
- 浮動小数点の数を出力する問題
- インタラクティブ問題
- 生成する解答テンプレートのカスタマイズはできません。コンテストに出場するなら Go の開発環境はあると思いますので、 template.go の文字列定数を編集して自分でビルドしてください。
今後の計画
- AtCoder 以外のコンテストの対応: Codeforces などにも対応したいです。
- AtCoder 解答提出対応: ログインしてテストケースをダウンロードする処理まで作ったのですから、あと一歩で実現できる機能だと思いますが、 web ブラウザにコピペ提出で十分早いのでまだ手をつけずにいます。
- テスト判定の改善: 前述の制限事項を解消するため改善が必要だと思っています。カスタムジャッジが書けるとよいのですが、実際のコンテンストではそんなのを書いている余裕はないため、必要ない機能かもしれません。
- テンプレートのカスタマイズ: ~/.contest-cli フォルダにテンプレートファイルを置くだけの簡単なもので十分かもしれません。
おわりに
きっと同じような目的でもっと便利なソフトがすでに存在すると思うのですが、よく調査しておりませんでした。申しわけありません。
自分がこれまでのコンテストで Go 言語で作った解答プログラムやライブラリは diligence.go というリポジトリに記録していますので、よろしければ参考にしてください。
- 投稿日:2020-12-18T23:11:01+09:00
LeetCodeに毎日挑戦してみた 118. Pascal's Triangle(Python、Go)
Leetcodeとは
leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)
28問目(問題118)
118. Pascal's Triangle
問題内容
Given a non-negative integer numRows, generate the first numRows of Pascal's triangle.
(日本語訳)
負でない整数numRowsが与えられた場合、 パスカルの三角形の最初のnumRowsを生成します。
In Pascal's triangle, each number is the sum of the two numbers directly above it.Example:
Input: 5 Output: [ [1], [1,1], [1,2,1], [1,3,3,1], [1,4,6,4,1] ]考え方
空の配列を作成し、数字の数だけfor分を回します
ループ内、appendでその数に応じた[1]の要素を追加します
左端、右端は1なのでそれを除いた範囲で上の階層を参考に値を代入していきます
最終的に作成したlistsを戻り値とします
解答コード
class Solution: def generate(self, numRows): lists = [] for i in range(numRows): lists.append([1]*(i+1)) if i>1 : for j in range(1,i): lists[i][j]=lists[i-1][j-1]+lists[i-1][j] return lists
- Goでも書いてみます!
func generate(numRows int) [][]int { answer := make([][]int, numRows) for i := 0; i < numRows; i += 1 { a := make([]int, i+1) // add ones a[0], a[i] = 1, 1 if i > 1 { for j := 1; j <= i/2; j += 1 { a[j] = answer[i-1][j-1] + answer[i-1][j] a[len(a)-1-j] = a[j] } } answer[i] = a } return answer }
- 投稿日:2020-12-18T21:38:21+09:00
Goのredigoを使って堅牢なRedisとPub/Sub通信のPublishを行ってみる
はじめに
この記事はand factory Advent Calendar 2020の18日目の記事です。
昨日は @ichi-nakashimaさんの【React Hooks】Component から stateを伴ったロジックを切り出す技術たちでした。RedisにはPub/Sub機能がある
Redisと言えばKVS(Key-Value Store)として使われるのが一般的と思いますが、Pub/Sub機能も備わっているので、こいつを使ってGoとやり取りをしてみたいと思います。
RedisのPub/Sub機能とは
それぞれPublish(発行)、Subscrib(購読)という意味で、RedisではあるChannelを作成して、
そのChannelに対してメッセージをPublish(発行)したりSubscrib(購読)したりする(=メッセージの発信・受信みたいな)ことができる機能を指します。今回は堅牢なRedisを立ててそいつに対してGoからPublishを行ってみます。
Redisサーバーを立てる
一応自分で試した環境はVagrant+VM上のCentOS7にRedisをインストールしました。
以下はインストール時の一連(EPELのリポジトリ追加→Redisインストール)のログまずはEPELのリポジトリの追加
(※EPELについては CentOSなどで使う、EPELってなんだ? - Qiita などを参照してください。)[vagrant@localhost ~]$ sudo yum install epel-release Loaded plugins: fastestmirror Loading mirror speeds from cached hostfile * base: ftp.tsukuba.wide.ad.jp * extras: ftp.tsukuba.wide.ad.jp * updates: download.nus.edu.sg Resolving Dependencies --> Running transaction check ---> Package epel-release.noarch 0:7-11 will be installed --> Finished Dependency Resolution Dependencies Resolved =================================================================================================================================================================== Package Arch Version Repository Size =================================================================================================================================================================== Installing: epel-release noarch 7-11 extras 15 k Transaction Summary =================================================================================================================================================================== Install 1 Package Total download size: 15 k Installed size: 24 k Is this ok [y/d/N]: y Downloading packages: epel-release-7-11.noarch.rpm | 15 kB 00:00:00 Running transaction check Running transaction test Transaction test succeeded Running transaction Installing : epel-release-7-11.noarch 1/1 Verifying : epel-release-7-11.noarch 1/1 Installed: epel-release.noarch 0:7-11 Complete!次にRedisのインストール
[vagrant@localhost ~]$ sudo yum install redis -y Loaded plugins: fastestmirror Loading mirror speeds from cached hostfile epel/x86_64/metalink | 6.4 kB 00:00:00 * base: ftp.tsukuba.wide.ad.jp * epel: download.nus.edu.sg * extras: ftp.tsukuba.wide.ad.jp * updates: download.nus.edu.sg epel | 4.7 kB 00:00:00 (1/3): epel/x86_64/group_gz | 95 kB 00:00:01 (2/3): epel/x86_64/updateinfo | 1.0 MB 00:00:02 (3/3): epel/x86_64/primary_db | 6.8 MB 00:00:01 Resolving Dependencies --> Running transaction check ---> Package redis.x86_64 0:3.2.12-2.el7 will be installed --> Processing Dependency: libjemalloc.so.1()(64bit) for package: redis-3.2.12-2.el7.x86_64 --> Running transaction check ---> Package jemalloc.x86_64 0:3.6.0-1.el7 will be installed --> Finished Dependency Resolution Dependencies Resolved =================================================================================================================================================================== Package Arch Version Repository Size =================================================================================================================================================================== Installing: redis x86_64 3.2.12-2.el7 epel 544 k Installing for dependencies: jemalloc x86_64 3.6.0-1.el7 epel 105 k Transaction Summary =================================================================================================================================================================== Install 1 Package (+1 Dependent package) Total download size: 648 k Installed size: 1.7 M Downloading packages: warning: /var/cache/yum/x86_64/7/epel/packages/jemalloc-3.6.0-1.el7.x86_64.rpm: Header V3 RSA/SHA256 Signature, key ID 352c64e5: NOKEY0 B/s | 0 B --:--:-- ETA Public key for jemalloc-3.6.0-1.el7.x86_64.rpm is not installed (1/2): jemalloc-3.6.0-1.el7.x86_64.rpm | 105 kB 00:00:02 (2/2): redis-3.2.12-2.el7.x86_64.rpm | 544 kB 00:00:02 ------------------------------------------------------------------------------------------------------------------------------------------------------------------- Total 293 kB/s | 648 kB 00:00:02 Retrieving key from file:///etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 Importing GPG key 0x352C64E5: Userid : "Fedora EPEL (7) <epel@fedoraproject.org>" Fingerprint: 91e9 7d7c 4a5e 96f1 7f3e 888f 6a2f aea2 352c 64e5 Package : epel-release-7-11.noarch (@extras) From : /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-7 Running transaction check Running transaction test Transaction test succeeded Running transaction Installing : jemalloc-3.6.0-1.el7.x86_64 1/2 Installing : redis-3.2.12-2.el7.x86_64 2/2 Verifying : redis-3.2.12-2.el7.x86_64 1/2 Verifying : jemalloc-3.6.0-1.el7.x86_64 2/2 Installed: redis.x86_64 0:3.2.12-2.el7 Dependency Installed: jemalloc.x86_64 0:3.6.0-1.el7 Complete!Redisのインストールが完了したので立ち上げる
[vagrant@localhost ~]$ sudo systemctl start redis.serviceRedisの自動起動を有効にする
[vagrant@localhost ~]$ sudo systemctl enable redis Created symlink from /etc/systemd/system/multi-user.target.wants/redis.service to /usr/lib/systemd/system/redis.service.最後にRedisの状態を確認する
[vagrant@localhost ~]$ sudo systemctl status redis.service ● redis.service - Redis persistent key-value database Loaded: loaded (/usr/lib/systemd/system/redis.service; enabled; vendor preset: disabled) Drop-In: /etc/systemd/system/redis.service.d └─limit.conf Active: active (running) since Mon 2020-07-13 09:43:21 UTC; 31s ago Main PID: 22002 (redis-server) CGroup: /system.slice/redis.service └─22002 /usr/bin/redis-server 127.0.0.1:6379 Jul 13 09:43:21 localhost.localdomain systemd[1]: Starting Redis persistent key-value database... Jul 13 09:43:21 localhost.localdomain systemd[1]: Started Redis persistent key-value database.念のため
redis-cli
コマンドでping
を行い、PONG
と返ってくるか確認する。
※このping
コマンドは疎通確認によく使用されるコマンドです。[vagrant@localhost ~]$ redis-cli ping PONGあとは
/etc/redis.conf
を編集してIPアドレスなどを設定すればRedisのインストールは完了[vagrant@localhost ~]$ sudo vi /etc/redis.confRedisを堅牢にする(パスワードの設定)
[vagrant@localhost ~]$ echo "secretkey" | sha256sum 92d4e0a2ad853e81e6d4bf147618aae5a06f201e6e72a67794a542409c9d125a - [vagrant@localhost ~]$ sudo vi /etc/redis.conf
SECURITY
の項目があるので、その下にrequirepass
を追加し、パスワードを記述する。/etc/redis.conf################################## SECURITY ################################### # Require clients to issue AUTH <PASSWORD> before processing any other # commands. This might be useful in environments in which you do not trust # others with access to the host running redis-server. # # This should stay commented out for backward compatibility and because most # people do not need auth (e.g. they run their own servers). # # Warning: since Redis is pretty fast an outside user can try up to # 150k passwords per second against a good box. This means that you should # use a very strong password otherwise it will be very easy to break. # requirepass 92d4e0a2ad853e81e6d4bf147618aae5a06f201e6e72a67794a542409c9d125a(※ここでは
sha256sum
を使って生成したハッシュ値をパスワードとして設定していますが、ある程度の長さがあればいいと思います。)
/etc/redis.conf
編集後にRedisを再起動し、redis-cli
から先ずはパスワードなしの状態でコマンドを実行する。[vagrant@localhost ~]$ sudo systemctl restart redis.service [vagrant@localhost ~]$ redis-cli 127.0.0.1:6379> set key 10 (error) NOAUTH Authentication required.上記のように認証エラーがでることを確認。
次にauth
コマンドを書き、先ほど設定したパスワードを入れて実行する。127.0.0.1:6379> auth 92d4e0a2ad853e81e6d4bf147618aae5a06f201e6e72a67794a542409c9d125a OKOKが出れば認証された状態なので、コマンドを実行すれば以下のようにふつうに動くようになる。
127.0.0.1:6379> set key 10 OK 127.0.0.1:6379> get key "10" 127.0.0.1:6379> quitGoのサンプルコード
サンプルコードはパスワードを設定したRedisにパブリッシュする処理を書いています。(※パスワード設定は必須ではないです。念のため)
package main import ( "fmt" "github.com/gomodule/redigo/redis" ) // RedisPublish : コマンドをRedisへパブリッシュする func RedisPublish(message string) (int, error) { conn, redisAuthPass, err := ConnectionRedis() if err != nil { fmt.Println(err) return 0, err } defer conn.Close() // パス認証 if redisAuthPass != "" { _, err = conn.Do("AUTH", redisAuthPass) if err != nil { fmt.Println(err) return 0, err } } // パブリッシュ res, err := redis.Int(conn.Do("PUBLISH", "test_channel", message)) if err != nil { fmt.Println(err) return 0, err } return res, err } // ConnectionRedis : Redisと接続し接続情報とAuthパスを返す func ConnectionRedis() (redis.Conn, string, error) { // RedisのAUTHパス AuthPass := "92d4e0a2ad853e81e6d4bf147618aae5a06f201e6e72a67794a542409c9d125a" // RedisのIPアドレス RedisIPAddress := "192.168.33.100" // Redisのポート番号 RedisPort := "6379" // 接続 conn, err := redis.Dial("tcp", RedisIPAddress+":"+RedisPort) if err != nil { fmt.Println(err) return nil, "", err } return conn, AuthPass, err }実際にはAUTHパスとかは環境によって変わると思うので、ソースに書かずに
.env
とかに記述して読み込むのがいいと思います。
参考:【Go】.envファイルをGolangでも使用するためのライブラリ「godotenv」 - QiitaRedisをさらに堅牢にする
個人的にはRedisに接続する際のAuthパスだけでもいいと思うんですが念には念を入れて強化する場合は以下のことが出来ます。
ポート番号の変更
Redisのデフォルトポート番号は
6379
番ですが、これを変更することで多少なりともセキュリティが向上します。/etc/redis.conf# Accept connections on the specified port, default is 6379 (IANA #815344). # If port 0 is specified Redis will not listen on a TCP socket. # port 6379 port 6597変更後はRedisの再起動すること。
重要なコマンドの変更
例えば
CONFIG
やSHUTDOWN
、FLUSHDB
コマンドなど、実行されるとマズいコマンドをほかの文字列に置き換えることが出来ます。
代わりのコマンド名を以下のredis.conf
に追記して再起動をすることで適用されます。/etc/redis.conf# Command renaming. # # It is possible to change the name of dangerous commands in a shared # environment. For instance the CONFIG command may be renamed into something # hard to guess so that it will still be available for internal-use tools # but not available for general clients. # # Example: # # rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 # # It is also possible to completely kill a command by renaming it into # an empty string: # # rename-command CONFIG "" # # Please note that changing the name of commands that are logged into the # AOF file or transmitted to slaves may cause problems. rename-command CONFIG configconfig rename-command FLUSHDB SF_FLUSHDB rename-command SHUTDOWN RENAME_SD以下コマンドがかわっているか確認の様子
[vagrant@localhost ~]$ sudo systemctl restart redis.service [vagrant@localhost ~]$ redis-cli 127.0.0.1:6379> auth 92d4e0a2ad853e81e6d4bf147618aae5a06f201e6e72a67794a542409c9d125a // 上で設定したパス OK 127.0.0.1:6379> config get requirepass (error) ERR unknown command 'config' // configというコマンドは無い(上で変更したため) 127.0.0.1:6379> configconfig get requirepass 1) "requirepass" 2) "92d4e0a2ad853e81e6d4bf147618aae5a06f201e6e72a67794a542409c9d125a" 127.0.0.1:6379> exitRedisデータディレクトリの権限変更
RedisユーザーとRedisグループのみがアクセスできるように
770
に変更する。[vagrant@localhost ~]$ ls -l /var/lib | grep redis drwxr-x---. 2 redis redis 22 Jul 13 13:31 redis [vagrant@localhost ~]$ sudo chmod 770 /var/lib/redis [vagrant@localhost ~]$ ls -l /var/lib | grep redis drwxrwx---. 2 redis redis 22 Jul 13 13:31 redisおわり
- なんか走り書きしたら雑なまとめになってしまった感がある・・・
- ちなみにSubscribは今度時間あれば書くかもしれません・・・(予定は未定です)
- あとまだRedisは構成ファイルの権限変更したりなどで強固にできますがここでは割愛します(DigitalOceanさんの参考記事をどうぞ)
参考URL
- 投稿日:2020-12-18T17:21:19+09:00
レガシー環境用のTLS1.2対応リバースプロキシを golang で簡単に作る
はじめに
CentOS5 などのレガシーシステムでは TLS1.2 が利用できません。
OSバージョンアップや別サーバで Reverse Proxy を構築するのが一般的ですが結構な手間です。
そこで今回は、golang でちょちょいと TLS1.2 対応の Reverse Proxy を実装します。やってみよう
必用なもの
- golang v1.9以前(※ レガシーシステムではv1.9以前でコンパイルしないと動きません)
やりかた
golang ならわずか10行くらいのロジックでリバースプロキシができちゃいます。
main.gopackage main import ( "log" "net/http" "net/http/httputil" "net/url" ) func main() { // プロキシしたいレガシーサーバのURL target, _ := url.Parse("http://localhost:80") // シングルホストの場合はこれだけでOK proxy := httputil.NewSingleHostReverseProxy(target) // ハンドラ内でヘッダーを設定してプロキシする http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { r.Header.Set(`X-Forwarded-Host`, r.Header.Get(`Host`)) r.Header.Set(`X-Forwarded-Proto`, r.URL.Scheme) proxy.ServeHTTP(w, r) }) // TLS1.2でListen err = http.ListenAndServeTLS(":443", "path to ssl cert", "path to ssl key", nil) if err != nil { log.Fatal(err) } }また、http.HandlerFunc()内では、以下のように Request Header も設定できます。
レガシーサーバ側が X-Forwarded-Proto などに対応していない場合は、Request をゴニョゴニョやって対応しましょう!http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { r.Header.Set(`X-Forwarded-Host`, r.Header.Get(`Host`)) r.Header.Set(`X-Forwarded-Proto`, r.URL.Scheme) r.URL.Host = target.Host r.URL.Scheme = target.Scheme proxy.ServeHTTP(w, r) })
- 投稿日:2020-12-18T13:41:30+09:00
GORMがv2になっていたので、変更点を調べてみる
はじめに
golangでプロトタイプを作成していて、いつも通り
GORM
を利用しようとした際、どうせなら最新版でと思ったところ、 大きく変わっていました。
詳細は、v2のリリースノートを見てもらうのが一番ですが、変更点も多いため、単純にバージョンアップする際にひっかかりそうなところを調べてみました。インストール
この記事の段階での最新バージョンは、
1.20.8
になりますが、
1.20系にバージョン番号が上がると、インストールする際のパスが変更されます。
いままではgo get -u github.com/jinzhu/gormとなっていましたが、 v2になって
go get -u gorm.io/gormドキュメントも https://v1.gorm.io と https://gorm.io/ に分かれています。
Open
初期化する際の関数も引数が変更されています。
v1
func Open(dialect string, args ...interface{}) (db *DB, err error)第1引数が、
"mysql"
や"postgres"
といった接続するRDBMSの種類を文字列で指定し、第2引数に接続文字列を渡します。
また、必要なDriverパッケージをブランク識別子を使用してインポートする必要があります。import ( "github.com/jinzhu/gorm" _ "github.com/go-sql-driver/mysql" ) func main() { db, err := gorm.Open("mysql", "test:test@/testdb") ... }v2
func Open(dialector Dialector, config *Config) (db *DB, err error)第1引数に
gorm.Dialector
という型で渡すように変更されています。
こちらは接続するRDBMSの種類に応じて、gorm用のDriverを初期化して渡す必要があります。
v2の場合は、ブランク識別子は不要になります。
また、第2引数のConfigでいろいろと細かい設定や機能が追加されていますが、今回は省略します。import ( "gorm.io/driver/mysql" "gorm.io/gorm" ) func main() { db, err := gorm.Open(mysql.Open("test:test@/testdb"), &gorm.Config{}) ... }DeletedAt
論理削除をしたい場合、v1では
DeletedAt
フィールドを *time.Timeで用意していましたが、
v2では、フィールド名ではなく、gorm.DeletedAt
という型で定義する必要があります。v1type Example struct { ... DeletedAt *time.Time }v2type Example struct { ... Deleted gorm.DeletedAt }Hooks
実際にSQLが発行される前後のHook処理もインターフェースの条件が変更されています。
v1
DOMに下記の名前のメソッドを実装することで処理を呼ぶことができます。
BeforeSave BeforeCreate AfterCreate AfterSavev2
メソッド名に変更はありませんが、Hooks の
The type of hook methods should be func(*gorm.DB) error
記述にあるように
それぞれのメソッドの引数に*gorm.DB
を渡す必要があります。
メソッドの型が異なると、実際の処理時には実行されないため注意が必要です。RecordNotFound
Selectを発行して該当レコードがない場合に、db.RecordNotFound() メソッドで判定していましたが、
当該メソッドがなくなりました。errors.Is()を使用して、判定を行う必要があります。v1db = db.First(entity) if db.RecordNotFound() { ... }v2db = db.First(entity) if errors.Is(db.Error, gorm.ErrRecordNotFound) { ... }ログ出力
デバッグログの出力設定が変わりました。
v1
LogModeをtrueに設定するだけで、SQLがログ出力されます。
v1db.LogMode(true)v2
Logger を設定する必要があります。
新しくGORM用のLoggerが提供されており、出力レベルが変更できるようになりました。
レベルをInfo
にすることで、SQLのログ出力が行われます。logger/logger.goconst ( Silent LogLevel = iota + 1 Error Warn Info )v2import ( "gorm.io/gorm" "gorm.io/gorm/logger" ) ... db.Logger = db.Logger.LogMode(logger.Info) ...DB()
地味な変更ですが、v1の時には、 *gorm.DB の DB()メソッドを呼ぶことで、*sql.DB だけを取得することができましたが、
v2においては、errorも返すようになりました。v1db.DB().SetMaxIdleConns(10)v2if sqlDb, err := db.DB(); err != nil { panic(err) } else { sqlDB.SetMaxIdleConns(10) }Close
v1においてはgorm.DBにCloseメソッドがありましたが、v2ではなくなりました。
v2では、sql.DBを取得してCloseを実行する必要があります。v1db.Close()v2if sqlDB, err := db.DB(); err != nil { ... } else { if err := sqlDB.Close(); err != nil { ... } }まとめ
上にあげた点だけではなく、マイグレーションや、バッチ更新など、かなり機能が追加されています。
これから新しく作るプロダクトにおいては、最新版を使うことで、他のライブラリを探す手間がはぶけるというところでしょうか。
すでに動いているものを急いで更新するメリットはあまり感じません。
それでもバージョンアップを行う場合には、単にビルドが通ればいいというわけではなく、
ビルド時のエラーがでない変更も多いため、事前の動作確認を確実に行う必要があります。
- 投稿日:2020-12-18T09:12:14+09:00
Go1.16で追加されるembedとio/fsパッケージについてざっと調べた
はじめに
これは Kyash Advent Calendar 2020 の20日目の記事です。
Go1.16ではバイナリへのファイルやコンテンツを埋め込む
//go:embed
ディレクティブが有効になったり、ファイルシステムへの共通interfaceを提供するio/fs
などが追加されてます。それぞれ、configファイルをバイナリに埋め込むときにサードパーティライブラリを使わずにGo自体の仕組みで埋め込めるようになっていたり、ファイルツリーの形式を抽象化して統一したinterfaceで扱えるようにしてあったりします。
とても便利そうな機能なので、リリースされたら早速使ってみたいので下調べしてみました。
(余談ですが ちょうど記事を書いてる途中にGo1.16 Betaがリリースされた のでめちゃくちゃタイミングが良かったです)
embedについて
draftから気になってた機能その1です。
機能はシンプルで、
embed
パッケージをimportしつつ目的の変数に//go:embed target/file/path
というコメントを追加するとその内容がビルド時に変数に埋め込まれる!というすぐれものです。使うのもカンタンだしバイナリへのファイル埋め込みは全部これにしたいですね。
例として適当な
config.json
があったと想定してembedをつかって埋め込んでみます。
Go1.16 Betaを使うと手元でも試せます。config.json{"key1":"val1","key2":"val2"}example_embed_bytes.gopackage main import ( _ "embed" "encoding/json" "fmt" "log" ) //go:embed config.json var configBytes []byte type config struct { Key1 string `json:"key1"` Key2 string `json:"key2"` } func main() { var c config if err := json.Unmarshal(configBytes, &c); err != nil { log.Fatal(err) } fmt.Printf("%#v\n", c) }ビルドして実行した結果は以下。ちゃんとよみこめてる!
$ go build -o embedtest $ ./embedtest main.config{Key1:"val1", Key2:"val2"}バイト列以外にもstringとしてもうけとれます。
examble_embed_string.gopackage main import ( _ "embed" "fmt" ) //go:embed config.json var configString string func main() { fmt.Printf("%#v\n", configString) }$ go build -o embedtest $ ./embedtest "{\"key1\":\"val1\",\"key2\":\"val2\"}\n"すごい便利だし楽!
埋め込みの際には、Go1.16から抽象化された
io/fs.FS
を実装してるembed.FS
という型でも受け取れます。
後述しますが、この型は標準パッケージ間ですでに色々なところで使えるようになっていて非常に便利です。いままでは、なにがしかのサードパーティツールを利用して
go generate
で埋め込み実行していたのがgo build
でできるようになるので、ファイルの埋め込みに関して開発者が気を使う必要がほぼなくなります!うれしい!(なんかエラー出ると思ったら
go generate
忘れててファイル埋め込めてないやん、みたいな経験をしたのは僕だけではないはず)io/fsについて
draftから気になってた機能その2です。
こちらは1.16には入らないと思ってたのですが、beta版の内容を確認したら入ってたのでうれしいです。これはどういうパッケージかというと、
io.Reader
やio.Writer
の成功にならって、ファイルシステムツリーも抽象化しよう!というものです。Interfaceは以下です
https://go.googlesource.com/go/+/refs/tags/go1.16beta1/src/io/fs/fs.go#20
FS.gotype FS interface { // Open opens the named file. // // When Open returns an error, it should be of type *PathError // with the Op field set to "open", the Path field set to name, // and the Err field describing the problem. // // Open should reject attempts to open names that do not satisfy // ValidPath(name), returning a *PathError with Err set to // ErrInvalid or ErrNotExist. Open(name string) (File, error) }https://go.googlesource.com/go/+/refs/tags/go1.16beta1/src/io/fs/fs.go#72
File.gotype File interface { Stat() (FileInfo, error) Read([]byte) (int, error) Close() error }要はいろいろなパッケージを使用し、いろいろな手段でファイル読み取りしていた部分を、読み取り専用の
fs.File
interfaceとそれを取得するためのfs.FS
interfaceを使うことで統一しよう!というわけですね。
(今まではtext/template
などにおけるParseFiles
のようにパッケージごとに実装されてたりしていた)なるほどそれは便利そう!
以下はembedで受け取ったFSを同じくGo1.16で新規に追加されたhttp.FS
に渡してつかってみる例です。example_http_fs.gopackage main import ( "embed" "log" "net/http" ) //go:embed config.json var configFS embed.FS func main() { http.Handle("/", http.FileServer(http.FS(configFS))) log.Fatal(http.ListenAndServe(":8080", nil)) }$ go build -o fstest $ ./fstestサーバーが立ち上がって
fs.FS
で渡したコンテンツが/
配下に配置されてることが確認できます。$ curl localhost:8080/config.json {"key1":"val1","key2":"val2"}今回は試してませんが、
text/template
やhtml/template
パッケージもfs.FS
を受け取れるようになってるようです。
いままでなら ParseFiles() で初期化していたものが、 ParseFS()
が使えるようになったそう。これも便利っぽい。Go1.16では、このほかにもいくつかのパッケージが
fs.FS
に対応したようです!
一回取得したfs.FS
を標準パッケージ間で統一したinterfaceの上で使い回せるのはすごくうれしいです!さいごに
embed, io/fsともに必要最低限かつあると助かるものが追加されて個人的にはとても嬉しいです!
Kyashでもいくつか使いたい部分ががあるので今からリリースが楽しみです。Go1.16はほかにもM1 Macのサポートがされたり、
go install
の挙動が整理されたり、いろいろと使いやすそうになっているので早く使ってみたいですね!Kyash Advent Calendar 2020 はまだまだつづきます、あしたの投稿もおたのしみに〜。
- 投稿日:2020-12-18T02:06:43+09:00
gRPC-WebとGoとVue.jsで簡素なチャット
はじめに
何だか良くわからないけどよく聞くgRPC-Webなるものを触りだけでも理解すべく辛うじてチャット呼べそうなものを作ってみました。
gRPC
https://grpc.io/
Protocol BuffersやHTTP2などを利用した環境に依存せず実行できる高パフォーマンスのRPCフレームワーク。Protocol Buffers
https://developers.google.com/protocol-buffers
言語やプラットフォームに依存しない構造データを定義できる。
コンパイルして指定の言語のコードを生成できる。proto
test.proto
service TestService { rpc Login(User) returns (User) {} } message User { string name = 1; string token = 2; }Go
protoファイルからコンパイルしてGoのコードを生成。
test.pb.gofunc (t *testServiceClient) Login(ctx context.Context, in *User, opts ...grpc.CallOption) (*User, error) { out := new(User) err := t.cc.Invoke(ctx, "/test.TestService/Login", in, out, opts...) if err != nil { return nil, err } return out, nil } type User struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` }HTTP2
https://http2.info/
HTTP1からの変更例
- テキストからバイナリ
- ステートレスからステートフル
- 1つのTCPコネクションの中で複数のHTTP Requestと複数のHTTP Response
gRPC-Web
https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md
https://grpc.io/docs/platforms/web/basics/
ブラウザの制限によりネイティブのgRCPとは違う実装。envoy
https://www.envoyproxy.io/docs/envoy/latest/
gRCPとgRCP-Webを接続するためには特別なプロキシが必要でデフォルトがenvoy。コード
https://github.com/tayusa/grpc-web-simple-chat
protoファイル定義
syntax = "proto3"; package chat; option go_package = "server/proto"; // よくあるデータ型は定義してあるので読み込む import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; // やりとりを定義 service ChatService { rpc Login(User) returns (User) {} rpc Logout(User) returns (google.protobuf.Empty) {} rpc SendMessage(Message) returns (Message) {} // 複数の場合、stream使う。 rpc GetMessage(User) returns (stream Message) {} } // やりとりするデータを定義 message Message { // 番号はただの順番 string content = 1; // 自分で定義した型 User user = 2; google.protobuf.Timestamp created_at = 3; } message User { string name = 1; string token = 2; }上記以外にもいろんな書式があって表現力高い。
コンパイルしてコード生成
https://github.com/protocolbuffers/protobuf
からコンパイラをダウンロード。
パッケージマネージャーからインストールもできる。
Arch Linuxなら
$ sudo paman -S protobuf
Goのコードを生成するときは
$ go get -u github.com/golang/protobuf/protoc-gen-go
gRPC-Webのコードを生成するときは
$ npm install -g protoc-gen-grpc-web
言語、出力先をを指定してコンパイル$ protoc chat.proto \ --go_out=plugins="grpc:." \ --js_out=import_style=commonjs:client/src/proto \ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:client/src/proto生成したコード
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/proto/chat.pb.go
https://github.com/tayusa/grpc-web-simple-chat/tree/master/client/src/protoDocker
Go
FROM golang:latest WORKDIR /server COPY . . RUN go mod download RUN go build -o app CMD ./appJavaScript
FROM node:lts-slim WORKDIR /client COPY . . RUN npm installdocker-compose.yml
3つのコンテナ動かす。
version: '3' services: envoy: image: envoyproxy/envoy:v1.14.1 command: /usr/local/bin/envoy -c /etc/envoy/envoy.yaml -l debug volumes: - ./envoy:/etc/envoy ports: - '10000:10000' links: - 'server' container_name: 'envoy' server: build: context: ./server dockerfile: Dockerfile command: /server/app ports: - '50051:50051' volumes: - ./server:/go/src/server container_name: 'server' client: build: context: ./client dockerfile: Dockerfile command: npm run serve ports: - '8080:8080' volumes: - ./client:/client links: - 'envoy' container_name: 'client'Envoy
$ docker run --rm -it envoyproxy/envoy:v1.14.1 bash
で/etc/envoy/envoy.yamlをコピーして来てポートなどを書き換えて利用します。
.ymlにするとエラーになり時間が消えてなくなります。admin: access_log_path: /tmp/admin_access.log address: socket_address: { address: 0.0.0.0, port_value: 9901 } static_resources: listeners: - name: listener_0 address: socket_address: { address: 0.0.0.0, port_value: 10000 } filter_chains: - filters: - name: envoy.http_connection_manager config: codec_type: auto stat_prefix: ingress_http route_config: name: local_route virtual_hosts: - name: local_service domains: ["*"] routes: - match: { prefix: "/" } route: cluster: chat_service max_grpc_timeout: 0s cors: allow_origin_string_match: - prefix: "*" allow_methods: GET, PUT, DELETE, POST, OPTIONS allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout max_age: "1728000" expose_headers: custom-header-1,grpc-status,grpc-message http_filters: - name: envoy.grpc_web - name: envoy.cors - name: envoy.router clusters: - name: chat_service connect_timeout: 0.25s type: logical_dns http2_protocol_options: {} lb_policy: round_robin # win/mac hosts: Use address: host.docker.internal instead of address: localhost in the line below hosts: [{ socket_address: { address: server, port_value: 50051 }}]Go
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/main.go
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/server.go
https://github.com/tayusa/grpc-web-simple-chat/blob/master/server/signal.go生成されたインターフェイス
type ChatServiceServer interface { Login(context.Context, *User) (*User, error) Logout(context.Context, *User) (*empty.Empty, error) SendMessage(context.Context, *Message) (*Message, error) // 複数のレスポンスの場合、戻り値がない。引数にレスポンスのためのコネクション。 GetMessage(*User, ChatService_GetMessageServer) error }protoで生成したGoのインターフェイスに合わせてメソッドを定義していく。
1つのリクエストで1つのレスポンス
func (s *server) Login(ctx context.Context, user *pb.User) (*pb.User, error) { log.Println("Try to logged in.") clientExists := false s.clients.Range(func(_, client interface{}) bool { if value, ok := client.(string); ok && value == user.GetName() { clientExists = true return false } return true }) if clientExists { return &pb.User{}, fmt.Errorf("\"%s\" is already in use.", user.GetName()) } user.Token = genToken() s.clients.Store(user.GetToken(), user.GetName()) log.Printf("%s logged in.\n", user.GetName()) return user, nil }1つのリクエストで複数のレスポンス
func (s *server) GetMessage(user *pb.User, stream pb.ChatService_GetMessageServer) error { s.wg.Add(1) defer s.wg.Done() streamCh := s.createStreamCh(user.GetToken()) defer s.deleteStreamCh(user.GetToken()) for { select { case msg, ok := <-streamCh: if !ok { return nil } // ここでレスポンスしてる。メソッドは終了しない。 if err := stream.Send(msg); err != nil { log.Println("Sending error.") return err } case <-s.exitCh: log.Printf("%s exit.\n", user.GetName()) return nil } } }JavaScript
https://github.com/tayusa/grpc-web-simple-chat/blob/master/client/src/api/client.js
https://github.com/tayusa/grpc-web-simple-chat/blob/master/client/src/components/Chat.vueコンパイルして生成したクライアント
import { ChatServiceClient } from '../proto/chat_grpc_web_pb' export default new ChatServiceClient('http://localhost:10000', null, null)Vueのscript
// クライアント読み込む import client from '../api/client.js' // コンパイルして生成した型を読み込む import { Message, User } from '../proto/chat_pb' // googleが定義してる型を読み込む // import { Empty } from 'google-protobuf/google/protobuf/empty_pb'; import { Timestamp } from 'google-protobuf/google/protobuf/timestamp_pb'; export default { name: "Chat", data: () => ({ userName: "", userToken: "", message: "", messages: [], stream: null, }), filters: { toLocaleString: (value) => { return (new Date(value.getSeconds() * 1000)).toLocaleString() } }, methods: { login: async function(e) { e.preventDefault(); if (!this.userName) { return; } await client .login(this.getUser(), {}, (err, user) => { if (err != null) { console.log(err); } else { this.userToken = user.getToken(); this.stream = this.fetchMessageStream() } }) }, sendMessage: async function(e) { e.preventDefault(); if (!this.message) { return; } // 生成した型に入れてく。 // セッターが生えてるので利用する。 const message = new Message(); message.setContent(this.message); message.setUser(this.getUser()); const timestamp = new Timestamp(); // ここはどこにも書いてなくて、開発者コンソールで中身を全部読んだ。 timestamp.fromDate(new Date()); message.setCreatedAt(timestamp); await client .sendMessage(message, {}, (err, res) => { if (err != null) { console.log(err); } this.message = ''; }) }, fetchMessageStream: function() { const stream = client.getMessage(this.getUser()); // メッセージが来たら発火するイベント stream.on('data', message => { console.log(message); this.messages = [...this.messages, message]; }); return stream; }, getUser: function() { const user = new User(); user.setName(this.userName); user.setToken(this.userToken); return user; } } };参考
GoでgRPC使う際のクイックスタート
https://grpc.io/docs/languages/go/quickstart/
protocol bufferが生成するGoのコードの説明
https://developers.google.com/protocol-buffers/docs/reference/go-generated
gRCPのGo実装
https://github.com/grpc/grpc-go
ブラウザためのgRCPのJavaScript実装
https://github.com/grpc/grpc-web
Goのライブラリのドキュメント
https://godoc.org/google.golang.org/grpc
GCPのドキュメントにある構成例
https://cloud.google.com/endpoints/docs/grpc/grpc-service-config?hl=ja試す
$ git clone https://github.com/tayusa/grpc-web-simple-chat.git
$ cd grpc-web-simple-chat
$ docker-compose up -d --build
$ chromium http://localhost:8080
サーバーだけ試す
curlは使えないのでgrpc-cli
パッケージマネージャーからインストール
$ sudo paman -S grpc-cli
$ grpc_cli ls localhost:50051 chat.ChatService -l
$ grpc_cli call localhost:50051 ChatService.Login 'name: "John"'
$ grpc_cli call localhost:50051 ChatService.SendMessage 'content: "Hey"'