読者です 読者をやめる 読者になる 読者になる

初老のボケ防止日記

おっさんのひとりごとだから気にしないようにな。

スポンサーリンク

いまからのGo(3)

Go言語


歩きながらGo言語は命を落とすのでやめましょう。


本を読んでも実際に何か作ってみないと全く理解が進まないのでサンプルを組みながら理解していくコーナー其の3。

前回迄のあらすじ

osa030.hatenablog.com

前回はGoルーチンとチャネルを使ってみて、ちょっとだけGoっぽくなったが、

まだ、構造体すら使ってないわ。

ということで、パッケージ化も含めてやってTRY!

環境

前回通り。

OS Windwos 10 Pro
Go 1.6.2
IntelliJ IDEA 2016.2
Goプラグイン 0.12.1724
GB 0.4.x

構成

今迄の通り、IDEAのプロジェクトフォルダを「pachimongo」とする。

$ cd pachimongo/
$ tree -d
.
`-- src
    |-- cmd
    |   |-- client
    |   `-- server
    `-- common
        `-- net
            `-- echo

IDEAで補完を有効化するために「pachimongo/src」を「Go Libraries」に追加するのを忘れずに。

手順は以下参照

osa030.hatenablog.com

「common/net/echo」パッケージ

今まで各main.goに記載していたビッグECHOサーバとECHOクライアントの処理を配置する。

共通処理

再構築にあたって、ビッグECHOサーバとECHOクライアントで共通化できる処理をまとめる。

  • common.go
package echo

import (
	"bufio"
	"errors"
	"net"
	"time"
)

const (
	ReadTimeOut      = time.Millisecond * 100
	MessageDelimiter = '\n'
)

func readMessage(conn net.Conn) (string, error) {
	if _, ok := conn.(net.Conn); !ok {
		return "", errors.New("not net.Conn")
	}

	data, err := bufio.NewReader(conn).ReadString(MessageDelimiter)
	if err != nil {
		return "", err
	}
	runes := []rune(data)
	msg := string(runes[:len(runes)-1])
	return msg, nil
}

func writeMessage(conn net.Conn, msg string) error {
	if _, ok := conn.(net.Conn); !ok {
		return errors.New("not net.Conn")
	}

	if len(msg) == 0 {
		return errors.New("msg blank")
	}

	data := string(append([]rune(msg), MessageDelimiter))
	_, err := conn.Write([]byte(data))
	return err
}

ECHOプロトコルの送受信処理をまとめる。といっても、文字列に改行コード「'\n'」を終端文字として付与して送受信するだけのプロトコルなので処理としてはそれだけ。今まではコード内に「"\n"」と「'\n'」が混在していたのだけれどもGo言語では文字列(string)と文字(rune)が明確に分かれているので定数をruneだけで済むように処理を変えている。
文字列文末の終端文字削除は以下を参考にした。

github.com

ECHOクライアント

さて、いよいよクライアントの処理を再構築。前回の処理でECHOクライアントのGoルーチン側と複数のチャネルを使用してデータのやり取りをしていたのだが、面倒なので構造体化しておく。

  • EchoClient構造体
type EchoClient struct {
	conn     net.Conn
	close    chan struct{}
	closed   chan struct{}
	request  chan string
	response chan string
}

前回と比較して「close」というチャネルが増えているが、これは安全にECHOクライアントを終了させる為に追加したもの。前回はmain()を抜ける時にnet.Conn.Close()を呼んでいたのだが、Goルーチン側でアクセスしているのでできればそちらで切断させるのが筋ではないかと考え、切断指示用に追加した。

それとGo言語は型名、関数名、構造体のメンバ名の先頭を大文字にするか否かで公開範囲を指定できる。大文字の場合はパッケージ外からも参照できる所謂パブリックなもの、大文字以外の場合はパッケージ内のみ参照可能なプライベートなものとなる。Go言語の仕様上はクラスは存在しないのだが、構造体のメンバ名をプライベートにすることで、クラスのように隠蔽化を行うことができる。ということで、EchoClient構造体のメンバはプライベートにしてみた。

  • コンストラクタ
func newEchoClient(conn net.Conn) *EchoClient {
	return &EchoClient{
		conn:     conn,
		close:    make(chan struct{}, 1),
		closed:   make(chan struct{}, 1),
		request:  make(chan string, 1),
		response: make(chan string, 1),
	}
}

Go言語にはクラスは存在しないのにコンストラクタ的な関数が用意されていることが多い。EchoClient構造体のようにメンバがプライベートの場合は必須となる。今回はパッケージ外からコンストラクタを呼ぶことは許容しないのでプライベートコンストラクタとしている。

  • 接続処理
func Connect(address string) (*EchoClient, error) {

	conn, err := net.Dial("tcp", address)
	if err != nil {
		return nil, err
	}

	ec := newEchoClient(conn)
	go ec.handle()

	return ec, nil
}

パッケージ外からは本関数をコールしてECHOサーバと接続後のEchoClient構造体を取得する。

  • クライアントメインルーチン
func (ec *EchoClient) handle() {
	defer func() {
		log.Println("disconnect from:", ec.conn.RemoteAddr())
		ec.conn.Close()
		ec.conn = nil
		ec.closed <- struct{}{}
	}()

MESSAGE_LOOP:
	for {
		select {
		case req := <-ec.request:
			err := writeMessage(ec.conn, req)
			if err != nil {
				log.Println("ERROR:", err)
				break MESSAGE_LOOP
			}
		case <-ec.close:
			break MESSAGE_LOOP
		default:
			ec.conn.SetReadDeadline(time.Now().Add(ReadTimeOut))
			resp, err := readMessage(ec.conn)
			if err != nil {
				if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
					continue
				}
				log.Println("ERROR:", err)
				break MESSAGE_LOOP
			}
			ec.response <- resp
		}
	}
}

Connet()でECHOサーバとのTCPコネクション接続が成功すると、呼び元にEchoClient構造体を返却する前にGoルーチンとして起動される。ECHOサーバとのTCPセッションに係る処理は全てこのルーチン内で行うようにしているのが今回のポイントである。

  • メッセージ送信
func (ec *EchoClient) Send(msg string) error {
	if len(msg) == 0 {
		return errors.New("msg is blank")
	}

	ec.request <- msg
	return nil
}

引数に指定された文字列をチャネルを経由してクライアントメインルーチンに送る。チャネルから送信文字列を受信したクライアントメインルーチンはECHOサーバに対して送信する。

  • メッセージ受信
func (ec *EchoClient) OnResponse() chan string {
	return ec.response
}

チャネルを経由してクライアントメインルーチンがECHOサーバから受信した応答文字列を受信する。

  • TCPコネクション切断検知
func (ec *EchoClient) OnClosed() chan struct{} {
	return ec.closed
}

チャネルを経由してクライアントメインルーチンが検出したECHOサーバとのTCPコネクション切断を受信する。

  • TCPコネクションクローズ
func (ec *EchoClient) Close() {
	if ec.conn != nil {
		ec.close <- struct{}{}
		<-ec.closed
	}
}

チャネルを経由してクライアントメインルーチンへ切断を指示する。チャネルから切断指示を受けたクライアントメインルーチンは、Goルーチンを終了してECHOサーバとのTCPコネクションを切断する。Close()呼び出し側はクライアントメインルーチンが切断するのを待ちたいのでチャネル受信で処理を待機(ブロック)する。

  • client.go

完全版のソースはコチラ。

package echo

import (
	"errors"
	"log"
	"net"
	"time"
)

type EchoClient struct {
	conn     net.Conn
	close    chan struct{}
	closed   chan struct{}
	request  chan string
	response chan string
}

func newEchoClient(conn net.Conn) *EchoClient {
	return &EchoClient{
		conn:     conn,
		close:    make(chan struct{}, 1),
		closed:   make(chan struct{}, 1),
		request:  make(chan string, 1),
		response: make(chan string, 1),
	}
}

func (ec *EchoClient) handle() {
	defer func() {
		log.Println("disconnect from:", ec.conn.RemoteAddr())
		ec.conn.Close()
		ec.conn = nil
		ec.closed <- struct{}{}
	}()

MESSAGE_LOOP:
	for {
		select {
		case req := <-ec.request:
			err := writeMessage(ec.conn, req)
			if err != nil {
				log.Println("ERROR:", err)
				break MESSAGE_LOOP
			}
		case <-ec.close:
			break MESSAGE_LOOP
		default:
			ec.conn.SetReadDeadline(time.Now().Add(ReadTimeOut))
			resp, err := readMessage(ec.conn)
			if err != nil {
				if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
					continue
				}
				log.Println("ERROR:", err)
				break MESSAGE_LOOP
			}
			ec.response <- resp
		}
	}
}

func Connect(address string) (*EchoClient, error) {

	conn, err := net.Dial("tcp", address)
	if err != nil {
		return nil, err
	}

	ec := newEchoClient(conn)
	go ec.handle()

	return ec, nil
}

func (ec *EchoClient) Send(msg string) error {
	if len(msg) == 0 {
		return errors.New("msg is blank")
	}

	ec.request <- msg
	return nil
}

func (ec *EchoClient) Close() {
	if ec.conn != nil {
		ec.close <- struct{}{}
		<-ec.closed
	}
}

func (ec *EchoClient) OnResponse() chan string {
	return ec.response
}

func (ec *EchoClient) OnClosed() chan struct{} {
	return ec.closed
}

ECHOクライアント(main)

「common/net/echo」のEchoClientを用いたECHOクライアント。

package main

import (
	"bufio"
	"common/net/echo"
	"log"
	"os"
	"os/signal"
	"strings"
)

const EOL = '\r' // for WINDOWS

func getInput(input chan string) {

	for {
		msg, err := bufio.NewReader(os.Stdin).ReadString(EOL)
		if err != nil {
			log.Println("getInput ERROR:", err)
			break
		}

		input <- strings.Trim(msg, string(EOL))
	}
}

func main() {
	service := "127.0.0.1:5555"

	log.Println("start client", service)
	defer log.Println("stop client")

	echoClient, err := echo.Connect(service)
	if err != nil {
		log.Fatal(err)
	}
	defer echoClient.Close()

	sig := make(chan os.Signal, 1)
	signal.Notify(sig, os.Interrupt)

	input := make(chan string, 1)
	go getInput(input)

MESSAGE_LOOP:
	for {
		select {
		case <-sig:
			break MESSAGE_LOOP
		case i := <-input:
			err := echoClient.Send(i)
			if err != nil {
				log.Println("ERROR:", err)
				break MESSAGE_LOOP
			}
			log.Printf("SEND[%s]\n", i)
		case o := <-echoClient.OnResponse():
			log.Printf("RECV[%s]\n", o)
		case <-echoClient.OnClosed():
			break MESSAGE_LOOP
		}
	}
}

標準入力からの入力処理を除いてすべての処理はEchoClient側に含めている。このようにしておけば、送信文字列をユーザ入力以外(例えばファイル等)に切り替えることも容易に行えるし、一つのクライアントプログラムから複数のECHOサーバに接続するということもやりやすいのではないだろうか。
Go言語は決してオブジェクト指向ではないのだが、Goルーチン含めて上手くまとめることでオブジェクト指向っぽい何かを書きやすい。
疲れたのでサーバ側は次回に続く。

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)

スターティングGo言語

スターティングGo言語

プログラミング言語Goフレーズブック

プログラミング言語Goフレーズブック

  • 作者: David Chisnall,デイビッド・チズナール,柴田芳樹
  • 出版社/メーカー: ピアソン桐原
  • 発売日: 2012/10/04
  • メディア: 単行本(ソフトカバー)
  • 購入: 1人 クリック: 5回
  • この商品を含むブログを見る

スポンサーリンク