今回は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 件のコメント:
コメントを投稿