💧
Goで立てたWebサーバーでソケットを学ぶ
目的
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の機能を呼び出しています
- システムコールの概要
- Goでのシステムコール
- cdコマンドで現在の作業ディレクトリを変更するのもシステムコール
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)とは
- プロセスがパケット(データ)を送受信するために使われるもの
- ローカルのアドレス、ポート番号、リモートのアドレス、ポート番号などを持つ
- ソケットも普通のファイルやディレクトリ、シンボリックリンクと同様に実体はファイル
- UNIXドメインソケットと呼ばれる種類のものもありますが今回は関係ないです
- https://ja.wikipedia.org/wiki/UNIXドメインソケット
- Dockerを利用していると偶に見る/var/run/docker.sock
- パケット(データ)はNIC(ネットワークインターフェースカード)から流れてくるようです
ソケットは以下のような使われ方をするようです
サーバーでのソケットの使い方
- SOCKET
- ソケットを作成
- IPv4 インターネットプロトコルなどプロトコルファミリー指定
- TCP、UDPの指定
- https://linuxjm.osdn.jp/html/LDP_man-pages/man2/socket.2.html
- ソケットを作成
- BIND
- ソケットにローカルのアドレスやポート番号を紐付ける
- https://linuxjm.osdn.jp/html/LDP_man-pages/man2/bind.2.html
- LISTEN
- ACCEPT
- 接続要求から新たにソケットを作成
- ここでリモートのアドレス、ポート番号を得ます
- このソケットを使って読み書きします
- https://linuxjm.osdn.jp/html/LDP_man-pages/man2/accept.2.html
クライアントでのソケットの使い方
- SOCKET
- ソケットを作成
- CONNECT
- サーバーに接続
- リモートのアドレス、ポート番号を渡します
- curlのデフォルトではローカルのポート番号はランダムに設定されます
- https://linuxjm.osdn.jp/html/LDP_man-pages/man2/connect.2.html
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