2014年2月7日金曜日

Golang Cafe #15を開催しました。

Golang Cafe #15を開催しました。

今回はnetパッケージを使ったsocket通信でした。
サンプルコードはgithubにあります。
netパッケージは、tcp、udp、unixsocketを同じインターフェイスで扱える
きれいな設計になっています。

まず、サーバ側のプログラムのポイントから。
C言語だと、bind()→listen()→accept()の順番で関数を呼び出しますが、Goの場合は、bind()はありません。
したがって、以下のようにListen()とAccept()の順番で呼び出せばいいです。

 // Listenはtcp/tcp4/tcp6/unix/unixpacketでなければエラーになる。
 // エラーを繰り返すと、指定したアドレス名のファイルができる?(for MacOSX)
 listener, err := net.Listen("tcp", "localhost:22000")
 if err != nil {
  log.Fatalln(err)
 }

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

  go receiveGoroutine(conn)
 }

Accept()の戻り値でnet.Connインターフェイスのインスタンスが受け取れますので、Read()、Write()を使って、クライアント側に受信、送信します。
(実際の型は、net.TCPConnなので、Type Assertionすれば型変換ができます。
タイムアウトの設定は、net.Connからはできないので、net.TCPConnに変換してから、設定する必要があります)
Read()とWrite()の処理はos.Fileの時と変わりませんので、説明は省略します。

UnixSocketを使う場合も同様に、net.Listen("tcp", アドレス)を、net.Listen("unix", アドレス)に変更すれば、net.UnixConnが返されるのでTCPConnと同じように扱えます。

今回のサンプルはAccept()後、goroutineを使って別スレッドで送受信するようにしていますので、たくさんのクライアントからの接続があっても、同じ挙動をします。もし、1対1の通信であれば、goroutineにしなくても良いです。(そのようなサーバプログラムを書くことは無いかもしれませんが…。)

net.Connはio.Reader、io.Writer、io.Closerなどのインターフェイスは持っていますので、ioパッケージ周りの便利関数群は使えます。
また、encoding/binaryパッケージを利用して、エンディアンを考慮したデータ構造を作る処理も入れてあります。
encoding/binaryは便利ですが、引数で使える型の制約があるので、(int32とかだと、errorが返される)符号がない、uint32などを利用しなければいけません。

クライアント側のポイントは、Connect()の代わりにDial()を使うことです。

 var recvData uint32

 // net.Dial() タイムアウトがないConnect
 // net.DialTimeout() タイムアウト検知するConnect
 // -5とかだとi/o timeoutと出るが…。
 // Dialerからtimeoutを設定する方法もあるが…。

 conn, err := net.DialTimeout("tcp", "192.168.0.6:8888", 5 * time.Second)
 // conn, err := net.Dial("tcp", "localhost:22000")
 // dialer := net.Dialer{Timeout: 10 * time.Second}
 // conn, err := dialer.Dial("tcp", "localhost:22000")
 if err != nil {
  log.Fatalln(err)
 }

 for i := 0; i < 2; i++ {
  err = binary.Read(conn, binary.LittleEndian, &recvData)
  if err != nil {
   log.Printf("Receive: %s\n", err)
   break
  }
  log.Printf("Receive From Server %v\n", recvData)

  log.Printf("Send To Server %d\n", recvData + 1)
  sendData := uint32(recvData + 1)

  err = binary.Write(conn, binary.BigEndian, sendData)
  if err != nil {
   log.Printf("Send: %v\n", err)
   break
  }
 }

 if err = conn.Close(); err != nil {
  log.Printf("Close: %v\n", err)
 }

サンプルのコードは、タイムアウトの検証をしようと思った途中のところだったので、net.DialTimeout()を使っていますが、net.Dial()で接続開始します。

net.Dial()の戻り値もnet.Connなので、サーバ側と同様の通信処理を実装すれば通信可能です。

本番の時に、「UDPだけは、うまく動作しない」状態だったのですが、私がUDPのプログラムを作ったことがなかっただけのようで、本番時に送受信ができるようになりました。
ただ、TCP/Unixsocketとは違って、コツがいるのでメモを残しておきます。

UDPのListenは、udpConn, err := net.ListenUDP("udp", udpAddr)を使います。
関数が違いますので注意して下さい。

func receiveGoroutine(conn *net.UDPConn, ch chan<- int) {
 var count uint32 = 1

 for i := 0; i < 2; i++ {
  var data [1024]byte

  _, addr, err := conn.ReadFrom(data[:])
  if err != nil {
   log.Printf("Recv: %v", err)
  }

  buf := bytes.NewBuffer(data[:])
  err = binary.Read(buf, binary.BigEndian, &count)
  if err != nil {
   log.Printf("Buffer: %v\n", err)
   break
  }
  log.Printf("Receive From Client %d\n", count)

  count++

  log.Printf("Send To Client %d\n", count)
  bufW := new(bytes.Buffer)
  err = binary.Write(bufW, binary.LittleEndian, count)
  if err != nil {
   log.Printf("Buffer: %v\n", err)
  }
  _, err = conn.WriteTo(bufW.Bytes(), addr)
  if err != nil {
   log.Printf("Send: %v\n", err)
   break
  }

 }

 if err := conn.Close(); err != nil {
  log.Printf("Close: %v\n", err)
 }

 ch <- 1
}

受信の処理は、ReadFrom()、送信の処理はWriteTo()を呼び出すようにしなければ、クライアントとキャッチボールができません。(これに気がつくのに時間がかかってしまいました)
また、TCPのサンプルは「わざと」接続後にサーバ側から送信させるようにしましたが、UDPだと、そのシーケンスは不可能(?)ということになります。理由は、ReadFrom()で受け取った、Addrインターフェイスの情報をWriteTo()の引数に設定しなければいけないので、最初は必ず受信ということになってしまいます。
(そういうものなの?って感じはするが…。)

次回は、メール関連(net/mail、net.smtpパッケージ)の予定です。