Goでバイナリを作る

ネットワークパケットを作るために,構造体や変数からバイト型のスライスへの変換方法を調べた.

テストで書いてみたプログラムは次の通り.
とりあえずint型の変数をいくつか対象としてバイト変換を試してみた.

何らかの型(構造体やint等のプリミティブ型)からバイト型のスライスに変換するには, encoding/binary パッケージの Write メソッドを使用すると良い.

binary.Write() メソッドは次のような定義になっている.

func Write(w io.Writer, order ByteOrder, data interface{}) error

第3引数に指定したデータを,第2引数で指定したバイトオーダーで第1引数の w に書き込む.

このメソッドを使うために,まず io.Writer インタフェースを実装したものが必要になる.
バイト型のデータを書き込みたいので, bytes パッケージの Buffer を使用することにする.

buf := new(bytes.Buffer)

注意点として, new() を使う点が挙げられる.
bytes.BufferWrite() メソッドはポインタ型( *bytes.Buffer )がレシーバとなっているため, new() を使ってポインタを取得して上げる必要がある.

これで io.Writer を用意できたので, binary.Write() を使ってデータを書き込む.
適当にint型の変数を用意して binary.Write() による書き込みを行う.
なお,今回はネットワークパケットを作りたいので,第2引数のバイトオーダはビッグエンディアンを指定する.

var val32bit int32
val32bit = 123

err = binary.Write(buf, binary.BigEndian, val32bit)
if err != nil {
    log.Fatalln(err)
}
fmt.Printf("uint32: % x\n", buf.Bytes())

これで無事書き込みができた.

サンプルプログラムの実行結果

uint8: 7b
uint16: 00 7b
uint32: 00 00 00 7b

今回はint型のみを扱ったが,構造体を書き込みたいときも同じようにして扱うことができるようだ.

HTTPリダイレクトについて調査した

HTTPのリダイレクトには,大きく分けて次の二つ存在する.

  • 一時的なリダイレクト
  • 恒久的なリダイレクト

一般的に用いられるリダイレクトのステータスコードは 301302 である(個人的な考えですが).
しかし,他にもいくつかリダイレクトのステータスコードは存在する.

また,似たような意味を持つステータスコードもあるので,整理するために少し調査した.

今回対象とするリダイレクトのステータスコード

今回は,次の4つを対象とする.

  • 301 Moved Permanently
  • 302 Found
  • 307 Temporary Redirect
  • 308 Permanent Redirect

それぞれのステータスコードについて,RFCを参考に概要を以下に示す.
308以外はRFC 7231,308はRFC 7238で定義されている.

301 Moved Permanently

  • 恒久的なリダイレクトを表す
  • 歴史的な経緯から,301を受け取ったクライアントはリクエストメソッドをPOSTからGETに変更する可能性がある
    • これを望まない場合,代わりに307を用いることができる
    • しかし,307とは意味が異なる(恒久的↔一時的)
      • 308が新しく定義された

302 Found

  • 一時的なリダイレクトを表す
  • 歴史的な経緯から,301を受け取ったクライアントはリクエストメソッドをPOSTからGETに変更する可能性がある
    • これを望まない場合,代わりに307を用いることができる

307 Temporary Redirect

  • 一時的なリダイレクトを表す
  • これを受け取ったクライアントはリクエストメソッドを変更してはいけない

308 Permanent Redirect

  • 恒久的なリダイレクトを表す
  • これを受け取ったクライアントはリクエストメソッドを変更してはいけない

挙動の確認

それぞれのステータスコードに対する挙動を確認するため,簡単なサーバプログラムをGoで作成し,そのサーバに4種類のクライアントで接続した.
作成したサーバプログラムは mas9612/http-redirect-test に置いている.

用いたクライアント
* curl 7.43.0
* Safari 11.1.2
* Google Chrome 67.0.3396.99
* Firefox 59.0.2

上記それぞれのクライアントから,GETとPOST2つのメソッドで作成したサーバに接続した.
結果を次の表に示す.

Method GET

client 301 302 307 308
curl GET GET GET GET
Safari GET GET GET GET
Chrome GET GET GET GET
Firefox GET GET GET GET

Method POST

client 301 302 307 308
curl POST POST POST POST
Safari GET GET POST POST
Chrome GET GET POST POST
Firefox GET GET POST POST

実験結果より,curl以外のクライアントは,POSTリクエストの応答結果が301と302の時,リダイレクト先へのリクエストメソッドをGETに変更していることがわかった.
また,RFCの定義どおり,307と308はきちんとリクエストメソッドが維持されていることが確認できた.

References

Docker Engine API試用

前に少し気になっていたDocker Engine APiを使ってみたので,それについて.

特に複雑なことはせず,引数に与えたイメージを削除するというプログラムを作ってみた.
ただ単純に削除するだけではつまらないので,何世代分保存しておくか,というのをオプションで指定できるようにした.

コードは mas9612/docker-tools/image-remove に置いてある.
あまりきれいなコードではないのでご注意ください.

Client.ImageList() メソッドでローカルにあるイメージの一覧が取得できるが, filter でイメージ名を指定できなさそうだったので,愚直にfor文で1つ1つ確認している.
アルゴリズムは得意ではないので,良い方法があれば教えてください…

images, err := client.ImageList(ctx, types.ImageListOptions{})
if err != nil {
    log.Fatalf("[ERROR] client.ImageList(): %s\n", err)
}

for _, image := range images {
    for _, repotag := range image.RepoTags {
        repository := strings.Split(repotag, ":")
        if repository[0] == *imageName {
            imageInfos = append(imageInfos, imageInfo{
                ID:      image.ID,
                Created: image.Created,
                Name:    repotag,
            })
        }
    }
}

削除対象のイメージをリスト出来たら,それを作成日時でソートし,指定した世代分は残してそれ以外を Client.ImageRemove() メソッドで削除している.
デフォルトでは,イメージ名にマッチしたもの全てを削除するようになっているのでお気をつけください.

if *generation > len(imageInfos) {
    *generation = len(imageInfos)
}
removeOptions := types.ImageRemoveOptions{
    Force: *force,
}
for _, image := range imageInfos[*generation:] {
    _, err := client.ImageRemove(ctx, image.ID, removeOptions)
    if err != nil {
        log.Fatalf("[ERROR] client.ImageRemove(): %s\n", err)
    }
    fmt.Printf("Image %s was deleted.\n", image.Name)
}

やっている事自体は簡単なので,ドキュメントと見比べて頂ければわかると思います.

GoでMySQLを使う – database/sql package

GoからMySQLを使う方法について調べた.
O/Rマッパーを使う方法も気になったが,まずGo標準パッケージで用意されている機能を使い,SQLを地道に実行していく方法を試した.

ソースコードは以下.

実行

Dockerを使って簡単にローカルにMySQLを準備する.

$ docker run -p 3306:3306 -e MYSQL_ROOT_PASSWORD=mypass -e MYSQL_DATABASE=testdb -e MYSQL_USER=mysql -e MYSQL_PASSWORD=mypass -d --name mysql mysql

DBの準備後,作成したソースコードを実行する.

$ go run mysql_example.go
ID: 1, Name: Tom
ID: 2, Name: Bob
ID: 3, Name: Alice

INSERTしたデータが正しく取得できていそうである.
念のため,MySQLに入って確認してみる.

$ docker exec -it mysql mysql -u root -p
Enter password:

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
| testdb             |
+--------------------+
5 rows in set (0.01 sec)

mysql> use testdb;
Database changed

mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| test_tbl         |
+------------------+
1 row in set (0.00 sec)

mysql> select * from test_tbl;
+------+-------+
| id   | name  |
+------+-------+
|    1 | Tom   |
|    2 | Bob   |
|    3 | Alice |
+------+-------+
3 rows in set (0.00 sec)

上記の通り,正常にテーブルの作成とデータの追加が行えていることが確認できた.

解説

準備

Goの database/sql パッケージを使うと,色々なDBを扱うことができる.
しかし, database/sql パッケージとは別に,ここから使いたいDBのdriverを探してインストールしておく必要がある.
今回はMySQLを使いたいので,go-sql-driver/mysqlを利用した.
下記コマンドで go-sql-driver/mysql をインストールする.

$ go get -u github.com/go-sql-driver/mysql

データベースへの接続

データベースへ接続するには, sql.Open() メソッドを使用する.
第1引数に使用したいdriver名,第2引数に接続先を指定する.

db, err := sql.Open("mysql", "mysql:mypass@/testdb")

接続確認を行いたい場合は, sql.Open() の後に DB.Ping() メソッドを呼び出すことでできる.

if err = db.Ping(); err != nil {
    log.Fatalf("db.Ping(): %s\n", err)
}

SQLの実行

SQLの実行は, DB.Exec() 及び DB.Query() メソッドで行うことができる.
CREATE文やINSERT文など,DBからデータが返ってこないものに関しては DB.Exec() メソッドを用い,SELECT文などDBからデータを取得するのが目的であるものに関しては DB.Query() メソッドを用いる.

Exec()

_, err = db.Exec("create table test_tbl (id int, name varchar(32))")
if err != nil {
    log.Fatalf("db.Exec(): %s\n", err)
}

Query()

DBからの結果は sql.Rows に入っている.
Rows.Scan() メソッドで,1レコードの中から値(今回であればSELECT文での取得対象に * をしているため,全てのカラム = idname )を取得することができる.
Rows.Scan() メソッドの引数にはポインタを渡すことに注意する.

1レコード分の処理が終了し,次のレコードに移るためには Rows.Next() メソッドを呼び出す.

var rows *sql.Rows
rows, err = db.Query("select * from test_tbl")
if err != nil {
    log.Fatalf("db.Query(): %s\n", err)
}
defer rows.Close()

for rows.Next() {
    var (
        id   int
        name string
    )
    err = rows.Scan(&id, &name)
    if err != nil {
        log.Fatalf("rows.Scan(): %s\n", err)
    }

    fmt.Printf("ID: %d, Name: %s\n", id, name)
}
if err = rows.Err(); err != nil {
    log.Fatalf("rows.Err(): %s\n", err)
}

Go net package – Goでソケット通信

Goのnetパッケージについて軽く勉強した.
簡単なソケット通信についてまとめる.

ソケット通信

ソースコードは以下.

package main

import (
    "fmt"
    "log"
    "net"
)

func main() {
    host := "localhost"
    port := "8000"
    address := net.JoinHostPort(host, port)
    conn, err := net.Dial("tcp", address)
    if err != nil {
        log.Fatalf("net.Dial(): %s\n", err)
    }
    defer conn.Close()

    request := "GET / HTTP/1.1\n\n"
    fmt.Println([]byte(request))
    _, err = conn.Write([]byte(request))
    if err != nil {
        log.Fatalf("Conn.Write(): %s\n", err)
    }

    buffer := make([]byte, 1024)
    var n int
    for {
        n, err = conn.Read(buffer)
        if n == 0 {
            break
    }
    if err != nil {
        log.Fatalf("Conn.Read(): %s\n", err)
    }
    fmt.Print(string(buffer))
    }
}

以下に簡単な解説を.

Goでのソケット通信は, Conn オブジェクトを作成するところから始まる.
まず, net.Dial()Conn オブジェクトを作成する.

conn, err := net.Dial("tcp", address)

第1引数にはネットワークの種類を,第2引数には接続したい先のアドレスを指定する.
今回はサンプルとしてローカルに立てたHTTPサーバへ接続してみるので,ネットワークの種類は TCP を指定しておく.

Conn オブジェクトを作成できたら,後は Conn.Read()Conn.Write() でデータの読み書きができる.

_, err = conn.Write([]byte(request))
...
...
n, err = conn.Read(buffer)

簡単なソケット通信は以上の3つのメソッドを使うことで簡単にできる.

Pythonで簡単にローカルHTTPサーバをたて,作成したプログラムを実行してみる.

$ python -m http.server
$ go run main.go
HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.6.2
Date: Sun, 04 Feb 2018 12:47:14 GMT
Content-type: text/html; charset=utf-8
Content-Length: 336

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Directory listing for /</title>
</head>
<body>
<h1>Directory listing for /</h1>
<hr>
<ul>
<li><a href="main.go">main.go</a></li>
</ul>
<hr>
</body>
</html>

JoinHostPort()/SplitHostPort()

上の例でも使っているが,これらのメソッドを使うことでアドレスとポート番号の結合・分離が簡単にできる.

address := net.JoinHostPort("localhost", "8000")
fmt.Println(address) // localhost:8000

host, port, err := net.SplitHostPort("localhost:3000")
if err != nil {
log.Fatalf("net.SplitHostPort(): %s\n", err)
}
fmt.Printf("Host: %s, Port: %s\n", host, port) // Host: localhost, Port: 3000