こちらは、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さんです。