Go言語によるビットコインのフルノード実装btcdを調べる(2)

btcdを実行した際のプログラムの流れを最初から確認してみます。

server.Start()

btcdを実行すると、設定読み込んだり、データベースの準備したりしたあとに、サーバを作成し、起動します。サーバの起動は、server.goのStart()です。そこで、s.peerHandler()とs.rebroadcastHandler()と、s.rpcServer.Start()を実行してます。

  • s.peerHandler()は、サーバーへのピアの追加と削除、ピアの禁止、ピアへのメッセージのブロードキャストなどのピア操作を処理するために使用されます。
  • s.rebroadcastHandler()は、RPCサーバーが受信したユーザーtxがブロックに含まれるまで再ブロードキャストします。
  • s.rpcServer.Start()は、RPCリスナーをスタートします。

今回は、peerと接続し、ブロックをもらうまでの流れを確認したいと思います。とりあえずpeerHandler()を確認します。

peerHandler()

peerHandler()は、s.addrManager.Start()と、s.blockManager.Start()を実行してます。

addrManager()

addrManagerは、Address Managerという意味で、peerのアドレスを管理しているようです。addrManager.Start()は、まずloadPeers()を実行し、peers.jsonから既知のアドレスを読み込んでいます。その後、addressHandler()を実行しています。addressHandler()は、定期的に新しいアドレスを保存するっぽいです。P2Pなのでアドレスは常に複数の新鮮なやつをゲットしておく必要があります。savePeers()が実際にアドレスの保存をしているようです。addressHandler()は、savePeers()をdumpAddressIntervalに規定されている間隔(デフォルトで10分)で実行しつづけています。新しいアドレスは、DNSseedからではなく、他のpeerから教えてもらうのかなと思うのですが、どうなっているのかな?savePeers()では、AddrManager structのaddrNewを読み込んでるだけぽいので、別のところで、peerが送ってきたアドレスをこれに入れているのかもしれない。

blockManager()

blockManager.Start()は、blockHandler()を実行してます。blockHandlerはブロックマネージャのメインハンドラです。それはgoroutineとして実行する必要があります。それは、ブロック(MsgBlock)メッセージがメモリデータ構造をロックする必要なく単一のスレッドによって処理されるように、ピアハンドラとは別のゴルーチンでブロックメッセージとinvメッセージを処理します。これは、ブロックマネージャがどのブロックが必要か、またどのようにフェッチを進めるべきかを制御するので重要です。ブロックマネージャが、ブロックについて俺ここまで持ってんだけどお前は?みたいなやつのメッセージを作成したり、関連するメッセージを受信した場合のその解析等を行っているぽいです。

connManager()

peerHandler()は、addrManagerとblockManager以外に、connManagerもStart()してます。まさしく接続管理みたいな名前です。connManager.Start()の後は、チャネルの受信待ち状態になっています。connManager.Start()は、btcd/connmgr/connmanager.goにあります。connManager.Start()によって、connection managerを起動し、ネットワーク接続を開始します。connManager.Start()は、とりあえず、connHandler()を実行してます。connHandlerはすべての接続関連の要求を処理します。それはgoroutineとして実行する必要があります。connection handlerは、アクティブなアウトバウンド接続のプールを維持して、ネットワークに接続したままにします。接続要求は、割り当てられたIDによって処理され、マッピングされます。

connHandler()実行後に、cm.cfg.ListenersのListener毎にcm.listenHandler(Listener)を実行します。その後、cm.NewConnReq()を実行しています。

listenHandler()

cm.listenHandler()のコードは下記です。

// listenHandler accepts incoming connections on a given listener.  It must be
// run as a goroutine.
func (cm *ConnManager) listenHandler(listener net.Listener) {
    log.Infof("Server listening on %s", listener.Addr())
    for atomic.LoadInt32(&cm.stop) == 0 {
        conn, err := listener.Accept()
        if err != nil {
            // Only log the error if not forcibly shutting down.
            if atomic.LoadInt32(&cm.stop) == 0 {
                log.Errorf("Can't accept connection: %v", err)
            }
            continue
        }
        go cm.cfg.OnAccept(conn)
    }

    cm.wg.Done()
    log.Tracef("Listener handler done for %s", listener.Addr())
}

connManagerのConfig

connection managerのConfigは、下記があります。

Listeners       []net.Listener
OnAccept        func(net.Conn)
TargetOutbound  uint32
RetryDuration   time.Duration
OnConnection    func(*ConnReq, net.Conn)
OnDisconnection func(*ConnReq)
GetNewAddress   func() (net.Addr, error)
Dial            fun(net.Addr) (net.Conn, error)
  • Listenersは、接続マネージャーが接続の所有権を受け取り、接続を受け入れるリスナーのスライスを定義します。接続が受け入れられると、接続と共にOnAcceptハンドラが呼び出されます。接続マネージャーはこれらのリスナーのネーションをとるため、接続マネージャーが停止すると閉じられます。OnAcceptフィールドも指定されていない場合、このフィールドは効果がありません。呼び出し元が着信接続を待機したくない場合は、nilになることがあります。
  • OnAcceptは、インバウンド接続が受け入れられたときに起動されるコールバックです。接続を閉じるのは発信者の責任です。接続を閉じないと、接続マネージャが終了します。接続が依然としてアクティブであると信じており、したがって最大接続限界にまだカウントしているような望ましくない副作用を有する。このフィールドには、リスナーフィールドも指定されていないと効果がありません。その場合、受け入れ可能な接続が存在しない可能性があるからです。

  • OnConnectionは、新しいアウトバウンド接続が確立されたときに起動されるコールバックです。

  • GetNewAddressは、ネットワーク接続を確立するためのアドレスを取得する方法です。 nilの場合、新しい接続は自動的に行われません。

NewConnReq()

cm.NewConnReq()のコードは下記です。

// NewConnReq creates a new connection request and connects to the
// corresponding address.
func (cm *ConnManager) NewConnReq() {
    if atomic.LoadInt32(&cm.stop) != 0 {
        return
    }
    if cm.cfg.GetNewAddress == nil {
        return
    }

    c := &ConnReq{}
    atomic.StoreUint64(&c.id, atomic.AddUint64(&cm.connReqCount, 1))

    addr, err := cm.cfg.GetNewAddress()
    if err != nil {
        cm.requests <- handleFailed{c, err}
        return
    }

    c.Addr = addr

    cm.Connect(c)
}

// Connect assigns an id and dials a connection to the address of the
// connection request.
func (cm *ConnManager) Connect(c *ConnReq) {
    if atomic.LoadInt32(&cm.stop) != 0 {
        return
    }
    if atomic.LoadUint64(&c.id) == 0 {
        atomic.StoreUint64(&c.id, atomic.AddUint64(&cm.connReqCount, 1))
    }
    log.Debugf("Attempting to connect to %v", c)
    conn, err := cm.cfg.Dial(c.Addr)
    if err != nil {
        cm.requests <- handleFailed{c, err}
    } else {
        cm.requests <- handleConnected{c, conn}
    }
}

新しいアドレスに接続してその結果を、cm.requestsチャネルに入れてます。requestsチャネルの受信を待ち受けているのが、connHandler()です。接続成功した場合、cm.cfg.OnConnection(connReq, msg.conn)を実行してます。

newServer()

一番最初の、btcd/btcd.goでのserver.Start()の前にserverを、newServer()で作成していますが、このnewServer()内で、上記のonConnection()やOnAccept()やGetNewAddress()は設定されています。

cmgr, err := connmgr.New(&connmgr.Config{
    Listeners:      listeners,
    OnAccept:       s.inboundPeerConnected,
    RetryDuration:  connectionRetryInterval,
    TargetOutbound: uint32(targetOutbound),
    Dial:           btcdDial,
    OnConnection:   s.outboundPeerConnected,
    GetNewAddress:  newAddressFunc,
})

newAddressFunc()

newAddressFunc()のコードは下記です。

newAddressFunc = func() (net.Addr, error) {
    for tries := 0; tries < 100; tries++ {
        addr := s.addrManager.GetAddress()
        if addr == nil {
            break
        }

                // Address will not be invalid, local or unroutable
        // because addrmanager rejects those on addition.
        // Just check that we don't already have an address
        // in the same group so that we are not connecting
        // to the same network segment at the expense of
        // others.
        key := addrmgr.GroupKey(addr.NetAddress())
        if s.OutboundGroupCount(key) != 0 {
            continue
        }

        // only allow recent nodes (10mins) after we failed 30
        // times
        if tries < 30 && time.Since(addr.LastAttempt()) < 10*time.Minute {
            continue
        }

        // allow nondefault ports after 50 failed tries.
        if tries < 50 && fmt.Sprintf("%d", addr.NetAddress().Port) !=
            activeNetParams.DefaultPort {
            continue
        }

        addrString := addrmgr.NetAddressKey(addr.NetAddress())
        return addrStringToNetAddr(addrString)
    }

    return nil, errors.New("no valid connect address")
}

outboundPeerConnected()

s.outboundPeerConnected()のコードは下記です。

// outboundPeerConnected is invoked by the connection manager when a new
// outbound connection is established.  It initializes a new outbound server
// peer instance, associates it with the relevant state such as the connection
// request instance and the connection itself, and finally notifies the address
// manager of the attempt.
func (s *server) outboundPeerConnected(c *connmgr.ConnReq, conn net.Conn) {
    sp := newServerPeer(s, c.Permanent)
    p, err := peer.NewOutboundPeer(newPeerConfig(sp), c.Addr.String())
    if err != nil {
        srvrLog.Debugf("Cannot create outbound peer %s: %v", c.Addr, err)
        s.connManager.Disconnect(c.ID())
    }
    sp.Peer = p
    sp.connReq = c
    sp.AssociateConnection(conn)
    go s.peerDoneHandler(sp)
    s.addrManager.Attempt(sp.NA())
}

inboundPeerConnected()

s.inboundPeerConnected()のコードは下記です。

// inboundPeerConnected is invoked by the connection manager when a new inbound
// connection is established.  It initializes a new inbound server peer
// instance, associates it with the connection, and starts a goroutine to wait
// for disconnection.
func (s *server) inboundPeerConnected(conn net.Conn) {
    sp := newServerPeer(s, false)
    sp.Peer = peer.NewInboundPeer(newPeerConfig(sp))
    sp.AssociateConnection(conn)
    go s.peerDoneHandler(sp)
}

やっと、peerパッケージが登場しました。接続できたらNewOutBoundPeer()でアウトバウンド用peerを作成しています。peerパッケージは、peerを作って接続すると勝手に握手とかしだすらしいので、次はpeer同士の握手が確認できると思います。

Go言語によるビットコインのフルノード実装btcdを調べる(1)

btcsuite/btcdを調べてみます。Goで書かれたビットコインのフルノード実装です。ウォレット機能はありません。コマンドラインのオプションの説明はここです。ドキュメントはここです。今使っているのは、btcd version 0.12.0-betaです。

設定ファイルなど

$HOME/.btcdに各種設定ファイルやブロックデータ、接続先peer情報等が保存されます。設定ファイルは、.btcd/btcd.confです。このファイルで、testnet=1とするとテストネットを利用できます。接続先peer情報は、.btcd/data/mainnet/peers.jsonにあります。下記のような形式で格納されていました。testnetの場合は、mainnetがtestnetになります。

{
    "Addr":"133.33.333.33:8333",
    "Src":"114.44.444.44:8333",
    "Attempts":0,
    "TimeStamp":1497783231,
    "LastAttempt":-62135596800,
    "LastSuccess":-62135596800
}

データベースは、LevelDBというのが使われていますが、メタデータはLevelDBで、ブロックデータは、フラットファイル形式でffldbとかいう独自?の形式を使っているみたいです。よくわからないので後で調べます。データの場所は、.btcd/data/mainnet/blocks_ffldbにあります。ブロックデータ?は、000000149.fdbといったファイルです。metadataは、.btcd/data/mainnet/blocks_ffldb/metadataにあり、859650.ldbといったファイルになっています。それぞれどんなデータが入っているのかわかっていませんので後で調べます。

btcdを実行してみる

インストールしてbtcdコマンドを実行すると、既存の他ノードを検索・接続し、ブロックのダウンロードが始まります。最初は既存のノードとつながったことがないので、peers.jsonは空(多分)なはずで、perrs.json以外から接続先を発見することになりますが、DNSシードというノードのIP一覧を保有するDNSサーバに問い合わせをする形でIPを取得するそうです。ソースコード内のDNSseedを検索してみました。下記6つがDNSseedなのかなと思いました。一つfalseになってるけど。

DNSSeeds: []DNSSeed{
    {"seed.bitcoin.sipa.be", true},
    {"dnsseed.bluematt.me", true},
    {"dnsseed.bitcoin.dashjr.org", false},
    {"seed.bitcoinstats.com", true},
    {"seed.bitnodes.io", false},
    {"seed.bitcoin.jonasschnelli.ch", true},
},

ノード同士の接続の流れ

DNSシードへの問い合わせ等でノードを発見したら接続します。通常8333ポートにTCP接続して、versionメッセージを送信することで、handshake(握手)を始めます。このあたりの処理はbtcd/peerに入っているようです。peerのドキュメントやサンプルはここにあります。

次はpeerを調べてみます。

Go – leveldb

Goで使えるLevelDB。
syndtr/goleveldb
ドキュメントはここです。

参考:LevelDB入門 (基本編)

お試しコード

package main

import (
    "fmt"
    "log"
    "github.com/syndtr/goleveldb/leveldb"
)

func main() {
    db := db_init()
    defer func(){
        fmt.Println("Closing DB...")
        db.Close()
    }()

    val, err := db.Get([]byte("key"), nil)
    if err != nil {
        fmt.Println("Key is not exist")
        put(db, []byte("key"), []byte("hello"))
        val, err = db.Get([]byte("key"), nil)
        if err != nil {
            log.Fatal("get error")
        }
    }
    fmt.Printf("key is %s\n", string(val))

    del(db, []byte("key"))
}

func db_init() *leveldb.DB {
    fmt.Println("open database")
    db, err := leveldb.OpenFile("./data/db", nil)
    if err != nil {
        log.Fatal("open error")
    }
    return db
}

func put(db *leveldb.DB, key []byte, value []byte) {
    fmt.Println("put to db")
    err := db.Put(key, value, nil)
    if err != nil {
        log.Fatal("put error")
    }
}

func del(db *leveldb.DB, key []byte) {
    fmt.Println("del from db")
    err := db.Delete(key, nil)
    if err != nil {
        log.Fatal("delete error")
    }
}

Go – 2次関数のPNG画像作成

このライブラリをつかったら簡単にできる。
wcharczuk / go-chart

package main

import (
    "bytes"
    "fmt"
    "github.com/wcharczuk/go-chart"
    "io"
    "os"
)

func main() {
    var x []float64
    var y []float64
    for i := 0.0; i < 100; i++ {
        x = append(x, i-50.0)
        y = append(y, (i-50.0)*(i-50.0))
    }
    graph := chart.Chart{
        Series: []chart.Series{
            chart.ContinuousSeries{
                XValues: x,
                YValues: y,
            },
        },
    }
    buffer := bytes.NewBuffer([]byte{})
    err := graph.Render(chart.PNG, buffer)
    if err != nil {
        fmt.Println("render error")
    }
    out, err := os.Create("./data/graph.png")
    if err != nil {
        fmt.Println("create file error")
    }
    defer out.Close()
    io.Copy(out, buffer)
}

Go – ダウンロードとProgress bar表示

参考:Code-Hex/pget

rangeというのをheaderにセットすると、途中から途中までをダウンロードできるのか。これで分割ダウンロードとかリジュームとかできるらしい。pgetみながら自分でも一応簡単なダウンロード+Progress barをつくってみた。

main.go

package main

import (
    "fmt"
    "net/http"
    "log"
    "os"
    "io"
)

var target_url = "https://www.imagemagick.org/download/linux/CentOS/x86_64/ImageMagick-7.0.6-0.x86_64.rpm"
var out_dir = "."

type Target struct {
    url string
    dir_path string
    file_name string
    file_size int64
}

func main() {
    fmt.Println("test file download")
    target := Target{
        url: target_url,
        dir_path: out_dir,
    }
    check(&target)
    download(&target)
}

func check(t *Target) {
    fmt.Println("checking url...")
    res, err := http.Head(t.url)
    chkErr(err)
    if res.Header.Get("Accept-Ranges") != "bytes" {
        log.Fatal("not supported range access.")
    }
    if res.ContentLength <= 0 {
        log.Fatal("invalid content length.")
    }
    t.file_size = int64(res.ContentLength)
    t.file_name = GetFileName(t.url)
    fmt.Println("check ok.")
}

func download(t *Target){
    fmt.Println("downloading...")
    req, err := http.NewRequest("GET", t.url, nil)
    chkErr(err)
    req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", 0, t.file_size))
    res, err := http.DefaultClient.Do(req)
    chkErr(err)
    defer res.Body.Close()

    out_path := GetFilePath(t.dir_path, t.file_name)
    out, err := os.Create(out_path)
    chkErr(err)
    defer out.Close()
    ch := make(chan int)
    go ProgressBar(t, &out_path, ch)
    io.Copy(out, res.Body)
    <- ch
}

func chkErr(err error){
    if err != nil {
        log.Fatal(err)
    }
}

util.go

package main

import (
    "gopkg.in/cheggaaa/pb.v2"
    "time"
    "net/url"
    "path/filepath"
    "os"
)

func GetFileName(u string) string {
    result, err := url.Parse(u)
    chkErr(err)
    return filepath.Base(result.Path)
}

func GetFilePath(d, f string) string {
    return filepath.Join(d, f)
}

func ProgressBar(t *Target, file_path *string, ch chan int){
    file_size := t.file_size
    now_size := int64(0)
    bar := pb.Start64(file_size)
    for {
        fi, err := os.Stat(*file_path)
        chkErr(err)
        now_size = fi.Size()
        if now_size < file_size {
            bar.SetCurrent(now_size)
        } else {
            bar.SetCurrent(file_size)
            bar.Finish()
            ch <- 1
            break
        }
        time.Sleep(100 * time.Millisecond)
    }
}

Go – Progress bar

このライブラリを使うと簡単に表示できる。
gopkg.in/cheggaaa/pb.v2

package main

import (
    "fmt"
    "gopkg.in/cheggaaa/pb.v2"
    "time"
)

func main() {
    fmt.Println("test progress bar")
    file_size := int64(1024)
    now_size := int64(0)
    bar := pb.Start64(file_size)

    for {
        now_size += 50
        if now_size < file_size {
            bar.SetCurrent(now_size)
        } else {
            bar.SetCurrent(file_size)
            bar.Finish()
            break
        }
        time.Sleep(100 * time.Millisecond)
    }
}

Go – 画像リサイズ

package main

import (
    "github.com/nfnt/resize"
    "image/jpeg"
    "io"
    "log"
    "os"
)

func main() {
    f, err := os.Open("./data/hoge.jpg")
    chkErr(err)
    img, err := jpeg.Decode(f)
    chkErr(err)
    _ = f.Close()

    thumb := resize.Thumbnail(300, 300, img, resize.Lanczos3)

    out, err := os.Create("./data/thumb.jpg")
    chkErr(err)
    defer Close(out)
    err = jpeg.Encode(out, thumb, nil)
    chkErr(err)
}

func Close(c io.Closer) {
    _ = c.Close()
}

func chkErr(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

Go – チャットアプリをつくってみた

コマンドライン上で、複数人でチャットできるクライアント・サーバ型のやつをつくってみた。まだとりあえず動くレベル。スレッドセーフ?な状態でクライアントをclientListから消す方法がわからなかったから消すのをやめた。あと、参考サイトだとチャンネルをたくさん使ってるので、自分がつくったチープなやつと比較して研究したいと思ってます。せっかくなので、暗号化とかしてみたい。あとはp2pバージョンもつくってみる。自動でpがpを発見できる仕組みをつくりたい。

参考サイト

8.1 Socketプログラミング
チャットで学ぶ Go ネットワークプログラミング
mshahriarinia/Golang

Github

コード(サーバ)

package main

import (
    "./util"
    "fmt"
    "io"
    "math/rand"
    "net"
    "os"
    "time"
)

type Client struct {
    name  []byte
    conn  net.Conn
    color int
}

var clientList []*Client
var colorList = [5]int{32, 33, 34, 35, 36}

func send(msg []byte) {
    for _, cl := range clientList {
        _, err := cl.conn.Write(msg)
        if err != nil {
            continue
        }
    }
}

func receiver(cl *Client) {
    buf := make([]byte, 560)
    for {
        n, err := cl.conn.Read(buf)
        if err != nil {
            go send(makeMsgForAdmin(string(cl.name) + " Quit."))
            break
        }
        go send(makeMsg(buf[:n], cl))
        buf = make([]byte, 560)
    }
}

func createClient(conn net.Conn) {
    name := getName(conn)
    color := getColor()
    cl := Client{
        name:  name,
        conn:  conn,
        color: color,
    }
    clientList = append(clientList, &cl)
    send(makeMsgForAdmin(string(name) + " joined!!"))
    go receiver(&cl)
}

func getName(conn net.Conn) []byte {
    buf := make([]byte, 560)
    n, err := conn.Read(buf)
    if err != nil {
        fmt.Println("Fail get name")
        Close(conn)
        os.Exit(1)
    }
    return buf[:n]
}

func getColor() int {
    rand.Seed(time.Now().UnixNano())
    return colorList[rand.Intn(5)]
}

func Close(c io.Closer) {
    err := c.Close()
    if err != nil {
        fmt.Fprintf(os.Stderr, err.Error())
    }
}

func SprintColor(msg string, color int) string {
    return fmt.Sprintf("\x1b[%dm%s\x1b[0m", color, msg)
}

func getTime() string {
    return time.Now().Format("15:04")
}

func makeMsg(msg []byte, cl *Client) []byte {
    new_msg := fmt.Sprintf("%s[%s] %s", getTime(), cl.name, string(msg))
    return []byte(SprintColor(new_msg, cl.color))
}

func makeMsgForAdmin(msg string) []byte {
    new_msg := fmt.Sprintf("(%s) %s", getTime(), msg)
    return []byte(SprintColor(new_msg, 31))
}

func main() {
    service := ":7777"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    util.ChkErr(err, "tcpaddr")
    li, err := net.ListenTCP("tcp", tcpAddr)
    util.ChkErr(err, "tcpaddr")
    for {
        conn, err := li.Accept()
        if err != nil {
            fmt.Println("Fail to connect.")
            continue
        }
        defer Close(conn)
        createClient(conn)
    }
}

コード(クライアント)

package main

import (
    "./util"
    "bufio"
    "fmt"
    "io"
    "net"
    "os"
    "time"
)

var running = true

func sender(conn net.Conn, name string) {
    reader := bufio.NewReader(os.Stdin)
    for {
        input, _, _ := reader.ReadLine()
        if string(input) == "\\q" {
            running = false
            break
        }
        _, err := conn.Write(input)
        util.ChkErr(err, "sender write")
    }
}

func receiver(conn net.Conn, name string) {
    buf := make([]byte, 560)
    for running == true {
        n, err := conn.Read(buf)
        util.ChkErr(err, "Receiver read")
        fmt.Println(string(buf[:n]))
        buf = make([]byte, 560)
    }
}

func Close(c io.Closer) {
    err := c.Close()
    if err != nil {
        fmt.Fprintf(os.Stderr, err.Error())
    }
}

func main() {
    fmt.Print("Please input your name: ")
    reader := bufio.NewReader(os.Stdin)
    name, _, err := reader.ReadLine()

    host := "127.0.0.1:7777"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", host)
    util.ChkErr(err, "tcpAddr")

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    util.ChkErr(err, "DialTCP")
    defer Close(conn)

    _, err = conn.Write(name)
    util.ChkErr(err, "Write name")

    go receiver(conn, string(name))
    go sender(conn, string(name))

    for running {
        time.Sleep(1 * 1e9)
    }
}

Go – defer conn.Close()がエラーといわれる

deferが謎です。ソケット通信のプログラムをつくってみてるんですが、色んな所に書いてある下記部分で、絶対エラーがでます。といっても実行するとエラーにならないのですが、Vimに組み込んでる、GoErrCheckがエラーといってきます。きっと何かおかしいんだと思いました。

def conn.Close()

わかりやすいサイトを見つけました。
http://www.blevesearch.com/news/Deferred-Cleanup,-Checking-Errors,-and-Potential-Problems/

GoErrCheckは、関数を使った時にリターンを受け取るように書かないとエラーをだすのですが、それのことのようです。conn.Close()は結果の成否をリータンするのでそれを受け取らなくてはいけませんでした。しかし、そうなると単純にdefer err := conn.Close()と書けなくなるので、下記のようにします。

defer func() {
  err := conn.Close()
  if err != nil {
    log.Fatal(err)
  }
}()

上記を下記のようにすると便利

conn, err := net.DialTCP("tcp", nil, tcpAddr)
defer Close(conn)

Close関数を別でつくる。

func Close(c io.Closer){
  err := c.Close()
  if err != nil {
    log.Fatal(err)
  }
}

Close関数の引数は、Closerインターフェースを持ってるやつということだと思うので、上記のconnでも使えると思う。log.Fatalはいらないケースも多いのかなと思いました。deferが呼び出された時点のconnが使われるので、複数のconnがある場合も、引数に入れることでそれぞれのconnに対応できるようになるようです。

Go – ソケット通信

P2P通信をつくりたいので、ソケット通信やってみます。超わかりやすいサイトを発見しました。
現在日時を返すサーバと、それを取得・表示するクライアントです。ほぼ上記のサイトの最初のコードですが、上記のサイトのクライアントだと、サーバから返ってくる日付が表示されなかったのでちょっと修正しました。

勉強中のGithub(Server, Client)

Server

package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

func main() {
    service := ":7777"
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    chkErr(err, "ResolveTCPAddr")

    listener, err := net.ListenTCP("tcp", tcpAddr)
    chkErr(err, "ListenTCP")

    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }

        daytime := time.Now().String()
        _, err = conn.Write([]byte(daytime))
        chkErr(err, "Write")

        _ = conn.Close()
    }

}

func chkErr(err error, place string) {
    if err != nil {
        fmt.Printf("(%s)", place)
        fmt.Fprintf(os.Stderr, "%s", err.Error())
        os.Exit(0)
    }
}

Client

package main

import (
    "fmt"
    "net"
    "os"
)

func main() {
    host := "127.0.0.1:7777"

    tcpAddr, err := net.ResolveTCPAddr("tcp4", host)
    chkErr(err, "tcpAddr")

    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    chkErr(err, "DialTCP")

    buf := make([]byte, 1024)
    n, err := conn.Read(buf)
    chkErr(err, "Read")
    fmt.Println(string(buf[:n]))
}

func chkErr(err error, place string) {
    if err != nil {
        fmt.Printf("(%s)", place)
        fmt.Fprintf(os.Stderr, "%s", err.Error())
        os.Exit(0)
    }
}

あと、下記に、チャットのサンプルプログラムがあるのであとで見てみる。
tinode/chat
mshahriarinia/Golang

Go – 超高速webフレームワーク iris

irisは超高速webフレームワークだそうです。すごくシンプルっぽい。でも色々なことができるしサンプルもたくさん紹介されている。名前を読むたびにアイリスオオヤマが浮かんできていやだ。

下記のようにシンプルにWEBアプリがつくれた。viewもアタッチできるし、cssとかも簡単に使えるし、ルーティングもシンプルっぽい。メール送信とか、oauthとかwebsocketとか色々簡単に機能追加できるっぽい。

勉強中コードのgithub

package main

import (
    "github.com/kataras/iris"
    "github.com/kataras/iris/context"
    "github.com/kataras/iris/view"
)

func main() {
    app := iris.New()
    app.AttachView(view.HTML("./views", ".html").Reload(true))

    app.Handle("GET", "/", top)
    app.Handle("GET", "/about", func(ctx context.Context) {
        ctx.View("about.html")
    })  
    app.Handle("GET", "/users", func(ctx context.Context) {
        ctx.HTML("<p>this is users page.</p>")
    })  

    app.StaticWeb("/assets", "./assets")
    app.Run(iris.Addr(":8080"), iris.WithCharset("UTF-8"))
}

func top(ctx context.Context) {
    ctx.ViewData("Username", "Taro")
    ctx.View("top.html")
}

Ubuntu16.04 – キーボードのキー割り当て(キーバインド)をxkbで変更する

ホームディレクトリに.xkbというのを作って、.xkb以下に設定ファイルを作成して、.bashrcとか、.zshrcとかでそれを読みこませることで細かいキーバインド設定が可能。

[xkb] Ubuntu 14.04 で Caps Lock を別のキーにする方法
Ubuntu 14.10でキー配置を変更してみた xkb編

上記を全部一通り読んだらできた。ちなみに下記は英語だけど色々詳しく書いてある。
An Unreliable Guide to XKB Configuration

以下はメモ。

下記で現在のキー設定を確認できる。

$ setxkbmap -print

下記でイベントリスナーが取得してる内容をみることができて、各ボタンをおしたときに、コードや反応しているアクション名などを確認することができる。

$xev

下記パス以下に、xkbの設定が入っている。

/usr/share/X11/xkb/

下記で、xkbの設定を反映することができる。mykbdは自分で命名した内容による。

$ xkbcomp -I$HOME/.xkb ~/.xkb/keymap/mykbd $DISPLAY 2> /dev/null

自分が設定してみた内容は下記です。

~/.xkb/keymap/myxkb

xkb_keymap {
    xkb_keycodes  { include "evdev+aliases(qwerty)" };
    xkb_types     { include "complete"  };
    xkb_compat    { include "complete"  };
    xkb_symbols   { include "pc+jp+us:2+inet(evdev)+myswap(swapkeys)"   };
    xkb_geometry  { include "pc(pc105)" };
};

~/.xkb/symbols/myswap

partial modifier_keys
xkb_symbols "swapkeys" {
  key <AB11> { [ underscore, backslash ] };
  key <MUHE>  { [ Escape, Escape ] };
  key <HENK>  { [ BackSpace, BackSpace] };
  key <HKTG>  { [ Return, Return] };
  key <LALT>  { [ Control_L, Control_L] };
  key <LCTL>  { [ Alt_L, Alt_L] };
};

windows10とubuntu16.04のデュアルブート

このubuntu16.04日本語版をデュアルブートさせました。このサイトを見ながらやりました。最初うまくいきませんでしたが、セキュアブート設定をオフにしていなかったためで、オフにしたらすんなりできました。ただ、パーティションの切り方が自分的によろしくないと思ったので、再設定・再インストールすることにしました。

やった手順は、完全に上記参考サイトと同じで、UbuntuのisoイメージをこのツールでUSBにコピーしました。メインPCとしてメールとかも送るのであれば、日本語版の方がいいのかなと思ってますが、フォルダ名が、「デスクトップ」とか、「ダウンロード」とか、そこは日本語じゃなくていいのにみたいのも多くて、英語版の17.04にしちゃおうかなとも思いました。まあでもとりあえず日本語版でいいか。

パーティションですが、Linuxの場合、SSDに起動プログラムやメインのプログラムを入れて、他のアプリケーションやファイルはHDに入れようと思ったものの、apt-getとかだと基本的にインストール先決まっているし、毎回インストール先を変更するというのは大変なので、/以下全てを小さい領域しかもたないSSDに作り、大きな領域を/media内にマウントさせるという方法はよろしくないのかもと思いました。起動プログラムとキャッシュみたいなやつだけSSDに入れて、あとの大部分のパスをHDにマウントさせられればよろしい感じになるのかなと思いました。

結局下記のような感じにしました。
・SSD 256MG -> /boot
・SSD 4GB -> swap
・SSD 45GB -> /
・HD 1TB -> /home

UbuntuにzshとvimとGoを設定する

zshはoh-my-zshというのを使います。vimはdeinでプラグインを入れます。Goはvim-goプラグインを入れます。

goのインストール

$ sudo -i
$ apt-get update
$ apt-get upgrade
$ apt-get install golang
$ which go
/usr/bin/go

zsh

zshのインストール

$ apt-get install zsh
$ which zsh
/usr/bin/zsh
$ chsh
Changing the login shell for hoge
Enter the new value, or press ENTER for the default
        Login Shell [/bin/bash]: /usr/bin/zsh

oh-my-zshをインストール

$ sh -c "$(wget https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh -O -)"

.zshrcの設定

$ vim .zshrc

.zshrcに下記を追加

export GOPATH=$HOME/go
export GOBIN=$HOME/go/bin
export PATH=$PATH:$HOME/go/bin
$ source .zshrc

Vimの設定

dein.vimをcloneする

$ cd
$ mkdir .vim
$ cd .vim
$ git clone https://github.com/Shougo/dein.vim.git

.vimrcを取得

$ cd
$ git clone https://github.com/endoyuta/vim_config.git
$ ln -s ~/vim_config/.vimrc

deinとプラグインのインストール

$ vim

vim-goプラグインの設定

vim内で、:GoInstallBinariesを実行。

:GoInstallBinaries

Go – Pecoを使ってみる

Pecoというのを使ってみます。

Pecoのインストール

$ go get github.com/peco/peco
$ cd $GOPATH/src/github.com/peco/peco
$ go build cmd/peco/peco.go
$ mv peco.exe $GOPATH/bin
$ peco --version
peco version v0.5.1

go getの仕組み

go getの仕組みは、git cloneだそうです。$GOPATH/binにexeファイルが入ってますが、srcは、gitリポジトリの内容がそのまま入っていて、フォルダが、github.com/hoge/hoge.gitだとすれば、$GOPATH/src/github.com/hoge/hogeというふうになっております。なので、$GOPATH/src/github.com/hoge/で、git cloneすると同じことがおこると思います。しかし、go getにガチで対応しているものは、go get github.com/hoge/hogeとやるだけで、$GOPATH/binにexeファイルが格納されるので、恐らくgo getというのは、git clone後にgo buildしてその結果を$GOPATH/binに移動させているのではないかと思いました。今度go getにガチで対応しているものとしていないものの違いを調べてみたいと思います。pecoはガチ対応じゃないので、buildして移動したら使えるようになりました。

このやや独特のフォルダ構成に対応したパッケージ管理ツールが、ghqだそうです。ちなみに今、みんなのGOという本を読んでます。ghqとpecoを使うと便利だそうです。この便利さをvim上で実現したいです。

ghqのインストール・設定

go getでexeファイルも作成されました。

$ go get github.com/motemen/ghq

設定

下記のようにgit configにghq.rootを設定します。

$ git config --global ghq.root $GOPATH/src

ghqの使い方

下記のような感じで使います。listでリポジトリのリストが表示されます。getでフォルダ構造を維持しながら、なんのリポジトリでももってこれます。別にここまでして、$GOPATH/srcにGOと無関係のリポジトリまで格納する必要があるのかよく分からないんだけど便利な気もする。

$ ghq list
$ ghq get

ghqからのpeco

$ ghq list -p | peco

これで、リポジトリリストをペコで検索できます。本だと、ここで選んだ結果に対してcdで移動させてました。ただ、これwindows10のgit bashだと使えないっぽくて、コマンドプロンプトだと動きました。ghqはgit bashで使えるけど。peco便利だな。