今回は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パッケージ)の予定です。
0 件のコメント:
コメントを投稿