GoでTCP サーバ/クライアントを動かしてみた

Goを使って、TCP通信でサーバー⇄クライアント間でTCP通信を行なっていきたいと思います。 Goにには、標準でnetパッケージというものが用意されているので今回はこちらを使用します。

TCPとは

TCPとは、信頼性の高い通信を実現するために使用されるプロトコルになります。IPと同様にインターネットにおいて標準的に利用されています。TCPは、IPの上位プロトコルトランスポート層で動作するプロトコルで、ネットワーク層のIPとセッション層以上のプロトコルの橋渡しをする形で動作しています。

TCPソケット

TCP/IPアプリケーションを作成するための抽象化されたインターフェースのことです。 互いのソケットをネットワークを通して接続し、通信をすることが可能となります。

サーバー側の実装

package main

import (
    "io"
    "log"
    "net"
    "time"
)

func receiveTCPConn(ln *net.TCPListener) {
    for {
        err := ln.SetDeadline(time.Now().Add(time.Second * 10))
        if err != nil {
            log.Fatal(err)
        }
        conn, err := ln.AcceptTCP()
        if err != nil {
            log.Fatal(err)
        }
        go echoHandler(conn)
    }
}

func echoHandler(conn *net.TCPConn) {
    defer conn.Close()
    for {
        _, err := io.WriteString(conn, "Socket Connection!!\n")
        if err != nil {
            return
        }
        time.Sleep(time.Second)
    }
}

func main() {
    tcpAddr, err := net.ResolveTCPAddr("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }

    ln, err := net.ListenTCP("tcp", tcpAddr)
    if err != nil {
        log.Fatal(err)
    }
    receiveTCPConn(ln)
}

net.ResolveTCPAddr()メソッドを使用し、ソケットの作成とアドレスとポートのバインドを行いますnet.ListenTCP()メソッドで、リッスン。 AcceptTCP()メソッドで、クライアントからの接続を待ち受けます。 作成したechoHandlerに接続情報を渡します。リクエストを受けたら、Socket Connection!!を返しますようにしてます。ゴルーチンを使用してるのは、1つのリクエストの処理が完了するまでブロックされるので、別スレッドで実行し複数リクエストに対応するためです。

クライアント側の実装

クライアント側の実装を行なっていきます。

package main

import (
    "io"
    "log"
    "net"
    "os"
)

func response(dst io.Writer, src io.Reader) {
    if _, err := io.Copy(dst, src); err != nil {
        log.Fatal(err)
    }
}

func main() {
    conn, err := net.Dial("tcp", ":8080")
    if err != nil {
        log.Fatal(err)
    }
    response(os.Stdout, conn)
}

net.Dial()メソッドを使用し、localhost:8080に接続します。 responseメソッドで、標準出力に、connをコピーして出力します。

$ go run server/server.go
$ go run client/client.go
Socket Connection!!
Socket Connection!!
Socket Connection!!
Socket Connection!!

通信できてますね。 ネットワークわからないって方は、まずはマスタリングTCP /IP入門編を読んでみてはいかがでしょうか。 かなりわかりやすく解説されているので、最初に勉強するにはうってつけかと思います。

Goで静的ファイルを読み込む方法

前回、フレームワークを使わずにGolangの機能のみで、簡易的なアプリケーションを作成しました。 今回は続きで、CSSやjsを適用したいので、CSSJavaScriptなどの静的ファイルを読み込めるようにしたいと思います。

以下、ディレクトリ、ファイルを追加します。

$ mkdir static
$ touch static/style.css
$ touch static/main.js

htmlで、CSSJavaScriptを読み込みます。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!--css読み込み-->
    <link rel="stylesheet" href="/static/style.css">
</head>
<body>
<h1>読んだ書籍</h1>
<form action="/view/create" method="post">
    <div><input type="text" name="value"></div>
    <div><input type="submit" value="追加"></div>
</form>
<div>
    {{ range .Books }}
    <p>{{.}}</p>
    {{ end }}
</div>
<!--js読み込み-->
<script src="/static/main.js"></script>
</body>
</html>

CSSとjsファイルを追加します。CSSは、h1タグの文字をピンクにする。jsは、バックグランドカラーを白から徐々に青に変わるように記述しています。

h1 {
    color: deeppink;
}
document.body.animate(
    {
        background: ["#FFFFFF", "#3399CC"],
    },
    {
        fill: "forwards",
        duration: 3000
    }
);

作成した、CSSとjsを読み込めるように、サーバー側にもコードを追加していきます。

func main() {
    http.HandleFunc("/view", viewHandler)
    http.HandleFunc("/view/create", createHandler)
    // 追加
    http.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir("./static/"))))
    fmt.Println("Server Start Up........")
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

ここで大事なのが、test1/test2/statictest1/staticという2つのリクエストどちらも対応させる必要があります。 http.StripPrefixメソッドを使って対応させることが可能となります。 http.FileServer(http.Dir("path"),pathにあるディレクトリをハンドラーとして返す。 http.StripPrefix("path", handler),pathより前を取り除いたpathをhandlerに渡す http.Handle("path",handler),pathにリクエストが来たらhandlerを返す

http.StripPrefixメソッドで、あるpathから前を除外する。localhost:8080/test1/test2/staticlocalhost:8080/test1/staticも同じリクエストとして扱ってくれます。

サーバーを起動します。

 $ go run server.go
Server Start Up........

Webサーバーが立ち上がったのでlocalhost:8080/viewにアクセスします。 しっかり、反映されていますね!(jsの徐々に色が変わる部分は、画像ではわからないですね‥、バックグラウンドが最初は白でした笑)

GoでWebサーバーを構築

Goで、フレームワークを使わなくてもサーバーを構築してアプリ開発することができます。 Goの機能のみを使って、簡易的なアプリを作ってみたいと思います。

プロジェクト作成

$ mkdir list-app
$ touch server.go

net/httpパッケージ

HTTPを扱うパッケージで、HTTPクライアントとHTTPサーバーを実装するために必要な機能が提供されています。HTTPサーバー用の機能を使用することで、簡単にWebサーバーを立てることができます。

・http.HandleFunc

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) 指定したパターンとハンドラー関数をDefaultServeMuxに登録します。

・http.ListenAndServe

func ListenAndServe(addr string, handler Handler) error TCPネットワークアドレスでリッスン(第一引数)、ハンドラは通常はnil(第二引数)で、その場合はDefaultServeMuxが使用されます。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    hello := []byte("Hello World!!!")
    _, err := w.Write(hello)
    if err != nil {
        log.Fatal(err)
    }
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    fmt.Println("Server Start Up........")
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
$ go run server.go
Server Start Up........

Webサーバーが立ち上がったのでlocalhost:8080/helloにアクセスします。

リストアプリの作成

好きな言語を追加していくだけの、簡単なアプリケーションを作成したいと思います。 HTMLファイルを返すことができるようにします。

$ touch view.html

view.htmlにコードを記述します。

<h1>読んだ書籍</h1>
<div>
    <p>・スッキリわかるSQL入門</p>
    <P>・達人に学ぶSQL徹底指南書</P>
    <P>・達人に学ぶDB設計 徹底指南書</P>
</div>

view.htmlをサーバーのレスポンスとして返すようにします。 viewHandlerを作成し、template.ParseFilesで引数に渡したhtmlをパースします。 Executeメソッドを使用。第一引数に出力先、第二引数にテンプレートに埋め込みたいデータを渡します。 今回は、渡すデータがないのでnilとします。

package main

import (
    "fmt"
    "html/template"
    "log"
    "net/http"
)

func viewHandler(w http.ResponseWriter, r *http.Request) {
    html, err := template.ParseFiles("view.html")
    if err != nil {
        log.Fatal(err)
    }
    if err := html.Execute(w, nil); err != nil {
        log.Fatal(err)
    }
}

func main() {
    http.HandleFunc("/view", viewHandler)
    fmt.Println("Server Start Up........")
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

サーバーを起動します。

 $ go run server.go
Server Start Up........

Webサーバーが立ち上がったのでlocalhost:8080/viewにアクセスします。

データをテンプレートに埋め込む

読書した内容を記載したファイルを作成し、その中身をテンプレートに埋め込むように、コードを追加していきたいと思います。

$ touch reading.txt

ファイルの中身を読み取る

reading.txtの中身を読み取る関数を作成し、読書内容が記載されたファイルを読み取りたいと思います。

func fileRead(fileName string) []string {
    var bookList []string
    file, err := os.Open(fileName)
    if os.IsNotExist(err) {
        return nil
    }
    defer file.Close()
    scaner := bufio.NewScanner(file)
    for scaner.Scan() {
        bookList = append(bookList, scaner.Text())
    }
    return bookList
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
    bookList := fileRead("reading.txt")
    fmt.Println(bookList)
    html, err := template.ParseFiles("view.html")
    if err != nil {
        log.Fatal(err)
    }
    if err := html.Execute(w, nil); err != nil {
        log.Fatal(err)
    }
}

ファイルの中身を保持できるようにstructを追加します。

type BookList struct {
    Books []string
}

func New(books []string) *BookList {
    return &BookList{Books: books}
}

viewHandlerを変更します。

func viewHandler(w http.ResponseWriter, r *http.Request) {
    bookList := fileRead("reading.txt")
    html, err := template.ParseFiles("view.html")
    if err != nil {
        log.Fatal(err)
    }
    getBooks := New(bookList)
    if err := html.Execute(w, getBooks); err != nil {
        log.Fatal(err)
    }
}

reading.txtを出力できるようになったので、これをhtmlで表示できるように変更します。

<h1>読んだ書籍</h1>
<div>
    {{ range .Books }}
    <p>{{.}}</p>
    {{ end }}
</div>

サーバーを起動します。

 $ go run server.go
Server Start Up........

Webサーバーが立ち上がったのでlocalhost:8080/viewにアクセスします。 違いが分かるよう、reading.txtに読んだ本を追加してます。 しっかり表示できてますね。

フォームを追加

読んだ本が増えるたびに追加していきたいので、フォームを作成して、フォームからのデータをreading.txtに書き込むように変更したい思います。 view.htmlにフォームを追加します。

<h1>読んだ書籍</h1>

<form action="/view/create" method="post">
    <div><input type="text" name="value"></div>
    <div><input type="submit" value="追加"></div>
</form>
<div>
    {{ range .Books }}
    <p>{{.}}</p>
    {{ end }}
</div>
package main

import (
    "bufio"
    "fmt"
    "html/template"
    "log"
    "net/http"
    "os"
)

type BookList struct {
    Books []string
}

func New(books []string) *BookList {
    return &BookList{Books: books}
}

func fileRead(fileName string) []string {
    var bookList []string
    file, err := os.Open(fileName)
    if os.IsNotExist(err) {
        return nil
    }
    defer file.Close()
    scaner := bufio.NewScanner(file)
    for scaner.Scan() {
        bookList = append(bookList, scaner.Text())
    }
    return bookList
}

func viewHandler(w http.ResponseWriter, r *http.Request) {
    bookList := fileRead("reading.txt")
    html, err := template.ParseFiles("view.html")
    if err != nil {
        log.Fatal(err)
    }
    getBooks := New(bookList)
    if err := html.Execute(w, getBooks); err != nil {
        log.Fatal(err)
    }
}

func createHandler(w http.ResponseWriter, r *http.Request) {
    formValue := r.FormValue("value")
    file, err := os.OpenFile("reading.txt", os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.FileMode(0600))
    defer file.Close()
    if err != nil {
        log.Fatal(err)
    }
    _, err = fmt.Fprintln(file, formValue)
    if err != nil {
        log.Fatal(err)
    }
    http.Redirect(w, r, "/view", http.StatusFound)
}

func main() {
    http.HandleFunc("/view", viewHandler)
    http.HandleFunc("/view/create", createHandler)
    fmt.Println("Server Start Up........")
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

サーバーを起動します。

 $ go run server.go
Server Start Up........

Webサーバーが立ち上がったのでlocalhost:8080/viewにアクセスします。 フォームから読んだ本を追加してみます。

追加できてますね。 このようにして、Goではフレームワークを使わなくてアプリケーション開発行える機能がたくさん備わってて便利ですね。

GitHub運用

私の勤めている会社では、GitHub運用にgit-flow。Issueドリブン開発を採用しています。 gitは開発する上で、とても重要な概念なので自分自身の理解と整理のために、git-flow、Issueドリブン、ブランチの命名規則などについてまとめてみました。

git-flow

正確にいうと「A successful Git branching model」をサポートするためのツールの名称らしいです。

git-flowでは、下記の5つのブランチを利用します。

masterブランチ

リリースしたソースコードを管理するためのブランチ。

developブランチ

開発を行うためのブランチ。開発者は、主にこのブランチ上で作業を行う。 featureブランチなど、他のブランチで行った作業は、ここにマージされる。

featureブランチ

主要な機能を実装するためのブランチ。機能の実装やバグフィックスなど、タスクごとにdevelopブランチより featureブランチを作成し、作業を行う。

releaseブランチ

リリースの準備を行うためのブランチ。プロダクトをリリースする前に、このブランチをdevelopブランチより作成し、微調整を行う。 releaseブランチで作業した内容を、同時にdevelopブランチにもマージし、最新の開発版を反映させる。 releaseブランチを作成することで、リリース準備と次のバージョンに向けた開発のコードを分けることができる。

hotfixesブランチ

リリースされたソフトウェアに緊急の修正を行うためのブランチ。

  • master: 本番、releaseからマージ
  • develop: 開発環境
  • release: ステージング環境
  • feature: 各機能開発環境 developからブランチを切る
  • hotfixes: バグ対応 

Issueドリブン開発・命名規則

Issueドリブン開発とは、実装における課題をタスクとして管理・運用する開発手法になります。 新規機能の追加やバグ修正などをIssueとして立てて、それを解決するためのブランチを切り、 コミットやPRでIssueを参照しながら開発を進めるという流れになります。

Issueドリブン開発の流れ

1. GitHub上で、課題(タスク)のIssueを立てる。

2. Issue番号が発行されるので、Issue番号を付与したブランチを作成する。

例)feature/#1-add-login

3. Issueの課題(タスク)に沿った、開発を行う。

4. 開発が完了したら、developブランチにコミットする。PRのコメントにclose #1と記述すると、マージ後にIssueがクローズされます。

コミットメッセージ

例)[fix]refs #1 add-login

  • fix:バグ修正

  • add:新規(ファイル)機能追加

  • update:機能修正(バグではない)

  • clean:整理(リファクタリング等)

  • remove:削除(ファイル)