見出し画像

4連休でgoのウェブ関係を学ぼう(goでどのようにWebサーバを実現するか)

前回まででWebでのクライアントとサーバのやりとりの仕組みはわかったと思います。


今回は特にサーバの方でクライアントからリクエストが来た時どのようにして処理をしているか、またGoではそれをどのようにして実現しているかをみていきましょう。今回も以下を参考にします。

正直Webの仕組みが曖昧な中でコードを追っていくのはとても大変でした。参考サイトでは細かいところは端折ってコンパクトにまとめてあるので、それを補足するつもりで書きました。ただ量が多くなったと思うので読む方を今回はそこそこ骨が折れると思いますがよろしくお願いします。

Webサーバの処理

Webサーバは以下のようにしてクライアントのリクエストを処理します。

1. Listen Socketを作成し、指定したポートを監視します。クライアントのリクエストを待ちます。
2. Listen Socketはクライアントのリクエストを受け付けます。Client Socketを得ると、Client Socketを通じてクライアントと通信を行います(通信の確立)。
3. クライアントのリクエストのヘッダーから必要な処理を把握する。
4. 対応するhandlerがリクエストを処理する。
5. handlerがクライアントの要求するデータを準備する。
6. Client Socketを通して準備したデータを書き出す。

Socketは、ネットワーク上で動作しているプログラムを結ぶ通信の出入り口です。ここではListen Socketがサーバの方で、Client Socketがクライアントの方になります。
handlerはリクエストを処理し要求されたデータを準備するプログラムです。(画像は参考サイト引用)

画像1

GoではどのようにWebサーバを実現しているか

上のWebサーバの処理をみてみると以下のことをGoでどのように実現しているかを理解すればGoでWebサーバを実現しているかが理解出来ます。

どのようにポートを監視するか?
クライアントのリクエストをどのように受け付けるか?
handlerにどのように受け渡すか?

長くなりますが前回作った簡単なWebサーバを元にソースコードを参照しながら追っていきたいと思います。前回のコードではhttp.ListenAndServe(":9090", nil)でサーバを立てていました。この関数の中を細かくみていきます。

どのようにポートを監視するか、またクライアントのリクエストをどのように受け付けるか?

以下はhttp.ListenAndServeのソースコードです。みての通り、Server型のListenAndServeメソッドを返していることがわかります。

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

次にserver.ListenAndServeのソースコードです。ここでは最後にln変数を引数に持つServeメソッドを返していることに注目してください。
ln変数はnet.ListenerというListen Socketの振る舞いをする型の変数です。

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)
}

Serveのソースコードです(一部省略)。これがクライアントのリクエストを持ち受ける関数です。forの無限ループになっています。これでクライアントからのリクエストを待ち受ける状態を維持します。Acceptメソッドで接続を受信します。途中はエラー処理なので無視して最後の方のnewConnメソッドをみてみましょう。

func (srv *Server) Serve(l net.Listener) error {
   defer l.Close()
   var tempDelay time.Duration // how long to sleep on accept failure
   for {
       rw, e := l.Accept()
       if e != nil {
           if ne, ok := e.(net.Error); ok && ne.Temporary() {
               if tempDelay == 0 {
                   tempDelay = 5 * time.Millisecond
               } else {
                   tempDelay *= 2
               }
               if max := 1 * time.Second; tempDelay > max {
                   tempDelay = max
               }
               log.Printf("http: Accept error: %v; retrying in %v", e, tempDelay)
               time.Sleep(tempDelay)
               continue
           }
           return e
       }
       tempDelay = 0
       c, err := srv.newConn(rw)
       if err != nil {
           continue
       }
       go c.serve()
   }
}

newConn関数のソースコードですconn型の変数cを返しています。これだけだとわかりませんが、簡単に説明するとクライアントのリクエストデータへのリンクです。その後上のコードではgo c.serve()を実行しているのでクライアントのリクエストをゴルーチンで並行処理しています。

func (srv *Server) newConn(rwc net.Conn) *conn {
	c := &conn{
		server: srv,
		rwc:    rwc,
	}
	if debugServerConnections {
		c.rwc = newLoggingConn("server", c.rwc)
	}
	return c
}

handlerにどのように受け渡すか?

c.serveメソッドは新しいコネクションを受け取り処理を行います。コードの量が多いのでここでは割愛します。みたい人は以下のリンクから参照してください。

今回大事なのは1925行目の

serverHandler{c.server}.ServeHTTP(w, w.req)

です。

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
	handler := sh.srv.Handler
	if handler == nil {
		handler = DefaultServeMux
	}
	if req.RequestURI == "*" && req.Method == "OPTIONS" {
		handler = globalOptionsHandler{}
	}
	handler.ServeHTTP(rw, req)
}

ここでhandlerを取得していることがわかります。これはListenAndServe関数の2つ目の引数で指定したものです。前回のコードではnilを指定しています。なのでDefaultServeMuxが渡されます。この変数はルータです。これはマッチするURLを対応するhandler関数にリダイレクトするために用いられます。このマッチするURLに対応するhandler関数がhttp.HandleFunc関数になります(以下前回のコード)。

func main() {
	http.HandleFunc("/", sayhelloName)
	err := http.ListenAndServe(":9090", nil)
	fmt.Println("Listen in 9090.")
	if err != nil {
		log.Fatal("Listen and server:", err)
	}
}

HandleFuncでは第1引数にpath、第2引数にURLがそのpathを指定した時に実行する関数を指定してあります。そこで指定した関数が実行されレスポンスを作成します。


これでGoがどのようにしてWebサーバを実現しているかの説明はおしまいです。
今回は公式ドキュメントを追っていくことが多かったのですが、読む時のコツとしては安全な処理のためどのコードもエラーハンドリングが必ず出てきますが、本筋の処理を追う時はそこを無視するようにすることが効率が良いと気づけたのがよかったです。

この記事が気に入ったらサポートをしてみませんか?