プログラマのためのSQL 読書会(27)に参加
余談ですが参加者にdbtech show case tokyo 2018の参加者も多かったので、そちらの話題で雑談などが盛り上がりました。*1
「29.7ビット単位の集約関数」から読みました。
- 作者: ジョー・セルコ,Joe Celko,ミック
- 出版社/メーカー: 翔泳社
- 発売日: 2013/05/24
- メディア: 大型本
- この商品を含むブログ (16件) を見る
- 「30.高度な集約、ウィンドウ関数、OLAP」にあるスノーフレークとは
適切に設計されたOLTPデータベースは正規化されている。通常、DWHはスタースキーマスキーマまたはスノーフレークスキーマを採用しており、これはかなり非正規化されている。
- スノーフレークはITでは多義語なので注意すること
- Amazon RedShift
- https://kyrt.in/2014/06/08/snowflake_c.html
- twitter社が作成した分散環境でユニークなIDを振る仕組みと、DWHでのテーブル設計パターンの2つともがスノーフレークを表しているような印象を受けました。
ペタバイト以上のデータを持つ企業
がペタバイト以上のデータを持つ企業だと書かれているが、 このほんの情報は古く、2014年時点では売上10億ドル以上の 企業の56パーセントが 1PB以上のデータを管理している*2
MySQL 8を試したくbrew upgradeしたところ、前バージョンで正常終了してないため起動できない的なエラーで怒られてるなう。
— yuYabu@転職活動中 (@yuyabu2) September 19, 2018
MySQL Sandboxで独立した環境を構築できる
- dbdeployerも検討すべき。
MySQLはなぜ5の次が8なのか
11月にpostgreSQLのカンファレンスおすすめ
P.562のクエリがおかしい
▼P.562 SELECT B.region_nbr, S.city_id, SUM(S.sales_amt) AS total_sales FROM SalesFacts AS S, MarketLookup AS M WHERE EXTRACT (YEAR FROM trans_date) = 2011 AND S.city_id = B.city_id AND B.region_nbr = 6 GROUP BY ROLLUP(B.region_nbr, S.city_id);
- Bという別名はどこから出て来たのか。(Mではないのか?誤植?)
このSQLを実行できるTableが書かれていないのでそちらも欲しい
- 最新のMySQLにwindow関数に対応した
- 最新のSQLiteはWindow関数に対応した
- PostgreSQLの挙動に準拠している
CUME_DIST()
関数- 初めて見たので検証予定
Window Frame
- 理解しきれてないので検証予定
前職ではウインドウ関数を多少していたのですが*3、WindowFrameを弄ったりは普段やりません。*4 ので、この辺は技術検証の必要があるかなと思います。
次は「30.6ベンダー拡張」から!
分散処理本第38回に参加
Distributed Computing: Principles, Algorithms, and Systemsを読む勉強会です。やはり難しいので少し予習していきました。アルゴリズムの分類までは一応軽く読んで概要をつかんでから臨みました
- 分散システムに置けるデッドロックの検出アルゴリズムの分類
- path-pushing
- edge-chasing
- diffusion computation
- global state detection.
- 10.6 Mitchell and Merritt’s algorithm for the single-resource model
- 余談:ACM Degital Libaryは高い
予習をしてブログの下書きを作っておくと、感想のポストがサクッとかけて良いですね。
Distributed Computing: Principles, Algorithms, and Systems
- 作者: Ajay D. Kshemkalyani,Mukesh Singhal
- 出版社/メーカー: Cambridge University Press
- 発売日: 2011/03/03
- メディア: ペーパーバック
- この商品を含むブログを見る
分散システムに置けるデッドロックの検出アルゴリズムの分類
- path-pushing
- edge-chasing
- diffusion computation
- global state detection.
出典:E. Knapp, Deadlock detection in distributed databases, ACM Computing Surveys, 19(4), 1987, 303–328.
※詳しい方が概要を知っているがその分類名を知らない、ということが発生していたので、一般的な分類ではないのかもしれない。
path-pushing
各サイトがWFGを送り合いグローバルなWFGを構築する。 送信しながらどんどんWFGを更新してデッドロックの発生、検知を明らかにして行く
edge-chasing
グラフのエッジに剃ってprobesと呼ばれる特殊なメッセージを送信して行く。 以前送信したprobeと同じものを送信した場合、それでcycleを検出できる
diffusion computation
deadlockが発生していれば帰ってくるようなエコーメッセージを送る。
よく解らん?という話になった
参考にしてある論文は以下のもの
Herman and K. M. Chandy, A Distributed Procedure to Detect AND/OR Deadlock, Technical Report TR LCS-8301, Department of Computer Sciences, University of Texas, Austin, TX, 1983.
本書記載のアルゴリズム例は2つあり、
- Chandy–Misra–Haas algorithm for one OR model
- Chandy–Herman algorithm
前者はWikipediaの記事があった
https://en.wikipedia.org/wiki/Chandy-Misra-Haas_algorithm_resource_model
CMHアルゴリズムとしても有名らしい
global state detection.
分散システムの各スナップショットからデッドロック有無を確認する。しかし以下の条件が必要である
(i) a consistent snapshot of a distributed system can be obtained without freezing the underlying computation.
(ii) a consistent snapshot may not represent the system state at any moment in time, but if a stable property holds in the system before the snapshot collection is initiated, this property will still hold in the snapshot.
10.6 Mitchell and Merritt’s algorithm for the single-resource model
- WFGの逆向きにprobを飛ばす
- edge-chasingアルゴリズムのうちの一つ
アルゴリズムとしてはpublic/privateなlabelを使う 文章化するのがちょっと難しいが一言で表すなら
- 待ち状態に対して2種類(private/public)のラベル作成し値zを与える
- 下の例はu->v間の待ちを表しているこのzはu+vより大きな値かつユニークでなければならない
- この操作を待ちの連鎖が終わるところまで続けて行く
2週することで図のDetect
の状態が発生が把握できる(1週しただけだとDeadlockかどうかわからない)
p360の図は誤植
- Figure 10.2 The four possible state transitions [22]
という図があるが元ネタが「E. Knapp, Deadlock detection in distributed databases, ACM Computing Surveys, 19(4), 1987, 303–328.」という論文の図であり、引用するときに一部記号が間違っている。
また10.6
は本書中の記載のみだと理解が辛く、引用元の論文をよく読み込まないと何を行っているかわからない。
余談:ACM Degital Libaryは高い
500ドルくらいするらしい。。。
次回は「10.7 Chandy–Misra–Haas algorithm for the AND model」から!
a tour of Goをやった
Goならわかるシステムプログラミングで推奨していたのでとりあえず一周して見た*1
https://go-tour-jp.appspot.com/list
Go言語の文法については、全くのプログラミング初心者でなければ、tour of Goという公式のチュートリアルを一通りこなすだけで、本書の範囲なら十分理解できるでしょう
Goならわかるシステムプログラミングp4より抜粋
と書かれていたのでとりあえずやっておこう、というつもりで進めたのだが。。。 だらだらやったのもあってか1.5日ほどかかった。
- フィボナッチ
- ワードカウント?的なやつ。一応テストは全てパスした
- なんか画像を作るやつ
- ASCII文字 'A' の無限ストリームを出力する Reader
- io.Readerをラップした暗号復号処理(rot13)
- conncurenncyを使った二分木探索
- mutexを使ったクローラー
- 他:違和感のある翻訳
- 感想
とりあえず自分は日頃競プロ的な頭を使うプログラミングをやらないので、 プログラミング能力が劇的に落ちていることを実感した。
時間をかけて競プロを強めようとは思わないが、アルゴリズムとかのエンジニアとしての基礎的な部分を固める必要があると再度認識した。
コードはあまり人に見せられるような綺麗なものではないけど、時間がかかった課題のものを中心に公開しておきます。
フィボナッチ
https://go-tour-jp.appspot.com/moretypes/26
DRYにものすごく反しているような???
package main import "fmt" // fibonacci is a function that returns // a function that returns an int. func fibonacci() func() int { count := 0 v1 := 0 v2 := 0 value := 0 return func() int { defer func() { count++ }() if count == 0 { return 0 } else if count == 1 { v1 = 0 v2 = 1 return 1 } else { value = v1 + v2 v1 = v2 v2 = value return value } } } func main() { f := fibonacci() for i := 0; i < 15; i++ { fmt.Println(f()) } }
ワードカウント?的なやつ。一応テストは全てパスした
https://go-tour-jp.appspot.com/moretypes/23
package main import ( "golang.org/x/tour/wc" "strings" ) func WordCount(s string) map[string]int { wordlists := strings.Fields(s) countMap := make(map[string]int) for _, v := range wordlists { countMap[v]++ } //return map[string]int{"x": 1} return countMap } func main() { wc.Test(WordCount) }
なんか画像を作るやつ
https://go-tour-jp.appspot.com/moretypes/18
package main import "golang.org/x/tour/pic" func Pic(dx, dy int) [][]uint8 { value := make([][]uint8, dy) for i := 0; i < dy; i++ { value[i] = make([]uint8, dx) for j:=0;j<dx;j++{ value[i][j] = uint8(i^j) } } return value } func main() { pic.Show(Pic) }
生成する画像は、好きに選んでください。例えば、面白い関数に、 (x+y)/2 、 x*y 、 xy などがあります。
ASCII文字 'A' の無限ストリームを出力する Reader
package main import "golang.org/x/tour/reader" type MyReader struct{} // TODO: Add a Read([]byte) (int, error) method to MyReader. func(r MyReader) Read(b []byte) (int ,error){ b[0] = 'A' return 1,nil } func main() { reader.Validate(MyReader{}) }
io.Readerをラップした暗号復号処理(rot13)
rot13Reader(io.Readerをラップして換字式暗号を標準入出力に変換してだす)
readerが最後にerrorとしてio.EOFを返すのが解らずに、1時間ほど試行錯誤した。
package main import ( "fmt" "io" "os" "strings" ) type rot13Reader struct { r io.Reader } func (r2 rot13Reader) Read(p []byte) (n int, e error) { i := 0 b := make([]byte, 1) for { _, err := r2.r.Read(b) //fmt.Printf("n = %v err = %v b = %v\n", n, err, b) //fmt.Printf("b[:n] = %q(%v)\n", b[0], b[0]) if err == io.EOF { break } value := b[0] //文字の場合のみ暗号置換を実施する if 64 < b[0] && b[0] < 91 { value = value + 13 if value > 90 { value = value - 26 } } if 96 < b[0] && b[0] < 123 { value = value + 13 if value > 122 { value = value - 26 } } //fmt.Printf("lot13 = %q(%v)\n", value, value) p[i] = value i++ } return i, io.EOF } func main() { s := strings.NewReader("Lbh penpxrq gur pbqr!") r := rot13Reader{s} //b := make([]byte, 100);r.Read(b) io.Copy(os.Stdout, &r) }
解読後は普通の文章になっているので多分あっていると思うが、if文の境界値条件はきちんと検査していないのでバッグっているかもしれない。
conncurenncyを使った二分木探索
https://go-tour-jp.appspot.com/concurrency/8
package main import ( "fmt" "golang.org/x/tour/tree" ) // Walk walks the tree t sending all values // from the tree to the channel ch. func Walk(t *tree.Tree, ch chan int) { //fmt.Println(t) //左があるときはどんどん左に行っちゃう if t.Left != nil { Walk(t.Left, ch) ch <- t.Value if t.Right != nil { Walk(t.Right, ch) } //左がなくなったけど右がある場合はそっちに行く } else if t.Right != nil { ch <- t.Value Walk(t.Right, ch) } else { //葉 ch <- t.Value } } // Same determines whether the trees // t1 and t2 contain the same values. func Same(t1, t2 *tree.Tree) bool { ch1:=make(chan int,10) ch2:=make(chan int,10) Walk(t1,ch1) Walk(t2,ch2) for i:=0;i<10;i++{ v1:= <-ch1 v2:= <-ch2 if(v1!=v2){ return false } } return true } func main() { fmt.Println("test") /* ch := make(chan int) go Walk(tree.New(2), ch) count := 0 for i := range ch { fmt.Println(i*10) count++ if(count == 10){ //closeするタイミングをどこに入れるか解らなかったので //呼び出し側にいれた。こういうプログラミングは良くないと思う close(ch) } } */ b1:= Same(tree.New(1), tree.New(2)) fmt.Println(b1) }
mutexを使ったクローラー
https://go-tour-jp.appspot.com/concurrency/10
Crawlメソッドの呼び出しを並列に(goroutine)にしなければ動く。 並列にするとなぜか正しく動かない。これは自分がmutexとか並列プログラミングにまだまだ無知だからだと思う。 とりあえず私が並列プログラミングをやるとdeadlockだらけになることがわかった。
package main import ( "fmt" "sync" ) type Fetcher interface { // Fetch returns the body of URL and // a slice of URLs found on that page. Fetch(url string) (body string, urls []string, err error) } // Crawl uses fetcher to recursively crawl // pages starting with url, to a maximum of depth. func Crawl(url string, depth int, fetcher Fetcher) { // TODO: Fetch URLs in parallel. // TODO: Don't fetch the same URL twice. // This implementation doesn't do either: if depth <= 0 { return } mux.Lock() if check[url] == 1 { mux.Unlock() return } check[url] = 1 body, urls, err := fetcher.Fetch(url) mux.Unlock() if err != nil { fmt.Println(err) return } fmt.Printf("found: %s %q\n", url, body) for _, u := range urls { //go Crawl(u, depth-1, fetcher) //並列だと正しく動かない Crawl(u, depth-1, fetcher) } return } func main() { check = make(map[string]int) Crawl("https://golang.org/", 4, fetcher) } // fakeFetcher is Fetcher that returns canned results. type fakeFetcher map[string]*fakeResult type fakeResult struct { body string urls []string } var mux sync.Mutex var check map[string]int func (f fakeFetcher) Fetch(url string) (string, []string, error) { if res, ok := f[url]; ok { return res.body, res.urls, nil } return "", nil, fmt.Errorf("not found: %s", url) } // fetcher is a populated fakeFetcher. var fetcher = fakeFetcher{ "https://golang.org/": &fakeResult{ "The Go Programming Language", []string{ "https://golang.org/pkg/", "https://golang.org/cmd/", }, }, "https://golang.org/pkg/": &fakeResult{ "Packages", []string{ "https://golang.org/", "https://golang.org/cmd/", "https://golang.org/pkg/fmt/", "https://golang.org/pkg/os/", }, }, "https://golang.org/pkg/fmt/": &fakeResult{ "Package fmt", []string{ "https://golang.org/", "https://golang.org/pkg/", }, }, "https://golang.org/pkg/os/": &fakeResult{ "Package os", []string{ "https://golang.org/", "https://golang.org/pkg/", }, }, }
他:違和感のある翻訳
呼び出す 具体的な メソッドを示す型がインターフェースのタプル内に存在しないため、 nil インターフェースのメソッドを呼び出すと、ランタイムエラーになります。
https://go-tour-jp.appspot.com/methods/13
編集履歴を辿って原文を確認すると以下のようであった
Calling a method on a nil interface is a run-time error because there is no type inside the interface tuple to indicate which concrete method to call.
https://github.com/atotto/go-tour-jp/commit/123f593e8270035b7f59a8d4eaa0e61bbee98239
具象メソッドを含むinterfaceタプルが存在しないのでnilインターフェースの呼び出しはエラーになります。くらいが妥当な気がする?
というかinterfaceタプル、ってなに?っていう感じでgoをそこまでわかっていないので具体的に今どういう役が適切かは提案できない。
https://qiita.com/umisama/items/e215d49138e949d7f805
だたgoのnilはJavaのnullとも違うし、nil自体がnil型のような特性を持っているような振る舞いがあるようなので、 この辺りは今後注意して学習して行きたい。
感想
今までは写経中心だったり、ロジックというよりもAPIのドキュメントを調べて書かれている通りに実装することが中心だったが、Goできちんとプログラミングを書くのは初めてだったのでいい経験になったと思う。
何よりも、tour of Goをやらねばいけない、という憑き物のようなものが落とせてよかった。
*1:と行っても周回するつもりはあいrません
GolangでUDPサーバー&クライアントを作成してWiresharkで検証する
Goならわかるシステムプログラミング第7章 UDPソケットを使ったマルチキャスト通信に書かれている内容をWiresharkを使ったり、デバッグでライブラリ(netパッケージ中心)のソースを読んだりして検証してみました
サーバー側
- net.ListenPacket()の戻り値のnet.PacketConnインターフェースを使う
package main import ( "fmt" "net" ) func main() { fmt.Println("Server is runnign at localhost:8888") conn, err := net.ListenPacket("udp", "localhost:8888") if err != nil { panic(err) } defer conn.Close() buffer := make([]byte, 1500) for { length, remoteAddress, err := conn.ReadFrom(buffer) if err != nil { panic(err) } fmt.Printf("Received from %v: %v\n",remoteAddress, string(buffer[:length])) _, err = conn.WriteTo([]byte("Hello from Server"), remoteAddress) if err != nil { panic(err) } } }
ReadFrom関数の実装が気になったので確認してみた
//udpsock.go func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) { if !c.ok() { return 0, nil, syscall.EINVAL } n, addr, err := c.readFrom(b) if err != nil { err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err} } if addr == nil { return n, nil, err } return n, addr, err }
//udpsock_posix.go func (c *UDPConn) readFrom(b []byte) (int, *UDPAddr, error) { var addr *UDPAddr n, sa, err := c.fd.readFrom(b) switch sa := sa.(type) { case *syscall.SockaddrInet4: addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port} case *syscall.SockaddrInet6: addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port, Zone: zoneCache.name(int(sa.ZoneId))} } return n, addr, err }
//fd_unix.go func (fd *netFD) readFrom(p []byte) (n int, sa syscall.Sockaddr, err error) { n, sa, err = fd.pfd.ReadFrom(p) runtime.KeepAlive(fd) return n, sa, wrapSyscallError("recvfrom", err) }
//syscall_unix.go func Recvfrom(fd int, p []byte, flags int) (n int, from Sockaddr, err error) { var rsa RawSockaddrAny var len _Socklen = SizeofSockaddrAny if n, err = recvfrom(fd, p, flags, &rsa, &len); err != nil { return } if rsa.Addr.Family != AF_UNSPEC { from, err = anyToSockaddr(&rsa) } return }
//zsyscall_darwin_amd64.go // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT func recvfrom(fd int, p []byte, flags int, from *RawSockaddrAny, fromlen *_Socklen) (n int, err error) { var _p0 unsafe.Pointer if len(p) > 0 { _p0 = unsafe.Pointer(&p[0]) } else { _p0 = unsafe.Pointer(&_zero) } r0, _, e1 := Syscall6(SYS_RECVFROM, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(flags), uintptr(unsafe.Pointer(from)), uintptr(unsafe.Pointer(fromlen))) n = int(r0) if e1 != 0 { err = errnoErr(e1) } return }
このSyscall6
の中身は見れなかった。名前的にもこれはネットワーク系のシステムコールなのでしょうか。
という訳で、コールスタックは私の場合は
syscall.recvfrom (/usr/local/go/src/syscall/zsyscall_darwin_amd64.go:146) syscall.Recvfrom (/usr/local/go/src/syscall/syscall_unix.go:262) internal/poll.(*FD).ReadFrom (/usr/local/go/src/internal/poll/fd_unix.go:215) net.(*netFD).readFrom (/usr/local/go/src/net/fd_unix.go:208) net.(*UDPConn).readFrom (/usr/local/go/src/net/udpsock_posix.go:47) net.(*UDPConn).ReadFrom (/usr/local/go/src/net/udpsock.go:121) main.main (/User/Study/go-sys/chap7/udp_server/udpServer.go:18)
となった。
クライアント側
package main import ( "fmt" "net" ) func main() { conn, err := net.Dial("udp4", "localhost:8888") if err != nil { panic(err) } defer conn.Close() fmt.Println("Sending to server") _, err = conn.Write([]byte("Hello from client")) if err != nil { panic(err) } fmt.Println("Receiving from server") buffer := make([]byte, 1500) length, err := conn.Read(buffer) if err != nil { panic(err) } fmt.Printf("Received: %s\n", string(buffer[:length])) }
net.Dial
でnet.Connインターフェース(実装はnet.UDPConn)を取得してそちらにWrite,Readで送受信ができる。
とてもシンプルですね。
キャプチャ結果
この2つのソースで送受信それぞれ1つづつしかパケットが飛んでいない。
UDPはとてもシンプルですが、TCPが信頼性を担保するためにいかに頑張ってくれているか、というのが実感できるのではないでしょうか。
- 作者: 渋川よしき
- 出版社/メーカー: Lambda Note
- 発売日: 2017/10/19
- メディア: テキスト
- この商品を含むブログを見る
Golangで書かれたWebサーバーでHTTP/1.1のkeep-aliveを検証する
HTTP/1.1にはKeep-Aliveという通信方法があります。HTTP/1.0の頃は1セットの通信が終わるたびに接続・切断処理が入っていたので非効率だったため、複数のリクエストが来た場合にコネクションを使い回す機能になります。 今回はGolangで書かれたサーバーを実行してWiresharkで通信の様子をキャプチャしています
コード
keep-aliveの実装コードの一部です。Requestを終わりまで読み取り、コネクションを再利用します。
//Accept後のソケットでなんども応答を返すためにループ for { //タイムアウトを設定 conn.SetReadDeadline(time.Now().Add(5 * time.Second)) //リクエストを読み込む request, err := http.ReadRequest(bufio.NewReader(conn)) if err != nil { //タイムアウトもしくはソケットクローズ時は終了 //それ以外はエラーにする neterr, ok := err.(net.Error) //ダウンキャスト if ok && neterr.Timeout() { fmt.Println("timeout") break } else if err == io.EOF { break } panic(err) } //リクエストを表示 dump, err := httputil.DumpRequest(request, true) if err != nil { panic(err) } fmt.Println(string(dump)) content := "Hello World\n" //レスポンスを書き込む //HTTP/1.1かつ,ContentLengthの設定が必要 response := http.Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 1, ContentLength: int64(len(content)), Body: ioutil.NopCloser( strings.NewReader(content)), } response.Write(conn) }
通信内容
curlで2回リクエストを投げています。成功すると2回hello world
が帰ってきます
$ curl localhost:8888 localhost:8888 hello world hello world
キャプチャ結果
続いてWiresharkのキャプチャ結果を貼りますちょっと分かり辛いかもしれませんが、
keep aliveが無い方は2回のリクエストに対してそれぞれ接続、切断処理を行なっていますが、keep aliveがある方はコネクションを使いまわして全部で1度しか接続、切断処理をしていないことがわかります。
Golangで作成したWEBサーバーのTCP通信(HTTP GET)をWiresharkで検証
ソースコード
サーバー側のソースコードです8888ポートでクライアントからの送信を待ち受けます
package main import ( "bufio" "fmt" "io/ioutil" "net" "net/http" "net/http/httputil" "strings" ) func main() { listener, err := net.Listen("tcp", "localhost:8888") if err != nil { panic(err) } fmt.Println("Server is running at localhost:8888") for { conn, err := listener.Accept() if err != nil { panic(err) } go func() { //fmt.Printf("Accept %v\n",conn.RemoteAddr()) //リクエストを読みこむ request, err := http.ReadRequest( bufio.NewReader(conn)) if err != nil { panic(err) } dump, err := httputil.DumpRequest(request, true) if err != nil { panic(err) } fmt.Println(string(dump)) //レスポンスを書き込む response := http.Response{ StatusCode: 200, ProtoMajor: 1, ProtoMinor: 0, Body: ioutil.NopCloser( strings.NewReader("hello world\n")), } response.Write(conn) conn.Close() }() } }
クライアントの方は後述のcurlを利用します
通信方法
サーバーの方で8888のポートで受信を受け付けているのでこちらにcurlで通信を試みています
$ curl localhost:8888 hello world
サーバーの方でリクエストを受け取るとhello world
という文字列を返すようにしているのでコマンドを実行するとこちらの文字列が帰ります
localhostに対する送受信をキャプチャする準備
こちらの記事を参考にLoopback:Io0を選択しています
netstatコマンドを利用したTCPコネクションの状態
以下のタイミングでnetstatコマンドを実行してコネクションの状態を確認しました
サーバー起動時
サーバーは起動したタイミングではnetstat
コマンドの出力に8888portの情報は含まれていません。
listner.Accept()メソッドに置いたブレイクポイントで処理を止めたタイミング
tcp4 78 0 localhost.ddi-tcp-1 localhost.53315 ESTABLISHED tcp4 0 0 localhost.53315 localhost.ddi-tcp-1 ESTABLISHED
クライアント側のポート番号として53315が選ばれていることがわかります
なおこの状態でパケットをキャプチャすると以下の情報が得られました。 サーバーがGETを受け取ってOKを返す直前という感じです。
接続終了時
netstatコマンドにport8888の出力は含まれませんでした
次の記事でHTTP/1.1のKeep-Aliveを検証するのでこのnetstatの情報の差分は興味深い結果になると思います
キャプチャ結果
Wiresharkを起動しフィルタをtcp.port==8888
に設定して通信内容を記録しました。すると下のような通信内容が得られました。
解説
- 色枠1のところでTCPの3way HandShakeをしています
- 色枠2のところでデータを送受信しています
- 色枠3のところで切断処理をしています
3Way HandShake
TCPの接続開始を示すこの3つのパケットのやりとりを3 Way HandShakeと言います。
データ送受信
- データ送信直後に、受信側が送信するパケットのACKがデータを受け取った分だけ増えていることが確認できます。
- データ受信確認の後、送信側から送ったもので受信が確認できたオクテット数だけSEQが増えていることが確認できます
切断
サーバーから送ったFINのパケットに対してクライアントもFINのパケットを送り返しています。 こちらは確認応答のたびにACKを+1しています
感想
キャプチャ結果冒頭*1の[SYN]パケットを送った後の[RST,ACK]が送り返されて再度SYNパケットを送信している理由がわかりませんでした。 一般的な挙動には見えないのですが。。。これはGoのListnerの実装などを読めばわかるのでしょうか。次回はKeep-Aliveを実験してみたいと思います
参考
*1:①の上に位置する2つのパケット
Wiresharkでlocalhost(127.0.0.1)宛のパケット送受信をキャプチャする方法
「Goならわかるシステムプログラミング」のネットワークプログラミングの章でプログラムの検証をやりたかったのでLoopbackアドレスに対するパケット送受信ができないのか調べてみたところ、日本語ではあまりズバリな解決策*1が出なかったため英語で調べると解決した。
「wireshark how to capture localhost」でググったところ以下の投稿を参考にwiresharkのwikiにたどり着いた
https://stackoverflow.com/questions/5847168/wireshark-localhost-traffic-capture
https://wiki.wireshark.org/CaptureSetup/Loopback
Summary: you can capture on the loopback interface on Linux, on various BSDs including Mac OS X, and on Digital/Tru64 UNIX, and you might be able to do it on Irix and AIX, but you definitely cannot do so on Solaris, HP-UX, or Windows.
上記引用にある通り、WiresharkのNIC選択一覧に出ているものの中には、loopbackのパケットをキャプチャできるものがある。
今回はこちらを選択することで無事localhost宛のパケットをキャプチャすることができた
ただしmacOSではこの方法が使えたが
では使えないので該当OSユーザーの方は別の方法を模索してください
*1:ルーティングテーブルを書き換えろとか、別のツールを入れろとか