口上
そういえばソケット通信とかの、ネットワークプログラミングの基本を全然理解していなかったので、入門中のgoでとりあえず書いてみた。
ググってもいまいち書きたい仕様にマッチするものがなかったので、試行錯誤でできた内容を、備忘録兼ねて記載。
サーバー
まずはサーバー側。
package main
import (
"fmt"
"net"
utilsErr "sample/utils/error"
utilsNet "sample/utils/net"
"time"
)
func main() {
// 意味はないが雰囲気を出すため、敢て自分のIPを表示
myIP, _ := utilsNet.GetMyLocalIP()
fmt.Println("my ip address is now ...", myIP)
// 50030ポート(適当)でメッセージを受け取る
port := ":50030"
protocol := "tcp"
tcpAddr, err := net.ResolveTCPAddr(protocol, port)
utilsErr.CheckWithExit(err)
listner, err := net.ListenTCP(protocol, tcpAddr)
utilsErr.CheckWithExit(err)
fmt.Println("waiting for the connection ...")
for {
conn, err := listner.Accept()
if err != nil {
continue
} else {
fmt.Println("connected by .. ", conn.RemoteAddr().String())
}
// ゴルーチンほんと便利
go handleClient(conn)
}
}
// 送信されたバイトからメッセージを取り出して表示する
func handleClient(conn net.Conn) {
defer conn.Close()
conn.SetReadDeadline(time.Now().Add(10 * time.Second))
messageBuf := make([]byte, 1024)
messageLen, err := conn.Read(messageBuf)
utilsErr.CheckWithExit(err)
message := string(messageBuf[:messageLen])
fmt.Println(message)
// この書き込み処理がないと、連続してclientからメッセージを送信すると以下のエラーが発生する
// fatal: error: dial tcp 192.168.3.22:50031->192.168.3.22:50030: bind: address already in useexit status 1
// 理由は理解していない・・
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
conn.Write([]byte(message))
}
最後のところ、いったんnet.Connから読み出したメッセージを、再度Connに書き込まないと、クライアントから2回め以降に接続した際にコメントのエラーが発生する。
ちゃんと調べれてないが、書き込むことで接続が一旦クリアされている?
クライアント
次、クライアント側。
package main
import (
"fmt"
"net"
"os"
utilsErr "sample/utils/error"
utilsNet "sample/utils/net"
"time"
)
func main() {
// メッセージ用の引数を一つ受け取る仕様
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s message", os.Args[0])
os.Exit(1)
}
message := os.Args[1]
// 今回はサーバー/クライアント共にローカルマシン
// ポートは適当に
serverIP, _ := utilsNet.GetMyLocalIP()
serverPort := "50030"
clientIP, _ := utilsNet.GetMyLocalIP()
clientPort := 50031
// tcpで接続
serverAdd, err := net.ResolveTCPAddr("tcp", serverIP+":"+serverPort)
utilsErr.CheckWithExit(err)
clientAddr := new(net.TCPAddr)
clientAddr.IP = net.ParseIP(clientIP)
clientAddr.Port = clientPort
conn, err := net.DialTCP("tcp", clientAddr, serverAdd)
utilsErr.CheckWithExit(err)
defer conn.Close()
// メッセージをバイトに変換して送信
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
conn.Write([]byte(message))
}
ポイントは、
・コマンドライン引数で送信したいメッセージを渡す
・クライアント自身のIPを動的に取得する(後述のGetMyLocalIPメソッド)
・今回はローカルマシン内の動作なので、サーバーIPもローカルアドレスになる
くらいかな。
ユーティリティー達
一応載せとく。
package net
import (
"errors"
"net"
)
// ローカルIPアドレスを、文字列で取得する
//
// ・すべてのネットワークインターフェースを走査しする
// ・ループバックアドレスは無視する
// 出展:https://codeday.me/jp/qa/20190120/147217.html
func GetMyLocalIP() (string, error) {
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
addrs, err := iface.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
var ip net.IP
switch v := addr.(type) {
case *net.IPNet:
ip = v.IP
case *net.IPAddr:
ip = v.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
ip = ip.To4()
if ip == nil {
continue // not an ipv4 address
}
return ip.String(), nil
}
}
return "", errors.New("are you connected to the network?")
}
package error
import (
"fmt"
"os"
)
func CheckWithExit(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "fatal: error: %s", err.Error())
os.Exit(1)
}
}
使い方
# サーバー起動
$ go run server.go
# クライアントからメッセージを送る
$ go run client/client.go "こんにちは!"
$ go run client/client.go "今日はいい天気ですね"
$ go run client/client.go "さようなら〜"
結果
出展
https://golang.org/pkg/net/
http://kudohamu.hatenablog.com/entry/2014/11/03/071802
https://codeday.me/jp/qa/20190120/147217.html
※サンプルの仕様は、こちらの本に出てくるソケット通信のサンプル(Python)を元にした。
次は、この知識を元に、goでもう少し複雑なP2Pプログラムを書いてみる予定。