Help us understand the problem. What is going on with this article?

goでソケット通信

口上

そういえばソケット通信とかの、ネットワークプログラミングの基本を全然理解していなかったので、入門中のgoでとりあえず書いてみた。
ググってもいまいち書きたい仕様にマッチするものがなかったので、試行錯誤でできた内容を、備忘録兼ねて記載。

サーバー

まずはサーバー側。

server.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回め以降に接続した際にコメントのエラーが発生する。
ちゃんと調べれてないが、書き込むことで接続が一旦クリアされている?

クライアント

次、クライアント側。

client.go
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もローカルアドレスになる
くらいかな。

ユーティリティー達

一応載せとく。

ipUtil.go
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?")
}
errUtil.go
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プログラムを書いてみる予定。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away