こんにちは、いっちゃんです。10ANTZでは、サーバーサイドのソフトウェア開発に携わっています。
本記事では、2023年3月20日に開催された社内勉強会の復習をします。10ANTZのエンジニアの間では不定期で勉強会が開催されており、業務と直接関係あるかどうかに関わらず幅広いテーマが取り上げられています。今回は、TCP/IPによる通信処理がテーマでした。
以下では、まず、通信プロトコルの階層モデル、その中の具体的なプロトコル(IP、TCP、UDP)について簡単にまとめます。その後、GoによりTCPおよびUDPを用いて通信を行うプログラムを実装します。
目次
通信プロトコルの階層モデル
通信で必要な機能やプロトコルを階層的にまとめたモデルとして、OSI参照モデルとTCP/IPモデルがあります。
OSI参照モデルは、サービスに必要な機能を以下のような階層に分けてモデル化したものです。
階層 | 名称 | 機能 |
---|---|---|
7 | アプリケーション層 | サービスを提供する。 |
6 | プレゼンテーション層 | バイナリデータのフォーマットを変換する。 |
5 | セッション層 | 転送元と転送先の間の接続を制御する。 |
4 | トランスポート層 | 転送元と転送先の間のデータ転送を管理する。 |
3 | ネットワーク層 | 転送元と転送先の間でネットワーク間のルーティングを行う。 |
2 | データリンク層 | 同一ネットワーク内のデバイス間のデータ転送を行う。 |
1 | 物理層 | 伝送媒体上でビット転送を行う。 |
TCP/IPモデルは、サービスを実装するために必要なプロトコルを以下のような階層に分けてモデル化したものです。
階層 | 名称 | プロトコルの役割 |
---|---|---|
4 | アプリケーション層 | メッセージの送受信を行う。 |
3 | トランスポート層 | 転送元プロセスと転送先プロセスの間のデータ転送を制御する。 |
2 | インターネット層 | 転送元ホストと転送先ホストの間でネットワークをまたいでパケットを転送する。 |
1 | データリンク層 | 隣接するデバイス間でデータ転送を行う。 |
インターネットで標準的に利用されているのは、TCP/IPモデルに基づいて実装されたプロトコルです。
インターネット層
TCP/IPモデルのインターネット層における代表なプロトコルの一つであるIPについて簡単にまとめます。
IPでは、転送するべきデータをパケットに分割(フラグメンテーション)し、データリンク層のプロトコルを介して隣接するルーターへ転送します。パケットは、IPヘッダとIPペイロードからなり、パケットのサイズは、データリンク層のプロトコルが扱えるサイズ(MTU)となっています。IPヘッダには、転送元ホストのIPアドレスや転送先ホストのIPアドレスのようなルーティングに必要なデータが含まれています。IPペイロードには、転送するべきデータの断片が含まれています。ルーターは、転送されてきたパケットのIPヘッダの転送先ホストのIPアドレスを、ルーター自身が持つルーティングテーブルと照らし合わせて、次の転送先ルーターを決定します。このようなルーティングにより、転送元ホストと転送先ホストの間でネットワークをまたいでパケットを転送することが可能となります。
トランスポート層
TCP/IPモデルのインターネット層における代表なプロトコルであるTCPとUDPについて簡単にまとめます。
TCP
TCPでは、転送するべきデータをセグメントに分割し、IPを介して転送先プロセスへ転送します。セグメントは、TCPヘッダとTCPペイロードからなり、セグメントのサイズはセグメントを乗せるパケットのIPペイロードのサイズとなります。TCPヘッダには、転送元プロセスのポート番号や転送先プロセスのポート番号のように通信相手を特定するための情報、その他転送を制御するための情報が含まれています。TCPペイロードには、転送するべきデータの断片が含まれています。TCPでは、信頼性のある通信を実現するための制御が行われます。TCPで行われる制御には以下のようなものがあります。
- コネクションの確立:接続元と接続先の間でデータ転送が可能であることを確認する。
- コネクションの切断:接続元と接続先の間でデータ転送を行わないことを確認する。
- データ受け取りの確認:転送されたデータを受け取った後で、確認応答を返すことで受け取ったことを伝える。
- データの再転送:確認応答が返されなかった場合、もう一度データ転送を行う。
UDP
UDPでは、転送するべきデータにUDPヘッダを付けたものを、IPを介して転送先プロセスへ転送します。UDPヘッダには、転送元プロセスのIPアドレスや転送先プロセスのポート番号のように通信相手を特定するための情報が含まれています。UDPでは、TCPのように信頼性のある通信を実現するための制御は行わず、転送元プロセスと転送先プロセスの間のデータ転送を行うための最低限の機能を提供します。
Goによる実装
実装するアプリケーション層のプロトコル
Goにより、以下のようなアプリケーション層のプロトコルを実装します。
サーバーは、クライアントからメッセージを受け取り、その後メッセージをクライアントへ送り返します。
ここで、サーバーが受け取るメッセージは、64ビットの符号付き整数としてパースできる文字列である必要があります。また、サーバーが送り返すメッセージは、受け取ったメッセージをパースした整数が15の倍数であるときは”FizzBuzz”、3の倍数であるときは”Fizz”、5の倍数であるときは”Buzz”、それ以外のときは受け取ったメッセージそのものであるとします。
サーバーが送り返すメッセージを構築するプログラムのソースコードは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package fizzbuzz import "strconv" func FizzBuzz(message string) string { integer, err := strconv.ParseInt(message, 10, 64) if err != nil { return "Error" } switch { case integer%15 == 0: return "FizzBuzz" case integer%3 == 0: return "Fizz" case integer%5 == 0: return "Buzz" default: return message } } |
TCPを用いて通信を行うプログラム
上で示したアプリケーション層のプロトコルをTCPの上に実装したサーバーおよびクライアントのスースコードを示します。次のコマンドにより実行することができます。
1 2 |
go run tcp/server.go & go run tcp/client.go localhost:1234 20 |
サーバー
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 |
package main import ( "bufio" "fmt" "github.com/tomohiko-ito-10antz/go-dev/fizzbuzz" "log" "net" ) func main() { tcpAddr := &net.TCPAddr{ IP: net.ParseIP("0.0.0.0"), Port: 1234, } listener, err := net.ListenTCP("tcp", tcpAddr) if err != nil { log.Fatalf(`Fail to listen: %+v`, err) } log.Println("Listen") for { conn, err := listener.Accept() if err != nil { log.Fatalf(`Fail to accept: %+v`, err) } log.Println("Accept") go func(conn net.Conn) { defer conn.Close() scanner := bufio.NewScanner(conn) scanner.Split(bufio.ScanWords) for scanner.Scan() { log.Println("Receive") _, err := fmt.Fprintln(conn, fizzbuzz.FizzBuzz(scanner.Text())) if err != nil { log.Printf(`Fail to send: %+v`, err) return } log.Println("Send") } }(conn) } } |
クライアント
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 |
package main import ( "bufio" "fmt" "log" "net" "os" "strconv" ) func main() { if len(os.Args) != 3 { log.Fatalf(`Fail to parse args: %v`, os.Args) } address := os.Args[1] n, err := strconv.ParseInt(os.Args[2], 10, 64) if err != nil { log.Fatalf(`Fail to dial: %+v`, err) } conn, err := net.Dial("tcp", address) if err != nil { log.Fatalf(`Fail to dial: %+v`, err) } scanner := bufio.NewScanner(conn) scanner.Split(bufio.ScanWords) for i := int64(0); i < n; i++ { _, err := fmt.Fprintln(conn, strconv.FormatInt(i, 10)) if err != nil { log.Fatalf(`Fail to send %d: %+v`, i, err) } if scanner.Scan() { fmt.Println(scanner.Text()) } if err := scanner.Err(); err != nil { log.Fatalf(`Fail to read: %+v`, err) } } } |
UDPを用いて通信を行うプログラム
上で示したアプリケーション層のプロトコルをUDPの上に実装したサーバーおよびクライアントのスースコードを示します。次のコマンドにより実行することができます。
1 2 |
go run udp/server.go & go run udp/client.go localhost:5678 20 |
サーバー
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 |
package main import ( "bytes" "fmt" "github.com/tomohiko-ito-10antz/go-dev/fizzbuzz" "log" "net" ) func main() { udpAddr := &net.UDPAddr{ IP: nil, Port: 5678, } conn, err := net.ListenUDP("udp", udpAddr) if err != nil { log.Fatalf(`Fail to listen: %+v`, err) } log.Println("Listen") buffers := map[net.Addr]*bytes.Buffer{} var buf = make([]byte, 128) for { nRead, addr, err := conn.ReadFrom(buf) if err != nil { log.Fatalf(`Fail to read: %+v`, err) } buffer, ok := buffers[addr] if !ok { buffer = bytes.NewBuffer(nil) buffers[addr] = buffer } _, err = buffer.Write(buf[:nRead]) if err != nil { log.Fatalf(`Fail to read: %+v`, err) } for { var text string if _, err := fmt.Fscan(buffer, &text); err != nil { break } log.Println("Receive") _, err = conn.WriteTo([]byte(fizzbuzz.FizzBuzz(text)+"\n"), addr) if err != nil { log.Printf(`Fail to send: %+v`, err) break } log.Println("Send") } } } |
クライアント
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 |
package main import ( "bufio" "fmt" "log" "net" "os" "strconv" ) func main() { if len(os.Args) != 3 { log.Fatalf(`Fail to parse args: %v`, os.Args) } address := os.Args[1] n, err := strconv.ParseInt(os.Args[2], 10, 64) if err != nil { log.Fatalf(`Fail to dial: %+v`, err) } conn, err := net.Dial("udp", address) if err != nil { log.Fatalf(`Fail to dial: %+v`, err) } scanner := bufio.NewScanner(conn) scanner.Split(bufio.ScanWords) for i := int64(0); i < n; i++ { _, err := fmt.Fprintln(conn, strconv.FormatInt(i, 10)) if err != nil { log.Fatalf(`Fail to send %d: %+v`, i, err) } if scanner.Scan() { fmt.Println(scanner.Text()) } if err := scanner.Err(); err != nil { log.Fatalf(`Fail to read: %+v`, err) } } } |
まとめ
TCP/IPによる通信処理をテーマとする社内勉強会に参加しました。本記事では、その復習として、通信処理に関する基礎知識をまとめ、Goによる実装例を示しました。
また、今回の社内勉強会への参加と本記事の執筆をきっかけに、通信処理に関する用語を整理して理解することができました。