Open15

Goのhttpサーバを仕組み

にいさんにいさん

これで curl http://localhost:8080/greet と打てば、レスポンスが返ってくる。

疑問点は HandleFunc って実行しただけでパスが登録されるのは何故か、と ListenAndServe の第二引数が nil な時に何が起きるかの二点。

connect だと第二引数に http2 用の handler を渡しているので気になった。

func main() {
	http.HandleFunc("/greet", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})

	http.ListenAndServe(
		"localhost:8080",
		nil,
	)
}
にいさんにいさん

ListenAndServe の中身を見るとこんな感じ。

func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

addr はアドレスなのでまあいい。
handler は interface になっているから nil を渡せるっぽい。
nil 渡されたくないなら、 nil チェックした方が良さそう。

にいさんにいさん

Server 構造体はプロパティが多い。コメントにゼロ値は有効な値だ、と書いてある。
handler のコメントを読むと、 handler to invoke, http.DefaultServeMux if nil とある。
nil だったら http.DefaultServeMux が使われる。

にいさんにいさん

Server 構造体の ListenAndServe に潜るとこんな感じ。

func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(ln)
}
にいさんにいさん

srv.shuttingDown は Server 構造体の inShutdown の Load の戻り値。

inShutdown は atomic.Bool という構造体。
普通の boolean 値と何が違うのか。

そもそも atomic ってなんだ。

「アトミックな操作」であるというとき、ある操作を行なうときに他者がその操作に割り込めないことを指す。
引用: https://www.wdic.org/w/TECH/アトミック

なるほど。並行処理チックなところだと atomic な bool が出てくるっぽい。
一旦それくらいに留めておく。

にいさんにいさん

Addr が空文字の場合は :http と言う値が入る。
server.go のコメント見ると、空文字だと http://localhost:80 になるっぽい。
:http と言う名前に特別な意味があるっぽい。

Addr optionally specifies the TCP address for the server to listen on, in the form "host:port". If empty, ":http" (port 80) is used. The service names are defined in RFC 6335 and assigned by IANA. See net.Dial for details of the address format.

にいさんにいさん

ln, err := net.Listen("tcp", addr) でリスナーを生成して、それを Serve に渡す。
リスナーをサーブ(配る)イメージ。
Listen は潜ると深そうなので、一旦 Serve メソッドに行く。

にいさんにいさん

server.go からめちゃくちゃ抜粋。

func (srv *Server) Serve(l net.Listener) error {
	for {
		rw, err := l.Accept()
		tempDelay = 0
		c := srv.newConn(rw)
		c.setState(c.rwc, StateNew, runHooks) // before Serve can return
		go c.serve(connCtx)
	}
}
にいさんにいさん

まず l.Accept を潜る。

サーバ側のライフサイクル
create ソケットの作成
bind ソケットを特定のIPアドレスとポートに紐付け
listen 接続の待受を開始
accept 接続を受信
close 接続を切断
引用: https://qiita.com/megadreams14/items/32a3eed4661e55419e1c

システムコールしてるからここから先は os の話っぽい。撤退。

func (fd *netFD) accept() (netfd *netFD, err error) {
	d, rsa, errcall, err := fd.pfd.Accept()
	if err != nil {
		if errcall != "" {
			err = wrapSyscallError(errcall, err)
		}
		return nil, err
	}

	if netfd, err = newFD(d, fd.family, fd.sotype, fd.net); err != nil {
		poll.CloseFunc(d)
		return nil, err
	}
	if err = netfd.init(); err != nil {
		netfd.Close()
		return nil, err
	}
	lsa, _ := syscall.Getsockname(netfd.pfd.Sysfd)
	netfd.setAddr(netfd.addrFunc()(lsa), netfd.addrFunc()(rsa))
	return netfd, nil
}
にいさんにいさん

めちゃくちゃ雑な結論としては、 server は listerner を無限 for 文で繰り返している感じ。
ネットワーク的な話は listener がやっていて、 server は文字通り listener を serve している感じ。

にいさんにいさん

当初の疑問点に戻る。

  • HandleFunc って実行しただけでパスが登録されるのは何故か
  • ListenAndServe の第二引数が nil な時に何が起きるか
にいさんにいさん

まず前者の話。

HandleFunc の中身。

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
	DefaultServeMux.HandleFunc(pattern, handler)
}

DefaultServeMux はグローバル変数になってるので、それで成り立っているっぽい。

var DefaultServeMux = &defaultServeMux

エラーハンドリングのぞくと Handle メソッドはこんな感じ。

func (mux *ServeMux) Handle(pattern string, handler Handler) {
	mux.mu.Lock()
	defer mux.mu.Unlock()

	if mux.m == nil {
		mux.m = make(map[string]muxEntry)
	}
	e := muxEntry{h: handler, pattern: pattern}
	mux.m[pattern] = e
	if pattern[len(pattern)-1] == '/' {
		mux.es = appendSorted(mux.es, e)
	}

	if pattern[0] != '/' {
		mux.hosts = true
	}
}
にいさんにいさん

DefaultServeMux の m に handler と pattern を追加している。

ServeMux の m に登録していく形っぽい。

type ServeMux struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

ServeMux の主な役割はこの pattern と handler の紐付けを持つこと、にあるっぽい。

にいさんにいさん

connect のサーバ立てようとすると、 nil じゃなくて handler を渡す形になっている。

package main

import (
	"example/gen/greet/v1/greetv1connect"
	"example/presentation"
	"net/http"

	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"
)

func main() {
	serviceHandler := presentation.NewGreetServiceHandler()
	mux := http.NewServeMux()
	path, handler := greetv1connect.NewGreetServiceHandler(serviceHandler)
	mux.Handle(path, handler)
	http.ListenAndServe(
		"localhost:8080",
		// Use h2c so we can serve HTTP/2 without TLS.
		h2c.NewHandler(mux, &http2.Server{}),
	)
}

mux.Handle の部分が、普通の rest サーバで言う http.HandleFunc にあたる。
そして http2 で使いたいから、っていう感じだな。