これは、なにをしたくて書いたもの?
Goの勉強にということで、Echo Server、Clientを書いてみようかなと。
お題
Goを使って、Echo ServerとClientをそれぞれ書いていきます。
- Echo Server
- 引数でバインドするアドレスとポートを受け取る
- 指定しない場合は
0.0.0.0
と8080
にバインドする - ポートのみの指定は可とする
- 指定しない場合は
- クライアントからの複数接続を受け入れられるようにする
- メッセージを返す時に、「★」で装飾する
- 引数でバインドするアドレスとポートを受け取る
- Echo Client
- サーバーに送るメッセージを引数で受け取る
- 接続先のサーバーは、コマンドラインオプションで指定する
- 指定しない場合は、
localhost
と8080
をターゲットに送信する
- 指定しない場合は、
- メッセージを送信して、サーバーからの応答を受け取ったらプログラムを終了する
あとは、適度にログ出力します。
環境
今回の環境は、こちらです。
$ go version go version go1.15.6 linux/amd64
Echo Server
まずはサーバー側を作ります。モジュールの作成。
$ go mod init echo-server go: creating new go.mod: module echo-server
go.mod
module echo-server go 1.15
作成したプログラムは、こんな感じです。
server.go
package main import ( "bufio" "context" "flag" "fmt" "log" "net" "os" "strconv" "strings" "syscall" ) func main() { log.SetOutput(os.Stdout) var address string var port int flag.Parse() if len(flag.Args()) > 1 { address = flag.Arg(0) port, _ = strconv.Atoi(flag.Arg(1)) } else if len(flag.Args()) > 0 { address = "0.0.0.0" port, _ = strconv.Atoi(flag.Arg(0)) } else { address = "0.0.0.0" port = 8080 } server := &Server{address: address, port: port} err := server.Start() if err != nil { log.Fatalf("TCP Echo Server startup failed, reason: %s", err) // os.Exit(1) } } type Server struct { address string port int } func (s *Server) Start() error { listenConfig := net.ListenConfig{Control: SetUpSocket} listener, err := listenConfig.Listen(context.Background(), "tcp", fmt.Sprintf("%s:%d", s.address, s.port)) if err != nil { return err } log.Printf("TCP Echo Server[%s], startup.", listener.Addr()) for { conn, err := listener.Accept() if err != nil { log.Printf("Accept error, reason: %s", err) continue } go func() { defer conn.Close() log.Printf("[%s] Accepted, client", conn.RemoteAddr()) reader := bufio.NewReader(conn) message, err := reader.ReadString('\n') if err != nil { log.Printf("[%s] Read error, reason: %s", conn.RemoteAddr(), err) return } log.Printf("[%s] Received message: %s", conn.RemoteAddr(), message) if strings.HasSuffix(message, "\r\n") { message = message[:len(message)-2] } else if strings.HasSuffix(message, "\n") { message = message[:len(message)-1] } replyMessage := fmt.Sprintf("★%s★", message) writer := bufio.NewWriter(conn) writer.WriteString(replyMessage) writer.Flush() log.Printf("[%s] Replied, client", conn.RemoteAddr()) }() } } func SetUpSocket(network string, address string, conn syscall.RawConn) error { return conn.Control(func(fd uintptr) { syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) }) }
サーバーには、バインドするアドレスとポートに対応する構造体を用意しました。
type Server struct { address string port int }
TCPソケットを使ってのリッスンを行うのは、こちら。構造体のメソッドとして定義しました。
func (s *Server) Start() error { listenConfig := net.ListenConfig{Control: SetUpSocket} listener, err := listenConfig.Listen(context.Background(), "tcp", fmt.Sprintf("%s:%d", s.address, s.port)) if err != nil { return err } log.Printf("TCP Echo Server[%s], startup.", listener.Addr()) // 省略 }
TCPソケットを扱うには、net
パッケージを使用するようです。
net - The Go Programming Language
ListenConfig
を使っているのは、ソケットのオプションを指定するためですね。
syscall.RawConn
で生のコネクションが渡ってくるようなので、こちらに対して設定を行えばいいみたいです。
func SetUpSocket(network string, address string, conn syscall.RawConn) error { return conn.Control(func(fd uintptr) { syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) }) }
Package syscall / type RawConn
今回はSO_REUSEADDR
を指定しました。
参照しているのは、このあたりです。
Package syscall / func SetsockoptInt
クライアントからの接続は、Listener#Accept
で待ちます。ここは無限ループになりますね。
for { conn, err := listener.Accept() if err != nil { log.Printf("Accept error, reason: %s", err) continue } // 省略 }
Listener#Accept
で受け取ったコネクションからは、クライアントからのメッセージを読み込み、★を付けて返します。
この処理は、goroutineを使って並行処理として実行します。
The Go Programming Language Specification / Go statements
go func() { defer conn.Close() log.Printf("[%s] Accepted, client", conn.RemoteAddr()) reader := bufio.NewReader(conn) message, err := reader.ReadString('\n') if err != nil { log.Printf("[%s] Read error, reason: %s", conn.RemoteAddr(), err) return } log.Printf("[%s] Received message: %s", conn.RemoteAddr(), message) if strings.HasSuffix(message, "\r\n") { message = message[:len(message)-2] } else if strings.HasSuffix(message, "\n") { message = message[:len(message)-1] } replyMessage := fmt.Sprintf("★%s★", message) writer := bufio.NewWriter(conn) writer.WriteString(replyMessage) writer.Flush() log.Printf("[%s] Replied, client", conn.RemoteAddr()) }()
コネクションに対する読み書きは、bufio
パッケージを使ってラップしました。
bufio - The Go Programming Language
最後は、コマンドライン引数の取り扱いです。こちらはflag
パッケージを使います。
func main() { log.SetOutput(os.Stdout) var address string var port int flag.Parse() if len(flag.Args()) > 1 { address = flag.Arg(0) port, _ = strconv.Atoi(flag.Arg(1)) } else if len(flag.Args()) > 0 { address = "0.0.0.0" port, _ = strconv.Atoi(flag.Arg(0)) } else { address = "0.0.0.0" port = 8080 } server := &Server{address: address, port: port} err := server.Start() if err != nil { log.Fatalf("TCP Echo Server startup failed, reason: %s", err) // os.Exit(1) } }
flag - The Go Programming Language
サーバーの起動に失敗した場合は、log#Fatalf
関数を使用しました。log#Fatal
もそうですが、このいずれかの関数を使用すると
ログ出力後にos#Exit(1)
を呼び出してプログラムを終了するようです。
これで完成です。起動してみましょう。
$ go run server.go 2021/01/04 23:13:17 TCP Echo Server[[::]:8080], startup.
確認。
$ telnet localhost 8080 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Hello ★Hello★Connection closed by foreign host.
OKです。
サーバー側のログ。
2021/01/04 23:13:39 [127.0.0.1:48812] Accepted, client 2021/01/04 23:13:42 [127.0.0.1:48812] Received message: Hello 2021/01/04 23:13:42 [127.0.0.1:48812] Replied, client 2021/01/04 23:13:44 [127.0.0.1:48814] Accepted, client 2021/01/04 23:13:48 [127.0.0.1:48814] Received message: World 2021/01/04 23:13:48 [127.0.0.1:48814] Replied, client
ポートを指定して起動したりするのもいいですが、確認結果は省略します。
$ go run server.go localhost 5000 2021/01/04 23:15:10 TCP Echo Server[127.0.0.1:5000], startup.
Echo Client
続いては、クライアント側を作成します。まずはモジュールの作成。
$ go mod init echo-client go: creating new go.mod: module echo-client
go.mod
module echo-client go 1.15
作成したプログラムは、こちら。
client.go
package main import ( "bufio" "flag" "fmt" "log" "net" "os" ) func main() { log.SetOutput(os.Stdout) const defaultHost = "localhost" const defaultPort = 8080 var host string var port int flag.StringVar(&host, "host", defaultHost, "target host") flag.StringVar(&host, "H", defaultHost, "target host (short option: -host)") flag.IntVar(&port, "port", defaultPort, "target port") flag.IntVar(&port, "p", defaultPort, "target port (short option: -port)") flag.Parse() var message string if len(flag.Args()) > 0 { message = flag.Arg(0) } else { log.Printf("required 1 argument, send message") os.Exit(1) } client := &Client{host: host, port: port} err := client.SendMessage(message) if err != nil { log.Fatalf("TCP Echo Client startup failed, reason: %s", err) } } type Client struct { host string port int } func (c *Client) SendMessage(message string) error { conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.host, c.port)) if err != nil { return err } defer conn.Close() log.Printf("Connect Server[%s]", conn.RemoteAddr()) writer := bufio.NewWriter(conn) reader := bufio.NewReader(conn) fmt.Printf("[%s] Send message => %s\n", conn.RemoteAddr(), message) writer.WriteString(message) writer.WriteString("\r\n") writer.Flush() readBytes := make([]byte, 1024) readLength, err := reader.Read(readBytes) if err != nil { log.Printf("[%s] Read error, reason : %s", conn.RemoteAddr(), err) return nil } replyMessage := string(readBytes[:readLength]) fmt.Printf("[%s] Replied message => %s\n", conn.RemoteAddr(), replyMessage) log.Printf("Disconnect") return nil }
サーバー側と同様、接続先の情報は構造体に持たせることにします。
type Client struct { host string port int }
サーバーへメッセージを送信している部分。
func (c *Client) SendMessage(message string) error { conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.host, c.port)) if err != nil { return err } defer conn.Close() log.Printf("Connect Server[%s]", conn.RemoteAddr()) writer := bufio.NewWriter(conn) reader := bufio.NewReader(conn) fmt.Printf("[%s] Send message => %s\n", conn.RemoteAddr(), message) writer.WriteString(message) writer.WriteString("\r\n") writer.Flush() readBytes := make([]byte, 1024) readLength, err := reader.Read(readBytes) if err != nil { log.Printf("[%s] Read error, reason : %s", conn.RemoteAddr(), err) return nil } replyMessage := string(readBytes[:readLength]) fmt.Printf("[%s] Replied message => %s\n", conn.RemoteAddr(), replyMessage) log.Printf("Disconnect") return nil }
クライアント側は、net#Dial
を使用します。
コネクションが確立した後の読み書きは、サーバー側とそう変わりません。が、メッセージの読み込みの部分だけ改行をあてにしない
形になりました…。
引数とオプションを扱っているのは、こちら。
func main() { log.SetOutput(os.Stdout) const defaultHost = "localhost" const defaultPort = 8080 var host string var port int flag.StringVar(&host, "host", defaultHost, "target host") flag.StringVar(&host, "H", defaultHost, "target host (short option: -host)") flag.IntVar(&port, "port", defaultPort, "target port") flag.IntVar(&port, "p", defaultPort, "target port (short option: -port)") flag.Parse() var message string if len(flag.Args()) > 0 { message = flag.Arg(0) } else { log.Printf("required 1 argument, send message") os.Exit(1) } client := &Client{host: host, port: port} err := client.SendMessage(message) if err != nil { log.Fatalf("TCP Echo Client startup failed, reason: %s", err) } }
flag - The Go Programming Language
サーバーに送信するメッセージは引数として受け取り、接続先はオプションとして指定します。
オプションは、ロングオプションとショートオプションの両方を扱うようにしました。
flag.StringVar(&host, "host", defaultHost, "target host") flag.StringVar(&host, "H", defaultHost, "target host (short option: -host)") flag.IntVar(&port, "port", defaultPort, "target port") flag.IntVar(&port, "p", defaultPort, "target port (short option: -port)")
-h
を使わなかったのは、デフォルトのオプション(-help
、-h
)と重複するからですね。
では、確認してみましょう。
まずはヘルプを見てみます。
$ go run client.go -h Usage of /tmp/go-build922131759/b001/exe/client: -H string target host (short option: -host) (default "localhost") -host string target host (default "localhost") -p int target port (short option: -port) (default 8080) -port int target port (default 8080)
実行(サーバー側は起動しているものとします)。
$ go run client.go 'Hello World!!' 2021/01/04 23:36:08 Connect Server[127.0.0.1:8080] [127.0.0.1:8080] Send message => Hello World!! [127.0.0.1:8080] Replied message => ★Hello World!!★ 2021/01/04 23:36:08 Disconnect $ go run client.go -p 5000 'Hello World!!' 2021/01/04 23:36:27 Connect Server[127.0.0.1:5000] [127.0.0.1:5000] Send message => Hello World!! [127.0.0.1:5000] Replied message => ★Hello World!!★ 2021/01/04 23:36:27 Disconnect $ go run client.go -H 192.168.0.2 -p 5000 'Hello World!!' 2021/01/04 23:37:17 Connect Server[192.168.0.2:5000] [192.168.0.2:5000] Send message => Hello World!! [192.168.0.2:5000] Replied message => ★Hello World!!★ 2021/01/04 23:37:17 Disconnect
OKですね。
まとめ
GoでTCPソケットを扱い、Echo ServerとClientを書いてみました。
Goのパッケージはまだまだ把握していないので、勉強になりますね…。