Goを用いてTCPソケット通信を学ぶ

この記事は GMOペパボエンジニア Advent Calendar 2021 - Adventar の6日目の記事です。

昨日は buty4649 さんの業務で使っているPCをLinuxデスクトップにしてから3年半が経った でした。 これまでデスクトップPCはWindowsMacしか使ったことがなかったので、Linuxデスクトップの話は個人的にはかなり新鮮な内容でした。その他にも普段開発で利用しているアプリケーションの紹介もされていて、最近リモートで働く中でなかなか自分以外の開発環境を見る機会がほとんど無いので、めちゃくちゃ参考になりました!

はじめに

以前同僚がこんな記事を書いてて

ryuichi1208.hateblo.jp

ん....そもそもソケット自体よく分かっていない....どうしよう....って状態だったので、このタイミングで改めて学習し直しました。 今回はTCPソケット通信の処理の流れだったり、その中で使われているシステムコールについて見ていきながら、最後にGoを用いて簡単なTCPサーバ / クライアントを動かしてみようと思います。

TCPソケットとは

TCP/IP アプリケーションを作成するための抽象化されたインターフェースのことで、ユーザープロセスとTCP/IPを繋ぐ出入り口になる。 お互いのソケットをネットワークを通して接続し,その口を通して通信を実現することができます。 ソケットを利用することで、TCPの処理はOS側で行ってくれることによって、アプリケーション側では意識しなくてもよくなります。


ソケット自体は以下の2種類があります。

※訂正: 下記の2つを含め他にもたくさんの種類のソケットがあるそうです。( Man page of SOCKET ) ご指摘頂きありがとうございます!

  • INETドメイン(ネットワークソケット)

  • UNIXドメイン

    • 同じマシン内でプロセス間の通信を行うためのソケット。
    • ソケットファイルを利用する (例: /run/php-fpm/php-fpm.sock)
    • INETドメインソケットよりも高速

今回はINETドメインに着目してTCP/IPにおけるソケット通信の流れを見ていきます。

ソケット通信の基本となる流れは以下のようになっています。 その中でいくつかのシステムコールを呼び出して通信を行なっています。

f:id:rnakamine:20211206173510p:plain

socket()

ソケットを作成する。(サーバ/クライアント)

Man page of SOCKET

bind()

作成したソケットに対してポートをなどを割り当てる。(サーバ)

Man page of BIND

listen()

ソケットを接続待ちの状態にする。(サーバ)

Man page of LISTEN

accept()

クライアントから来た接続要求に対してソケットへの接続を受ける。(サーバ)

Man page of ACCEPT

connect()

サーバに対してソケット接続を要求する。(クライアント)

Man page of CONNECT

read() / write()

クライアント or サーバから来たデータの受信と送信を行う。(サーバ/クライアント)

Man page of READ Man page of WRITE

close()

接続を切断する。(サーバ/クライアント)

Man page of CLOSE

TCP接続の確立(3 Way Handshake)については以下の記事がとても参考になりました! milestone-of-se.nesuke.com

簡単なTCP サーバ / クライアントを動かしてみる

Goを用いてTCP通信を行う際に、標準で用意されているnetパッケージというものがあります。 今回はこちらを使用してサーバ⇄クライアント間でTCP通信を行なっていきます。

ここではクライアント側で書き込んだ文字列をサーバ側で受け取り、それをそっくりそのままクライアントに返すだけの構成になっています。

サーバ側のコード

package main

import (
    "io"
    "log"
    "net"
)

func main() {
    ln, err := net.Listen("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }

    for {
        conn, err := ln.Accept()
        if err != nil {
            log.Fatal(err)
        }

        io.Copy(conn, conn)
        conn.Close()
    }
}

かなりざっくりコードを追うと、net.Listen()メソッドの内部では、上記で挙げたsocket()bind()listen()といったシステムコールの呼び出しを良い感じに行ってくれます。

net.Listen()で取得したTCPListenerに対してAccept()メソッドを呼び出すことで、クライアントからの接続を待ち受けることができます。

実際にどんなシステムコールがどのような順番で呼び出されているかは、こちらを参考にさせて頂きました。

ks888.hatenablog.com

クライアント側

package main

import (
    "fmt"
    "log"
    "net"
)

func main() {
    conn, err := net.Dial("tcp", "localhost:8080")
    if err != nil {
        log.Fatal(err)
    }

    str := "Hello!"
    _, err = conn.Write([]byte(str))
    if err != nil {
        log.Fatal(err)
    }

    buf := make([]byte, 1024)
    count, err := conn.Read(buf)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(string(buf[:count]))
}

クライアントも同様にnet.Dial()メソッドを呼び出すことで、内部的には良い感じにsocket()connect()といったシステムコールを呼び出し、ソケットの作成やリモートのソケットに対して3 Way Handshakeを実施してコネクションを確立するといったことを行っています。

net.Dial()から取得されるTCPConnは、Connインターフェース( net package - net - pkg.go.dev )を満たしており、Write()Read()が実装されていることから コネクションへの読み書きは上記のサンプルコードのようにconn.Write()conn.Read()を用いてバイト列を読み書きすることで、データの送受信を行うことができます。

終わりに

ソケット通信って聞いたことあるけど、実際にどんなことをしているかほぼ雰囲気でしか分かっていなかったのですが、今回(TCPだけですが...)処理の流れを掴むことができたり、中でどんなシステムコールが呼ばれているかなど知る良いきっかけになりました。

また、絶賛勉強中のGoのサンプルコードを用いることで、netパッケージに内部を少しだけ追うことができたので、かなり勉強になりました!

netパッケージについてさらに深く読んでみたいのであれば、このあたりがとても参考になると思います。 zenn.dev


アドベントカレンダー7日目は しばっちさん です!宜しくお願いします!