いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-550 (net/textproto)(テキストベースのプロトコル実装のためのライブラリ)

関連記事

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。忘れないようにここにメモメモ。。。

使ったこと無かったのですが、net/textproto というパッケージがあるのを最近知りました。

テキストベースのプロトコル(HTTPとかSMTPとか)の通信処理が作りやすくなるライブラリとのこと。

Package textproto implements generic support for text-based request/response protocols in the style of HTTP, NNTP, and SMTP.

(textprotoパッケージは、HTTP、NNTP、SMTPスタイルのテキストベースのリクエスト/レスポンスプロトコルの汎用サポートを実装します。)

\r\nがデリミタになってて、リクエストにコマンドが入ってて、レスポンスにコードが入っているやつですね。

こんな感じ。

HELO hoge.example.com
250 Hello hoge.example.com

こういうプロトコルを作るときに便利なライブラリです。ついでに ドットエンコーディング なデータを処理するための DotReaderDotWriter というのも用意されています。

サンプル

main.go

いちいちファイル分けるのが面倒なので、サーバとクライアント兼用です。

/*
net/textproto パッケージのサンプルです。
*/
package main

import (
    "flag"
    "fmt"
    "net"
    "net/textproto"
    "strings"
)

type (
    Args struct {
        IsServer bool
    }
)

const (
    OK = 200
    NG = 400

    CmdGet  = "GET"
    CmdSet  = "SET"
    CmdQuit = "QUIT"
)

var (
    args Args
)

func init() {
    flag.BoolVar(&args.IsServer, "server", false, "server mode")
}

func main() {
    flag.Parse()

    if err := run(); err != nil {
        panic(err)
    }
}

func run() error {
    var err error

    if args.IsServer {
        err = runServer()
    } else {
        err = runClient()
    }

    if err != nil {
        return err
    }

    return nil
}

func runServer() error {
    l, err := net.Listen("tcp", ":8888")
    if err != nil {
        return err
    }
    defer l.Close()

    // サンプルなので1回だけ受付
    // クライアントからは
    //   - SET
    //   - GET
    //   - QUIT
    // の3コマンドのシーケンスが来るとする
    conn, err := l.Accept()
    if err != nil {
        return err
    }
    defer conn.Close()

    // net/textprotoの接続として処理
    // 
    // textproto.NewConn() は、引数に io.ReadWriteCloser を要求しているが
    // net.Conn は、os.Fileと同様に io.ReadWriteCloser を実装している。
    tpConn := textproto.NewConn(conn)
    defer tpConn.Close()

    // ウェルカムメッセージ
    err = tpConn.PrintfLine("%d %s", OK, "WELCOME AVAILABLE COMMANDS: {SET, GET, QUIT}")
    if err != nil {
        return err
    }

    // コマンド処理
    data := make(map[string]string)
    for {
        line, err := tpConn.ReadLine()
        if err != nil {
            return err
        }

        parts := strings.Fields(line)
        if len(parts) == 0 {
            tpConn.PrintfLine("%d %s", NG, "コマンドが読み取れません")
            continue
        }

        command := strings.ToUpper(parts[0])
        cmdArgs := parts[1:]

        switch command {
        case CmdGet:
            if v, ok := data[cmdArgs[0]]; ok {
                tpConn.PrintfLine("%d %s", OK, v)
            } else {
                tpConn.PrintfLine("%d %s", NG, "KEY NOT FOUND")
            }
        case CmdSet:
            data[cmdArgs[0]] = cmdArgs[1]
            tpConn.PrintfLine("%d %s", OK, "VALUE SET SUCCESSFULLY")
        case CmdQuit:
            tpConn.PrintfLine("%d %s", OK, "BYE")
            return nil
        default:
            tpConn.PrintfLine("%d %s", NG, "UNKNOWN COMMAND")
        }
    }
}

func runClient() error {
    conn, err := net.Dial("tcp", ":8888")
    if err != nil {
        return err
    }
    defer conn.Close()

    tpConn := textproto.NewConn(conn)
    defer tpConn.Close()

    // helper funcs
    var (
        send = func(tp *textproto.Conn, msg string) error {
            fmt.Printf("< %s\n", msg)
            return tp.PrintfLine("%s", msg)
        }
        recv = func(tp *textproto.Conn) error {
            code, msg, err := tp.ReadCodeLine(OK)
            if err != nil {
                return err
            }
            fmt.Printf("%d %s\n", code, msg)

            return nil
        }
    )

    // WELCOME
    {
        err = recv(tpConn)
        if err != nil {
            return err
        }
    }

    // SET
    m := fmt.Sprintf("%s %s %s", CmdSet, "Hello", "World")
    {
        err = send(tpConn, m)
        if err != nil {
            return err
        }

        err = recv(tpConn)
        if err != nil {
            return err
        }
    }

    // GET
    m = fmt.Sprintf("%s %s", CmdGet, "Hello")
    {
        err = send(tpConn, m)
        if err != nil {
            return err
        }

        err = recv(tpConn)
        if err != nil {
            return err
        }
    }

    // 存在しないコマンド
    m = fmt.Sprintf("%s %s", "GOLANG", "HELLO")
    {
        err = send(tpConn, m)
        if err != nil {
            return err
        }

        err = recv(tpConn)
        if err != nil {
            fmt.Printf("%[1]s (%[1]T)\n", err)
        }
    }

    // QUIT
    m = CmdQuit
    {
        err = send(tpConn, m)
        if err != nil {
            return err
        }

        err = recv(tpConn)
        if err != nil {
            return err
        }
    }

    return nil
}

データの送信には *textproto.Conn.PrintfLine を、データの受信には *textproto.Conn.ReadLine を使います。このメソッドで送受信すると \r\n の付与やレスポンスのコード解析などをうまくやってくれる。

Taskfile.yml

# https://taskfile.dev

version: "3"

tasks:
  default:
    cmds:
      - task: build
      - task: run
  build:
    cmds:
      - go build -o app .
  run:
    cmds:
      - ./app -server &
      - sleep 1
      - ./app
  watch:
    cmds:
      - cmd: sudo tcpdump -i lo -n 'tcp port 8888' -S -X
    interactive: true

shell

実行すると以下のように出力されます。

$ task
task: [build] go build -o app .
task: [run] ./app -server &
task: [run] sleep 1
task: [run] ./app
200 WELCOME AVAILABLE COMMANDS: {SET, GET, QUIT}
< SET Hello World
200 VALUE SET SUCCESSFULLY
< GET Hello
200 World
< GOLANG HELLO
400 UNKNOWN COMMAND (*textproto.Error)
< QUIT
200 BYE

ついでにパケットも眺めてみましょう。

$ task watch
task: [watch] sudo tcpdump -i lo -n 'tcp port 8888' -S -X
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lo, link-type EN10MB (Ethernet), snapshot length 262144 bytes
09:16:24.913973 IP 127.0.0.1.53082 > 127.0.0.1.8888: Flags [S], seq 3624143825, win 43690, options [mss 65495,sackOK,TS val 316896963 ecr 0,nop,wscale 7], length 0
        0x0000:  4500 003c f12b 4000 4006 4b8e 7f00 0001  E..<.+@.@.K.....
        0x0010:  7f00 0001 cf5a 22b8 d804 0bd1 0000 0000  .....Z".........
        0x0020:  a002 aaaa fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  12e3 76c3 0000 0000 0103 0307            ..v.........
09:16:24.913989 IP 127.0.0.1.8888 > 127.0.0.1.53082: Flags [S.], seq 3473127062, ack 3624143826, win 43690, options [mss 65495,sackOK,TS val 316896963 ecr 316896963,nop,wscale 7], length 0
        0x0000:  4500 003c 0000 4000 4006 3cba 7f00 0001  E..<..@.@.<.....
        0x0010:  7f00 0001 22b8 cf5a cf03 b696 d804 0bd2  ...."..Z........
        0x0020:  a012 aaaa fe30 0000 0204 ffd7 0402 080a  .....0..........
        0x0030:  12e3 76c3 12e3 76c3 0103 0307            ..v...v.....
09:16:24.913998 IP 127.0.0.1.53082 > 127.0.0.1.8888: Flags [.], ack 3473127063, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 0
        0x0000:  4500 0034 f12c 4000 4006 4b95 7f00 0001  E..4.,@.@.K.....
        0x0010:  7f00 0001 cf5a 22b8 d804 0bd2 cf03 b697  .....Z".........
        0x0020:  8010 0156 fe28 0000 0101 080a 12e3 76c3  ...V.(........v.
        0x0030:  12e3 76c3                                ..v.
09:16:24.914117 IP 127.0.0.1.8888 > 127.0.0.1.53082: Flags [P.], seq 3473127063:3473127112, ack 3624143826, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 49
        0x0000:  4500 0065 abdc 4000 4006 90b4 7f00 0001  E..e..@.@.......
        0x0010:  7f00 0001 22b8 cf5a cf03 b697 d804 0bd2  ...."..Z........
        0x0020:  8018 0156 fe59 0000 0101 080a 12e3 76c3  ...V.Y........v.
        0x0030:  12e3 76c3 3230 3020 e382 88e3 8186 e381  ..v.200.........
        0x0040:  93e3 819d 2120 e382 b3e3 839e e383 b3e3  ....!...........
        0x0050:  8389 efbc 9a47 4554 2c20 5345 542c 2051  .....GET,.SET,.Q
        0x0060:  5549 540d 0a                             UIT..
09:16:24.914128 IP 127.0.0.1.53082 > 127.0.0.1.8888: Flags [.], ack 3473127112, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 0
        0x0000:  4500 0034 f12d 4000 4006 4b94 7f00 0001  E..4.-@.@.K.....
        0x0010:  7f00 0001 cf5a 22b8 d804 0bd2 cf03 b6c8  .....Z".........
        0x0020:  8010 0156 fe28 0000 0101 080a 12e3 76c3  ...V.(........v.
        0x0030:  12e3 76c3                                ..v.
09:16:24.914203 IP 127.0.0.1.53082 > 127.0.0.1.8888: Flags [P.], seq 3624143826:3624143843, ack 3473127112, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 17
        0x0000:  4500 0045 f12e 4000 4006 4b82 7f00 0001  E..E..@.@.K.....
        0x0010:  7f00 0001 cf5a 22b8 d804 0bd2 cf03 b6c8  .....Z".........
        0x0020:  8018 0156 fe39 0000 0101 080a 12e3 76c3  ...V.9........v.
        0x0030:  12e3 76c3 5345 5420 4865 6c6c 6f20 576f  ..v.SET.Hello.Wo
        0x0040:  726c 640d 0a                             rld..
09:16:24.914212 IP 127.0.0.1.8888 > 127.0.0.1.53082: Flags [.], ack 3624143843, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 0
        0x0000:  4500 0034 abdd 4000 4006 90e4 7f00 0001  E..4..@.@.......
        0x0010:  7f00 0001 22b8 cf5a cf03 b6c8 d804 0be3  ...."..Z........
        0x0020:  8010 0156 fe28 0000 0101 080a 12e3 76c3  ...V.(........v.
        0x0030:  12e3 76c3                                ..v.
09:16:24.914248 IP 127.0.0.1.8888 > 127.0.0.1.53082: Flags [P.], seq 3473127112:3473127140, ack 3624143843, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 28
        0x0000:  4500 0050 abde 4000 4006 90c7 7f00 0001  E..P..@.@.......
        0x0010:  7f00 0001 22b8 cf5a cf03 b6c8 d804 0be3  ...."..Z........
        0x0020:  8018 0156 fe44 0000 0101 080a 12e3 76c3  ...V.D........v.
        0x0030:  12e3 76c3 3230 3020 5641 4c55 4520 5345  ..v.200.VALUE.SE
        0x0040:  5420 5355 4343 4553 5346 554c 4c59 0d0a  T.SUCCESSFULLY..
09:16:24.914285 IP 127.0.0.1.53082 > 127.0.0.1.8888: Flags [P.], seq 3624143843:3624143854, ack 3473127140, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 11
        0x0000:  4500 003f f12f 4000 4006 4b87 7f00 0001  E..?./@.@.K.....
        0x0010:  7f00 0001 cf5a 22b8 d804 0be3 cf03 b6e4  .....Z".........
        0x0020:  8018 0156 fe33 0000 0101 080a 12e3 76c3  ...V.3........v.
        0x0030:  12e3 76c3 4745 5420 4865 6c6c 6f0d 0a    ..v.GET.Hello..
09:16:24.914327 IP 127.0.0.1.8888 > 127.0.0.1.53082: Flags [P.], seq 3473127140:3473127151, ack 3624143854, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 11
        0x0000:  4500 003f abdf 4000 4006 90d7 7f00 0001  E..?..@.@.......
        0x0010:  7f00 0001 22b8 cf5a cf03 b6e4 d804 0bee  ...."..Z........
        0x0020:  8018 0156 fe33 0000 0101 080a 12e3 76c3  ...V.3........v.
        0x0030:  12e3 76c3 3230 3020 576f 726c 640d 0a    ..v.200.World..
09:16:24.914360 IP 127.0.0.1.53082 > 127.0.0.1.8888: Flags [P.], seq 3624143854:3624143860, ack 3473127151, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 6
        0x0000:  4500 003a f130 4000 4006 4b8b 7f00 0001  E..:.0@.@.K.....
        0x0010:  7f00 0001 cf5a 22b8 d804 0bee cf03 b6ef  .....Z".........
        0x0020:  8018 0156 fe2e 0000 0101 080a 12e3 76c3  ...V..........v.
        0x0030:  12e3 76c3 5155 4954 0d0a                 ..v.QUIT..
09:16:24.914391 IP 127.0.0.1.8888 > 127.0.0.1.53082: Flags [P.], seq 3473127151:3473127160, ack 3624143860, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 9
        0x0000:  4500 003d abe0 4000 4006 90d8 7f00 0001  E..=..@.@.......
        0x0010:  7f00 0001 22b8 cf5a cf03 b6ef d804 0bf4  ...."..Z........
        0x0020:  8018 0156 fe31 0000 0101 080a 12e3 76c3  ...V.1........v.
        0x0030:  12e3 76c3 3230 3020 4259 450d 0a         ..v.200.BYE..
09:16:24.914412 IP 127.0.0.1.8888 > 127.0.0.1.53082: Flags [F.], seq 3473127160, ack 3624143860, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 0
        0x0000:  4500 0034 abe1 4000 4006 90e0 7f00 0001  E..4..@.@.......
        0x0010:  7f00 0001 22b8 cf5a cf03 b6f8 d804 0bf4  ...."..Z........
        0x0020:  8011 0156 fe28 0000 0101 080a 12e3 76c3  ...V.(........v.
        0x0030:  12e3 76c3                                ..v.
09:16:24.914427 IP 127.0.0.1.53082 > 127.0.0.1.8888: Flags [F.], seq 3624143860, ack 3473127161, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 0
        0x0000:  4500 0034 f131 4000 4006 4b90 7f00 0001  E..4.1@.@.K.....
        0x0010:  7f00 0001 cf5a 22b8 d804 0bf4 cf03 b6f9  .....Z".........
        0x0020:  8011 0156 fe28 0000 0101 080a 12e3 76c3  ...V.(........v.
        0x0030:  12e3 76c3                                ..v.
09:16:24.914440 IP 127.0.0.1.8888 > 127.0.0.1.53082: Flags [.], ack 3624143861, win 342, options [nop,nop,TS val 316896963 ecr 316896963], length 0
        0x0000:  4500 0034 abe2 4000 4006 90df 7f00 0001  E..4..@.@.......
        0x0010:  7f00 0001 22b8 cf5a cf03 b6f9 d804 0bf5  ...."..Z........
        0x0020:  8010 0156 fe28 0000 0101 080a 12e3 76c3  ...V.(........v.
        0x0030:  12e3 76c3                                ..v.
^C
15 packets captured
30 packets received by filter
0 packets dropped by kernel

そのまんまですが、ちゃんとその通りに流れてて、ペイロードの末尾に「0x0d0a」、つまり、\r\n が付与されていますね。

参考情報

try-golang/examples/basic/bufferop/to_readwritecloser.go at main · devlights/try-golang · GitHub

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。