Go言語で学ぶコンピュータサイエンス (ネットワーク編①)


GO言語について

Go言語は、標準モジュールの豊富さやその文法などの恩恵もあり、簡潔な実装を通して、コンピュータについて学習できる素晴らしい言語だと個人的に思っています。

ネットワーク

今回は、ネットワークについて扱います。ネットワークといっても対して複雑なことを扱うつもりはなく、
- TCPソケットの役割とその実装
- HTTPサーバーの実装
などについて扱います。

ネットワークの仕組み

現在のネットワークは、TCP/IPという仕組みで動いています。TCP/IPとは、HTTP、TCP、IP、UDP、Ethernetなどの、複数のネットワークのプロトコル(規格みたいなもの)で動いているプロトコルスイートです。

TCP/IPモデルという通信の規格では、この4つの層に分かれて通信が行われています。それぞれがどういう役割を持っているかの詳細は別記事に任せますが、ざっと説明します。

ネットワークインターフェース層

実際のデバイス、ハードウェアの通信をする層です。wifi接続などでPCの接続を行っているときはここの部分です。

インターネット層

通信機と通信機の接続を行う層です。IP(Internet Protocol)で通信機同士の通信を実現しています。

トランスポート層

通信するプログラムの間でのデータの送受信を行う層です。TCPかUDPで通信を行います。
TCPは信頼性が高いプロトコルで、UDPは信頼性はやや劣るものの、速度が早いプロトコルです。

アプリケーション層

通信するアプリケーション間でのデータの送受信を行う層です。HTTPやFTPなどで通信を行います。WebサーバーなどにはHTTPが使われます。

今回はトランスポート層とアプリケーション層のみに注目します。

トランスポート層

トランスポート層においては、プログラム同士が通信を行うのですが、このときに使うのが、ソケットと呼ばれるものです。このソケット通信によって、トランスポート層内や、トランスポート層とアプリケーション層が通信しています。
今回は、TCPソケットを実装します。

クライアントとサーバー

少しでもWebをかじったことがある人なら、この言葉を聞いたことがあると思いますが、概念としては非常に単純で、クライアントがサーバーにアクセスし、サーバーがデータを返す。ただこれだけです。

実装

今回は、TCPクライアントとTCPサーバーを標準モジュールを使って実装し、TCP/IPを体感します。
まずはクライアントのプログラムから。

tcp_client.go
package main

import  (
    "fmt"
    "io/ioutil"
    "net"
    "os"
)

func main() {
    // os.Args[0]には、必ずファイル名が入る。なので、サーバーのportのみを、コマンドラインで与える。
    if len(os.Args) != 2{
        fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
        os.Exit(1)
    }

    service := os.Args[1]
    // tcpAddrを取得、IPとPortの組み合わせ
    // "tcp4"は、IPv4型で返すという意味。"tcp6"(IPv6)や"tcp"(両方)で指定できる
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)

    // 接続を確率させる
    conn, err := net.DialTCP("tcp", nil, tcpAddr)
    checkError(err)

    // メッセージを送る
    _, err = conn.Write([]byte("HEAD /HTTP/1.0\r\n\r\n"))
    checkError(err)

        // 帰ってきた結果を受け取る
    result, err := ioutil.ReadAll(conn)
    checkError(err)

    // 出力!!
    fmt.Println(string(result))
    os.Exit(0)
}

func checkError(err error) {
    if err != nil {
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

まず、指定したポートにサーバーがあるかどうかを確認します。(net.ResolveTCPAddr関数)
もしポートが開いていた場合、そこにtcpプロトコルで接続を確立します。(net.DialTCP関数)
そして、確立している接続上で、メッセージを送ります。(conn.Write関数)
帰ってきた文字列を取得します。(ioutil.ReadAll関数)
これでクライアントができました。

次はサーバー側

tcp_server.go
package main

import (
    "fmt"
    "net"
    "os"
    "time"
)

func main(){
    service := ":7777"
    // portのtcpAddrを取得
    tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
    checkError(err)
    // tcpAddrの中でデータを受信できる状態にする
    listener, err := net.ListenTCP("tcp", tcpAddr)
    checkError(err)
    for {
        conn, err := listener.Accept()
        if err != nil {
            continue
        }
        // 通信が確立されたらdaytimeをStringにしたもののbyte列を送信する。
        daytime := time.Now().String()
        conn.Write([]byte(daytime))
        conn.Close()
    }
}

func checkError(err error){
    if err != nil{
        fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
        os.Exit(1)
    }
}

今回はサーバーなので、net.ListenTCP関数で指定したポートを監視します。クライアントから指定したポートと同じにしないと、お互いに通信できません。
一番大事なのは、for文の中。これは、While文と同じで、forの中身が永遠に繰り返されます。
まず最初に、listener.Accept関数を使って、監視しているポートにアクセスがきた場合、

        ....
        daytime := time.Now().String()
        conn.Write([]byte(daytime))
        conn.Close()        daytime := time.Now().String()
        conn.Write([]byte(daytime))
        conn.Close()
        ....

こっちが実行されます。
通信が確立されなかった場合は、

        ....
        if err != nil {
            continue
        }
        ....

が実行されます。
このサーバーは、今の時間を返すサーバーです。

実験

実験というほどなにもないですが、

$ go run tcp_server.go

を実行し、別ウィンドウで

$ go run tcp_client.go

を実行すると、うまく通信できることがわかります。

アプリケーション層

次は、アプリケーション層のプロトコルをいじっていきます。
代表的なのは、HTTPというプロトコルで、Webの通信などに使われているプロトコルです。
今回は標準モジュールを使って簡単なWebサーバーを立てて、curlコマンドで叩いてみます。

web_server.go
package main

import (
    "fmt"
    "net/http"
    "log"
)

func server(w http.ResponseWriter, r *http.Request){
    // ReseponseWriterであるwを通じて、クライアントにメッセージが送信される
    fmt.Fprintf(w, "Hello World")
}

func main(){
    http.HandleFunc("/", server)
    err := http.ListenAndServe(":9090", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

これだけでOKです。エンドポイントは/で、ポートは:9090を監視することにします。
あとは、ターミナルで

$ curl localhost:9090

とすると、クライアントとしてサーバーを叩くことができます。
このコマンドで、Hello Worldが出てくるはずです。

参考レポジトリ
https://github.com/yamad07/computer_science_by_go

まとめ

今回は、TCPソケットとWebサーバー実装し、TCP/IPを体感してもらいました。
ただ文章を読むよりも、手を動かすことで初めて理解できると思うので、ぜひ実装もしてみてください!!