Yabu.log

ITなどの雑記

プログラマのためのSQL 読書会(27)に参加

余談ですが参加者にdbtech show case tokyo 2018の参加者も多かったので、そちらの話題で雑談などが盛り上がりました。*1

「29.7ビット単位の集約関数」から読みました。

プログラマのためのSQL 第4版

プログラマのためのSQL 第4版

適切に設計されたOLTPデータベースは正規化されている。通常、DWHはスタースキーマスキーマまたはスノーフレークスキーマを採用しており、これはかなり非正規化されている。

ペタバイト以上のデータを持つ企業

ペタバイト以上のデータを持つ企業だと書かれているが、 このほんの情報は古く、2014年時点では売上10億ドル以上の 企業の56パーセントが 1PB以上のデータを管理している*2

  • 巨大なデータを扱うDB

  • dbtech show caseで話題のMySQL8.0の環境構築がうまくいかなかった

  • 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が書かれていないのでそちらも欲しい

  • 99年にANSI SQLRoll up,Cube関数が入った

  • 最新のMySQLにwindow関数に対応した
  • 最新のSQLiteはWindow関数に対応した
  • CUME_DIST()関数
    • 初めて見たので検証予定
  • Window Frame
    • 理解しきれてないので検証予定

前職ではウインドウ関数を多少していたのですが*3、WindowFrameを弄ったりは普段やりません。*4 ので、この辺は技術検証の必要があるかなと思います。

次は「30.6ベンダー拡張」から!

*1:参加されていない方申し訳ない

*2:http://www.jdsf.gr.jp/backup/stm/img/201507/01.pdf

*3:ウインドウ関数簡単なものなら空でかけます

*4:普通はウインドウ関数もあまり利用しないと思いますが、プロダクションのSQLの品質が疑わしい場合かなり使えます

分散処理本第38回に参加

Distributed Computing: Principles, Algorithms, and Systemsを読む勉強会です。やはり難しいので少し予習していきました。アルゴリズムの分類までは一応軽く読んで概要をつかんでから臨みました

予習をしてブログの下書きを作っておくと、感想のポストがサクッとかけて良いですね。

Distributed Computing: Principles, Algorithms, and Systems

Distributed Computing: Principles, Algorithms, and Systems

分散システムに置けるデッドロックの検出アルゴリズムの分類

  • 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が発生していれば帰ってくるようなエコーメッセージを送る。

よく解らん?という話になった

参考にしてある論文は以下のもの

    1. Chandy, J. Misra, and L. M. Haas, Distributed deadlock detection, ACM Transactions on Computer Systems, 1(2), 1983, 144–156.
  1. 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

アルゴリズムとしてはpublic/privateなlabelを使う 文章化するのがちょっと難しいが一言で表すなら

  • 待ち状態に対して2種類(private/public)のラベル作成し値zを与える
    • 下の例はu->v間の待ちを表しているこのzはu+vより大きな値かつユニークでなければならない
    • この操作を待ちの連鎖が終わるところまで続けて行く

f:id:yuyubu:20180918234652j:plain

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日ほどかかった。

とりあえず自分は日頃競プロ的な頭を使うプログラミングをやらないので、 プログラミング能力が劇的に落ちていることを実感した。

時間をかけて競プロを強めようとは思わないが、アルゴリズムとかのエンジニアとしての基礎的な部分を固める必要があると再度認識した。

コードはあまり人に見せられるような綺麗なものではないけど、時間がかかった課題のものを中心に公開しておきます。

フィボナッチ

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 などがあります。

f:id:yuyubu:20180918023900p:plain
(x+y)/2
f:id:yuyubu:20180918023856p:plain
x * y
f:id:yuyubu:20180918023902p:plain
x ^ y

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のnilJavaのnullとも違うし、nil自体がnil型のような特性を持っているような振る舞いがあるようなので、 この辺りは今後注意して学習して行きたい。

感想

今までは写経中心だったり、ロジックというよりもAPIのドキュメントを調べて書かれている通りに実装することが中心だったが、Goできちんとプログラミングを書くのは初めてだったのでいい経験になったと思う。

何よりも、tour of Goをやらねばいけない、という憑き物のようなものが落とせてよかった。

*1:と行っても周回するつもりはあいrません

GolangでUDPサーバー&クライアントを作成してWiresharkで検証する

Goならわかるシステムプログラミング第7章 UDPソケットを使ったマルチキャスト通信に書かれている内容をWiresharkを使ったり、デバッグでライブラリ(netパッケージ中心)のソースを読んだりして検証してみました

サーバー側

  • net.ListenPacket()の戻り値のnet.PacketConnインターフェースを使う
    • TCPの時のnet.Listenerインタフェースのようなクライアントを待つインタフェースではない
    • ReadFromメソッドで受信内容を読み取る
      • 引数で渡しているbufferに通信内容を格納している
      • remoteAddressに通信して来たクライアントのアドレスを受け取っている
      • bufferの中身はあくまでudpパケットの中身しか入らない
        • だからIPヘッダの情報などはReadだけでは読み取れず、ReadFromで別途IPアドレスなどを取得する必要がある
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つづつしかパケットが飛んでいない。

f:id:yuyubu:20180917005441p:plain

UDPはとてもシンプルですが、TCPが信頼性を担保するためにいかに頑張ってくれているか、というのが実感できるのではないでしょうか。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング

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のキャプチャ結果を貼りますちょっと分かり辛いかもしれませんが、

  • 「接」がTCPの接続処理(3Way HandShake)です
  • 「通」がデータの通信処理です
  • 「断」がTCPの切断処理です(FIN)

f:id:yuyubu:20180915230426p:plain
図1.keep aliveなしの場合

f:id:yuyubu:20180915230429p:plain
図2.keep aliveありの場合

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に対する送受信をキャプチャする準備

yuyubu.hatenablog.com

こちらの記事を参考に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を返す直前という感じです。

f:id:yuyubu:20180915165701p:plain

接続終了時

netstatコマンドにport8888の出力は含まれませんでした

次の記事でHTTP/1.1のKeep-Aliveを検証するのでこのnetstatの情報の差分は興味深い結果になると思います

キャプチャ結果

Wiresharkを起動しフィルタをtcp.port==8888に設定して通信内容を記録しました。すると下のような通信内容が得られました。

f:id:yuyubu:20180915165058p:plain

解説

  • 色枠1のところでTCPの3way HandShakeをしています
  • 色枠2のところでデータを送受信しています
  • 色枠3のところで切断処理をしています

3Way HandShake

TCPの接続開始を示すこの3つのパケットのやりとりを3 Way HandShakeと言います。

f:id:yuyubu:20180915165237p:plain

データ送受信

f:id:yuyubu:20180915165250p:plain

  • データ送信直後に、受信側が送信するパケットのACKがデータを受け取った分だけ増えていることが確認できます。
  • データ受信確認の後、送信側から送ったもので受信が確認できたオクテット数だけSEQが増えていることが確認できます

切断

f:id:yuyubu:20180915165307p:plain

サーバーから送ったFINのパケットに対してクライアントもFINのパケットを送り返しています。 こちらは確認応答のたびにACKを+1しています

感想

キャプチャ結果冒頭*1の[SYN]パケットを送った後の[RST,ACK]が送り返されて再度SYNパケットを送信している理由がわかりませんでした。 一般的な挙動には見えないのですが。。。これはGoのListnerの実装などを読めばわかるのでしょうか。次回はKeep-Aliveを実験してみたいと思います

参考

  • Goならわかるシステムプログラミング第6章TCPソケットとHTTPの実装
  • コンピュータネットワーク第5版
  • マスタリングTCP/IP入門

*1:①の上に位置する2つのパケット

Wiresharkでlocalhost(127.0.0.1)宛のパケット送受信をキャプチャする方法

「Goならわかるシステムプログラミング」のネットワークプログラミングの章でプログラムの検証をやりたかったのでLoopbackアドレスに対するパケット送受信ができないのか調べてみたところ、日本語ではあまりズバリな解決策*1が出なかったため英語で調べると解決した。

wireshark how to capture localhost」でググったところ以下の投稿を参考にwiresharkwikiにたどり着いた

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.

上記引用にある通り、WiresharkNIC選択一覧に出ているものの中には、loopbackのパケットをキャプチャできるものがある。

f:id:yuyubu:20180913133347p:plain
Loopback:Io0を利用した

今回はこちらを選択することで無事localhost宛のパケットをキャプチャすることができた

f:id:yuyubu:20180913133516p:plain
TCPハンドシェイクを含むHTTP通信をキャプチャした様子

ただしmacOSではこの方法が使えたが

では使えないので該当OSユーザーの方は別の方法を模索してください

*1:ルーティングテーブルを書き換えろとか、別のツールを入れろとか