LoginSignup
27
30

More than 5 years have passed since last update.

Goで簡易なプロトコルの実装をしてみた

Last updated at Posted at 2018-12-02

こちらは、Go3 Advent Calendar 2018 の 3 日目の記事です。
昨日は、@takochuu さんの 標準パッケージから見るパッケージ構成のパターンの解説でした。
(標準パッケージでも、様々な構成があって勉強になりました。)

概要

Go で非常に簡単なプロトコルを実装してみました。
(本当に簡単なのでご容赦ください。)

対象

  • プロトコルってどうやって作るのか気になる方
  • なんとなく読んでみたい方

内容

  • 簡単なプロトコルの Go 実装
    • アプリケーション層のプロトコルを実装
    • ネットワーク層は TCP 通信

詳細

プロトコル仕様

簡単なユーザー認証が入ったプロトコルで、下記の通りに通信します。

1. 初期リクエストパケット (Clinet -> Server)

対象リクエストか否かを判定するため、Clientから特定のbyte(ENQ)のリクエストを送ります。

|-----------------------------|
| 1byte | リクエスト通知 (0x05) |
|-----------------------------|
2. 初期レスポンスパケット (Server -> Client)

対象の場合は、認証のための情報を返します。
今回はパスワードハッシュ化のためのキーを返しています。

|-----------------------------------------|
| 1byte  | リクエスト結果(OK=0x06, NG=0x06) |
| 3byte  | バージョン ex)1.0.0             |
| 10byte | リクエストID                    |
| 20byte | パスワードハッシュ化キー           |
|-----------------------------------------|
3. 認証リクエストパケット (Client -> Server)

ユーザーIDとパスワードを送信します。パスワードは先程受け取ったハッシュ化キーを利用してハッシュ化します。
(※ ユーザーIDは10byte以上のデータは利用できないです)

|------------------------------|
| 3byte  | バージョン            |
| 10byte | リクエストID          |
| 10byte | ユーザーID            |
| 32byte | ハッシュ化済みパスワード |
|------------------------------|
4. 認証レスポンスパケット (Server -> Client)

受信したユーザーID/パスワードから、結果を返します。

|------------------------------------|
| 3byte  | バージョン                  |
| 10byte | リクエストID                |
| 1byte  | 認証結果(OK=0x06, NG=0x06) |
|------------------------------------|
5. データ送/受信

(※ ここからは、まだ未実装かつプロトコル未定)

実装

GitHub上に上げています。
midorigreen/groto

├── example         # サンプル
│   ├── client
│   │   └── client.go
│   └── server
│       └── server.go
├── README.md
├── go.mod
├── groto.go        # protocol定義
├── client.go       # protocol client実装
└── server.go       # protocol server実装

使い方

groto/example

プロトコルの利用方法としては、 net.Conn interfaceの実装を渡すだけで、認証まで実行するようにしています。

Client

// ユーザー/パスワードを渡してClient生成
cli := groto.NewClient(user, password)
// 初期→認証まで実行
if err := cli.Do(conn); err != nil {
  return err
}

Server

l, err := net.Listen("tcp", ":8080")
if err != nil {
  return err
}
for {
  conn, err := l.Accept()
  if err != nil {
    log.Println(err)
  }
  go func(conn net.Conn) {
    defer conn.Close()
    // Server生成
    s := groto.NewServer()
    // 初期→認証まで実行
    if err := s.Do(conn); err != nil {
      return
    }

    // やりたい処理
    for {
      b := make([]byte, 2*1024)
      _, err := conn.Read(b)
    }
  }(conn)
}

(書きながら気づいたんですが、このあたりの待受の処理等もプロトコル側の実装で持たせればよかったかなと思います。)

追記: ループ時にconnが上書きされる問題を修正しています

プロトコル側

基本的に各送信ごとのパケットをstructで定義しています。
例えば、2. 初期レスポンスパケットは下記の通りに定義しています。

type PacketHandshake struct {
  status    Status
  version   []byte
  id        []byte
  pwHashKey []byte
}

パケット間通信はbyte配列をやりとりするため、byteへの変換処理が必要になります。struct to byteをMarshal、byte to structをUnmarshalとしてメソッドを定義します。愚直な実装をしてます。

func (i *PacketHandshake) Marshal() []byte {
  b := make([]byte, 0, initLen)

  b = append(b, byte(i.status))
  b = append(b, i.version...)
  b = append(b, i.id...)
  b = append(b, i.pwHashKey...)

  return b
}

func UnmarshalHandshake(b []byte) (PacketHandshake, error) {
  return PacketHandshake{
    status:    Status(b[0]),
    version:   b[1:4],
    id:        b[4:14],
    pwHashKey: b[14:initLen],
  }, nil
}

各パケットを上記のように定義して、後は決められた通りにパケットをやり取りする実装を書きました。
パケットの交換は net.Conn interfaceのRead/Writeメソッドを利用して実装しています。

全体像

サーバー側

type Server struct {
  hashKey []byte
}

func NewServer() *Server {
  return &Server{}
}

func (s *Server) Do(conn net.Conn) error {
  // 初期パケットの処理
  if err := s.stepHandshake(conn); err != nil {
    return err
  }
  // 認証パケットの処理
  if err := s.stepAuthN(conn); err != nil {
    return err
  }
  return nil
}

クライアント側

type Client struct {
  user      string
  password  string
  id        []byte
  pwHashKey []byte
}

func NewClient(user, password string) *Client {
  return &Client{
    user:     user,
    password: password,
  }
}

func (c *Client) Do(conn net.Conn) error {
  // 初期パケット処理
  if err := c.stepHandshake(conn); err != nil {
    return err
  }
  // 認証パケット処理
  if err := c.stepAuthN(conn); err != nil {
    return err
  }
  return nil
}

各パケット実装

パケット送受信を実装しているメソッドは下記で、net.Conn を引数に取ります。

// Client側 メソッド
func (c *Client) stepHandshake(conn net.Conn) error {}

// Server側メソッド
func (s *Server) stepHandshake(conn net.Conn) error {}

パケットの流れベースで見ると、処理は下記の通りになっています。

例) 初期パケット

① Client (初期リクエストパケット送信)

// conn = net.Connを実装したstruct
// 今回は *TCPConn

// 初期パケット送信
_, err := conn.Write([]byte{0x05})
if err != nil {
  return err
}


② Server (初期リクエストパケット受信)

b := make([]byte, 2*1024)
// 初期パケットを読み込み
_, err := conn.Read(b)
if err != nil {
  return fmt.Errorf("failed read connection: %v", err)
}

// 対象パケットか否かを判定
if b[0] != 0x05 {
  // エラー処理
}
// 結果を返すパケットのstructを作成
i, err := NewPacketHandshake(OK)
if err != nil {
  return fmt.Errorf("failed create init proto: %v", err)
}
// 結果をconnectionへ書き込み
_, err = conn.Write(i.Marshal())
if err != nil {
  return err
}


③ Client

b := make([]byte, 33)
// 初期リクエスト結果を読み込み
_, err = conn.Read(b)
if err != nil {
  return err
}
// structへ変換
i, err := UnmarshalHandshake(b)
if err != nil {
  return err
}
// 結果を確認
if i.status != OK {
  return errors.New("failed init")
}

このような処理を、各ステップごとにClient/Server側で実装しています。
一応、1~4の仕様を満たした実装が完了しています。

まとめ

非常に簡易で実用性はほぼなさそうですが、プロトコル実装してみました。
このプロトコルを拡張して、作ろうとしているツールの初期認証かつその後のデータのやり取りに利用しようと考えています。
作る流れとしては、プロトコル仕様を作ってから実装の想定でしたが、
とりあえず動くものが見たくなったため、ソケット通信を先に実装して動かしながら作りました。
ソケット通信部分は、こちらを参考しました。(書籍版)
GoでたたくTCPソケット(前編)|Goならわかるシステムプログラミング

おわりに

ここまで読んでいただきありがとうございました。
明日は、@r-fujimotoさんです。

参考

27
30
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
27
30