Golang中处理WebSocket写入错误的解决方案

Golang中处理WebSocket写入错误的解决方案 我将WebSocket连接保存在一个使用UUID作为键的映射中,例如:

sockets["123456"] = *websocket.Conn

并且有一个将消息传递到通道的函数:

func (mr MessageRequest) Send(msg []byte) {
	mr.mu.RLock()
	defer mr.mu.RUnlock()
	for _, socket := range mr.sockets {
		mr.chat.broadcast <- Message{
			socket:  socket,
			message: msg,
			chat:    mr.chat,
		}
	}
}

处理消息的通道如下:

case mr := <-c.broadcast:
			msg := mr.message

			if c.sockets[mr.socket] != nil {
				if err := c.sockets[mr.socket].WriteMessage(websocket.TextMessage, msg); err != nil {
					c.sockets[mr.socket].SetWriteDeadline(time.Now().Add(10 * time.Second))
					c.sockets[mr.socket].WriteMessage(websocket.CloseMessage, []byte{})
					c.sockets[mr.socket].Close()
					go c.Unregister(c.sockets[mr.socket])
				}
			}

当有许多客户端连接,并且一些客户端快速断开连接时,问题出现在这一行:

if err := c.sockets[mr.socket].WriteMessage(websocket.TextMessage, msg); err != nil 

错误信息是:

panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x139b430] goroutine 138 [running]: github.com/fasthttp/websocket.(*Conn).WriteMessage(0x164f3c0?, 0xc0002a57d0?, {0xc0000380e0, 0x70, 0x70}) /Users/username/go/pkg/mod/github.com/fasthttp/websocket@v1.5.0/conn.go:760 +0x30

我认为这是因为当执行注销操作时,sockets["123456"] 已经被删除了。我已经使用 if c.sockets[mr.socket] != nil { 进行了检查,但我的应用程序仍然因为上面那行代码而崩溃。

我不知道如何解决这种情况,我需要帮助。

PS:如果有所帮助,我的注销函数如下所示

		case connection := <-c.unregister:
			uuid := connection.uuid
			if c.sockets[uuid] != nil { // 避免多次删除
				c.mu.Lock()
				delete(c.sockets, uuid)
				c.mu.Unlock()
			}

更多关于Golang中处理WebSocket写入错误的解决方案的实战教程也可以访问 https://www.itying.com/category-94-b0.html

8 回复

哎呀!!!谢谢,很高兴知道这一点……

更多关于Golang中处理WebSocket写入错误的解决方案的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


请展示地图的定义…

要检查一个项是否已从映射中删除,请执行以下操作

if c, ok := sockets["123456"]; ok {
    // 仍然存在

我认为在发送消息之前,你需要检查套接字的状态。

if socket != nil && !socket.IsCloseError && !socket.IsUnexpectedCloseError {
   if err := socket.WriteMessage(websocket.TextMessage, msg); err != nil {
      ....
   }
}

@Yamil_Bracho 感谢您抽出时间。

经过长时间的摸索,结果发现这是 fasthttp websocket 的一个 bug,它在应该返回错误时却抛出了不必要的 panic。此问题已在此处修复 → Description: by gokpm · Pull Request #31 · fasthttp/websocket · GitHub

这是我的结构体

type Chat struct {
	register   chan Connection
	broadcast  chan Message
	unregister chan Connection
	join       chan RoomRequest
	leave      chan RoomRequest
	rooms      map[string][]string
	sockets    map[string]*websocket.Conn. //<-- 这是导致内存问题的那个
	mu         *sync.RWMutex
	stats      *Stats
}

提前感谢。

感谢。我已经改用了您的代码,但是当并发连接/断开连接很多时,我仍然遇到错误。

我尝试在向套接字写入之前打印套接字的 uuid,访问切片没有问题。

我也像下面代码那样添加了读锁(Rlock),但问题仍然存在。理论上讲,不应该出现空指针解引用错误,对吧?

panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x139b430]

	        c.mu.RLock()
			// if c.sockets[mr.socket] != nil {
			if socket, ok := c.sockets[mr.socket]; ok {
                pp.Print(socket.Locals("uuid")) //&lt;--- 这里打印(可访问)
				if err := socket.WriteMessage(websocket.TextMessage, msg); err != nil { //&lt;-- 这里出错
					socket.SetWriteDeadline(time.Now().Add(10 * time.Second))
					socket.WriteMessage(websocket.CloseMessage, []byte{})
					socket.Close()
					go c.Unregister(socket)
				}
			}
			c.mu.RUnlock()

错误也指向 fasthttp 中的这一行:websocket/conn.go at c98746c10029a4885af35f497980303ea218f409 · fasthttp/websocket · GitHub

问题出现在并发访问和竞态条件上。虽然你使用了 if c.sockets[mr.socket] != nil 检查,但在检查之后、调用 WriteMessage 之前,连接可能已经被注销并从映射中删除。

以下是解决方案:

case mr := <-c.broadcast:
    msg := mr.message
    
    // 获取连接时需要加锁
    c.mu.RLock()
    conn := c.sockets[mr.socket]
    c.mu.RUnlock()
    
    if conn != nil {
        if err := conn.WriteMessage(websocket.TextMessage, msg); err != nil {
            // 写入失败时也需要加锁
            c.mu.Lock()
            if c.sockets[mr.socket] == conn { // 再次验证连接是否仍然相同
                delete(c.sockets, mr.socket)
            }
            c.mu.Unlock()
            
            // 关闭连接
            conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
            conn.WriteMessage(websocket.CloseMessage, []byte{})
            conn.Close()
        }
    }

同时修改注销函数,确保在删除连接后不会留下悬空指针:

case connection := <-c.unregister:
    uuid := connection.uuid
    
    c.mu.Lock()
    conn := c.sockets[uuid]
    if conn != nil {
        delete(c.sockets, uuid)
    }
    c.mu.Unlock()
    
    // 如果找到了连接,关闭它
    if conn != nil {
        conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
        conn.WriteMessage(websocket.CloseMessage, []byte{})
        conn.Close()
    }

对于广播消息发送函数,建议改为:

func (mr MessageRequest) Send(msg []byte) {
    mr.mu.RLock()
    defer mr.mu.RUnlock()
    
    // 收集所有需要发送的连接
    connections := make([]*websocket.Conn, 0, len(mr.sockets))
    for _, socket := range mr.sockets {
        connections = append(connections, socket)
    }
    
    // 发送到通道
    for _, conn := range connections {
        mr.chat.broadcast <- Message{
            socket:  conn,  // 这里应该存储连接标识符,而不是连接本身
            message: msg,
            chat:    mr.chat,
        }
    }
}

注意:在 Message 结构中,socket 字段应该存储连接的唯一标识符(如UUID),而不是 *websocket.Conn 指针,这样可以避免在通道中传递可能已经失效的连接指针。

回到顶部