💧

Goで立てたWebサーバーでソケットを学ぶ

2022/05/22に公開

目的

TCPなどにまるで明るくないので、学習のために調べてみました

環境

  • Arch Linux(5.17.9-arch1-1)
  • go version go1.18.3 linux/amd64

やること

Goで書いたWebサーバーを動かして挙動を確認したり、少しコードを見てみます
コードは以下です

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "Hello, World")
	})

	if err := http.ListenAndServe("127.0.0.1:18080", nil); err != nil {
		log.Print(err)
	}
}

後述するソケットの説明のためにローカルのアドレスを指定します
ポート番号は8080ではない別の番号を指定します
8080はhttp-altと呼ばれるもので、コマンドで情報を見る際に表記が通常と変わってしまうので

ListenAndServe()を見る

名前そのまま、ListenとServeをしてます
それぞれ分けて見てみます

func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	return srv.Serve(ln)
}

Listen

いろいろ無視して深掘ると以下を実行しています

func RawSyscall6(trap, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err Errno)

システムコールと呼ばれる普通のプログラムだと使用できない制限のあるOSの機能を呼び出しています

Listenではまずsocketシステムコールを呼んでます

r0, _, e1 := RawSyscall(SYS_SOCKET, uintptr(domain), uintptr(typ), uintptr(proto))

そのあと、bind、listenと続いてシステムコールを呼んでいます

_, _, e1 := Syscall(SYS_BIND, uintptr(s), uintptr(addr), uintptr(addrlen))
_, _, e1 := Syscall(SYS_LISTEN, uintptr(s), uintptr(n), 0)

ソケット(socket)とは

ソケットは以下のような使われ方をするようです

サーバーでのソケットの使い方

クライアントでのソケットの使い方

Listenでやっていること

socket、bind、listenシステムコールを実行して、クライアントからの接続を待ち受けています

Serve

Serveは先述したacceptシステムコールを無限ループで実行してます

for {
  rw, err := l.Accept()
  if err != nil {
   ...
    if ne, ok := err.(net.Error); ok && ne.Temporary() {
      ...
      time.Sleep(tempDelay)
      continue
    }
    return err
  }
  ...
  go c.serve(connCtx)
}
r0, _, e1 := Syscall6(SYS_ACCEPT4, uintptr(s), uintptr(unsafe.Pointer(rsa)), uintptr(unsafe.Pointer(addrlen)), uintptr(flags), 0, 0)

コードを実行して挙動を確認する

実際に動かしてソケットを確認します

サーバー側

  • 実行
    • $ go run ./main.go
  • プロセス番号を確認
    • $ pgrep main
    627384 /tmp/go-build197999410/b001/exe/main
    
  • ソケットを確認
    • $ ls -l /proc/627384/fd | grep socket
    lrwx------ 1 atsuya0 wheel 64  5月 22 17:37 3 -> 'socket:[1122008]'
    
    • $ cat /proc/627384/net/tcp | grep -e local_address -e 1122008
    sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
     0: 0100007F:46A0 00000000:0000 0A 00000000:00000000 00:00000000 00000000  1000        0 1122008 1 000000006963cb39 99 0 0 10 0
    
    • addressの値は16進数で、local_addressの値を10進数に変換すると127.0.0.1:18080になっていることが確認できます

クライアント側

  • ソケットが消えないようにsleepを入れてサーバー側と同じように実行
    • $ go run ./main.go
    time.Sleep(time.Second * 3)
    fmt.Fprint(w, "Hello, World")
    
  • curlでアクセス
    • $ curl http://localhost:18080
  • 実はサーバー側でやっているようなことをせずとも、ssコマンドでソケットを確認できるので実行します
    • $ ss -tp | grep -e State -e curl
    State Recv-Q Send-Q  Local Address:Port    Peer Address:Port   Process
    ESTAB 0      0           127.0.0.1:34438      127.0.0.1:18080   users:(("curl",pid=1310859,fd=5))
    
    • Peer Address:Port(リモート)が127.0.0.1:18080になっていることが確認できます
    • Local Portはランダムになっています

Discussion