【Go】あけましてソケット通信解説2020

2020年になりましたね!みなさん明けましておめでとうございます。
昨年は「俺的働き方改革」を標語に作業の自動化による効率向上をそこかしこでやっておりました。
今年は「三十路前の基礎固め」と題しまして、生活や保険の見直しから基礎学力の向上、使用している技術の基礎をもう一度考えることをやっていこうかなと思っております。
ということで、あらゆる通信の基礎中の基礎。ソケット通信をGoでやっていきます!
前振りが長いのでGoのコードが見たい方はめっちゃ下に行ってください。

そもそも通信とは?

通信(つうしん)とは、情報の伝達を意味する言葉である。

通信 – Wikipedia https://ja.wikipedia.org/wiki/%E9%80%9A%E4%BF%A1

人に話しかけることも通信ですし、文字で人に何かを伝えることも通信です。
さすがにそこから通信について解説すると私の三ヶ日が無くなってしまうので、電気信号を使った情報の伝達から考えていきましょう。

電信の成立

電気信号による通信のことを電信と呼びます。
実用的かつ有名なものだと1836年に開発されたモールス式電信が挙げられますね。それより以前から色んな方式の電信が研究されていたのですが、一番実用的なものがモールス式電信だったようです。
スイッチを短くONしたり長めにONしてトントントンツーツーツートントントンみたいにして信号を送ります。
ちなみにトントントンが「S」ツーツーツーが「O」なので上記の通信は「SOS」となります。
こいつは使えるってことで電線を全国に張り巡らすようになります。
1866年には大西洋を横断する海底ケーブルまでできちゃいます。

電話の発明

電話の発明は色んな人(エジソンやベルなど)が同時期にやってまして、誰が発明者かと問われると難しいところです。一般には電話の発明はベルということで浸透しているかと思います。
2002年のアメリカ合衆国議会では「電話を最初に発明したのはベルでなく、イタリアのメウッチである。」と決議しています。
メウッチは1871年に電話を発明したと言われており、ベルは1876年だったとか。ちなみにエジソンも1876年だったらしいですがベルよりも早く特許出願したのに書類ミスでベルに先を越されたそうです。

無線通信(ラジオ)

1888年くらいにヘルツさんが実験で電磁波の存在を確認し、1900年頃には別の人が音声を載せた無線通信(ラジオ)に成功したみたいです。
モールス式電信からここまで早すぎません?

ついにコンピュータ(電子計算機)登場

やっとのことでコンピュータが出てきます。
第二次世界大戦のあたりで各国がほぼ同時期にコンピュータを作ります。
数いる天才の中でも大天才アラン・チューリングが「コンピュータってこうあるべきだよね(雑)」みたいなことを考えます(チューリングマシン)。
チューリングはドイツが使用していた暗号「エニグマ」の解読をする機械の製作をしていました。「イミテーション・ゲーム/エニグマと天才数学者の秘密」って映画が有名ですね。

そして大天才ジョン・フォン・ノイマンは現在のコンピュータの基礎を築いたわけです。多分頭の良さはアインシュタインと並ぶんじゃないかな?と私は思っています。
ちなみに我々が使用しているコンピュータ(パソコン)はノイマン型コンピュータと言われています。

ARPANETの到来

コンピュータはあれど、まだ実用的なコンピュータ同士の通信方法が存在していませんでしたが、1960年頃から今のインターネットの前身であるARPANET(アーパネット)の開発が開始されました。
そしてカリフォルニア大学のスタンフォード校とロサンゼルス校の間で初めての通信がなされました。
1969年10月29日22:30。通信が正常に行われたかをリアルタイムで確認するために電話をつなぎながら行ったそうです。
Wikipediaに詳しくその時の状況が書いてあるので引用しますね。

「私たちはSRIとの間に電話回線の接続を設定した…」とクラインロックはインタビューに応えて言った。「私たちは L と打ち込み、電話で尋ねた」
「L が見えるかい?」
「ああ、L が見える」との答えが返ってきた。
私たちは O と打ち込んで、訊いた。
「O が見えるかい」
「ああ、O が見える」
そこで G と打ち込んだところで、システムがクラッシュした…それでも、これが革命の始まりだった…

インターネットの歴史 – Wikipedia https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%8D%E3%83%83%E3%83%88%E3%81%AE%E6%AD%B4%E5%8F%B2

痺れますね!かっちょいいですね!
LOGINと打ちたかったんでしょうね!
その後ARPANETは色んなところに電線を伸ばしてつながっていきます。

そしてインターネットへ…

実はARPANET以外にも当時は様々なネットワークが開発され乱立していました。ネットワークAとネットワークBは独自の実装をしているので電線を繋いでもやりとりできない状態でした。
そこでネットワーク同士をつなげるための約束事を取り決めようという流れになるのは当然でしょうね。
このような通信における約束事のことをプロトコルと呼びます。
1974年には仕様が発表(RFC675)され、その中でinter(〜の間)とnetworking(ネットワーク)を合成し短縮したinternet(インターネット)という言葉が使用されました。
こうして現在も現役バリバリの通信方式「TCP/IP通信」が誕生したのです。

TCP/IP通信ってなんぞ?

IPアドレスって聞いたことありません?このIPってのはInternet Protocol(インターネットプロトコル)のことでして、「パソコンに番号を付けてデータを送る際は、「2番から150番へ愛を込めて」という挨拶文を入れて送りつける」みたいな約束事のことです。
そしてこの番号をIPアドレスと呼びます。
現在のIPの主要バージョンはバージョン4です。

ちなみにIPアドレスは2^32 = 4300000000番(43億)くらいまで存在できます。
多いと思います?でも世界の人口が70億なので1人1台パソコンを持っていたら足りないんですよね。
だから新たな取り決めでIPバージョン6では2^128 = 約340000000000000000000000000000000000000番(340澗)くらいまでになりました。

TCP(Transmission Control Protocol)は「データに送り状つける、届いたら受領サインをもらう」みたいな約束事をすることで確実にデータを届ける役目を担います。

このTCPとIPを組み合わせたTCP/IP通信によって

  • データに送り状つける(TCP)
  • データを送る(IP)
  • 受領サインを書く(TCP)
  • 受領サインを返送する(IP)
  • 受領サインを確認する(TCP)

こんな流れで確実に通信をすることができるわけです!
このTCP/IPを使用した通信がソケット通信になります。
WEBやメールもソケット通信の一種です。
WEBならTCP/IPにHTTPという約束事も追加してデータをやりとりします。
メールならSMTPという約束事が追加されます。
ソケット通信を知れば自分でプロトコルを制定して通信することが可能になります。

めっちゃ長くなりましたね。それではプログラムを書いていきましょう!

ソケット通信してみる

まずはコードの全体を書きます。
今回はサーバー側(待ち受ける側)のみ作成し、クライアント側(接続する側)はWEBアクセスをすることでテストします。

package main

import (
        "fmt"
        "net"
        "os"
        "os/signal"
        "strconv"
)

func main() {
        Listen(8080, "localhost")
}

func Listen(port int, host string) error {
        addr, err := net.ResolveTCPAddr("tcp", host+":"+strconv.Itoa(port))
        if err != nil {
                return err
        }

        listener, err := net.ListenTCP("tcp", addr)
        if err != nil {
                return err
        }
        defer listener.Close()

        // Ctrl + C で終了
        interrupt := make(chan os.Signal)
        signal.Notify(interrupt, os.Interrupt)

ListenLoop:
        for {
                cconn := make(chan *net.TCPConn)
                cerr := make(chan error)
                go func() {
                        conn, err := listener.AcceptTCP()
                        if err != nil {
                                cerr <- err
                                return
                        }
                        cconn <- conn
                }()

                select {
                case conn := <-cconn:
                        go Handle(conn)
                case err := <-cerr:
                        fmt.Println(err)
                case <-interrupt:
                        break ListenLoop
                }
                cconn = nil
                cerr = nil
        }
        return nil
}

func Handle(conn *net.TCPConn) {
        defer conn.Close()
        b := make([]byte, 1024)
        var reqb []byte
        for {
                n, err := conn.Read(b)
                if err != nil {
                        return
                }
                reqb = append(reqb, b[:n]...)
                if n != 1024 {
                        break
                }
        }
        // 色んな処理
        fmt.Println(string(reqb))
        // httpのレスポンスを返してみる
        conn.Write([]byte("HTTP/1.1 200 OK\n\n<h1>hello</h1>"))
}

接続を待ち受ける(Listen)

サーバーが接続を待ち受けることを英語ではListenと呼びます。
なんででしょうね?聞くっていうのが受け身な感じだからでしょうか?
それはさて置き、Listen関数を見ていきましょう。

func Listen(port int, host string) error {
        addr, err := net.ResolveTCPAddr("tcp", host+":"+strconv.Itoa(port))
        if err != nil {
                return err
        }

        listener, err := net.ListenTCP("tcp", addr)
        if err != nil {
                return err
        }
        defer listener.Close()
...

ポート番号とホスト名を引数にとります。
main関数では8080番とlocalhostを渡していますね。

net.ResolveTCPAddr(network, address string) (*net.TCPAddr, error)addressに与えたIPアドレスの形式がnetworkの形式にそった形かを判定・解決し、大丈夫そうなら*net.TCPAddrを返します。

net.ListenTCP(network string, addr *net.TCPAddr) (*net.Listener, error)でいよいよ待ち受け開始です。
待ち受けの開始に成功したら*net.TCPListenerを返します。
defer listener.Close()でこの関数終了時にリスナーを閉じます。

...
interrupt := make(chan os.Signal)
signal.Notify(interrupt, os.Interrupt)
...

サーバーは永続的に待ち受けるのに無限ループを使用します。
これでは止める手段がkillコマンドしかなくなってしまいますので、Ctrl+C (SIGINT)でサーバーを止められるようにします。
上記部分はSIGINTを受け取るチャンネルを開いているところですね。

...
ListenLoop:
        for {
                cconn := make(chan *net.TCPConn)
                cerr := make(chan error)
                go func() {
                        conn, err := listener.AcceptTCP()
                        if err != nil {
                                cerr <- err
                                return
                        }
                        cconn <- conn
                }()
...

ListenLoopというラベルを貼って無限ループスタートです。
for文の中で接続を受け取るチャンネルcconn、エラーを受け取るチャンネルcerrを作っておきます。
その後のgoroutineの中に注目しましょう。
listener.AcceptTCP()は接続があったら*net.TCPConnを返します。
こいつは接続があるまで処理をブロック(次の行に進まなくする)してしまいます。
エラーなく接続があったらcconnチャンネルに送信します。
エラーがあったらcerrチャンネルに送信します。

SIGINTによる終了を考慮しなければgoroutineに入れずに、無限ループをlistener.AcceptTCP()でブロックしつつ接続を捌くことも可能です。これならセレクタも必要ないので直感的で簡単に作れます。
よくあるGoのソケット通信の解説ではそれを最小構成とするものが多いです。
今回の解説ではAcceptはgoroutineの中に入れ、チャンネルによるやりとりにして可用性を高めています。実際に使用するとわかりますがこれがソケット通信の最小構成かなと思います。

...
                select {
                case conn := <-cconn:
                        go Handle(conn)
                case err := <-cerr:
                        fmt.Println(err)
                case <-interrupt:
                        break ListenLoop
                }
                cconn = nil
                cerr = nil
        }
        return nil
}

ここまで

  • interrupt(SIGINTを受け取るチャンネル)
  • cconn(接続があったらそれを受け取るチャンネル)
  • cerr(接続でエラーがあったら受け取るチャンネル)

を作ってきました。
これらを受け取るセレクタを作り無限ループをブロックします。
あとは接続を受け取ってそれをfunc Handle(conn *TCPConn)に渡すだけです。この時HandleをgoroutineにしないとHandleが終わるまで接続待機しなくなるので、複数アクセスを捌けません。
SIGINTを受け取ったらListenLoopを抜ける=終了です。

なんらかの処理をする(Handle)

func Handle(conn *net.TCPConn) {
        defer conn.Close()
        b := make([]byte, 1024)
        var reqb []byte
        for {
                n, err := conn.Read(b)
                if err != nil {
                        return
                }
                reqb = append(reqb, b[:n]...)
                if n != 1024 {
                        break
                }
        }
        // 色んな処理
        fmt.Println(string(reqb))
        // httpのレスポンスを返してみる
        conn.Write([]byte("HTTP/1.1 200 OK\n\n<h1>hello</h1>"))
}

この関数の中に処理を書きます。
今回はクライアントがWEBアクセスしてくる前提でいます。そのためレスポンスを返したら接続を切っちゃいます。

リクエストのバイト配列reqbを作ります。
これを文字列で出力して、HTTPの簡単なレスポンスを返します。

動かしてみよー

go run main.goでサーバー起動!
この状態で別ターミナルからcurl http://localhost:8080を実行すると、サーバー起動側のターミナルに

GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.64.1
Accept: */*

のようにHTTPリクエストが表示されます。
また、curlの結果を見てみると

$ curl http://localhost:8080
<h1>hello</h1>

無事にボディ部が表示されました。
ちなみにブラウザでアクセスしてみると大きな文字でhelloが表示されることがわかります。

さらにサーバーを起動したターミナルでCtrl+cでSIGINTをアプリケーションに送信すると無事にアプリケーションが終了するかと思います。
すばらしいですね!

終わりに

プロトコルってなんか難しいですよね。
でもこうやってプロトコルが無かった時代から追いかけていくと何となくわかった気になれます。コンピュータ史って改めて面白いですね!
私の好きなアニメ(小説)では、わらしべ長者の物々交換も一種のプロトコルと見て「わらしべプロトコル」と名付けていました。
知っている人が見ればクスッとなる良いシーンでしたね。

今回はサーバー実装の最小構成だと思います。
WEBサーバーをGoで立てる場合には標準で提供されているAPIがあるので、普通それを使用すると思いますが、自前でWEBサーバーを実装するのも楽しそうですね!
低レイヤーは奥が深くて楽しいですわ!
それじゃまた来週!

出典

  1. 通信 – Wikipedia https://ja.wikipedia.org/wiki/%E9%80%9A%E4%BF%A1
  2. 電信 – Wikipedia https://ja.wikipedia.org/wiki/%E9%9B%BB%E4%BF%A1
  3. 電話の歴史 https://ja.wikipedia.org/wiki/%E9%9B%BB%E8%A9%B1%E3%81%AE%E6%AD%B4%E5%8F%B2
  4. コンピュータの歴史 – Wikipedia https://ja.wikipedia.org/wiki/%E8%A8%88%E7%AE%97%E6%A9%9F%E3%81%AE%E6%AD%B4%E5%8F%B2
  5. インターネットの歴史 – Wikipedia https://ja.wikipedia.org/wiki/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%8D%E3%83%83%E3%83%88%E3%81%AE%E6%AD%B4%E5%8F%B2
  6. RFC675 https://tools.ietf.org/html/rfc675
  7. Internet Protocol – Wikipedia https://ja.wikipedia.org/wiki/Internet_Protocol
  8. net – The Go Programming Language https://golang.org/pkg/net/