【Go】net.Connを掘り起こす http, mysql編
Goのnet/http, go-sql-driver/mysqlからnet.Connを取り出し、HTTPやMySQLのコネクションを直接読み書きする方法を紹介する。通常のライブラリ利用ではほとんど必要ないが、トランスポート層レベルでペイロードを書き換えたい時などに役立つ。
net/http
Client, Serverそれぞれについて説明する。
Client
http.Client構造体はTransportフィールドを持つ。このフィールドは通信に使用するトランスポート層を定めるものである。デフォルトではDefaultTransportが使用され、これはTransport構造体のインスタンスである。
このTransport構造体のDialContext(DialTLSContext)フィールドを利用してnet.Connにアクセスする。
DialContextはTCPコネクションの生成方法を定める関数である。デフォルトではnet.DialerのDialContextを使用する。DialTLSContextも同様だが、こちらはTLSを用いて暗号化された通信を対象とする。
以下のようにDialContext(DialTLSContext)をwrapする関数を作成し、その中でnet.Connにアクセスする。 例えば必要な値をContextに含めておいて、それを利用してnet.Connに書き込むといったことができる。
type valueKeyType int
var valueKey contextKeyType = iota
func main(){
client := http.Client{
Transport: wrapTransport(nil)
}
// Contextに任意の値を含めておいてリクエストに付与
ctx := context.WithValue(context.Background(), valueKey, "test")
req, err := http.NewRequestWithContext(ctx, "GET", "http://~", nil)
if err != nil {
log.Fatal(err)
}
resp, err := client.Do(req)
...
}
func wrapTransport(base *http.Transport) *http.Transport {
if base == nil {
base = http.DefaultTransport.(*http.Transport)
}
t.DialContext = wrapDialContext(base.DialContext)
t.DialTLSContext = wrapDialContext(base.DialTLSContext)
return t
}
func wrapDialContext(dc func(ctx context.Context, network, addr string) (net.Conn, error)) func(ctx context.Context, network, addr string) (net.Conn, error) {
if dc == nil {
return nil
}
return func(ctx context.Context, network, addr string) (net.Conn, error) {
conn, err := dc(ctx, network, addr)
if err != nil {
return nil, err
}
// Contextから値を取り出す
value := ctx.Value(valueKey)
// net.Connに対する処理
conn.Write([]byte(value))
return conn, err
}
}
なおKeep Aliveには注意する必要がある。HTTPでは可能な限りコネクションを再利用し、一つのコネクションで複数のリクエスト・レスポンスをやり取りする。 つまり取り出したnet.Connは複数のリクエストで使いまわされている可能性がある。 防ぎたい場合は、Transport構造体のDisableKeepAlivesをtrueにする。
func wrapTransport(base *http.Transport) *http.Transport {
...
t.DisableKeepAlives = true
...
}
Server
http.Server構造体のConnContextフィールドを利用してnet.ConnをContextに含めることで、handleする際にContextを通じてnet.Connにアクセスできるようにする。
type connKeyType int
var connKey connKeyType = iota
func main() {
http.HandleFunc("/", handler)
server := &http.Server{
Addr: ":8080",
ConnContext: ConnContext,
}
log.Fatal(server.ListenAndServe())
}
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
conn := ctx.Value(connKey)
conn.Write([]byte("test"))
...
}
func ConnContext(ctx context.Context, conn net.Conn) context.Context {
return context.WithValue(ctx, connKey, conn)
}
なおhandlerでhttp.RequestからContextを取り出す処理は、echoなどのフレームワークごとにやり方が異なる。
go-sql-driver/mysql
go-sql-driver/mysqlはGoでMySQLサーバーに通信する際のクライアントとなる。SQL関連のインターフェースをまとめるdatabase/sqlのdriverとして実装される。
net/http.Clientと同様に、このライブラリにもDialContextが用意されている。それを利用する。
type valueKeyType int
var valueKey contextKeyType = iota
func main() {
// "mytcp"をDBオープン時のData Sourse Nameに使用する("tcp"を指定してもよい)
mysql.RegisterDialContext("mytcp", DialContext("tcp"))
db, err := sql.Open("mysql", "user:password@mytcp(localhost:3306)/database")
if err != nil [
log.Fatal(err)
]
defer db.Close()
// Contextに任意の値を含めておいてリクエストに付与
ctx := context.WithValue(context.Background(), valueKey, "test")
_, err := db.ExecContext(ctx, "INSERT INTO ...")
...
}
func DialContext(netP string) mysql.DialContextFunc {
return func(ctx context.Context, addr string) (net.Conn, error) {
// デフォルトのTCPコネクションの生成がハードコードされているのでこちらにも書く
// ref: https://github.com/go-sql-driver/mysql/blob/bcc459a906419e2890a50fc2c99ea6dd927a88f2/connector.go#L48-L49
nd := net.Dialer{}
conn, err := nd.DialContext(ctx, netP, addr)
if err != nil {
return nil, err
}
// Contextから値を取り出す
value := ctx.Value(valueKey)
// net.Connに対する処理
conn.Write([]byte(value))
return conn, err
}
}
net/http.ClientのKeep Aliveと同様に、MySQLでもデフォルトではコネクションを再利用する。防ぎたい場合は、MaxIdleConnsを0にする。
func main() {
...
defer.db.Close()
db.SetMaxIdleConns(0)
...
}
なおトランスポート層レベルでの実装はそれぞれのdriver依存なので、MySQLに限らないdatabase/sqlで同様の実装はできない。
まとめ
今回はnet/httpとgo-sql-driver/mysqlについて紹介した。それ以外のライブラリでもDialContext, ConnContextといった関数が用意されていれば同様のことが可能かもしれない。 気が向けば調べて記事にしたい。
研究で必要になったが参考になる記事がなく、ライブラリのコードを読んで調べた。 なかなか需要のない話ではあるが、だからこそ必要になった時に役立てばうれしい。
2023-12-02追記: Go Conference mini 2023 Winter IN KYOTOでこの内容について話したのでスライドもご覧ください。