goでWebサービス No.1(基本s)

4 min読了の目安(約4300字TECH技術記事

今回はいったんGoから離れてWebの基礎知識を復習しようと思います。後半はGoに用意されているパッケージを使用して簡易的なウェブサーバーを立ててみます。

注意

コマンドラインを使った操作が出て来ることがあります。cd, ls, mkdir, touchといった基本的なコマンドを知っていることを前提に進めさせていただきます。
環境の違いで操作や実行結果に差異が出てくる可能性があります。私の実行環境は以下になります。

MacBook Pro (Early 2015)
macOS Catalina ver.10.15.6
go version go1.14.7 darwin/amd64
エディタはVScode

HTTP通信の流れ

まずはブラウザとWebサーバがどのようなやりとりをしてWebページなどの情報を取得しているか、その流れを確認します。ここでブラウザなどユーザー側をクライアントと呼ぶことにします。

  1. IPアドレスの取得

    ブラウザでよく目にするURLは機械が理解出来るものではありません。機械はIPアドレスと呼ばれるものでインターネット上のどのサーバーに接続するべきかを判断します。ではURLからどのようにしてIPアドレスを取得しているかというとDNSサーバです。
    DNSサーバにはURLとIPアドレスの対応表のようなものがあり、送られてきたURLと対応するIPアドレスを返します。返ってきたIPアドレスはURLに対応するWebサーバの住所になります。

    IPアドレスの取得

  2. TCPコネクションの確立

    IPアドレスにより通信を行うWebサーバを把握したらWebサーバと接続の確立を行います。クライアントとWebサーバはお互いに通信を行っても良いか許可をもらうための要求のメッセージとそれを許可するメッセージを送る必要があります。

    TCPコネクションの確立

    お互いの要求が許可されたらTCPコネクションが確立されたことになります。

  3. Webサーバから情報の取得

    通信が確立されたのでクライアントは必要な情報をリクエストします。リクエストを受けたサーバは内容に応じた情報(例えばWebページのHTMLファイルなど)をレスポンスとして返します。

    情報の取得

  4. TCPコネクションの切断

    必要な通信が終了したら接続を切ります。Webページに限らずWeb上のいろいろな通信が上のような形で行われています。これはPC・スマホなどデバイスによらずHTTPによる通信を行う場合はこの流れになります。

HTTP

HTTPはWebサーバに、クライアントとインターネットを通してデータのやりとりをさせるプロトコルです。プロトコルとは、先ほどあげた通信の手順など、通信を行う上でのルールのことです。HTTPはTCPというプロトコル上で成り立っているプロトコルです。
コンピュータにはポートと呼ばれるデータの送受信口があり、0~65535番まであります。例えばnginxやnode.jsのexpressなどでポートを指定して開放するとローカルホストのポート番号からアクセスすることが出来ます。インターネットでの通信は基本的に80番が使用されます。

HTTPはリクエストとレスポンスのプロトコルです。クライアントが主体となりリクエストを送り、Webサーバはあくまで受け身で送られてきたリクエストに対してレスポンスを行うのみです。つまりクライアントのリクエストだけが通信を行うトリガーであり、Webサーバ側からクライアントを呼び出すことはないということです。

WebSocketなどサーバ側から自主的にクライアントに情報を送信し双方向通信を行う仕組みもあるので厳密的には上は正しくないのかもしれません。

HTTPはステートレスなプロトコルです。つまりこれまでにどのような通信を行ったのかという状態を持つことはありません。

HTTP自体は状態を持ちませんがCookieという機能を導入することで状態を管理することが出来ます。

URL

URLはUniform Resource Locaterの略で情報の場所をさす識別子になります。URLの構造は以下のようになっています(参考サイト参照)

scheme://host[:port#]/path/.../[?query-string][#anchor]
  • scheme 低レイヤーで使用されるプロトコルを指定します。(例えば:http, https, ftp)
  • host HTTPサーバのIPアドレスまたはドメイン
  • port# HTTPサーバのデフォルトのポート番号は80です。この場合ポート番号は省略することができます。もし別のポートを使用する場合は指定しなければなりません。例えば http://www.cnblogs.com:8080/
  • path リソースまでのパス
  • query-string httpサーバへ送るデータ
  • anchor アンカー

馴染みのないところでポートの指定やアンカーの指定が出来ます。私はここで初めて知りました。

HTTPでのやりとりの内容

リクエストやレスポンスはパケットで送ります。ここからはGo言語で実際にサーバを立ててリクエストやレスポンスの内容を確認してみましょう。Goの標準パッケージnet/httpを利用すると簡単にサーバを立てることが出来ます。

ワークディレクトリに適当なディレクトリを作成、その中にmain.goを作成し以下のコードを書いてください。

package main
import (
	"fmt"
	"log"
	"net/http"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
	r.ParseForm() // オプションを解析
	fmt.Println("----Http.Request----")
	fmt.Println("----line----")
	fmt.Println(r.Method, r.RequestURI, r.Proto)
	fmt.Println("----header----")
	fmt.Println(r.Header)
	fmt.Println("----body----")
	fmt.Println(r.Body)
	fmt.Println("----end----")
	// wに入れられた文字列がクライアントに表示される。
	fmt.Fprintf(w, "hello go server!")
}
func main() {
	http.HandleFunc("/", sayhelloName)
	err := http.ListenAndServe(":9090", nil)
	fmt.Println("Listen in 9090.")
	if err != nil {
		log.Fatal("Listen and server:", err)
	}
}

これだけでlocalhost:9090にアクセスすると”hello go server!”と表示されます。go buildでバイナリデータをビルドし、実行します。localhost:9090にアクセスするとコマンドラインにリクエストが表示されます。

リクエスト

----Http.Request----
----line----
GET /favicon.ico HTTP/1.1
----header----
map[Accept:[image/avif,image/webp,image/apng,image/*,*/*;q=0.8] 
Accept-Encoding:[gzip, deflate, br] Accept-Language:[ja,en-US;q=0.9,en;q=0.8] 
Connection:[keep-alive] Cookie:[_ga=GA1.1.1789600893.1591436885; 
csrftoken=Z66Vzq1xBX1gVzj7sA9S5uHR6wqaqjhhvCbPaxP8yn8UMlrw8s7U6vFPsvzab9es] 
Referer:[http://localhost:9090/] Sec-Fetch-Dest:[image] Sec-Fetch-Mode:[no-cors] 
Sec-Fetch-Site:[same-origin] User-Agent:[Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) 
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36]]
----body----
{}

リクエストはリクエストライン、ヘッダー、ボディに別れています。

レスポンス

$ curl -D - http://localhost:9090
HTTP/1.1 200 OK
Date: Mon, 21 Sep 2020 01:55:13 GMT
Content-Length: 16
Content-Type: text/plain; charset=utf-8
hello go server!

このようにリクエスト・レスポンスには日頃私たちが目にしている情報以外にもいろいろ情報が含まれています。