2023,江端さんの技術メモ

一回纏めて再学習

FastAPI入門https://zenn.dev/sh0nk/books/537bb028709ab9/viewer/f1b6fc

パラメタの違い

  • パスパラメタ   http://127.0.0.1:8000/items/3
  • クエリパラメタ http://127.0.0.1:8000/items/?skip=0&limit=10
  • リクエストボディ ???

環境構築

 pip install fastapi

pip install sqlalchemy uvicorn

を実施。途中でpythonのバージョンアップしろと言われたので、素直に従いました。

run.py (サーバ立ち上げ用)
urls.py (URLのルーティング用)
controllers.py (レスポンス処理用)
を作りましたが、以下のエラーが出てきました。

> python run.py
Traceback (most recent call last):
  File "run.py", line 1, in <module>
    from urls import app
  File "C:\Users\ebata\fastapi\urls.py", line 1, in <module>
    from controllers import *
  File "C:\Users\ebata\fastapi\controllers.py", line 3, in <module>
    app = FastAPI(
NameError: name 'FastAPI' is not defined

さて、python環境構築の経験もなく、python使うの数年ぶり、ということで、まあ、「ご挨拶」でしょう。

試しに、 https://wp.kobore.net/江端さんの技術メモ/post-7450/ を入れて実行してみたとこ、ちゃんと動くようなので、環境側の問題 と特定しました。

ーーーー

PS C:\Users\ebata\fastapi> uvicorn main:app --reload
ERROR: Error loading ASGI app. Attribute "app" not found in module "m

が出てきたので、いろいろ調べたら、なんとプログラムがセーブされていなかったという間抜けなオチでした。

実施した(履修した)ページ

現時点までの実施事項(続き)

https://fastapi.tiangolo.com/ja/tutorial/body/ の、「クエリパラメータ」まで履修完了。續きは、「リクエストボディ」から。

現在、履修に使っているホルダは、~/fastapi, ~/fasapi2

所感

外部インタフェースについては、Webや他の手段でAPIを叩くことができると思うが、モジュール間の関数にfastapiを使うことは可能だろうか。

原理的には、curl等で送り込めばば、何でも可能と思うが、相互通信を考えるとwebsocketを剥き出しにした方がよいのではないだろうか?

ちなみに、FastAPIでPostgresqlのDBアクセスをやろうと思ったけど、

$ pip install psycopg2

がどうしてもインストールできずに、断念(くやしい)

以上

2023,江端さんの技術メモ

問題

現在、192.168.0.23/postgresにアクセスを確認

で、A5:SQL Mk-2は接続成功している様子

だが、publicの下の「テーブル」「ビュー」などを叩いても何も出てこない。

念のためpgAdAdmin4 で試してみたが、こちらも問題なし

解決(T研究所のKさんに教えて貰いました)

『データベースを登録する際に、データベース名に「agent_db」 を指定していただく必要があります』

 

参考文献

https://izit-infobox.net/blog/2021/07/15/a5sql-mk-2/

2022/09,江端さんの技術メモ

こっちはPythonで書いたファジィ(Fuzzy)推論コードです。



# -*- coding: utf-8 -*-
# 上記の一行がないと、コメント分の和文も全部エラーになる 

# シンプルファジィ推論プログラム ver0.0 2015年12月15日
# シンプルファジィ推論プログラム ver0.1 2023年04月20日 print文の仕様変更対応

# メンバーシップ関数は、前件部、後件部ともに3つに固定した単純なもの
# 推論方式は、単純なmin-max重心法のみ



class condition_MF:
    
    center = 0
    width =  100
    PV ="LESS"

    def __init__(self, center, width, PV):
        self.center = center
        self.width = width
        self.PV = PV

    # x,yは、メンバーシップ関数上の座標を示す
    def func(self, x):
        if (self.PV == "LESS"):
            if (x <= self.center - self.width):
                y = 1.0
            elif (x <= self.center):
                y = - 1.0 / self.width * (x - self.center)
            else:
                y = 0.0        
        elif (self.PV == "ZERO"):
            if (x <= self.center - self.width):
                y = 0.0
            elif (x <= self.center):
                y = 1.0 / self.width * (x - self.center) + 1.0
            elif (x <= self.center + self.width):
                y = -1.0 / self.width * (x - self.center) + 1.0
            else:
                y = 0.0       
        elif (self.PV == "MORE"):
            if (x <= self.center):
                y = 0.0
            elif (x <= self.center + self.width):
                y = 1.0 / self.width * (x - self.center)
            else:
                y = 1.0
        
        return y

class action_MF:
    center = 0 # 値はダミー
    width =  100 # 値はダミー
    PV ="ZERO" # 値はダミー
    y = 0.0 # 最大値で更新されていく
    x = 0.0 # 値はダミー

    def __init__(self, center, width, PV):
        self.center = center
        self.width = width
        self.PV = PV

        if (self.PV == "LESS"):
            self.x = self.center - self.width
        elif (self.PV == "ZERO"):
            self.x = self.center
        elif (self.PV == "MORE"):
            self.x = self.center + self.width
        else:
            print ("error")

    # yを最大値で更新していく
    def func_Max(self,b):
        self.y = max(b, self.y)

    # X座標を返す
    def func_X(self):
        return self.x

    # (最大値で更新された、最後の)Y座標を返す
    def func_Y(self):
        return self.y

temp_High = condition_MF(20.0, 10.0, "MORE") # 温度が高い
temp_Middle = condition_MF(20.0, 10.0, "ZERO") #温度が普通
temp_Low = condition_MF(20.0, 10.0, "LESS")# 温度が低い

humi_High = condition_MF(50.0, 20.0, "MORE") # 湿度が高い
humi_Middle = condition_MF(50.0, 20.0, "ZERO") #湿度が普通
humi_Low = condition_MF(50.0, 20.0, "LESS")# 湿度が低い

switch_High = action_MF(0.0, 1.0, "MORE") # エアコンの温度設定を上げる("+1"にする)
switch_Middle = action_MF(0.0, 1.0, "ZERO")# エアコンの温度設定に何もしない("0"にする)
switch_Low = action_MF(0.0, 1.0, "LESS") # エアコンの温度設定を下げる("-1"にする)


# 入力値(温度27度、湿度57%)
t = 35.0
h = 80.0

# (1)「もし、温度が高くて、湿度が高ければ、エアコンの温度設定を下げる」
a1 = min(temp_High.func(t),humi_High.func(h))
print (a1)
switch_Low.func_Max(a1) 

print ("Low") 
print (switch_Low.func_Y()) 

# (2)「もし、温度が普通で、湿度が高ければ、何もしない」
a2 = min(temp_Middle.func(t),humi_High.func(h))
switch_Middle.func_Max(a2) 

print ("Middle") 
print (switch_Middle.func_Y()) 

# (3)「もし、温度が高くて、湿度が普通なら、エアコンの温度設定を下げる」

a3 = min(temp_High.func(t),humi_Middle.func(h))
switch_Low.func_Max(a3) 
print ("Low") 
print (switch_Low.func_Y()) 

# (4)「もし、温度が低くて、湿度が低ければ、エアコンの温度設定を上げる」
a4 = min(temp_Middle.func(t),humi_Low.func(h))
switch_High.func_Max(a4) 
print ("High") 
print (switch_High.func_Y()) 

# (5)(追加)もし温度が普通で、湿度が普通なら、何もしない
a5 = min(temp_Middle.func(t),humi_Middle.func(h))
switch_Middle.func_Max(a5) 
print ("Middle") 
print (switch_Middle.func_Y()) 

# 重心値を求める
# (ルールが推論空間を網羅していないと、ゼロ割が発生することがあるので注意)

reasoning =  (switch_High.func_X() * switch_High.func_Y()  + switch_Middle.func_X() * 
switch_Middle.func_Y()  + switch_Low.func_X() * switch_Low.func_Y()) / (switch_High.func_Y()  + switch_Middle.func_Y()  + switch_Low.func_Y())

print ("\n reasoning")
print (reasoning)

元ネタはこちら(クリックするとページに飛びます)。

ファジィ推論

2023,江端さんの技術メモ

package main

import (
	"fmt"
)

// (1)
var m = make(map[int]int)  // (2)をコメントアウトするならこっちを使う

func main() {

	// (2)
	//m := map[int]int{} // (1)をコメントアウトするならこっちを使う

	m[3124] = 9
	m[1992] = 2
	m[2020] = 3

	// キーのみ取り出す
	for key := range m {
		fmt.Println(key)
	}

	//3124
	//1992
	//2020

	fmt.Println()

	// キーと値
	for key, value := range m {
		fmt.Println(key, value)
	}
	fmt.Println()

	//1992 2
	//2020 3
	//3124 9

	// 値のみ必要な場合
	for _, value := range m {
		fmt.Println(value)
	}

	//9
	//2
	//3

	fmt.Println()

	// ループの回数を数える
	i := 0
	for key, value := range m {
		fmt.Println(key, value)
		i++
	}

	//3124 9
	//1992 2
	//2020 3

	fmt.Println()
	fmt.Println("delete(m, 1992)")
	delete(m, 1992)
	for key, value := range m {
		fmt.Println(key, value)
	}

	//delete(m, 1992)
	//2020 3
	//3124 9


	fmt.Println()
	fmt.Println("add as m[2999] = 2")
	m[2999] = 2
	for key, value := range m {
		fmt.Println(key, value)
	}

	//add as m[2999] = 2
	//3124 9
	//2999 2
	//2020 3

	_, ok := m[100]
	if ok {
		fmt.Println("OK")
	} else {
		fmt.Println("NG")
	}

	// NG

	_, ok = m[2999]
	if ok {
		fmt.Println("OK")
	} else {
		fmt.Println("NG")
	}

	// OK

	fmt.Println(m)
	// map[2020:3 2999:2 3124:9]

	m[2020]++

	fmt.Println(m)
	// map[2020:4 2999:2 3124:9]



}

2023,江端さんの技術メモ

研究室の学生さんたちに負荷テストに協力してもらっています。

今、"cannot parse invalid wire-format data" のエラーでGTFS_HUBのダウンを確認しました。

PrumeMobileの方は、そのままにして、GTFS_HUBの再起動をかけました。

(以下、後日対応)

2022/03,江端さんの技術メモ

C:\Users\ebata\goga\1-10>のI_hate_go_server.md が本体です。

1. Golangのサーバなんか大嫌い

このドキュメントは、絶対的な意味において「無保証」です

Golangで作るサーバは、HandleやらHandlerやら、ハンドル、ハンドルとうるさい! と叫びたくなること、甚しいです。

さすがに、C言語のfork()まで戻りたいとは思えませんが、『あれは、あれで、何をやっているのか分かった』とは言えました。

で、もう正しい理解かどうかは、無視して、もう、誰の話も聞かん! 江端はこういう風に理解すると決めた!! ことを記載しておきます。

2. サーバ側の江端の理解

2.1. http.Handle()は、ブラウザに入力するURLと、index.htmlの場所を教えるものである

http.Handle("/", http.FileServer(http.Dir(".")))

は、https://xxx.xxx/ でアクセスできて(http://xxx.xxx/yyyy のように"yyyy"はない)、index.htmlが、goのサーバのプログラムと同じディレクトリ(".")にいる、と宣言するもの。

http.Handle("/tomo", http.FileServer(http.Dir("./js")))

であれば、https://xxx.xxx/tomo でアクセスできてindex.htmlが、goのサーバのプログラムと同じディレクトリのしたのjs("./js")にいる、と宣言するもの。

2.2. "http.HandleFunc()"は、クライアントがやってくるごにに一つづつ立ち上があるfork()のようなものである→大嘘でした(現在修正検討中) 

具体的には、こちらを読んで頂くと良いと思います。

repeated read on failed websocket connection (一応解決)

要するにwebブラウザ(クライアント)からのアクセスがあれば、この関数がfork()の用に立ち上って、Webブラウザとの面倒を見る。→大嘘でした

面倒見ません。

まず第一に、http.HandleFunc()の誤解がありました。私は、これを、fork()のようにプロセスかスレッドを発生さるものと思っていましたが、これは、一言で言えば、単なるコールバック関数でした。

乱暴に言えば、Webからアクセスがあると、func()というファンクションに吹っ飛ばされる、という現象を発生させる"だけ"で、それ意外のことは何にもしてくれないのです。

これは、index.htmlの内容をクライアントの押しつけるfork()関数と考えれば足る。→大嘘でした

(後で述べるが)これで、

http://localhost:8080 

でアクセスできるようになる

http.ServerFileというのは、実装されているので、わざわざ main.goに書く必要はない。

一方、

http.HandleFunc("/chat", HandleClients)

は、

func HandleClients(w http.ResponseWriter, r *http.Request) { 
    //色々
}

で定義されている、コードをfork()のように立ち上げるものである、と考えれば足る。→大嘘でした(現在修正検討中) 

単にその関数に飛んでいくだけです(但し、ソケット情報を付けてくれます)

"/chat"とは何か?

(後で述べるが)これで、

http://localhost:8080/chat 

でアクセスできるようになる

2.3. "http.ListenAndServe(":8080", nil)"は、「localhost:8080をサーバにするぞ」を実行するものである

上記の関数は、"/"やら、"/chat"やらの(相対的)なパスを指定しているが、これは、サーバのアクセスするアドレスとポートを決定するものである。

err := http.ListenAndServe(":8080", nil)
if err != nil {
	log.Fatal("error starting http server::", err)
	return
}

で、これを宣言することで、サーバとして使えるようになる。

ちなみに、(":8080", nil)の"nil"は、上記のhttp.ServerFile()と、http.HandleFunc()を使うぜ、の意味になる(直接編集することもできるらしい)。

2.4. upgrader.Upgrade(w, r, nil)は、HTTP通信からWebSocket通信に更新してくれるものである

これは"github.com/gorilla/websocket"が提供してくれるもので、HTTP通信(一方通行)からWebSocket通信(相互通行)に更新してくれる便利なものらしい。

websocket, err := upgrader.Upgrade(w, r, nil)
if err != nil {
	log.Fatal("error upgrading GET request to a websocket::", err)
}

こうしてしまえば、websocket.ReadJSON()やら、websocket.WriteHSON()やらが、バカスカ使えるようになる。

2.5. これは何だろう

http.Handle("/static/", http.StripPrefix("/static", http.FileServer(http.Dir("static"))))

まあ、"/static/" は、通信コネクションでいいして、http.StripPrefix("/static", http.FileServer(http.Dir("static")))については、「/static」をhttp.FileServer()が捜索するURLから除く という意味です。

2.6. 乱暴に纏めると

http.HandleFunc()と、http.ListenAndServe()の』2つだけ覚えておけば、いいんじゃない?、と思う。

3. クライアント側の江端の理解

3.1. Dialer構造体のdial関数は、サーバとの接続要求をするものである

一般的にクライアントはWebブラウザなんだけど、これをgolangのプログラムからwebsocketでアクセスしようとする場合は、こんな感じになる。

var addr = flag.String("addr", "0.0.0.0:8080", "http service address")

func bus(bus_num int) {
    var bus BUS

    ///////////// 描画処理ここから ////////////////
    _ = websocket.Upgrader{} // use default options

    flag.Parse()
    log.SetFlags(0)
    u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo2"}
    log.Printf("connecting to %s", u.String())

    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatal("dial:", err)
    }
    defer c.Close()

まず、グローバルで、以下のようにサーバを場所を書いておく。

var addr = flag.String("addr", "0.0.0.0:8080", "http service address")

(よく分からないんだけど)以下のような書き方でwebsocket(のインスタンス?)が作れるらしい。

 _ = websocket.Upgrader{} // use default options

以下で、/echo2を使うぜ、の宣言

    u := url.URL{Scheme: "ws", Host: *addr, Path: "/echo2"}

で、以下で、websocket用のソケットができるらしい。

    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)

この後はc.ReadJSON()やら、c.WriteJSON()やらを使い倒す、ことができるようになります。

4. 江端の理解 その他について

4.1. flag.Parse()って何?

var addr = flag.String("addr", "0.0.0.0:8080", "http service address")

を"固定するもの"でいいのかな? → 間違っています。→ golang でコマンドライン引数を使う

4.2. log.SetFlags(0)って何?

import "log"
で、logを使う場合に、logの設定をリセットするもの、で良さそうです。

以上

内容間違っていたら、優しくご指摘下さい。

 

 

2023,江端さんの技術メモ

"github.com/gorilla/websocket"の有名なバグ(らしい)

■PruneMobile側(sever.go)

■gtfs_hub側(gtfs_hub.go)

■対策

[Go言語/JavaScript]WebSocketsでチャットアプリを実装!~ユーザーリスト表示編~


PruneMobile側(sever.go)の方が先に落ちる(確認) → で書き込みできなくなってgtfs_hub側(gtfs_hub.go)も落ちる、と、多分、そんな感じ。

"repeated read on failed websocket connection"を普通に読めば、「間違ったWebsocket接続で、*何度*も読み込みを繰り返している」なので、read errorが発生したら、即コネクションクローズして、クライアントも落してしまえばいいのかな、と考えました。→ ダメでした。サーバが落ちて処理が止まります。

で、以前、

Golangのサーバなんか大嫌い

で、

"http.HandleFunc()"は、クライアントがやってくるごにに一つづつ立ち上があるfork()のようなものである

てなことを書いていたので、read errorqを検知したら、forループから直ちにbreakして、クライアントをクラッシュさせてしまうこと(ができるものと)しました。→ ダメでした。サーバが落ちて処理が止まります。

 

for (){
      (色々)
           err = webConn.ReadJSON(&locMsg2) // ここでPanic serving  ... repeated read on failed websocket connectionが発生している可能性あり
           fmt.Printf("c6: ")
           if err != nil{
              fmt.Printf("Before webConn.close(), After c6:")
               webConn.Close()
               break → ダメ
           }
}

javascript (onload.js) の方にも、終了理由がでるように、以下のコードを追加しました。

// サーバを止めると、ここに飛んでくる
  socket.onclose = function (event) {
      console.log("socket.onclose");
  
      let obj = JSON.parse(event.data);
  
      console.log("socket.onclose: obj.id:", obj.id);
      console.log("socket.onclose: obj.lat:", obj.lat);
      console.log("socket.onclose: obj.lng:", obj.lng);
      console.log("socket.onclose: obj.type:", obj.type);
      console.log("socket.onclose: obj.popup:", obj.popup);
  
      socket = null;
  
      // 切断が完全に完了したかどうか
      if(event.wasClean){
          var closed = "完了";
      } else {
          var closed = "未完了";
      }
      info.innerHTML += "切断処理:" + closed + "<br>";
      info.innerHTML += "コード:" + event.code + "<br>";
      info.innerHTML += "理由:" + event.reason + "<br>";
  
  }
  
  window.onunload = function(event){
      // 切断
      ws.close(4500,"切断理由");
  }

上記の「ダメ」の対策中

https://ja.stackoverflow.com/questions/12389/golang%E3%81%A7%E3%83%9A%E3%83%BC%E3%82%B8%E3%82%92%E5%86%8D%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF%E3%81%99%E3%82%8B%E3%81%A8websocket-server%E3%81%8C%E8%90%BD%E3%81%A1%E3%82%8B


■問題解決編

色々問題があったのですが、順番に説明していきます。


	var addr = flag.String("addr", "192.168.0.8:8080", "http service address") // テスト
	....
	.....
	http.Handle("/", http.FileServer(http.Dir(".")))


	http.HandleFunc("/echo", echo)                                           // echo関数を登録 (サーバとして必要)
	log.Fatal(http.ListenAndServeTLS(*addr, "./cert.pem", "./key.pem", nil)) // localhost:8080で起動をセット
}

まず第一に、http.HandleFunc()の誤解がありました。私は、これを、fork()のようにプロセスかスレッドを発生さるものと思っていましたが、これは、一言で言えば、単なるコールバック関数でした。

乱暴に言えば、Webからアクセスがあると、echo()というファンクションに吹っ飛ばされる、という現象を発生させる"だけ"で、それ意外のことは何にもしてくれないのです。

では、echo()の方はどうなっているかというと、

func echo(w http.ResponseWriter, r *http.Request) { // JavaScriptとの通信
	webConn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println("websocket connection err:", err)
		return
	}
	defer webConn.Close()

とすることで、webSocket通信の準備ができて、その通信路が"webConn"にできる、ということです。
で、それ意外のことは何もしてくれないのです。

つまり、2つのWebブラウザからアクセスがくると、その度、echo()に飛んできて、その度に、異なるwebConnを生成する、ということです。

ここまでの説明で分かると思いますが、つまり、Webブラウザがくる度に、それをどっかに格納しておかないと、通信路の情報が上書きされてしまいます

なので、基本的には、以下のwebConnを、配列に格納しておきます。

var clients = make(map[*websocket.Conn]bool) // 接続されるクライアント
という動的な配列を準備しておき、アクセスがある度に、
// クライアントを新しく登録(だけ)
    m1Mutex.Lock() // 同時書き込みを回避するため、ミューテックスロックで囲っておく
    clients[webConn] = true  // これで、Webからのアクセスがある度に、通信路情報が動的に追加される
    m1Mutex.Unlock()

というように、Webブラウザとの通信路を別々に管理しておきます。

もし、fork()みたいなことがしたいのであれば、goroutineを起動して、そのgoroutineにWebブラウザの通信路を明け渡す必要があります。それでも、通信路の全体管理は、echo()が握っているので、webConnを消滅されたい場合は、echo()の方でやって貰う必要があります。

この方法を実現する方法として、GO言語のサンプルプログラムで良く登場するのが、以下のような方法です。

// 江端のPCでは、c:\users\ebata\test20230412\main.go

package main

(中略)

func handleConnections(w http.ResponseWriter, r *http.Request) {
    // 送られてきたGETリクエストをwebsocketにアップグレード
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatal(err)
    }
    // 関数が終わった際に必ずwebsocketnのコネクションを閉じる
    defer ws.Close()

    // クライアントを新しく登録
    clients[ws] = true

    for {
        var msg Message
        // 新しいメッセージをJSONとして読み込みMessageオブジェクトにマッピングする
        err := ws.ReadJSON(&msg)
        if err != nil {
            log.Printf("error: %v", err)
            delete(clients, ws)
            break
        }
        // 新しく受信されたメッセージをブロードキャストチャネルに送る
        broadcast <- msg
    }
}

func handleMessages() {
    for {
        // ブロードキャストチャネルから次のメッセージを受け取る
        msg := <-broadcast
        // 現在接続しているクライアント全てにメッセージを送信する
        for client := range clients {
            err := client.WriteJSON(msg)
            if err != nil {
                log.Printf("error: %v", err)
                client.Close()
                delete(clients, client)
            }
        }
    }
}

上記の例では、 http.HandleFunc("/ws", handleConnections) で、handleConnectionsをコールバック関数にしておき、こちらにWebブラウザからのアクセスを全部任せます

でもって、ついでにWebからやってくる通信の全てを受けつけています(ReadJSON())。さらに、webブラウザが突然閉じられた場合などは、通信エラーとして検知して、それをクローズした後に、webConnの配列から取り除いています。これで、このブラウザからの通信路は閉じらて、消されます。

で、この通信の内容を、チャネルを使って、go handleMessagerで作った、fork()みたいなgoroutineに流し込んでいます。

結論として、fork()みたなことがやりたければ、「自力で作れ」ということになります


しかし、WebSocket単位で別々のgoroutine作るのも面倒くさいです。それに、もしそのようなgoroutineを作ったとすれば、ブロードキャストを実現しなければなりませんが、チャネルには、ブロードキャスト機能がありません(どうしても使わなければならない時は、私はredisを使っています)。

ですので、私、チャネルの配列を作って疑似的なブロードキャストを実現しようとしたのですが、このサンプルプログラムが見るからなくて、困っていました。

『echo()関数の中で、全てのWebコネクションを相手にできないかな』と考え始めました。

基本形はこんな形

for {
     message <- channel // 外部から送られてきたデータ
     for client := range clients{ //clientsはWebConnのリスト
           client.WriteJSON(message)
           client.ReadJSON(message2}
     }
}

問題は、Webブラウザは終了処理などなどをせずに、閉じられてしまうことにあります(私たちが普通にやっていることです)。

とすれば、echo()の中でコネクションの切断を検知して、それをWebConnのリスト から取り除く必要があります。

で、こんなことやってみたんですよ。

for {
     message <- channel // 外部から送られてきたデータ
     for client := range clients{ //clientsはWebConnのリスト
           err = client.WriteJSON(message)
           if err != nil{
               client.Close()
               delete(client, clients)
           err =  client.ReadJSON(message2}
           if err != nil{
               client.Close()
               delete(client, clients)
     }
}

これでは、for ルーチンの中で、回している変数を減らすという処理が不味かったようで、この場合、 echo()が終了してしまうようでした。当然、repeated.....も含めて、エラーの嵐になりました。

なので、こんな風に変更しました。


var delete_client *websocket.Comm
for {
     delete_client = nil
     message <- channel // 外部から送られてきたデータ
     for client := range clients{ //clientsはWebConnのリスト
           err = client.WriteJSON(message)
           if err != nil{
               delete_client = client
           }
           err =  client.ReadJSON(message2}
           if err != nil{
               delete_client = client
           }
     }
     if delete_client != nil{
        delete_client.Close()
        delete(clients, delete_client)
}

つまり、ループの外でコネクション処理を行う、ということです。
ただ、この方法では、ループを回っている途中に2つ以上のWebブラウザが落された場合にどうなるか、という問題が残ります。
この場合、「次のループでエラーを検知するのを待つ」という処理で対応する、と腹を括りました。
なぜなら、repeat..... のエラーは、積り積って発生することを経験的に知っていましたので、それまで通信障害したまま走って貰えばいい、と割り来って考えることにしました。


結論としては、

(1)http.HandleFunc()は、Webからの接続要求時に、飛される先の関数を記述するもの(コールバック関数)であり、
(2)そのコールバック関数の中だけで、入出力処理がしたいのであれば、ループを壊さないような工夫をして、上手く運用すれば、上手くいく(ことがある)

という結論になりそうです。

 

2022/08,江端さんの技術メモ

以下の記述は間違っているようですので、参照にしないで下さい(現在検証中)。
(昨日、ソフト外注会社の方に教えて貰いました)

 

Golangで、http.HandleFunc と http.Handleについて、ずっと混乱しつづけています。

というか、私は、使い方が分かればよくて、その理屈なんぞ1mmも興味がないので、コードを書きながら理解しています(結局、遅くなっているような気がしますが)。

1. http.Handle()は、index.htmlをベースとしたサーバを立てるもの

// main16.go 現在の居場所は、c:\Users\ebata\hirohakama\199A2\others
/*
.
├── main16.go
├── index.html (A)
└── chart            # chartフォルダに静的ファイルがある
    └── index.html (B)
*/

package main

import (
	"net/http"
)

func main() {

	// 静的ファイル配信.
	// ディレクトリ名をURLパスに使う場合
	// 例:http://localhost:8080/chart/で index.html (B) の方を表示
	http.Handle("/chart/", http.FileServer(http.Dir("./")))

	// 例:http://localhost:8080/で index.html (A) の方を表示
	http.Handle("/", http.FileServer(http.Dir("./")))

	// ディレクトリ名とURLパスを変える場合
	// 例:http://localhost:8080/mysecret/sample1.txt
	// http.Handle("/mysecret/", http.StripPrefix("/mysecret/", http.FileServer(http.Dir("./contents"))))

// 例:http://localhost:8080/で index.html (A) の方を表示
	http.Handle("/", http.FileServer(http.Dir("./")))
	// 8080ポートで起動
	http.ListenAndServe(":8080", nil)
}

これで、main16.goが置いている場所が、基準点となります(それだけです)。

で、色々考えずに、基本は、

http.Handle("/", http.FileServer(http.Dir("./")))

としておきましょう(というか、これがデフォルトなら、記載すらしなくてもいい)

2. http.HandleFunc()は、ソースコードで書いたものをサーバとするもの

// main15.go 現在の場所はc:\Users\ebata\hirohakama\199A2\others

/*
.
└── main15.go

*/

package main

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

func h1(w http.ResponseWriter, _ *http.Request) {
	io.WriteString(w, "Hello from a HandleFunc #1!\n")
}

func h2(w http.ResponseWriter, _ *http.Request) {
	io.WriteString(w, "Hello from a HandleFunc #2!\n")
}

func main() {

	// http://localhost:8080/ で h1の内容を表示 (プログラムの内容を)
	http.HandleFunc("/", h1)

	// http://localhost:8080/endpoint で h2の内容を表示
	http.HandleFunc("/endpoint", h2)

	log.Fatal(http.ListenAndServe(":8080", nil))
}

3. http.Handle()1つとhttp.handleFunc()1つが混在しているものは、それぞれサーバが2つある、ということ

// main13.go

package main

import (
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{}

type GetLoc struct {
	ID    int     `json:"id"`
	Lat   float64 `json:"lat"`
	Lng   float64 `json:"lng"`
	TYPE  string  `json:"type"` // "PERSON","BUS","CONTROL
	POPUP int     `json:"popup"`
	//Address string  `json:"address"`
}

func echo3(w http.ResponseWriter, r *http.Request) {
	upgrader.CheckOrigin = func(r *http.Request) bool { return true } // おまじない
	conn2, err := upgrader.Upgrade(w, r, nil) //conn2でwebsocketを作成
	if err != nil {
		log.Println("websocket connection err:", err)
		return
	}
	defer conn2.Close()

	for {
		gl2 := new(GetLoc)
		gl2.ID = rand.Intn(20) // ここで乱数を発生されて、javascriptで受信させる
		gl2.Lat = 181.0
		gl2.Lng = 181.0
		gl2.TYPE = "BUS"
		gl2.POPUP = 101

		err := conn2.WriteJSON(&gl2)
		if err != nil {
			log.Println("ReadJSON:", err)
			break
		}
		fmt.Println("echo3:", gl2)
		time.Sleep(time.Second * 1)
	}

}

//var addr = flag.String("addr", "0.0.0.0:5000", "http service address") // テスト

func main() {

	http.Handle("/chart/", http.FileServer(http.Dir("./")))

	http.HandleFunc("/echo3", echo3)

	//log.Println("server starting...", "http://localhost:8080")
	//log.Fatal(http.ListenAndServe("localhost:8080", nil))
	log.Fatal(http.ListenAndServe(":8080", nil))
}

index.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">
    <title>test</title>

</head>

<script type="text/javascript" src="moment.js"></script>
<script type="text/javascript" src="Chart.js"></script>
<script type="text/javascript" src="chartjs-plugin-streaming.js"></script> 



<script>  
    var ws;

    // websocketのオープン(この段階で接続完了)
    ws = new WebSocket('ws://localhost:8080/echo3')  // ユーザ登録画面

    ws.onopen = function (event) {
    }

    ws.onmessage = function (event) {
        // 送られてきたデータを受信して、JSON形式に変更
        var obj = JSON.parse(event.data);
        console.log("obj:",obj);
        console.log("obj.id:",obj.id);
        aa = obj.id;
    }
</script>  

<body BGCOLOR="black" text="white"  STYLE="overflow: hidden;">

	<center>
	  <font size="5">Waking Time(min.) <br></font> <!--- 意味のない表示 -->
	  <font size="5"> 歩行時間(分)</font> <!--- 意味のない表示 -->
	</center>
	
    <canvas id="myChart" width="100" height="85"></canvas>


<script>  
    var ctx = document.getElementById('myChart').getContext('2d');
			var chart = new Chart(ctx, {
				type: 'line',
				data: {
					datasets: [{
                        data: [],  // 1つめ
                        borderColor: "rgba(255,0,0,1)",
                        backgroundColor: "rgba(0,0,0,0)",  
                        lineTension: 0,
                        label: 'Time',
					}]
				},
				options: {
					scales: {
						xAxes: [{
                            type: 'realtime',
                            realtime: {
                                duration: 30000, // 300000ミリ秒(5分)のデータを表示 (コメントアウトすると早く動く)
                                onRefresh: function(chart) {
                                    chart.data.datasets.forEach(function(dataset) {
                                        dataset.data.push({
                                            x: Date.now(),
                                            //y: (Math.floor(Math.random()*16)+30) //30-45の乱数(整数)
                                            y: aa, // この"aa"が、送られてきたデータ
                                        });
                                    });
                                }
                            }

                        }],
                        yAxes: [{
					        ticks: {
					        	max: 20,
					        	min: 0
        					}
                        }]
					}
				}
			});

</script>

</body>
</html>

 

この場合、

  • /chart/index.htmlをベースとするサーバと、
  • echo3()が作るサーバ

の2つがある、ということ。

実際のところ、echo3は、/chart/index.html のクライアント(データの送信元)でもあるんだけど、要求があれば、ポコポコ作り出される、という点ではサーバでもある、という形になっています。

―― という説明を、次に私が頭を抱えた時に、私が思い出せるのかが、不安です

 

2023,江端さんの技術メモ

公開回、秘密鍵の対応

log.Fatal(http.ListenAndServeTLS(*addr, "./cert.pem", "./key.pem", nil)) // localhost:8080で起動をセット

if httpErr = http.ListenAndServeTLS(*addr, "./fullchain.pem", "./privkey.pem", nil);

ということで、 cert.pem = fullchain.pem  key.pem = privkey.pem で良いのだろう

Let's encrypt を試してみた件(整理は明日)

2023,江端さんの技術メモ

T研究所のKさん。ありがとうございました。

 

(4)PruneClusterにおいて、クラスタリングを解除する方法 → leafletView の定義の後に、「leafletView.Cluster.Size = -1」のよう     に負の値を設定することによって、実現可能です。     - 参考URL:https://github.com/SINTEF-9012/PruneCluster/issues/52