Yabu.log

ITなどの雑記

GolangでUDPサーバー&クライアントを作成してWiresharkで検証する

Goならわかるシステムプログラミング第7章 UDPソケットを使ったマルチキャスト通信に書かれている内容をWiresharkを使ったり、デバッグでライブラリ(netパッケージ中心)のソースを読んだりして検証してみました

サーバー側

  • net.ListenPacket()の戻り値のnet.PacketConnインターフェースを使う
    • TCPの時のnet.Listenerインタフェースのようなクライアントを待つインタフェースではない
    • ReadFromメソッドで受信内容を読み取る
      • 引数で渡しているbufferに通信内容を格納している
      • remoteAddressに通信して来たクライアントのアドレスを受け取っている
      • bufferの中身はあくまでudpパケットの中身しか入らない
        • だからIPヘッダの情報などはReadだけでは読み取れず、ReadFromで別途IPアドレスなどを取得する必要がある
package main

import (
    "fmt"
    "net"
)

func main() {
    fmt.Println("Server is runnign at localhost:8888")
    conn, err := net.ListenPacket("udp", "localhost:8888")
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    buffer := make([]byte, 1500)
    for {
        length, remoteAddress, err := conn.ReadFrom(buffer)

    if err != nil {
            panic(err)
        }

        fmt.Printf("Received from %v: %v\n",remoteAddress, string(buffer[:length]))

    _, err = conn.WriteTo([]byte("Hello from Server"), remoteAddress)

        if err != nil {
            panic(err)
        }

    }
}

ReadFrom関数の実装が気になったので確認してみた

//udpsock.go
func (c *UDPConn) ReadFrom(b []byte) (int, Addr, error) {
    if !c.ok() {
        return 0, nil, syscall.EINVAL
    }
    n, addr, err := c.readFrom(b)
    if err != nil {
        err = &OpError{Op: "read", Net: c.fd.net, Source: c.fd.laddr, Addr: c.fd.raddr, Err: err}
    }
    if addr == nil {
        return n, nil, err
    }
    return n, addr, err
}
//udpsock_posix.go
func (c *UDPConn) readFrom(b []byte) (int, *UDPAddr, error) {
    var addr *UDPAddr
    n, sa, err := c.fd.readFrom(b)
    switch sa := sa.(type) {
    case *syscall.SockaddrInet4:
        addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port}
    case *syscall.SockaddrInet6:
        addr = &UDPAddr{IP: sa.Addr[0:], Port: sa.Port, Zone: zoneCache.name(int(sa.ZoneId))}
    }
    return n, addr, err
}
//fd_unix.go
func (fd *netFD) readFrom(p []byte) (n int, sa syscall.Sockaddr, err error) {
    n, sa, err = fd.pfd.ReadFrom(p)
    runtime.KeepAlive(fd)
    return n, sa, wrapSyscallError("recvfrom", err)
}
//syscall_unix.go
func Recvfrom(fd int, p []byte, flags int) (n int, from Sockaddr, err error) {
    var rsa RawSockaddrAny
    var len _Socklen = SizeofSockaddrAny
    if n, err = recvfrom(fd, p, flags, &rsa, &len); err != nil {
        return
    }
    if rsa.Addr.Family != AF_UNSPEC {
        from, err = anyToSockaddr(&rsa)
    }
    return
}
//zsyscall_darwin_amd64.go
// THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT

func recvfrom(fd int, p []byte, flags int, from *RawSockaddrAny, fromlen *_Socklen) (n int, err error) {
    var _p0 unsafe.Pointer
    if len(p) > 0 {
        _p0 = unsafe.Pointer(&p[0])
    } else {
        _p0 = unsafe.Pointer(&_zero)
    }
    r0, _, e1 := Syscall6(SYS_RECVFROM, uintptr(fd), uintptr(_p0), uintptr(len(p)), uintptr(flags), uintptr(unsafe.Pointer(from)), uintptr(unsafe.Pointer(fromlen)))
    n = int(r0)
    if e1 != 0 {
        err = errnoErr(e1)
    }
    return
}

このSyscall6の中身は見れなかった。名前的にもこれはネットワーク系のシステムコールなのでしょうか。 という訳で、コールスタックは私の場合は

syscall.recvfrom (/usr/local/go/src/syscall/zsyscall_darwin_amd64.go:146)
syscall.Recvfrom (/usr/local/go/src/syscall/syscall_unix.go:262)
internal/poll.(*FD).ReadFrom (/usr/local/go/src/internal/poll/fd_unix.go:215)
net.(*netFD).readFrom (/usr/local/go/src/net/fd_unix.go:208)
net.(*UDPConn).readFrom (/usr/local/go/src/net/udpsock_posix.go:47)
net.(*UDPConn).ReadFrom (/usr/local/go/src/net/udpsock.go:121)
main.main (/User/Study/go-sys/chap7/udp_server/udpServer.go:18)

となった。

クライアント側

package main

import (
    "fmt"
    "net"
)

func main() {
    conn, err := net.Dial("udp4", "localhost:8888")
    if err != nil {
        panic(err)
    }

    defer conn.Close()
    fmt.Println("Sending to server")
    _, err = conn.Write([]byte("Hello from client"))
    if err != nil {
        panic(err)
    }

    fmt.Println("Receiving from server")
    buffer := make([]byte, 1500)
    length, err := conn.Read(buffer)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Received: %s\n", string(buffer[:length]))
}

net.Dialでnet.Connインターフェース(実装はnet.UDPConn)を取得してそちらにWrite,Readで送受信ができる。 とてもシンプルですね。

キャプチャ結果

この2つのソースで送受信それぞれ1つづつしかパケットが飛んでいない。

f:id:yuyubu:20180917005441p:plain

UDPはとてもシンプルですが、TCPが信頼性を担保するためにいかに頑張ってくれているか、というのが実感できるのではないでしょうか。

Goならわかるシステムプログラミング

Goならわかるシステムプログラミング