GoでTCPソケット通信を実装する
golang TCP/IP
Lastmod: 2020-10-19

ソケット通信

ソケット(BSD)- Wikipedia

ソケット通信とは、プログラムから見たときのネットワーク通信を抽象化する概念をさす。通信の終端をソケットと呼ばれるオブジェクトとみなし、ソケットに対して何かしらの入力を行うと、もう一方のソケットから出力される。

このモデルにおいて、ネットワーク階層モデル(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 通信ほど情報を詰め込みたくないが、何らかの情報を別ノードに送信したい場合などに利用できそう。

参考文献