【Golang】GoでTCPソケット通信を実装する
ソケット通信
ソケット通信とは、プログラムから見たときのネットワーク通信を抽象化する概念をさす。通信の終端をソケットと呼ばれるオブジェクトとみなし、ソケットに対して何かしらの入力を行うと、もう一方のソケットから出力される。
このモデルにおいて、ネットワーク階層モデル(OSI,TCP/IP など)上のいわゆるネットワークレイヤ以下の詳細は隠蔽される。例えばパケット(セグメント)がどのように分割され、どのノードを経由して到達するのかなどはプログラムで制御しなくとも良い。
Go のソケット通信
Go では net
パッケージがソケット通信の機能を提供している。受け手側で net.Listen()
、送る側で net.Dial()
関数を呼び出し、それぞれから取得できる Conn
を介してメッセージを送受信できる。
net - The Go Programming Language
Usage
Server
サーバー側はソケットをオープンしたあと、クライアントからの入力を待ち続ける。 今回は TCP 通信で検証し、サーバー・クライアント共々同一ネットワークノード上にあるものとする。
package main import ( "fmt" "io" "net" "os" ) type HandleConnection func(c net.Conn) error func main() { handleConn := func(c net.Conn) error { buf := make([]byte, 1024) for { n, err := c.Read(buf) if err != nil { return err } if n == 0 { break } s := string(buf[:n]) fmt.Println(s) fmt.Fprintf(c, "accept:%s\n", s) } return nil } if err := start(handleConn); err != nil { fmt.Fprintln(os.Stderr, err) } } func start(f HandleConnection) error { ln, err := net.Listen("tcp", "localhost:8080") if err != nil { return err } defer ln.Close() conn, err := ln.Accept() if err != nil { return err } defer conn.Close() for { if err := f(conn); err != nil && err != io.EOF { return err } } }
Conn
(ソケット)が io.Reader
io.Writer
インタフェース要件を満たすので、標準入出力のように扱える。もともと BSD UNIX がソケット通信をファイル読み書きのように扱いたかったという思想があり、この概念を継承している様子。
Client
クライアントは一度きりの入力とした。もちろん繋ぎっぱなしのまま連続して入力もできる。
package main import ( "bufio" "fmt" "net" ) func main() { fmt.Println("client start.") err := start() if err != nil { fmt.Errorf("%s", err) } fmt.Println("client end.") } func start() error { conn, err := net.Dial("tcp", "localhost:8080") if err != nil { return err } defer conn.Close() fmt.Fprintf(conn, "Hello, Socket Connection !") status, err := bufio.NewReader(conn).ReadString('\n') if err != nil { return err } fmt.Println(status) return nil }
実行結果
Server
Hello, Socker Connection !
Client
client start. accept:Hello, Socket Connection ! client end.
net パッケージの実装
ソケット通信の実装部分は OS が使用する物がすでにあり、Go はそれを使用していると思われる。
net
パッケージの MacOS 用のロジックを追っていくと、最終的にシステムコールよりソケット通信 API を実行している様子。
(rawSyscall でシステムコールを実行できる)
syscall/zsyscall_darwin_amd64.go
//go:linkname libc_connect libc_connect //go:cgo_import_dynamic libc_connect connect "/usr/lib/libSystem.B.dylib" // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT func socket(domain int, typ int, proto int) (fd int, err error) { r0, _, e1 := rawSyscall(funcPC(libc_socket_trampoline), uintptr(domain), uintptr(typ), uintptr(proto)) fd = int(r0) if e1 != 0 { err = errnoErr(e1) } return }
考えられるユースケース
アプリケーションレイヤのペイロードを自由に記述できるので、例えば独自のアプリケーションプロトコルを定義したり、http 通信ほど情報を詰め込みたくないが、何らかの情報を別ノードに送信したい場合などに利用できそう。