Golang Cafe #15 まとめ netパッケージ

2014/02/02に開催された「Golang Cafe #15」についてのまとめです。
今回はnetパッケージ以下をみていきました。
所謂ソケット通信を行うためのパッケージです。今回は基本的なTCPUDP関連のパッケージを見てみました。


+TakashiYokoyama氏が準備してくれたサンプルはこちらにあります。
私の書いたサンプルはGitHubに置いてあります。

TCP

特に難しいことはありません。C#Java等でソケット通信をやったことがある方にとっては同じようなイメージです。
+TakashiYokoyama氏のサンプルを元にシンプルなエコーバックのサンプルを書いてみました。
サーバー側

func main() {
    runtime.GOMAXPROCS(3)

    listener, err := net.Listen("tcp", "localhost:8888")

    if err != nil {
        log.Fatalln(err)
    }

    for {
        conn, err := listener.Accept()

        if err != nil {
            log.Printf("Accept Error: %v\n", err)
            continue
        }

        log.Printf("Accept [%v]\n", conn.RemoteAddr())

        go doProcess(conn)
    }
}

func doProcess(conn net.Conn) {
    var rlen int
    var err error

    tcpConn := conn.(*net.TCPConn)

    defer tcpConn.Close()

    err = tcpConn.SetDeadline(time.Now().Add(5 * time.Second))

    if err != nil {
        log.Printf("[%v]: %v\n", tcpConn.RemoteAddr(), err)
        return
    }

    time.Sleep(time.Duration(rand.Intn(3)) * time.Second)

    buf := make([]byte, 1024)

    rlen, err = tcpConn.Read(buf)

    if err != nil {
        log.Printf("Receive Error [%v]: %v\n", tcpConn.RemoteAddr(), err)
        return
    }

    s := string(buf[:rlen])

    log.Printf("Receive [%v]: %v\n", tcpConn.RemoteAddr(), s)

    s = "Hello! " + s

    rlen, err = tcpConn.Write([]byte(s))

    if err != nil {
        log.Printf("Send Error [%v]: %v\n", tcpConn.RemoteAddr(), err)
        return
    }

    log.Printf("Send [%v]: %v\n", tcpConn.RemoteAddr(), s)
}

クライアント側

func exec(ch chan<- int, pos int) {
    var rlen int
    var err error

    defer func() {
        ch <- pos
    }()

    conn, err := net.DialTimeout("tcp", "localhost:8888", 5*time.Second)

    if err != nil {
        log.Fatalln(err)
    }

    log.Printf("Connect[%d]: %v\n", pos, conn.RemoteAddr())

    defer conn.Close()

    s := "user" + strconv.Itoa(pos)

    rlen, err = conn.Write([]byte(s))

    if err != nil {
        log.Fatalf("Send Error[%d]: %v\n", pos, err)
        return
    }

    log.Printf("Send[%d]: %v\n", pos, s)

    buf := make([]byte, 1024)

    rlen, err = conn.Read(buf)

    if err != nil {
        log.Fatalf("Receive Error[%d]: %v\n", pos, err)
        return
    }

    log.Printf("Receive[%d]: %v\n", pos, string(buf[:rlen]))
}

サーバー側の手順です。

  1. ネットワークとアドレス:ポートを指定してListenメソッドを呼ぶとListenerを取得できます。このサンプルではネットワークにtcpを指定しているので実態はTCPListenerになっています。ただしListenerはinterfaceですので、TCPListener固有のメソッドを実行するときはTypeAssertionする必要があります。
  2. Acceptメソッドでクライアントからの接続を待ちます。接続が完了した場合は、Connを返すので、あとはConnに対してReadまたはWriteの操作を行うだけです。Listenerと同じようにConnの実態はTCPConnですが、interfaceですので、TCPConn固有のメッソドを実行する場合はTypeAssertionする必要があります。
  3. 処理が完了したらCloseメソッドを呼びます。

クライアント側の手順です。

  1. DialメソッドまたはDialTimeメソッドで指定した接続先に接続します。DialTimeメソッドは名前の通りタイムアウトを同時に指定することができます。
  2. 接続が完了するとConnを取得できるので、あとはConnに対してReadまたはWriteの操作を行うだけです。サーバー側と同じようにConnはinterfaceで実態はTCPConnになっています。
  3. 処理が完了したらCloseメソッドを呼びます。

TCP接続なのでプロトコルさえ決めておけばやりとりはコネクションに対するReadとWriteだけです。
ちなみに実行結果はこのような感じです。

$ go run tcpserver.go
2014/02/11 00:01:08 Accept [127.0.0.1:1401]
2014/02/11 00:01:08 Accept [127.0.0.1:1402]
2014/02/11 00:01:08 Receive [127.0.0.1:1402]: user0
2014/02/11 00:01:08 Accept [127.0.0.1:1403]
2014/02/11 00:01:08 Send [127.0.0.1:1402]: Hello! user0
2014/02/11 00:01:08 Accept [127.0.0.1:1404]
2014/02/11 00:01:08 Accept [127.0.0.1:1405]
2014/02/11 00:01:09 Receive [127.0.0.1:1405]: user4
2014/02/11 00:01:09 Send [127.0.0.1:1405]: Hello! user4
2014/02/11 00:01:10 Receive [127.0.0.1:1401]: user3
2014/02/11 00:01:10 Send [127.0.0.1:1401]: Hello! user3
2014/02/11 00:01:10 Receive [127.0.0.1:1404]: user2
2014/02/11 00:01:10 Receive [127.0.0.1:1403]: user1
2014/02/11 00:01:10 Send [127.0.0.1:1404]: Hello! user2
2014/02/11 00:01:10 Send [127.0.0.1:1403]: Hello! user1
$ go run tcpclient.go
2014/02/11 00:01:08 Connect[3]: 127.0.0.1:8888
2014/02/11 00:01:08 Connect[2]: 127.0.0.1:8888
2014/02/11 00:01:08 Connect[1]: 127.0.0.1:8888
2014/02/11 00:01:08 Send[1]: user1
2014/02/11 00:01:08 Connect[4]: 127.0.0.1:8888
2014/02/11 00:01:08 Send[4]: user4
2014/02/11 00:01:08 Send[2]: user2
2014/02/11 00:01:08 Connect[0]: 127.0.0.1:8888
2014/02/11 00:01:08 Send[3]: user3
2014/02/11 00:01:08 Send[0]: user0
2014/02/11 00:01:08 Receive[0]: Hello! user0
2014/02/11 00:01:09 Receive[4]: Hello! user4
2014/02/11 00:01:10 Receive[3]: Hello! user3
2014/02/11 00:01:10 Receive[2]: Hello! user2
2014/02/11 00:01:10 Receive[1]: Hello! user1

UDP

基本的にはTCPと同じような処理になります。
大きつ異なるところはサーバー側の処理です。TCPではListenしてクライアントからの接続要求を待ち、Acceptするとクライアントと1対1のConnectionを取得することができましたが、UDPではその点が異なります。
簡単に説明すると、サーバー側がConnectionを用意していると、勝手に接続して処理されるイメージでしょうか。
TCPの様にAcceptのようなメソッドがないので接続されたタイミングが分かりません。
サーバー側

func main() {
    addr, err := net.ResolveUDPAddr("udp", "localhost:8888")

    if err != nil {
        log.Fatalln(err)
    }

    conn, err := net.ListenUDP("udp", addr)

    if err != nil {
        log.Fatalln(err)
    }

    defer conn.Close()

    buf := make([]byte, 1024)

    for {
        rlen, remote, err := conn.ReadFromUDP(buf)

        if err != nil {
            log.Fatalf("Error: %v\n", err)
        }

        s := string(buf[:rlen])

        log.Printf("Receive [%v]: %v\n", remote, s)

        s = "Hello! " + s

        rlen, err = conn.WriteToUDP([]byte(s), remote)

        if err != nil {
            log.Printf("Receive Error [%v]: %v\n", remote, s)
        }

        log.Printf("Send [%v]: %v\n", remote, s)
    }
}

クライアント側

func exec(ch chan<- int, pos int) {
    var rlen int
    var err error

    defer func() {
        ch <- pos
    }()

    remote, err := net.ResolveUDPAddr("udp", "localhost:8888")

    if err != nil {
        log.Fatalf("%v\n", err)
    }

    conn, err := net.DialUDP("udp", nil, remote)

    if err != nil {
        log.Fatalf("%v\n", err)
    }

    conn.SetDeadline(time.Now().Add(5 * time.Second))

    defer conn.Close()

    s := "user" + strconv.Itoa(pos)

    rlen, err = conn.Write([]byte(s))

    if err != nil {
        log.Printf("Send Error: %v\n", err)
        return
    }

    log.Printf("Send[%d]: %v\n", pos, s)

    buf := make([]byte, 1024)

    rlen, err = conn.Read(buf)

    if err != nil {
        log.Printf("Receive Error: %v\n", err)
        return
    }

    log.Printf("Receive[%d]: %v\n", pos, string(buf[:rlen]))
}

ちなみに実行結果はこのような感じです。

$ go run udpserver.go
2014/02/11 00:02:10 Receive [127.0.0.1:60089]: user4
2014/02/11 00:02:10 Send [127.0.0.1:60089]: Hello! user4
2014/02/11 00:02:10 Receive [127.0.0.1:60088]: user1
2014/02/11 00:02:10 Send [127.0.0.1:60088]: Hello! user1
2014/02/11 00:02:10 Receive [127.0.0.1:60090]: user2
2014/02/11 00:02:10 Send [127.0.0.1:60090]: Hello! user2
2014/02/11 00:02:10 Receive [127.0.0.1:60092]: user3
2014/02/11 00:02:10 Send [127.0.0.1:60092]: Hello! user3
2014/02/11 00:02:10 Receive [127.0.0.1:60091]: user0
2014/02/11 00:02:10 Send [127.0.0.1:60091]: Hello! user0
$ go run udpclient.go
2014/02/11 00:02:10 Connect[4]: 127.0.0.1:8888
2014/02/11 00:02:10 Connect[1]: 127.0.0.1:8888
2014/02/11 00:02:10 Connect[2]: 127.0.0.1:8888
2014/02/11 00:02:10 Connect[3]: 127.0.0.1:8888
2014/02/11 00:02:10 Connect[0]: 127.0.0.1:8888
2014/02/11 00:02:10 Send[4]: user4
2014/02/11 00:02:10 Send[1]: user1
2014/02/11 00:02:10 Send[2]: user2
2014/02/11 00:02:10 Send[3]: user3
2014/02/11 00:02:10 Send[0]: user0
2014/02/11 00:02:10 Receive[4]: Hello! user4
2014/02/11 00:02:10 Receive[1]: Hello! user1
2014/02/11 00:02:10 Receive[2]: Hello! user2
2014/02/11 00:02:10 Receive[3]: Hello! user3
2014/02/11 00:02:10 Receive[0]: Hello! user0

サーバー側の手順です。

  1. ネットワークとアドレスを指定してListenUDPメソッドを呼んだ時点でコネクションが作成されます。これで待機完了です。
  2. クライアントからデータを読み込むにはReadFromUDPメソッドを使用します。このメソッドを使用することでどのクライアントからデータを受信したかがわかります。
  3. クライアントにデータを送信するにはWriteToUDPメソッドを使用して送信先を指定してデータを送信します。
  4. すべてが完了したら(終了時)Closeメソッドを呼びます。

クライアント側の手順です。

  1. DialUDPメソッドでネットワークとアドレス:ポートを指定して接続するとコネクションが取得できます。
  2. 取得したコネクションに対してデータを送信、受信します。サーバー側のように接続先を意識する必要はありません。
  3. 処理が完了したらCloseメソッドを呼びます。

まとめ

TCPにしろUDPにしろ比較的簡単に実装できるようような気がします。
今回みた型以外にも何種類か定義されていますが、Conn型を実装しているので、基本的な操作方法は同じかと思います。


次回は(このブログをまとめている時点ではすでに終了していますが)mailパッケージsmtpパッケージになります。