やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

【はじめてのシステムコール】ソケットでTCPサーバーを実装する

みなさんはシステムコールについてご存知でしょうか。
システムコールを一言で説明すると、OSのサービスを利用する際に呼び出しされる機構のことです。
普段、WEBアプリケーションの開発をしている自分にとってシステムコールは、あまり馴染みのあるものではありません。しかし、システムコールを自分で直接、呼び出していないだけで、ファイル操作やディレクトリ変更(cd)を行うと裏側ではシステムコールが発行されています。
なので遠いような...近いような...不思議な存在なわけです。

今まで明示的にシステムコールを呼び出したことがなかったので、今回はシステムコールを使って簡単なTCPサーバーを実装してみたいと思います。

呼び出すシステムコールについて

TCPサーバーを実装するにあたりsocketというシステムコールを扱います。
socketを利用することで複数プロセス間で通信を行うためのソケットを作成することができます。
ただし、ソケットを作成するだけではTCPサーバーとしての機能を満たすことができないため、いくつかのシステムコールを呼び出す必要があります。

  • socket(ソケットの作成)
  • bind(ソケットにアドレスを割り当てる)
  • listen(ソケットの接続を待つ)
  • accept(ソケットへ接続を受け付ける)
  • write(ソケット(ファイルディスクリプタ)へ書き込む)

以下の記事を参考にさせて頂きました。
システムコールとソケットについて全く分からず困っていたのですが、非常に勉強になりました。

medium.com

できたもの

サーバー側

サーバー側の実装には低レイヤーを扱いやすく、パッケージが充実しているGolangを選択しました。
TCPのソケットを作成し、クライアントの接続を確認後に「hello world」という固定の文字列を返す非常にシンプルな作りになっています。

package main

import (
    "fmt"
    "os"
    "syscall"
    "net"
)

func main() {
    // TCPソケットを作成
    fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_STREAM, syscall.IPPROTO_IP)
    if err != nil {
        fmt.Println("Error creating socket:", err)
        os.Exit(1)
    }
    defer syscall.Close(fd)

    // ソケットにアドレスを割り当て(bind)
    addr := syscall.SockaddrInet4{ Port: 8001 }
    copy(addr.Addr[:], net.ParseIP("0.0.0.0").To4())
    if err := syscall.Bind(fd, &addr); err != nil {
        fmt.Println("Error binding socket:", err)
        os.Exit(1)
    }

    // ソケットのリッスンを開始
    if err := syscall.Listen(fd, syscall.SOMAXCONN); err != nil {
        fmt.Println("Error listen socket:", err)
        os.Exit(1)
    }

    // 無限ループを利用してクライアントの受付を開始
    fmt.Println("Server is listening on 0.0.0.0:8001")
    for {
        clientFd, _, err := syscall.Accept(fd)
        if err != nil {
            fmt.Println("Error accepting connection: ", err)
            continue
        }

        // クライアント受付後、並列処理でレスポンスを書き込み
        go func(fd int) {
            defer syscall.Close(fd)

            data := []byte("hello world")
            if _, err := syscall.Write(fd, data); err != nil {
                fmt.Println("Error writing: ", err)
                return
            }
        }(clientFd)
    }
}

先ほど紹介したシステムコールを順に呼び出しているだけですが、慣れない実装だったので非常に混乱しました。
特に引数に何を指定すれば良いのか判別できませんでした。 この程度の仕様であれば、netパッケージなどを使って実装すれば事足りるため、あえて難しい方法を選ぶ理由はありません。

クライアント側

クライアント側はソケットの作成と読み込みができれば、何でも良いのでRubyを選択しました。
今まで知らなかったのですが、RubyではTCPソケットが標準ライブラリとして提供されていたので、非常に手軽に実装を行うことができました。

require 'socket'

1.upto(5) do
  socket = TCPSocket.open('localhost', 8001)
  puts socket.gets
  sleep 1
end

実行結果

サーバー側を起動しておきます。

$ go run main.go 
Server is listening on 0.0.0.0:8001

この状態で、クライアント側を起動します。

$ ruby client.rb 
hello world
hello world
hello world
hello world
hello world

おぉ、無事に「hello world」が表示されました!
システムコールを呼び出してソケットの作成からデータの書き込みまで、問題なく実行されているようですね。

最後に

はじめてのシステムコール...という題目でシステムコールに入門してみました。
Golangではシステムコールをsyscallパッケージを利用して、簡単に呼び出すことが可能ですが、実際に実装を始めた際には、何をどう呼び出せば全く分かりませんでした。 いざ、ソケットを作れても他に必要なステップがいくつもあり、システムコールを使いこなすにはOSやソケットなどについての知識も必要だと感じました。

とはいえ、システムコールにこういった形で入門できたのは良かったです。

参考文献

少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。