本記事では、Neo4j Driver として公式サポートされている、neo4j-go-driver
における Bolt Protocol の実装について見ていきます。
本記事では、v4.3 branch の最新である
5a14c7024ca3203d89d54ae34bbfbc2886249401
commit hash のソースコードを前提にしています。
Bolt に関連する公開 API としては、neo4j.ConsoleBoltLogger()
があります。
セッションを開始する時に、SessionConfig
に引数として渡すことで、Neo4j データベースと接続する際に発生した Bolt Protocol の通信をログ出力します。
session := driver.NewSession(neo4j.SessionConfig{
BoltLogger: neo4j.ConsoleBoltLogger(),
})
defer session.Close()
例えば、以下は neo4j.ConsleBoltLogger()
を用いて接続に成功したときの通信ログ例です。仕様で定められた通り、まずは magic バイト列 6060 B017
を送信した後、Handshake を行い、利用するプロトコルのバージョンとして 0X00000304
をサーバーが選択したことが確認できます。
2021-10-09 19:10:15.542 BOLT C: <MAGIC> 0X6060B017
2021-10-09 19:10:15.542 BOLT C: <HANDSHAKE> 0X00010304 0X00000104 0X00000004 0X00000003
2021-10-09 19:10:15.750 BOLT S: <HANDSHAKE> 0X00000304
この時、クライアントが送信するプロトコルバージョンの候補は、internal/bolt/connect.go
で定義されています。
// neo4j/internal/bolt/connect.go
// Supported versions in priority order
var versions = [4]protocolVersion{
{major: 4, minor: 3, back: 1},
{major: 4, minor: 1},
{major: 4, minor: 0},
{major: 3, minor: 0},
}
サーバーがプロトコルバージョンを選択した後、クライアントが直ちに HELLO
リクエストメッセージを送信しています。username/password を利用した Basic 認証を用いて接続を申請した結果、サーバーから SUCCESS
レスポンスメッセージが返却されていることがわかります。
2021-10-09 19:10:15.750 BOLT C: HELLO {"credentials":"<redacted>","principal":"<hidden>","routing":{"address":"<hidden>.databases.neo4j.io:7687"},"scheme":"basic","user_agent":"Go Driver/4.3"}
2021-10-09 19:10:15.967 BOLT S: SUCCESS {"server":"Neo4j/4.3-aura","connection_id":"bolt-999999","has_more":false}
なお、SessionConfig.BoltLogger
には、log.BoltLogger
インターフェースを実装したオリジナルのロガーを渡すこともできます。
執筆現在、neo4j-go-driver はプロトコルバージョンとして、v3/v4 の両方をサポートしています。最新のプロトコルバージョンである v4 の実装は、neo4j/internalbolt/bolt4.go
に実装されています。
bolt.NewBolt4()
が Bolt クライアントのインスタンスを生成しますが、*bolt4 struct
は現在獲得しているトランザクションの ID や接続中のコネクションである net.Conn
といった状態を保持しています。
type bolt4 struct {
// ...
conn net.Conn
connId string
txId db.TxHandle
// ...
serverName string
serverVersion string
databaseName string
// ...
out outgoing
in incoming
}
func NewBolt4(serverName string, conn net.Conn, log log.Logger, boltLog log.BoltLogger) *bolt4 {
// ...
}
クライアントがサーバーに何らかのリクエストを送信する時、outgoing
型である bolt4.out
を通じて実行されます。
outgoing
型は neo4j/internal/bolt/outgoing.go
で実装されています。Bolt Protocol が仕様を定義するバイト列に変換するドメインロジックとしての立ち位置になります。対応するリクエストメッセージごとに appendX())
なる関数が実装されています。
type outgoing struct {
chunker chunker
packer packstream.Packer
// ...
}
func (o *outgoing) appendHello(hello map[string]interface{}) { /* .... */ }
func (o *outgoing) appendBegin(meta map[string]interface{}) { /* .... */ }
func (o *outgoing) appendCommit() { /* .... */ }
func (o *outgoing) appendRollback() { /* .... */ }
func (o *outgoing) appendRun(cypher string, params, meta map[string]interface{}) { /* .... */ }
// ...
実際のバイト列の Encode/Decode の処理は、後述する packstream.Packer
型である outoing.packer
に移譲されています。
Bolt Protocol は独自の仕様を定義した TCP 上のネットワーク・プロトコルになるので、仕様に定められたとおりにバイト列を Encode/Decode してあげる必要が有ります。関連処理は、neo4j/internal/packstream/{packer,unpacker}.go
で実装されています。
仕様は、メッセージ用の Bolt Protocol Message Specification とは別の、PackStream Specification として分離して策定されており、執筆現在最新のメジャーバージョンは v1 です。
エンコーディングは packer.go
で、デコーディングは unpacker.go
で実装されています。実際は、Go の標準ライブラリである encoding/binary
を利用しています。
HELLO
Request Message試しに、HELLO
リクエストメッセージを送る一連の処理過程を眺めてみましょう。
まず、先述したとおり、outgoing
型の appendHello()
関数が呼ばれます。
// neo4j/internal/bolt/outgoing.go
func (o *outgoing) appendHello(hello map[string]interface{}) {
//...
o.begin()
o.packer.StructHeader(byte(msgHello), 1)
o.packMap(hello)
o.end()
}
まずは、o.packer.StructHeader(byte(msgHello), 1)
の説明です。
ここでは、リクエストメッセージが HELLO
であることを伝えるため、バイト列にメッセージの種類をヘッダーとして伝えます。v4.3 の仕様 HELLO
リクエストメッセージの Signature は 01
です。msgHello
は、別ファイルで定義されていますが、0x01
となっていることが確認できます。
// neo4j/internal/bolt/messages.go
// Message struct tags
// Shared between bolt versions
const (
//...
msgHello byte = 0x01
//...
)
次に、o.packMap(hello)
の説明です。
HELLO
リクエストメッセージは、任意の key/value をフィールドとして送ることが仕様で許されています。具体的には、user_agent
/schema
/credentials
などです。そういった任意のフィールドが map[string]interface{}
で渡されているので、それを Map
型としてエンコードしてあげるだけです。
なお、Packstream Specification 上は、Dictionay として仕様が策定されています。
// neo4j/internal/bolt/outgoing.go
func (o *outgoing) packMap(m map[string]interface{}) {
o.packer.MapHeader(len(m))
for k, v := range m {
o.packer.String(k)
o.packX(v)
}
}
ここで、packX()
は長いので割愛しますが、一言でいうと reflection を活用しながら value の型に応じて適切にエンコードをしてあげるためのメソッドです。
以上、neo4j-go-driver の Bolt Protocol の実装についての紹介を行いました。