Goを用いてTCPソケット通信を学ぶ
この記事は GMOペパボエンジニア Advent Calendar 2021 - Adventar の6日目の記事です。
昨日は buty4649 さんの業務で使っているPCをLinuxデスクトップにしてから3年半が経った でした。 これまでデスクトップPCはWindowsかMacしか使ったことがなかったので、Linuxデスクトップの話は個人的にはかなり新鮮な内容でした。その他にも普段開発で利用しているアプリケーションの紹介もされていて、最近リモートで働く中でなかなか自分以外の開発環境を見る機会がほとんど無いので、めちゃくちゃ参考になりました!
はじめに
以前同僚がこんな記事を書いてて
ん....そもそもソケット自体よく分かっていない....どうしよう....って状態だったので、このタイミングで改めて学習し直しました。 今回はTCPソケット通信の処理の流れだったり、その中で使われているシステムコールについて見ていきながら、最後にGoを用いて簡単なTCPサーバ / クライアントを動かしてみようと思います。
TCPソケットとは
TCP/IP アプリケーションを作成するための抽象化されたインターフェースのことで、ユーザープロセスとTCP/IPを繋ぐ出入り口になる。 お互いのソケットをネットワークを通して接続し,その口を通して通信を実現することができます。 ソケットを利用することで、TCPの処理はOS側で行ってくれることによって、アプリケーション側では意識しなくてもよくなります。
ソケット自体は以下の2種類があります。
※訂正: 下記の2つを含め他にもたくさんの種類のソケットがあるそうです。( Man page of SOCKET ) ご指摘頂きありがとうございます!
INETドメイン(ネットワークソケット)
- 異なるマシンで動作しているプロセス間を行うためのソケット。
- IPアドレス + ポートを利用する (例: 192.168.10.10:9000)
- ループバックアドレスを使えば同じマシン内でも通信できるが、UNIXドメインを使う方が高速
今回はINETドメインに着目してTCP/IPにおけるソケット通信の流れを見ていきます。
ソケット通信の基本となる流れは以下のようになっています。 その中でいくつかのシステムコールを呼び出して通信を行なっています。
socket()
ソケットを作成する。(サーバ/クライアント)
bind()
作成したソケットに対してポートをなどを割り当てる。(サーバ)
listen()
ソケットを接続待ちの状態にする。(サーバ)
accept()
クライアントから来た接続要求に対してソケットへの接続を受ける。(サーバ)
connect()
サーバに対してソケット接続を要求する。(クライアント)
read() / write()
クライアント or サーバから来たデータの受信と送信を行う。(サーバ/クライアント)
Man page of READ Man page of WRITE
close()
接続を切断する。(サーバ/クライアント)
TCP接続の確立(3 Way Handshake)については以下の記事がとても参考になりました! milestone-of-se.nesuke.com
簡単なTCP サーバ / クライアントを動かしてみる
Goを用いてTCP通信を行う際に、標準で用意されているnetパッケージというものがあります。 今回はこちらを使用してサーバ⇄クライアント間でTCP通信を行なっていきます。
ここではクライアント側で書き込んだ文字列をサーバ側で受け取り、それをそっくりそのままクライアントに返すだけの構成になっています。
サーバ側のコード
package main import ( "io" "log" "net" ) func main() { ln, err := net.Listen("tcp", ":8080") if err != nil { log.Fatal(err) } for { conn, err := ln.Accept() if err != nil { log.Fatal(err) } io.Copy(conn, conn) conn.Close() } }
かなりざっくりコードを追うと、net.Listen()
メソッドの内部では、上記で挙げたsocket()
、bind()
、listen()
といったシステムコールの呼び出しを良い感じに行ってくれます。
net.Listen()
で取得したTCPListener
に対してAccept()
メソッドを呼び出すことで、クライアントからの接続を待ち受けることができます。
実際にどんなシステムコールがどのような順番で呼び出されているかは、こちらを参考にさせて頂きました。
クライアント側
package main import ( "fmt" "log" "net" ) func main() { conn, err := net.Dial("tcp", "localhost:8080") if err != nil { log.Fatal(err) } str := "Hello!" _, err = conn.Write([]byte(str)) if err != nil { log.Fatal(err) } buf := make([]byte, 1024) count, err := conn.Read(buf) if err != nil { log.Fatal(err) } fmt.Println(string(buf[:count])) }
クライアントも同様にnet.Dial()
メソッドを呼び出すことで、内部的には良い感じにsocket()
、connect()
といったシステムコールを呼び出し、ソケットの作成やリモートのソケットに対して3 Way Handshakeを実施してコネクションを確立するといったことを行っています。
net.Dial()
から取得されるTCPConn
は、Connインターフェース( net package - net - pkg.go.dev )を満たしており、Write()
やRead()
が実装されていることから
コネクションへの読み書きは上記のサンプルコードのようにconn.Write()
やconn.Read()
を用いてバイト列を読み書きすることで、データの送受信を行うことができます。
終わりに
ソケット通信って聞いたことあるけど、実際にどんなことをしているかほぼ雰囲気でしか分かっていなかったのですが、今回(TCPだけですが...)処理の流れを掴むことができたり、中でどんなシステムコールが呼ばれているかなど知る良いきっかけになりました。
また、絶賛勉強中のGoのサンプルコードを用いることで、net
パッケージに内部を少しだけ追うことができたので、かなり勉強になりました!
net
パッケージについてさらに深く読んでみたいのであれば、このあたりがとても参考になると思います。
zenn.dev
アドベントカレンダー7日目は しばっちさん です!宜しくお願いします!