repeated read on failed websocket connection (一応解決)
■PruneMobile側(sever.go)
■gtfs_hub側(gtfs_hub.go)
■対策
PruneMobile側(sever.go)の方が先に落ちる(確認) → で書き込みできなくなってgtfs_hub側(gtfs_hub.go)も落ちる、と、多分、そんな感じ。
"repeated read on failed websocket connection"を普通に読めば、「間違ったWebsocket接続で、*何度*も読み込みを繰り返している」なので、read errorが発生したら、即コネクションクローズして、クライアントも落してしまえばいいのかな、と考えました。→ ダメでした。サーバが落ちて処理が止まります。
で、以前、
で、
"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)そのコールバック関数の中だけで、入出力処理がしたいのであれば、ループを壊さないような工夫をして、上手く運用すれば、上手くいく(ことがある)
という結論になりそうです。