onemuri.space

golang で TCP ソケットを扱う

TCP/IP 参照モデル

ソケットの話をする前に、ネットワーク通信における話をします。そもそも私たちはインターネットを介して、様々なブログを読んだり、web サービスを使ったり、Youtube 見ていますが、どの通信もプロトコルと呼ばれる規約を守って通信します。OSI 参照モデルか TCP/IP 参照モデルがありますが、本記事では TCP/IP 参照モデルを示しておきます。

階層 レイヤー プロトコル
4 アプリケーション層 HTTP
3 トランスポート層 TCP, UDP
2 ネットワーク層 IP, ICMP, ARP, RARP
1 リンク層 WiFi, Ethernet, PPP

このようにして各階層のプロトコル(規約)を定めておくことで、各階層のプロトコルは他の階層のプロトコルを気にせずに設定する事ができるのである。例えば、HTTP は IP の実装を知る必要はありません。

本記事で扱うソケットは TCP, IP を結ぶ出入口を結ぶ通信で、HTTP(アプリケーション層)でできない事を扱う事ができるようになります。

前々回のio.Writerと、io.Readerの記事に続いて、ソケットを扱う事はアプリケーション層よりも低レイヤを扱う事が多くなりますが、それを言語レベルで便利なパッケージを提供してくれているおかげで、アプリケーション層を扱っている感覚でアプリを作っていく事ができるのです。

ソケット

アプリケーション層からトランスポート層のプロトコルを利用するときはソケットという仕組みを用います。

アプリケーション層
   ||
   || <- ソケット
   ||
トランスポート層
    …
ネットワーク層
    …
リンク層

ソケットは、アドレスとポート番号がわかれば通信が行えます。いくつかソケットには種類がありますが、最も使われているのは TCP です。メール送受信やファイル転送などに用いられています。TCP はデータ転送が確実に行われるため信頼性が高いです。その分転送速度は遅くなり、リアルタイム性には乏しくなります。

リアルタイム性、効率性、高速性が求められる通信は UDP が用いられます。例えば電話とかはわかりやすいかもしれませんね。無料通話アプリも多くなってきています。TCP であれば、いちいち通信が途切れた場所で再度送るようにしますが、UDP は一方的にデータを送るので途中で途切れても送り続けます。電話においてはいちいち途切れた場所を律儀に聞き逃したとしても、ユーザが聞き直せば良いので UDP で十分なのです。

ここら辺は、優秀なサイトがたくさんあるのでご自分で調べて良いと思います。
参考サイトです。

golang でソケット通信を扱ってみる(TCP)

golang には net/http パッケージがあります。このパッケージを使って、HTTP サーバーを作ってみましょう。少し長くなってしまうのでエラーハンドリングを削除しています。

HTTP サーバを作る

サーバーは Listen() 関数で立ち上げ、ソケットを開いてリクエストを待ち受けます。

nl, _ := net.Listen("tcp", ":8080")

for {
  conn, _ := nl.Accept()
  go func() {
    req, _ := http.ReadRequest(bufio.NewReader(conn))
    dump, _ := httputil.DumpRequest(req, true)
    fmt.Println(string(dump))
    res := http.Response{
      StatusCode: 200,
      ProtoMajor: 1,
      ProtoMinor: 0,
      Body:       ioutil.NopCloser(strings.NewReader("TCP connection")),
    }
    res.Write(conn)
    conn.Close()
  }()
}

このコードを go run で起動し、 curl localhost:8080 を実行するか、ブラウザで URL を localhost:8080 で起動してみてください。 go run で起動したサーバーにはリクエストしてきたクライアントの情報が出力されていると思います(コンソール上で)。また、リクエストした側は、curl ならコンソール上、ブラウザなら画面に、 TCP connection と表示されていると思います。

HTTP クライアントを作る

クライアントは Dial() 関数で起動して、空いているソケットに接続して通信を行います。

func main() {
  conn, _ := net.Dial("tcp", "localhost:8080")
  req, _ := http.NewRequest("GET", "http://localhost:8080", nil)
  req.Write(conn)
  res, _ := http.ReadResponse(bufio.NewReader(conn), req)
  dump, _ := httputil.DumpResponse(res, true)
  fmt.Println(string(dump))
}

先ほど書いた、HTTP サーバーを起動しておいて、HTTP クライアントを go run してみてください。自分で立ち上げておいた HTTP サーバーに対して、リクエストを投げている様子がわかると思います。

クライアント側には以下のようなレスポンスが表示されていると思います。

HTTP/1.0 200 OK
Connection: close

TCP connection

サーバー側には以下のようなリクエストが表示されていると思います。

GET / HTTP/1.1
Host: localhost:8080
User-Agent: Go-http-client/1.1

これで、私たちは、簡易的な HTTP サーバー/クライアントを TCP ソケットを使って作る事ができるようになりました。

golang でソケット通信を扱ってみる(UDP)

全章でも少し話しましたが、TCP はデータ転送が確実に行われるため信頼性が高いです。その分転送速度は遅くなり、リアルタイム性には乏しくなります。

UDP は逆にリアルタイム性・効率性・高速性が求められ、TCP であれば、いちいち通信が途切れた場所で再度送るようにしますが、UDP は一方的にデータを送るので途中で途切れても送り続けます。

TCP・UDP に関する参考資料の記事を読んでみると、UDP は高速であるが、その分パケットのロスが発生します。そのためそのロスをアプリケーションでカバーできるのか、TCP で使われるハンドシェイクの時間(信頼性)を削ってでも速度が重要なのかという点がコアな考え方だと思っています。正直それ以上の理解は僕はできていないので、さらに踏み込んだ知識が必要になったときにまた調べるつもりです。

さて、本題に戻り golang のコードレベルで UDP を扱ってみましょう。

サーバーを作る

TCP ソケットの場合にはnet.Listen()関数を用いてクライアントの接続を受け付けましたが、UDP ソケットの場合は net.ListenPacket()関数を用いてクライアントの接続を受け付けます。

func main() {
  conn, _ := net.ListenPacket("udp", "localhost:8080")
  defer conn.Close()

  buf := make([]byte, 1024)
  for {
    len, addr, _ := conn.ReadFrom(buf)
    fmt.Printf("%v %v\n", addr, string(buf[:len]))
    _, err := conn.WriteTo([]byte("udp server"), addr)
    if err != nil {
      panic(err)
    }
  }
}

サンプルコードを go run で起動した状態で次のクライアントを作る方へ進んでください。

クライアントを作る

前項にてサーバーを立ち上げている状態で、以下のクライアントサンプルコードを go run で起動してみると、サーバー・クライアントで通信がなされていることを確認できると思います。

func main() {
  conn, _ := net.Dial("udp4", "localhost:8080")
  defer conn.Close()

  _, err := conn.Write([]byte("udp request"))
  if err != nil {
    panic(err)
  }
  buf := make([]byte, 1024)
  len, _ := conn.Read(buf)
  fmt.Printf("Received: %s\n", string(buf[:len]))
}

TCP によるクライアントの場合は、 http.NewRequest()関数で *http.Request を返し *http.Request.Write() 関数によってリクエストを書き出していますが、UDP によるクライアントの場合は、net.Conn.Write() 関数でリクエストを書き出します。少しシンプルですね。GET(http method) を指定せずどもリクエストできるようになっています。

TCP と UDP の違い

TCP は接続のために 3 ウェイハンドシェイクと呼ばれる接続(コネクション)を確立するための手順が発生します。この接続には 1.5RTT(Round Trip Time)の時間がかかります。

クライアント → サーバー
クライアント ← サーバー
クライアント → サーバー

UDP はこの接続を確立するための手順はなく、一方的に送りつけるだけなのでその分高速になるということです。

次に、TCP の場合はパケットの順序を持っているため順序の入れ替えが可能であったり、再送処理があって、失ったパケットは再送して補う事ができますが、UDP の場合はできません。

まとめ

HTTP を使って TCP・UDP ソケットの通信を試してみました。

使っている技術のコアになっているのはio.Writerと、io.Readerです。出力と入力が基本ですね。まだまだ僕も使いこなせているとは言えませんが、クライアント(Dial)とサーバー(Listen)をの挙動を理解して早くて信頼性のある API を開発していきたいと思います。

参考

Go ならわかるシステムプログラミング

自己紹介用画像

Riki Akagi

2019年からDeNAで働いています。GCP(CloudSQL・GAE・Cloud Function etc)とGoでAPI開発に勤んでいます。睡眠やエンジニアリングに関することに興味を持って過ごしているのでその情報を皆さんに共有していけたらなと思っています。

自己紹介の詳細