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を適用したいので、CSSやJavaScriptなどの静的ファイルを読み込めるようにしたいと思います。
以下、ディレクトリ、ファイルを追加します。
$ mkdir static $ touch static/style.css $ touch static/main.js
htmlで、CSSとJavaScriptを読み込みます。
<!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/static
、test1/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/static
、localhost: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:削除(ファイル)