こんにちは。KOUKIです。
とある企業でWeb系の開発エンジニアをしています。
今日は、golangで作るTCPサーバーのハンズオン記事を書きたいと思います!
TCPについては、こちらの記事がわかりやすいです。
関連記事
ワークスペースの準備
まずは、作業場を作りましょう!
1 2 3 4 5 6 7 |
mkdir tcp cd tcp/ mkdir client mkdir server touch client/client.go touch server/server.go go mod init tcpserver |
TCPサーバーの実装
TCPサーバーを実装します。
リスナーの作成
最初に、netパッケージを活用してリスナーを作りましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// server.go package main import ( "fmt" "io" "log" "net" "time" ) // エラー処理 func logFatal(err error) { if err != nil { log.Fatal(err) } } func main() { // tcpの接続アドレスを作成する tcpAddr, err := net.ResolveTCPAddr("tcp", ":8080") logFatal(err) // リスナーを作成する listener, err := net.ListenTCP("tcp", tcpAddr) logFatal(err) fmt.Println("Start TCP Server...") receiveTCPConnection(listener) } |
net.ListenTCPメソッドにて、リスナーを作ります。このリスナーは、クライアントとの通信コネクションを制御する様々なAPIを提供しています。
このリスナーの引数には、net.ResolveTCPAddrから作成したリッスン情報を渡します。ここでは、「XXXX:8080」の情報が渡されることになります。
そのためローカルで動かす場合は、クライアント側からは「localhost:8080」でコネクションを貼ることが可能になります。
コネクションを受けとる
次は、クライアント側からコネクションを受け取る処理を実装します。
1 2 3 4 5 6 7 8 9 10 |
func receiveTCPConnection(listener *net.TCPListener) { for { // クライアントからのコネクション情報を受け取る conn, err := listener.AcceptTCP() logFatal(err) // ハンドラーに接続情報を渡す echoHandler(conn) } } |
リスナーから提供される機能の一つにAcceptTCPがあります。これによりクライアント側からのリクエストを受け取れるようになります。
ハンドラーを作成する
最後にハンドラーを実装しましょう。
1 2 3 4 5 6 7 8 9 10 11 |
func echoHandler(conn *net.TCPConn) { defer conn.Close() for { // リクエストを受け付けたらサーバー側に「response from server」を返す _, err := io.WriteString(conn, "response from server\n") if err != nil { return } time.Sleep(time.Second) } } |
クライアントからアクセスが発生したら「response from server」を返すようにしています。
一旦実装が完了しました。リファクタリングはあとで行うとして、今度はクライアント側を実装しましょう。
クライアントコードの実装
クライアント側は本記事の趣旨ではないので、ソースコードだけ記載しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// client/client.go package main import ( "io" "log" "net" "os" ) func response(dst io.Writer, src io.Reader) { if _, err := io.Copy(dst, src); err != nil { log.Fatal(err) } } func main() { // tcp, localhost:8080でリクエストを送る conn, err := net.Dial("tcp", ":8080") if err != nil { log.Fatal(err) } // connからレスポンスを標準出力にだす response(os.Stdout, conn) } |
動作確認
動作確認をしてみましょう。terminalを3つ開いてそれぞれ以下のコマンドを実行してください。
1 2 |
$ go run server/server.go Start TCP Server... |
1 2 3 4 5 6 |
$ go run client/client.go response from server response from server response from server response from server response from server |
1 2 3 4 5 |
$ go run client/client.go |
TCPサーバーは問題なく起動しました。2つのクライアントの内、一つはレスポンスが受け取れてますね。
しかし、もう一つのクライアントに関しては、処理が止まってしまいました。
複数リクエストを受け取れるようにする
実は、実装したTCPサーバーのコードには、バグがあります。
一つのクライアントからリクエストを送ると、そのリクエストの処理が完了するまで処理をブロックするのです。その場所は、echoHandlerです。
1 2 |
conn, err := listener.AcceptTCP() echoHandler(conn) <- echoHandlerの処理が完了するまで後続処理をストップ |
この問題を解決するには、echoHandlerを別プロセスで実行してあげれば良さそうです。
Go言語には、goroutineというものがあります。goキーワードを関数の前に指定することで、並行処理を実装することができます。
これを使ってechoHandlerを別プロセスで起動しましょう。
1 |
go echoHandler(conn) |
これで、複数のリクエストを受け取れるTCPサーバーになりました。
ちなみに、こっちの書き方も好きです。
1 2 3 |
go func(conn *net.TCPConn) { echoHandler(conn) }(conn) |
タイムアウトを設定する
便利なことに、リスナーが提供しているSetDeadlineメソッドでタイムアウトを設定できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
func receiveTCPConnection(listener *net.TCPListener) { for { // 接続後10秒後にタイムアウトする listener.SetDeadline(time.Now().Add(time.Second * 10)) log.Println("Connection Start") // クライアントからのコネクション情報を受け取る conn, err := listener.AcceptTCP() if err != nil { switch err := err.(type) { case net.Error: if err.Timeout() { log.Println("Connection Close") return } default: log.Println("Another Error!!!") return } } // ハンドラーに接続情報を渡す go func(conn *net.TCPConn) { echoHandler(conn) }(conn) } } |
サーバー起動後、10秒経過した時点でTCPサーバーが停止するようにしました。
1 2 3 4 |
$ go run server/server.go Start TCP Server... 2020/12/02 14:02:01 Connection Start 2020/12/02 14:02:11 Connection Close |
キャンセル処理
contextパッケージのWithDeadlineを活用するとキャンセル処理を実装できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
unc main() { fmt.Println("Start TCP Server...") // cancel ctx, cancel := context.WithCancel(context.Background()) cancel() receiveTCPConnection(ctx) } func receiveTCPConnection(ctx context.Context) { // リスナーをここで作る tcpAddr, err := net.ResolveTCPAddr("tcp", ":8080") listener, err := net.ListenTCP("tcp", tcpAddr) logFatal(err) // リスナーを閉じる defer listener.Close() for { select { case <-ctx.Done(): log.Println("Stopping TCP Server...") return default: log.Println("Running TCP Server...") listener.SetDeadline(time.Now().Add(time.Second * 100)) log.Println("Connection Start") conn, err := listener.AcceptTCP() if err != nil { switch err := err.(type) { case net.Error: if err.Timeout() { log.Println("Connection Close") return } default: log.Println("Another Error!!!") return } } go func(conn *net.TCPConn) { echoHandler(conn) }(conn) } } } |
もともと実装していたソースコードも変えました。
cancelが呼ばれたらreceiveTCPConnection内に設定した、select文の<-ctx.Doneが呼ばれます。
1 2 3 |
$ go run server/server.go Start TCP Server... 2020/12/02 14:12:45 Stopping TCP Server... |
OKですね。ちゃんとキャンセルされているようです。
cancelには、main関数を抜けたら確実に実行されるようにしたいので、deferをつけておきましょう。
1 |
defer cancel() |
皆さんの中には、「キャンセル処理ってあまり意味ないんじゃないか?」と思われる方もいるかもしれませんね。
くだんのreceiveTCPConnectionは、メインプロセスと同じプロセス上で動いているので、main関数を抜けたら処理が終了するからです。
しかし、goroutineを使ってサブプロセスで動かした場合は、メインプロセスが停止しても処理が残り続ける可能性があるので、それを防ぐという意味では有効だと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
func receiveTCPConnection(ctx context.Context) { tcpAddr, err := net.ResolveTCPAddr("tcp", ":8080") listener, err := net.ListenTCP("tcp", tcpAddr) logFatal(err) // receiveTCPConnection自体を別プロセスで動かす go func() { // リスナーを閉じる defer listener.Close() for { select { case <-ctx.Done(): log.Println("Stopping TCP Server...") return default: log.Println("Running TCP Server...") listener.SetDeadline(time.Now().Add(time.Second * 10)) log.Println("Connection Start") conn, err := listener.AcceptTCP() if err != nil { switch err := err.(type) { case net.Error: if err.Timeout() { log.Println("Connection Close") return } default: log.Println("Another Error!!!") return } } go func(conn *net.TCPConn) { echoHandler(conn) }(conn) } } }() } |
おわりに
これまで見てきたとおり、Go言語では簡単にTCPサーバーを作ることができます。。
日本でもどんどん流行っていく言語だと思うので、Go言語は習得しておいてそんはないと思います!
それでは、また!
TCPサーバーコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
/ server.go package main import ( "context" "fmt" "io" "log" "net" "time" ) func logFatal(err error) { if err != nil { log.Fatal(err) } } func main() { fmt.Println("Start TCP Server...") // cancel ctx, cancel := context.WithCancel(context.Background()) defer cancel() receiveTCPConnection(ctx) } func receiveTCPConnection(ctx context.Context) { // リスナーをここで作る tcpAddr, err := net.ResolveTCPAddr("tcp", ":8080") listener, err := net.ListenTCP("tcp", tcpAddr) logFatal(err) // リスナーを閉じる defer listener.Close() for { select { case <-ctx.Done(): log.Println("Stopping TCP Server...") return default: log.Println("Running TCP Server...") listener.SetDeadline(time.Now().Add(time.Second * 10)) log.Println("Connection Start") conn, err := listener.AcceptTCP() if err != nil { switch err := err.(type) { case net.Error: if err.Timeout() { log.Println("Connection Close") return } default: log.Println("Another Error!!!") return } } go func(conn *net.TCPConn) { echoHandler(conn) }(conn) } } } func echoHandler(conn *net.TCPConn) { defer conn.Close() for { _, err := io.WriteString(conn, "response from server\n") if err != nil { return } time.Sleep(time.Second) } } |
最近のコメント