我有一个通过API和Websocket通信的应用程序。 如果Websocket在数据库中被更改,则用于将更新的用户数据发布到客户端-这非常有效,除了websocket在某些情况下不接收任何数据的情况之外。几秒钟后,WebSocket再次开始工作。
服务器日志(首先,websocket不起作用,然后再次开始工作)
msg =“无法将数据写入Websocket:websocket:关闭发送”
msg =“向用户发送Ping消息”
msg =“无法将ping消息写入Websocket:websocket:关闭发送”
msg =“向用户发送Ping消息”
msg =“向用户发送Ping消息”
msg =“向用户发送Ping消息”
msg =“向用户发送Ping消息”
客户端代码:
<html>
<body>
<p id="data"></p>
</body>
<script>
var ws = new WebSocket("wss://example.com/ws");
function unloadPage() {
toggleLoader();
ws.onclose = function () {};
ws.close();
}
ws.onopen = function () {
ws.send('Ping');
};
ws.onerror = function (error) {
console.log('WebSocket Error ' + error);
var d = document.getElementById("data");
d.innerHTML += "<tr><td>Failed to connect to Server.</td></tr>"
};
ws.onmessage = function (e) {
console.log(e);
var data = e.data;
var d = document.getElementById("data");
var parsedjson = JSON.parse(data);
d.innerHTML = "";
for (var i = 0; i < parsedjson.length; i++) {
d.innerHTML += parsedjson;
}
};
ws.onclose = function () {
console.log("Websocket has been closed");
};
window.addEventListener("beforeunload", unloadPage);
</script>
</html>
Go代码(通过大猩猩多路复用器):
var (
upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { return true },
}
pingPeriod = (pongPeriod * 9) / 10
pongPeriod = 60 * time.Second
writeWait = 10 * time.Second
)
func PingResponse(ws *websocket.Conn) {
conf := storage.GetConfig()
defer ws.Close()
ws.SetReadLimit(512)
ws.SetReadDeadline(time.Now().Add(pongPeriod))
ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add(pongPeriod)); return nil })
for {
_, _, err := ws.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
conf.Log.Debugf("Websocket Ping Read Failed: %v", err)
}
return
} else {
conf.Log.Debugf("Received message from Websocket client")
}
}
}
func ServeAllUsersWebsocket(datachan chan *[]storage.UserResponse) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conf := storage.GetConfig()
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
conf.Log.Debugf("Failed to upgrade data to Websocket: %v", err)
return
}
go allUserWebsocketWriter(ws, datachan)
go PingResponse(ws)
})
}
func allUserWebsocketWriter(ws *websocket.Conn, datachan chan *[]storage.UserResponse) {
conf := storage.GetConfig()
pingticker := time.NewTicker(pingPeriod)
defer func() {
pingticker.Stop()
ws.Close()
}()
userresponse, err := conf.Database.GetAllUsers()
if err != nil {
conf.Log.Errorf("Failed to query users from database: %v", err)
return
}
ws.SetWriteDeadline(time.Now().Add(writeWait))
err = ws.WriteJSON(&userresponse)
if err != nil {
conf.Log.Debugf("Failed to write initial user response: %v", err)
return
}
for {
select {
case data := <-datachan:
ws.SetWriteDeadline(time.Now().Add(writeWait))
err := ws.WriteJSON(&data)
if err != nil {
conf.Log.Debugf("Failed to write data to Websocket: %v", err)
return
}
case <-pingticker.C:
ws.SetWriteDeadline(time.Now().Add(writeWait))
conf.Log.Debugf("Sending Ping Message to Client")
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
conf.Log.Debugf("Failed to write ping message to Websocket: %v", err)
return
}
}
}
}
基本上,我们将当前数据发布到新的Websocket连接上,当它更新时-这始终有效。之后,如果数据库发生更改,它将把更新的用户列表发布到频道中-然后,Websocket应该将其发布到更新列表的客户端上。我们还发送ping消息-失败(如上面的日志所示)。客户端本身不会记录任何错误或关闭websocket。
答案 0 :(得分:1)
websocket: close sent
错误表示服务器向客户端发送了关闭消息。因为应用程序服务器代码不发送消息,所以消息必须已经由连接发送,以响应来自客户端的关闭消息。
关闭消息从websocket读取方法返回为错误。因为没有消息记录,所以客户端必须发送了“ going away”关闭消息(唯一未记录的错误)。
当websocket连接返回错误时,读写goroutine会关闭该连接并返回。连接未打开。
在连接上的方法调用返回错误之前,读写goroutine不会检测到另一个已关闭连接。读取goroutine总是在读取状态,因此可以快速检测到已关闭的连接,但是写入goroutine可能会有延迟。这可能是应用程序的问题
要使写goroutine快速退出,请使用通道发出写goroutine信号。 dataChan
可以用于此目的,但是我不确定,因为问题不包括有关如何管理频道的信息。假设可以使用该通道,则读取goroutine应该关闭dataChan
。编写者应检测到关闭的通道并退出goroutine:
...
for {
select {
case data, ok := <-datachan:
if !ok {
// Done writing, return
return
}
ws.SetWriteDeadline(time.Now().Add(writeWait))
err := ws.WriteJSON(&data)
if err != nil {
conf.Log.Debugf("Failed to write data to Websocket: %v", err)
return
}
...
这是Gorilla Chat Example使用的方法。
如果无法使用dataChan
,请为此目的引入一个新频道。在处理程序中创建通道,并将该通道传递给读写goroutine:
done := make(chan struct{})
go allUserWebsocketWriter(ws, stop, datachan)
go PingResponse(ws, stop)
从阅读程序返回时关闭通道:
func PingResponse(ws *websocket.Conn, done chan struct{}) {
defer close(done)
conf := storage.GetConfig()
...
在写作gorountine中选择频道:
...
for {
select {
case <-done:
return
case data := <-datachan:
ws.SetWriteDeadline(time.Now().Add(writeWait))
err := ws.WriteJSON(&data)
...
这将导致写入goroutine在读取goroutine退出后迅速退出。
这是Gorilla Command Example使用的方法。
这两种方法都降低了在连接上写入返回websocket: close sent
错误的可能性,但是并不能消除这种可能性。发生此错误是因为读取goroutine可以在写入goroutine写入消息之前关闭连接。
无论如何,有证据表明客户端正在关闭连接。未关闭的连接不是问题所在。