[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を作成するところからかな。

    Ubuntu20.04にPostgreSQLをinstall

    ### postgresql, pgadminのinstall
    $ sudo apt-get install curl ca-certificates gnupg
    $ curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add –
    $ sudo sh -c ‘echo “deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main” > /etc/apt/sources.list.d/pgdg.list’
    $ sudo apt update

    $ sudo curl https://www.pgadmin.org/static/packages_pgadmin_org.pub | sudo apt-key add
    $ sudo sh -c ‘echo “deb https://ftp.postgresql.org/pub/pgadmin/pgadmin4/apt/$(lsb_release -cs) pgadmin4 main” > /etc/apt/sources.list.d/pgadmin4.list && apt update’

    $ sudo apt install postgresql-11 pgadmin4
    $ psql -V
    psql (PostgreSQL) 14.0 (Ubuntu 14.0-1.pgdg20.04+1)

    ### postアカウント
    $ sudo -i -u postgres
    $ psql
    // 操作終了
    postgres=# \q

    // アカウントの切り替えなし
    $ sudo -u postgres psql
    // パスワード変更
    $ ALTER USER postgres PASSWORD ‘hoge’;

    ### user作成
    $ sudo -i -u postgres
    $ createuser –interactive

    ### create table

    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
    );
    

    postgres=# \d
    List of relations
    Schema | Name | Type | Owner
    ——–+————————-+———-+———-
    public | playground | table | postgres
    public | playground_equip_id_seq | sequence | postgres
    (2 rows)

    シーケンスなしの場合
    postgres=# \dt

    ### insert
    $ 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’);

    $ SELECT * FROM playground;

    ### update
    $ UPDATE playground SET color = ‘red’ WHERE type = ‘swing’;
    $ DELETE FROM playground WHERE type = ‘slide’;

    次はpostgresのデータバックアップとリストアの方法