[Go言語] revelのexamplesのchatを覗いてみる

examplesのgit: https://github.com/revel/examples

$ git clone https://github.com/revel/examples.git
$ revel run github.com/revel/examples/chat

templateのjs

  var socket = new WebSocket('ws://'+window.location.host+'/websocket/room/socket?user={{.user}}')

  // Display a message
  var display = function(event) {
    $('#thread').append(tmpl('message_tmpl', {event: event}));
    $('#thread').scrollTo('max')
  }

  // Message received on the socket
  socket.onmessage = function(event) {
    display(JSON.parse(event.data))
  }

  $('#send').click(function(e) {
    var message = $('#message').val()
    $('#message').val('')
    socket.send(JSON.stringify(message))
  });

  $('#message').keypress(function(e) {
    if(e.charCode == 13 || e.keyCode == 13) {
      $('#send').click()
      e.preventDefault()
    }
  })

route

GET     /websocket/room                         WebSocket.Room
WS      /websocket/room/socket                  WebSocket.RoomSocket

top

<form action="{{url "Application.EnterDemo"}}">
      {{if .flash.error}}
        <p class="error">
          {{.flash.error}}
        </p>
      {{end}}
      <p>
        <label for="nick">Choose a nick name</label>
        <input type="text" name="user" id="user">
      </p>
      <p>
        <label for="nick">Demonstration</label>
        <select name="demo">
          <option></option>
          <option value="refresh">Ajax, active refresh</option>
          <option value="longpolling">Ajax, long polling</option>
          <option value="websocket">WebSocket</option>
        </select>
      </p>
      <p>
        <label></label>
        <input type="submit" id="enter" value="Enter the chat room">
      </p>
    </form>

controller

func (c Application) EnterDemo(user, demo string) revel.Result {
	c.Validation.Required(user)
	c.Validation.Required(demo)

	if c.Validation.HasErrors() {
		c.Flash.Error("Please choose a nick name and the demonstration type.")
		return c.Redirect(Application.Index)
	}

	switch demo {
	case "refresh":
		return c.Redirect("/refresh?user=%s", user)
	case "longpolling":
		return c.Redirect("/longpolling/room?user=%s", user)
	case "websocket":
		return c.Redirect("/websocket/room?user=%s", user)
	}
	return nil
}

controller

package controllers

import (
	"github.com/revel/revel"

	"github.com/revel/examples/chat/app/chatroom"
)

type WebSocket struct {
	*revel.Controller
}

func (c WebSocket) Room(user string) revel.Result {
	return c.Render(user)
}

func (c WebSocket) RoomSocket(user string, ws revel.ServerWebSocket) revel.Result {
	// Make sure the websocket is valid.
	if ws == nil {
		return nil
	}

	// Join the room.
	subscription := chatroom.Subscribe()
	defer subscription.Cancel()

	chatroom.Join(user)
	defer chatroom.Leave(user)

	// Send down the archive.
	for _, event := range subscription.Archive {
		if ws.MessageSendJSON(&event) != nil {
			// They disconnected
			return nil
		}
	}

	// In order to select between websocket messages and subscription events, we
	// need to stuff websocket events into a channel.
	newMessages := make(chan string)
	go func() {
		var msg string
		for {
			err := ws.MessageReceiveJSON(&msg)
			if err != nil {
				close(newMessages)
				return
			}
			newMessages <- msg
		}
	}()

	// Now listen for new events from either the websocket or the chatroom.
	for {
		select {
		case event := <-subscription.New:
			if ws.MessageSendJSON(&event) != nil {
				// They disconnected.
				return nil
			}
		case msg, ok := <-newMessages:
			// If the channel is closed, they disconnected.
			if !ok {
				return nil
			}

			// Otherwise, say something.
			chatroom.Say(user, msg)
		}
	}

	return nil
}

なるほど、なんとなく構成要素は分かったので、revelでやってみますか。

[Go言語] JSとGoでchatを作りたい その4

script.js

//
	document.getElementById("message").addEventListener("keydown", function(event){
		if (event.code === "Enter"){
			if(!socket){
				console.log("no connection")
				return false
			}
			event.preventDefault()
			event.stopPropagation()
			sendMessage()
		}
	})
//
function sendMessage(){
	console.log("Send Message...")
	let jsonData = {}
	jsonData["action"] = "broadcast"
	jsonData["username"] = document.getElementById("username").value
	jsonData["message"] = document.getElementById("message").value
	socket.send(JSON.stringify(jsonData))
	document.getElementById("message").value = ""
}

html

				<button id="submit" class="submit" onclick="sendMessage()">
					<i class="far far-paper-plane"></i>
				</button>

handler.go

func ListenToWsChannel(){
	var response domain.WsJsonResponse

	for {
		e := <-wsChan

		switch e.Action {
		case "username":
			clients[e.Conn] = e.Username
			users := getUserList()
			response.Action = "list_users"
			response.ConnectedUsers = users
			broadcastToAll(response)

		case "left":
			response.Action = "list_users"
			delete(clients, e.Conn)
			users := getUserList()
			response.ConnectedUsers = users
			broadcastToAll(response)
		case "broadcast":
			response.Action = "broadcast"
			response.Message = fmt.Sprintf(
				"<li class='replace'><strong>%s</strong>: %s</li>",
				e.Username,
				e.Message)
			broadcastToAll(response)
		}
	}
}

script.js

	socket.onmessage = msg => {
		let data = JSON.parse(msg.data)
		console.log({data})
		console.log("Action is", data.action)
		switch(data.action){
			case "list_users":
				let ul = document.getElementById("online-users")
				while (ul.firstChild) ul.removeChild(ul.firstChild)

				if (data.connected_users.length > 0){
					data.connected_users.forEach(function(item){
						let li = document.createElement("li")
						li.appendChild(document.createTextNode(item))
						ul.appendChild(li)
					})
				}
				break
			case "broadcast":
				let message = data.message
				let username = document.getElementById("username").value
				if (message.indexOf(username) > 0){
					message = message.replace("replace", "me")
				} else {
					message = message.replace("replace", "other")
				}
				messageList.innerHTML = messageList.innerHTML + message
				break
		}
	}

おおおおおおおおお、これは凄い
あとはrevelでやってfrontを書くか

[Go言語] JSとGoでchatを作りたい その3

usernameをserver側に送る

script.js

	let userInput = document.getElementById("username")
	userInput.addEventListener("change", function(){
		let jsonData = {}
		jsonData["action"] = "username"
		jsonData["username"] = this.value;

		socket.send(JSON.stringify(jsonData))
	})

handlers.go

func getUserList() []string {
	var clientList []string
	for _, client := range clients {
		if client != "" {
			clientList = append(clientList, client)
		}
	}
	sort.Strings(clientList)
	return clientList
}

connect.go

type WsJsonResponse struct {
	Action string `json:"action"`
	Message string `json:"message"`
	ConnectedUsers []string `json:"connected_users"`
}

script.js

	socket.onmessage = msg => {
		let data = JSON.parse(msg.data)
		console.log({data})
		console.log("Action is", data.action)
		switch(data.action){
			case "list_users":
				let ul = document.getElementById("online-users")
				while (ul.firstChild) ul.removeChild(ul.firstChild)

				if (data.connected_users.length > 0){
					data.connected_users.forEach(function(item){
						let li = document.createElement("li")
						li.appendChild(document.createTextNode(item))
						ul.appendChild(li)
					})
				}
				break
		}
	}

script.js

window.onbeforeunload = function(){
	console.log("User Leaving")
	let jsonData = {}
	jsonData["action"] = "left"
	socket.send(JSON.stringify(jsonData))
}
func ListenToWsChannel(){
	var response domain.WsJsonResponse

	for {
		e := <-wsChan

		switch e.Action {
		case "username":
			clients[e.Conn] = e.Username
			users := getUserList()
			response.Action = "list_users"
			response.ConnectedUsers = users
			broadcastToAll(response)

		case "left":
			response.Action = "list_users"
			delete(clients, e.Conn)
			users := getUserList()
			response.ConnectedUsers = users
			broadcastToAll(response)
		}
	}
}

ちょっとこんがらがってきました。

[Go言語] JSとGoでchatを作りたい その2

### Websocketのハンドリング
connection.go

package domain

import "github.com/gorilla/websocket"

type WsJsonResponse struct {
	Action string `json:"action"`
	Message string `json:"message"`
}

type WebSocketConnection struct {
	*websocket.Conn
}

type WsPlayload struct {
	Action String `json:"action"`
	Message string `json:"message"`
	Username string `json:"username"`
	Conn WebSocketConnection `json:"-"`
}

handlers.go

import (
	"chat/domain"
	"log"
	"net/http"
	"fmt"

	"github.com/CloudyKit/jet/v6"
	"github.com/gorilla/websocket"
)

var views = jet.NewSet(
	jet.NewOSFileSystemLoader("./html"),
	jet.InDevelopmentMode(),
)

var upgradeConnection = websocket.Upgrader {
	ReadBufferSize: 1024,
	WriteBufferSize: 1024,
	CheckOrigin: func(r *http.Request) bool {return true},
}

var (
	wsChan = make(chan domain.WsPayload)

	clients = make(map[domain.WebSocketConnection]string)
)

func WsEndpoint(w http.ResponseWriter, r *http.Request){
	ws, err := upgradeConnection.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
	}
	log.Println("OK client connecting")

	conn := domain.WebSocketConnection{Conn: ws}
	clients[conn] = ""

	go ListenForWs(&conn)


	var response domain.WsJsonResponse
	response.Message = `<li>Connect to server</li>`

	err = ws.WriteJSON(response)
	if err != nil {
		log.Println(err)
	}
}

func Home(w http.ResponseWriter, r *http.Request){
	err := renderPage(w, "home.jet", nil)
	if err != nil {
		log.Println(err)
	}
}

func renderPage(w http.ResponseWriter, tmpl string, data jet.VarMap) error {
	view, err := views.GetTemplate(tmpl)
	if err != nil {
		log.Println(err)
		return err
	}

	err = view.Execute(w, data, nil)
	if err != nil {
		log.Println(err)
		return err
	}
	return nil
}

func ListenForWs(conn *domain.WebSocketConnection){
	defer func(){
		if r := recover(); r != nil {
			log.Println("Error", fmt.Sprintf("%v", r))
		}
	}()

	var payload domain.WsPayload

	for {
		err := conn.ReadJSON(&payload)
		if err != nil {

		} else {
			payload.Conn = *conn
			wsChan <- payload
		}
	}
}

func broadcastToAll(response domain.WsJsonResponse){
	for client := range clients {
		err := client.WriteJSON(response)
		if err != nil {
			log.Println("websockets err")
			_ = client.Close()
			delete(clients, client)
		}
	}
}

func ListenToWsChannel(){
	var response domain.WsJsonResponse

	for {
		e := <-wsChan

		response.Action = "Got here"
		response.Message = fmt.Sprintf("Some message, and action was %s", e.Action)

		broadcastToAll(response)
	}
}

goroutineを使って、ListenForWs関数を別プロセスで呼び出すことで、ブラウザからの通信を常にキャッチし続ける状態を作る。gorouteは go と書くだけ

main.go

import (
	"chat/internal/handlers"
	"log"
	"net/http"
)

func main(){
	mux := routes()
	log.Println("Starting channel listener")
	go handlers.ListenToWsChannel()

	log.Println("Starting web server on port 8080")

	_ = http.ListenAndServe(":8080", mux)
}

successfully connected
script.js:21 {action: ”, message: ‘

  • Connect to server
  • ‘}

    2021/10/18 03:41:26 Error runtime error: invalid memory address or nil pointer dereference
    2021/10/18 03:41:30 Error repeated read on failed websocket connection
    2021/10/18 03:41:30 OK client connecting

    なんか上手くいってるっぽいが、どういう仕組みなのか全然理解できない…

    [Go言語] JSとGoでchatを作りたい その1

    $ mkdir go
    $ cd go
    $ go mod init chat
    $ mkdir -p cmd/web
    $ mkdir -p internal/handlers
    $ mkdir html
    $ touch cmd/web/main.go
    $ touch cmd/web/routes.go
    $ touch html/home.jet
    $ touch internal/handlers/handlers.go
    $ mkdir domain
    $ touch domain/connect.go
    $ mkdir static
    $ touch static/scripts.js
    $ touch static/style.css

    ### モジュールの追加
    $ go get github.com/CloudyKit/jet/v6
    $ go get github.com/bmizerany/pat
    $ go get github.com/gorilla/websocket

    ### handler, route.go, main.go省略
    internal/handlers/hanlders.go
    cmd/web/main.go
    cmd/web/routes.go

    ### template
    home/home.jet

    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<meta name="viewport" content="width=device-width, initial-scale=1.0">
    	<title>Document</title>
    	<link rel="stylesheet" href="/static/style.css">
    </head>
    <body>
    	<div class="chat-container">
    		<div class="chat-header">
    			<label for="username">Your Name</label>
    			<input type="text" id="username" class="username" autocomplete="off" placeholder=":) selfnote">
    		</div>
    		<div class="chat-body">
    			<ul id="message-list">
    				<li class="me">Sample</li>
    				<li class="other">Sample</li>
    			</ul>
    			<div class="send-area">
    				<input type="text" id="message" class="message" autocomplete="off" placeholder="message...">
    
    				<button id="submit" class="submit">
    					<i class="far far-paper-plane"></i>
    				</button>
    			</div>
    		</div>
    	</div>
    	<div class="oneline-user-container">
    		<ul id="online-users">
    			<li>XXXX</li>
    		</ul>
    	</div>
    	<script src="/static/scripts.js"></script>
    </body>
    </html>
    

    ### Websocketのエンドポイント作成
    /domain/connect.go
    L websocket返却用の構造体

    package domain
    
    type WsJsonResponse struct {
    	Action string `json:"action"`
    	Message string `json:"message"`
    }
    

    internal/handlers/hanlders.go

    import (
    	"chat/domain"
    	"log"
    	"net/http"
    
    	"github.com/CloudyKit/jet/v6"
    	"github.com/gorilla/websocket"
    )
    
    var upgradeConnection = websocket.Upgrader {
    	ReadBufferSize: 1024,
    	WriteBufferSize: 1024,
    	CheckOrigin: func(r *http.Request) bool {return true},
    }
    
    func WsEndpoint(w http.ResponseWriter, r *http.Request){
    	ws, err := upgradeConnection.Upgrade(w, r, nil)
    	if err != nil {
    		log.Println(err)
    	}
    	log.Println("OK client connecting")
    
    	var response domain.WsJsonResponse
    	response.Message = `<li>Connect to server</li>`
    
    	err = ws.WriteJSON(response)
    	if err != nil {
    		log.Println(err)
    	}
    }
    

    Route.go

    func routes() http.Handler {
    	mux := pat.New()
    
    	mux.Get("/", http.HandlerFunc(handlers.Home))
    	mux.Get("/ws", http.HandlerFunc(handlers.WsEndpoint)) // 追加
    
    	fileServer := http.FileServer(http.Dir("./static/"))
    	mux.Get("/static/", http.StripPrefix("/static", fileServer))
    
    	return mux
    }
    

    js

    let socket = null;
    
    document.addEventListener("DOMContentLoaded", function(){
    
    	socket = new WebSocket("ws://192.168.34.10:8080/ws")
    
    	socket.onopen = () => {
    		console.log("successfully connected")
    	}
    })
    

    console

    js

    document.addEventListener("DOMContentLoaded", function(){
    
    	socket = new WebSocket("ws://192.168.34.10:8080/ws")
    
    	socket.onopen = () => {
    		console.log("successfully connected")
    	}
    
    	socket.onclose = () => {
    		console.log("connection closed")
    	}
    
    	socket.onerror = error => {
    		console.log("there was an error")
    	}
    
    	socket.onmessage = msg => {
    		let j = JSON.parse(msg.data)
    		console.log(j)
    	}
    })
    

    うーむ、postされたらonloadというイメージなんだが…

    [Go言語] jet,patを使った書き方 : static追加

    $ tree
    ├── cmd
    │   └── web
    │   ├── main.go
    │   └── routes.go
    ├── go.mod
    ├── go.sum
    ├── html
    │   └── home.jet
    ├── internal
    │   └── handlers
    │   └── handlers.go
    └── static
    ├── script.js
    └── style.css

    routes.go

    package main
    
    import (
    	"webapp/internal/handlers"
    	"net/http"
    
    	"github.com/bmizerany/pat"
    )
    
    func routes() http.Handler {
    	mux := pat.New()
    
    	mux.Get("/", http.HandlerFunc(handlers.Home))
    
    	fileServer := http.FileServer(http.Dir("./static/"))
    	mux.Get("/static/", http.StripPrefix("/static", fileServer))
    	return mux
    }
    

    home.jet

    <head>
    	<meta charset="UTF-8">
    	<meta name="viewport" content="width=device-width, initial-scale=1.0">
    	<title>Document</title>
    	<link rel="stylesheet" href="/static/style.css">
    </head>
    <body>
    	<h1>jet test</h1>
    	<script src="/static/scripts.js"></script>
    </body>
    </html>
    

    scripts.js

    alert("js is read")
    

    sytle.css

    h1 {
    	color: orange;
    }
    

    なるほど

    [Go言語] jetを使ったテンプレート・Handlers

    $ go mod init webapp
    $ mkdir -p cmd/web
    $ mkdir -p internal/handlers
    $ mkdir html
    $ touch cmd/web/main.go
    $ touch cmd/web/routes.go
    $ touch html/home.jet
    $ touch internal/handlers/handlers.go

    home.jetは、Webページのテンプレートファイル
    home.jet

    <!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<meta name="viewport" content="width=device-width, initial-scale=1.0">
    	<title>Document</title>
    </head>
    <body>
    	<h1>jet test</h1>
    </body>
    </html>
    

    handlers.go

    package handlers
    
    import (
    	"log"
    	"net/http"
    
    	"github.com/CloudyKit/jet/v6"
    )
    
    var views = jet.NewSet(
    	jet.NewOSFileSystemLoader("./html"),
    	jet.InDevelopmentMode(),
    )
    
    func Home(w http.ResponseWriter, r *http.Request){
    	err := renderPage(w, "home.jet", nil)
    	if err != nil {
    		log.Println(err)
    	}
    }
    
    func renderPage(w http.ResponseWriter, tmpl string, data jet.VarMap) error {
    	view, err := views.GetTemplate(tmpl)
    	if err != nil {
    		log.Println(err)
    		return err
    	}
    
    	err = view.Execute(w, data, nil)
    	if err != nil {
    		log.Println(err)
    		return err
    	}
    	return nil
    }
    

    route.go

    package main
    
    import (
    	"webapp/internal/handlers"
    	"net/http"
    
    	"github.com/bmizerany/pat"
    )
    
    func routes() http.Handler {
    	mux := pat.New()
    
    	mux.Get("/", http.HandlerFunc(handlers.Home))
    	return mux
    }
    

    main.go

    package main
    
    import (
    	"log"
    	"net/http"
    )
    
    func main(){
    	mux := routes()
    	log.Println("Starting web server on port 8080")
    
    	_ = http.ListenAndServe(":8080", mux)
    }
    

    $ go run cmd/web/*.go

    Echoとかを使わずに、jetなど色々な書き方があるんやな
    Webサーバーの作成には、bmizerany/patモジュールを使用

    [TypeScript] jQueryを使いたい

    $ npm install –save jquery @types/jquery

    tsconfig.json

    "allowSyntheticDefaultImports": true,    
    "esModuleInterop": true, 
    

    app.ts

    import $ from 'jquery';
    
    $('input[name=password_change]').on('change', function(){
        if($('.changeterm').hasClass('displaynone')){
            $('.changeterm').removeClass('displaynone');
        } else {
            $('.changeterm').addClass('displaynone');
        }           
    });
    

    OK いい感じ

    [AWS RDS] Postgresを作成し、pg_dumpでbackupを取得する

    ### 前準備
    VPC, subnetを作って、EC2の作成

    ### DB用のネットワーク作成
    – Inbound rouleでPostgresとして、セキュリティグループはec2のセキュリティグループにする
     L これにより、ec2しかアクセスできないようになる

    – EC2のVPCでpostgres用のsubnetを作成
     L 192.168.x.0/28とする

    ### RDSでpostgresの作成
    – create database, postgres 13.3-R1
    – db2.t.micro
    – 作成に2~3分かかる。出来たら、endpointをメモ(hoge.fuga.ap-northeast-1.rds.amazonaws.com)

    ### EC2でpostgresのインストール
    $ sudo yum update
    $ sudo yum install -y postgresql.x86_64
    // 接続
    $ psql -h hoge.fuga.ap-northeast-1.rds.amazonaws.com -U root -d postgres
    postgres=>

    CREATE TABLE playground (
    equip_id serial PRIMARY KEY,
    type varchar (50) NOT NULL,
    color varchar (25) NOT NULL,
    location varchar(25) check (location in (‘north’, ‘south’, ‘west’, ‘east’, ‘northeast’, ‘southeast’, ‘southwest’, ‘northwest’)),
    install_date date
    );

    INSERT INTO playground (type, color, location, install_date) VALUES (‘slide’, ‘blue’, ‘south’, ‘2022-04-28’);
    INSERT INTO playground (type, color, location, install_date) VALUES (‘swing’, ‘yellow’, ‘northwest’, ‘2022-08-16’);

    postgres=> SELECT * FROM playground;
    equip_id | type | color | location | install_date
    ———-+——-+——–+———–+————–
    1 | slide | blue | south | 2022-04-28
    2 | swing | yellow | northwest | 2022-08-16

    ### バックアップの取得
    $ sudo pg_dump -U root -h hoge.fuga.ap-northeast-1.rds.amazonaws.com -p 5432 postgres -f /home/ec2-user/test.sql
    パスワード:
    pg_dump: サーババージョン: 13.3、pg_dump バージョン: 9.2.24
    pg_dump: サーババージョンの不整合のため処理を中断しています

    なんやと! EC2のpostgresのバージョンをRDSのバージョンと合わせないといけないらしい

    $ sudo yum install gcc
    $ sudo yum install readline-devel
    $ sudo yum install zlib-devel
    $ cd /usr/local/src
    $ sudo wget https://ftp.postgresql.org/pub/source/v13.3/postgresql-13.3.tar.gz
    $ sudo tar -xvzf postgresql-13.3.tar.gz
    $ cd postgresql-13.3
    $ ./configure –prefix=/usr/local/postgresql-13.3/ –with-pgport=5432
    $ make
    $ sudo make install

    再度バックアップの取得
    $ /usr/local/postgresql-13.3/bin/pg_dump -U root -h hoge.fuga.ap-northeast-1.rds.amazonaws.com -p 5432 postgres -f /home/ec2-user/test.sql
    $ cd /home/ec2-user/
    $ ls
    test.sql

    うおおおおおおおおおおおおおおおおおおお
    すげえ感動した

    Postgresのデータバックアップとリストア

    ### tmpフォルダにバックアップ
    pg_dump -U ユーザー名 –format=出力形式 –file=出力先 バックアップを取るDB名 と書く

    $ sudo -i -u postgres
    $ pg_dump –format=p –file=/tmp/test.sql postgres
    $ exit
    $ cd /tmp
    $ ls

    ### restore
    $ psql -f test.sql
    $ psql
    postgres=# \dt;

    OK これをrdsから取得したい

    RDSの場合は
    $ pg_dump -U USER_NAME -h HOST_NAME -p port DATABASE_NAME -f FILE_NAME

    なるほど、ほぼ一緒か。
    RDSでpostgresを作成するところからかな。