こんばんは、七色メガネです。
最近は参考本を読みながら、チャットアプリの開発に勤しんでいました。
しかしなんとか完成したものの、まだまだ写経止まりで完全に理解できていないかなあというのが正直なところ。
ということで今回は一度アプリを機能ごとにバラして、もう一回作ってみようと思いました。
全4回で、次のテーマごとに記事を書いていこうと思います。
- websocketを使ったチャット機能の実装
- gomniouthを使った認証機能の実装
- プロバイダ、あるいはローカルからの画像取得機能の実装
- チャットアプリの開発
1〜3は別々のパッケージで作り直そうと思っているので、最後の4で整合性が取れるかは謎です…。
なお、今回参考にした書籍は次のものです。
「Go言語によるWebアプリケーション開発」
参考にしましたが、今回のチャット機能は書籍の内容から少し異なるところが多々ありますので悪しからず。
チャット機能の仕様
今回実装するチャット機能は、次の仕様に基づいて開発するものとします。
- localhost:8080 にアクセスすることでチャットルームに移動すること。
- チャットルームとクライアントのモデルを作成すること。
- チャットルーム・モデルでは次のことを管理すること。
1. クライアントの入室
2. クライアントの退室
3. クライアントの情報
4. ルーム内のメッセージ - クライアント・モデルでは次のことを管理すること。
1. websocketへのコネクション
2. メッセージ
3. 所属するチャットルームの情報 - チャットルームでは、名前とメッセージの2種類を入力するものとすること。
- 送信したメッセージは、チャットルームに参加しているクライアントのブラウザに即時反映されること。
- サーバからのpushの実装として、websocketを使用すること。
機能のイメージ
今回のチャット機能の処理イメージを図に起こしてみました。手書きですまない…。
まずはwebサーバを起動させます。サーバの起動と同時に、チャットルーム・モデルを生成し、localhostのルートと/roomに対してハンドラを張ります。
次にブラウザからlocalhostのルートへアクセスすると、サーバからchat.htmlが返却されチャットページが表示されます。この時html内のscriptにより、/room との間でwebsocket 通信が開始されます。
websocket通信の開始通信をサーバが受けると、相手をクライアントとしてチャットルームに保存し、サーバ側のチャット準備が全て整います。
この状態でブラウザからメッセージを送信すると、開かれたsocketへとデータが送られ、それがサーバに届き、サーバが登録されたクライアント全員へメッセージを送信し、ブラウザに届き、チャットページに表示される、という仕組みです。
実装
今回は作成するのは次のファイルです。
- main.go
チャットルームの作成と起動、サーバの起動。 - chatroom.go
チャットルームの定義。クライアント管理とメッセージ管理。 - client.go
クライアントの定義。メッセージの送信と受信処理。 - handler.go
ハンドラの実装。 - message.go
メッセージの仕様を定義。 - templates/chat.html
チャットページのHTMLファイル。
githubにソースを上げておきます。
https://github.com/NanairoMegane/chatFunc
main,go
- localhostの8080ポートでリクエストをlistenします。
- 起動時にチャットルームを生成し、起動させます。
- “/” と “/room” に対してハンドラを張ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package main import ( "log" "net/http" ) func main() { /* ルートへのアクセスに対してハンドラを貼り、chat.htmlをサーブする */ http.Handle("/", &templateHandler{filename: "/chat.html"}) /* チャットルームを作成する */ chatroom := newRoom() /* チャットルームへのハンドラを貼る。 /room へは、chat.html から遷移する。chatroomに実装されたServeHTTPは websocketの実装とclientの生成を行う。 */ http.Handle("/room", chatroom) /* チャットルームを起動する */ go chatroom.run() /* webサーバを開始する */ log.Println("webサーバを開始します。") if err := http.ListenAndServe(":8080", nil); err != nil { log.Fatalln("webサーバの起動に失敗しました。:", err) } } |
chatroom.go
- チャットルームを表す chatroom 構造体を持ちます。
- チャットルームを生成する newRoom() を持ちます。
- ブラウザからアクセスがあった時(html内のscriptによる /room へのアクセス)に起動する ServeHTTP() が定義されます。ここでクライアントをchatroom構造体に保存します。
- チャットルームを起動させる run() を持ちます。ここでは無限ループで chatroom 構造体のチャネルを監視し、クライアントの入室・退室・メッセージの到達に対しての処理を行います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
package main import ( "fmt" "log" "net/http" "time" "github.com/gorilla/websocket" ) const ( socketBufferSize = 1024 messageBufferSize = 256 ) /* websocket用の変数 */ var upgrader = &websocket.Upgrader{ ReadBufferSize: socketBufferSize, WriteBufferSize: socketBufferSize, } /* チャットルーム・モデル */ type chatroom struct { forward chan *message join chan *client leave chan *client clients map[*client]bool } /* chatroomをhttp.handleに適合させる。 ここでは以下のことを実装する。 ・websocketの開設 ・clientの生成 */ func (c *chatroom) ServeHTTP(w http.ResponseWriter, r *http.Request) { /* websocketの開設 */ socket, err := upgrader.Upgrade(w, r, nil) if err != nil { log.Fatalln("websocketの開設に失敗しました。:", err) } /* クライアントの生成 */ client := &client{ socket: socket, send: make(chan *message, messageBufferSize), room: c, } // チャットルームのjoinチャネルにアクセスし、クライアントを入室させる。最後には必ず退室させる。 c.join <- client defer func() { c.leave <- client }() go client.write() client.read() } /* チャットルームを生成する */ func newRoom() *chatroom { t := time.Now() layout := "2006-01-02 15:04:05" fmt.Println("chatroom が生成されました。:", t.Format(layout)) return &chatroom{ forward: make(chan *message), join: make(chan *client), leave: make(chan *client), clients: make(map[*client]bool), } } /* チャットルームを起動する */ func (c *chatroom) run() { // チャットルームは無限ルームで起動させる for { // チャネルの動きを監視し、処理を決定する select { /* joinチャネルに動きがあった場合(クライアントの入室) */ case client := <-c.join: // クライアントmapのbool値を真にする c.clients[client] = true fmt.Printf("クライアントが入室しました。現在 %x 人のクライアントが存在します。\n", len(c.clients)) /* leaveチャネルに動きがあった場合(クライアントの退室) */ case client := <-c.leave: // クライアントmapから対象クライアントを削除する delete(c.clients, client) fmt.Printf("クライアントが退室しました。現在 %x 人のクライアントが存在します。\n", len(c.clients)) /* forwardチャネルに動きがあった場合(メッセージの受信) */ case msg := <-c.forward: fmt.Println("メッセージを受信しました。") // 存在するクライアント全てに対してメッセージを送信する for target := range c.clients { select { case target.send <- msg: fmt.Println("メッセージの送信に成功しました。") default: fmt.Println("メッセージの送信に失敗しました。") delete(c.clients, target) } } } } } |
client.go
- クライアントを表す client 構造体を持ちます。
- socketからメッセージを読み出す read() が定義されます。htmlからsocketにsendされたメッセージがここで読み出され、クライアントが保持する(所属する)chatroom の forward チャネルに送り出します。
- socketにメッセージを書き出す write() が定義されます。chatroom のforward チャネルにメッセージが到達した時、chatroom からこの write へメッセージが転送され、各クライアントのsocketにメッセージが書き出されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
package main import ( "time" "github.com/gorilla/websocket" ) /* クライアント・モデル */ type client struct { socket *websocket.Conn // websocketへのコネクション send chan *message // メッセージ room *chatroom // 所属するチャットルーム } /* websocketに書き出されたメッセージを読み込む。 */ func (c *client) read() { // websocketからjson形式でメッセージを読み出し、forwardチャネルに流す。 // 読み込みは無限ループで実行される。 for { var msg *message if err := c.socket.ReadJSON(&msg); err == nil { t := time.Now() layout := "2006-01-02 15:04:05" msg.Time = t.Format(layout) c.room.forward <- msg } else { break } } c.socket.Close() } /* */ func (c *client) write() { for msg := range c.send { if err := c.socket.WriteJSON(msg); err != nil { break } } c.socket.Close() } |
handler.go
- ブラウザから”/” へアクセスがあった時に chat.html をサーブするためのハンドラが定義されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package main import ( "html/template" "net/http" "path/filepath" "sync" ) /* HTMLテンプレートをサーブするためのハンドラ */ type templateHandler struct { once sync.Once //HTMLテンプレートを1度だけコンパイルするための指定 filename string //テンプレートとしてHTMLファイル名を指定 tmpl *template.Template //テンプレート } /* templateHandlerをhttp.Handleに適合させるため、ServeHttpを実装する */ func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { // テンプレートディレクトリを指定する path, err := filepath.Abs("./templates/") if err != nil { panic(err) } // 指定された名称のテンプレートファイルを一度だけコンパイルする t.once.Do( func() { t.tmpl = template.Must(template.ParseFiles(path + t.filename)) }) t.tmpl.Execute(w, nil) } |
message.go
- やりとりされるメッセージの形式を定義します。Name はユーザ名、Message はメッセージ内容でブラウザでの入力値になります。Time は処理された時刻で、サーバで処理された時間を格納します。
1 2 3 4 5 6 7 |
package main type message struct { Name string Message string Time string } |
templates/chat.html
- ブラウザが “/” へアクセスした時にサーバからserveされるHTMLファイルです。
- チャットの表示スペースと、ユーザ名とメッセージの入力欄を持ちます。
- このページが開かれた時、script により /room に対してwebsocket通信を行いコネクションを形成します。
- メッセージがsubmitされた時、socketに対してメッセージを送信します。
- サーバでクライアントに対してメッセージが送信された時socketにメッセージが到達し、socketに貼られたイベントハンドラonmessageによりそれを感知し、htmlを動的に書き換えてチャットの表示スペースにメッセージの内容を反映します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
<html> <head> <title> chatroom </title> <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css"> <style> #name{margin-bottom:10px} </style> </head> <body> <!-- チャット表示スペース --> <div> <h3>Chat Func Test</h3> </div> <div class="container panel panel-info"> <div class="panel-heading"> ここにチャット内容が表示されます。 </div> <div class="panel-body"> <ul id="messages"></ul> </div> </div> <!-- 入力スペース --> <div> <form id="chatbox"> <div> <div> <div> <text>Name</text> </div> <input type="text" name="name" size="20" id="name" class="text"> </div> <div> <div> <text>Input Message</text> </div> <textarea class="text"></textarea> </div> </div> <input type="submit" value="送信" class="btn btn-success" /> </form> </div> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"> </script> <script> $(function(){ var socket = null; var name = $("#name"); // 入力された名前 var msgbox = $("#chatbox textarea"); // 入力されたメッセージ var messages = $("#messages"); // チャット表示スペース /* 初回起動時の処理 */ // websocket未対応のブラウザであれば、チャット機能は使えない if(!window["WebSocket"]){ alert("WebSocketに対応していないブラウザです。"); return false; // websocketに対応しているブラウザの場合 } else { /* socketの開設 */ socket = new WebSocket("ws://localhost:8080/room"); // メッセージ受信時の処理を実装 socket.onmessage = function(e) { var msg = eval("("+e.data+")"); messages.append( $("<li>").append( $("<strong>").text(msg.Name + " :"), $("<p>").text("----> " + msg.Message).append( " " + "<time>(" + msg.Time + ")</time>" ) ) ); } // socket終了時の処理を実装 socket.onclose = function() { //alert("websocket通信が終了しました。"); } } /* 送信ボタン押下時の挙動 */ $("#chatbox").submit(function(){ // エラーチェック if (!msgbox.val()) { alert("入力がありません。"); return false; } if (!socket){ alert("websocketに対応していません。"); return false; } /* socketにデータを送る */ socket.send(JSON.stringify({ "Message": msgbox.val(), // 入力されたメッセージ "Name": name.val() // 入力された名前 })); // メッセージボックスはリセットする msgbox.val(""); return false; }); }); </script> </body> </html> |
チャット機能の起動
ここまでのsrcを用意したら、ビルドして実行しましょう。
localhost:8080 でリクエストをlistenし始めます。
1 2 3 |
./app chatroom が生成されました。: 2019-03-20 11:39:14 2019/03/20 11:39:14 webサーバを開始します。 |
次にブラウザからアクセスを行います。
チャットの機能テストなので2つのブラウザからアクセスを行い、お互いのブラウザでチャットが行えていることを確認します。ここでは Vivaldi と GoogleChrome の2つのブラウザでテストしてみます。
・Vivaldi サイドでページにアクセス
・Chromeサイドでページにアクセス
・この時点でのコンソール
1 2 3 4 5 |
./app chatroom が生成されました。: 2019-03-20 11:39:14 2019/03/20 11:39:14 webサーバを開始します。 クライアントが入室しました。現在 1 人のクライアントが存在します。 クライアントが入室しました。現在 2 人のクライアントが存在します。 |
・Vivaldiサイドでメッセージ送信
・Chromeサイドで到達確認。及びメッセージ送信。
・Vivaldiサイドに到達を確認。及びこの時のコンソール。
1 2 3 4 5 6 7 8 9 10 11 |
./app chatroom が生成されました。: 2019-03-20 11:39:14 2019/03/20 11:39:14 webサーバを開始します。 クライアントが入室しました。現在 1 人のクライアントが存在します。 クライアントが入室しました。現在 2 人のクライアントが存在します。 メッセージを受信しました。 メッセージの送信に成功しました。 メッセージの送信に成功しました。 メッセージを受信しました。 メッセージの送信に成功しました。 メッセージの送信に成功しました。 |
一方のクライアント(ブラウザ)からメッセージがサーバに到達した時、チャットルーム・モデルに登録されているクライアント(ブラウザ)へメッセージが送信されます。今回はクライアントが二つなので、1つのメッセに対して2回の送信が行われていることが確認できますね。
まとめ
今回はwebsocketを使ったチャット機能を実装してみました。
この実装で学べたことは主に次のようなことです。
- webサーバの立て方
- ハンドラの使い方
- websocketの利用の仕方
- htmlへのメッセージの反映の仕方
以上です。ここまでご覧いただき、ありがとうございました!
参考
今回の実装は下記の書籍を参考にしています。
が、本記事での実装とは多少異なるところがあります。(主に見た目)
「Go言語によるWebアプリケーション開発」