みなさんはシステムコールについてご存知でしょうか。
システムコールを一言で説明すると、OSのサービスを利用する際に呼び出しされる機構のことです。
普段、WEBアプリケーションの開発をしている自分にとってシステムコールは、あまり馴染みのあるものではありません。しかし、システムコールを自分で直接、呼び出していないだけで、ファイル操作やディレクトリ変更(cd
)を行うと裏側ではシステムコールが発行されています。
なので遠いような...近いような...不思議な存在なわけです。
今まで明示的にシステムコールを呼び出したことがなかったので、今回はシステムコールを使って簡単なTCPサーバーを実装してみたいと思います。
呼び出すシステムコールについて
TCPサーバーを実装するにあたりsocket
というシステムコールを扱います。
socket
を利用することで複数プロセス間で通信を行うためのソケットを作成することができます。
ただし、ソケットを作成するだけではTCPサーバーとしての機能を満たすことができないため、いくつかのシステムコールを呼び出す必要があります。
- socket(ソケットの作成)
- bind(ソケットにアドレスを割り当てる)
- listen(ソケットの接続を待つ)
- accept(ソケットへ接続を受け付ける)
- write(ソケット(ファイルディスクリプタ)へ書き込む)
以下の記事を参考にさせて頂きました。
システムコールとソケットについて全く分からず困っていたのですが、非常に勉強になりました。
できたもの
サーバー側
サーバー側の実装には低レイヤーを扱いやすく、パッケージが充実している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やソケットなどについての知識も必要だと感じました。
とはいえ、システムコールにこういった形で入門できたのは良かったです。
参考文献
- Socket programming in Go — Syscalls
- syscall package - syscall - Go Packages
- A Go web server from scratch using syscalls
- class TCPSocket
少しでも「ええな〜」と思ったらはてなスター・はてなブックマーク・シェアを頂けると励みになります。