harukin721

主に学習記録 🔗 wantedly.com/id/harukin721

Go言語でソケットプログラミング

「ソケット」が見えたので買った。積読がすごい勢いで増えてる。最初から全部読む必要もないし、いいかなと思っている。

Goならわかるシステムプログラミング 第2版www.lambdanote.com

GitHub Copilot にも手伝ってもらった。ありがとうありがとう。

第6章 TCPソケットとHTTPの実装

ソケットを繋いでからできることは、書き込み、読み込み、クローズのみでシンプル。ソケットは、アドレスとポート番号がわかればローカルのコンピューター内だけではなく外部のコンピュータとも通信が行える。

HTTP はどういった仕組みで下位のレイヤーを使っているか。それはほとんどの OS で、アプリケーション層からトランスポート層プロトコルを利用するときの API としてソケットを利用する。TLS はソケットと HTTP の間に入る。通常ブラウザを利用した HTTP 通信では、サーバーの TCP ポート80番に対して、ソケットを使ったプロセス間通信を行う。

ソケット通信の基本構造

基本構成

  • サーバー : ソケットを開いて待ち受ける
  • クライアント : 開いているソケットに接続し、通信を行う

Go言語の場合

  • サーバーが呼ぶのは Listen() メソッド
  • クライアントが呼ぶのは Dial() メソッド

Go言語でHTTPサーバーを実装する

Go言語に組み込まれている TCPの機能(net.Conn) だけを使って HTTP 通信を実現してみる。GitHub Copilot にコメント入れてもらった。

  • listenerは、特定のネットワークインターフェースとポートでの接続を待ち受けるためのソケットを表す
  • net.Listen("tcp", "localhost: 8888") は、指定したアドレスとポートで新しいTCPリスナーを作成、サーバーは新しい接続を待ち受ける状態になる
  • listener.Accept()は、新しいクライアントからの接続を待ち、接続が確立されるとその接続を表す net.Conn オブジェクトを返す。これを通じて、クライアントとの間でデータの送受信が行われる
package main

import (
    "bufio"
    "fmt"
    "io"
    "net"
    "net/http"
    "net/http/httputil"
    "strings"
)

func main() {
    // TCPネットワーク上のlocalhost:8888でリッスンを開始
    listener, err := net.Listen("tcp", "localhost: 8888")
    if err != nil {
        panic(err) // エラーが発生した場合はpanicを発生
    }
    fmt.Println("Server is running at localhost: 8888")
    for {
        // 新しい接続を受け入れる
        conn, err := listener.Accept()
        if err != nil {
            panic(err) // エラーが発生した場合はpanicを発生
        }
        go func() { // 新しいゴルーチンを起動
            fmt.Printf("Accept %v\n", conn.RemoteAddr()) // 接続のリモートアドレスをログに出力
            // HTTPリクエストを読み取る
            request, err := http.ReadRequest(
                bufio.NewReader(conn))
            if err != nil {
                panic(err) // エラーが発生した場合はpanicを発生
            }
            // リクエストをダンプしてログに出力
            dump, err := httputil.DumpRequest(request, true)
            if err != nil {
                panic(err) // エラーが発生した場合はpanicを発生
            }
            fmt.Println(string(dump))
            // HTTPレスポンスを作成
            response := http.Response{
                StatusCode: 200, // ステータスコード200(成功)
                ProtoMajor: 1,   // HTTP/1.0を使用
                ProtoMinor: 0,   // HTTP/1.0を使用
                Body: io.NopCloser(
                    strings.NewReader("Hello World\n")), // ボディに"Hello World\n"を含む
            }
            response.Write(conn) // レスポンスを接続に書き込む
            conn.Close()         // 接続を閉じる
        }()
    }
}

サーバーを起動して確認する

% go run server.go 
Server is running at localhost: 8888

# 以下で curl 実行したら出た
Accept 127.0.0.1:64219
GET / HTTP/1.1
Host: localhost:8888
Accept: */*
User-Agent: curl/8.1.2


% curl -v localhost:8888
*   Trying 127.0.0.1:8888...
* Connected to localhost (127.0.0.1) port 8888 (#0)
> GET / HTTP/1.1
> Host: localhost:8888
> User-Agent: curl/8.1.2
> Accept: */*
> 
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< 
Hello World
* Closing connection 0

この処理は新しい接続が来るたびに繰り返されるので、このサーバーは同時に複数の接続を処理することができる。