在 GoFrame 中实现 WebSocket 通信与心跳机制的实战指南

322 阅读6分钟

在现代Web开发中,WebSocket已成为实现实时通信的重要技术之一。它允许在客户端和服务器之间建立全双工通信通道,使得数据可以双向实时传输。GoFrame作为一个功能强大的Web应用开发框架,提供了便捷的WebSocket支持。本文将详细介绍如何在GoFrame中使用WebSocket。

WebSocket简介

WebSocket是一种在单个TCP连接上进行全双工通信的协议。它提供了浏览器和服务器之间的双向通信渠道,使得服务器可以主动向客户端发送数据,而无需客户端发起请求。这对于需要实时更新的应用程序(如聊天室、在线游戏等)非常有用。

GoFrame对WebSocket的支持

GoFrame提供了对WebSocket的原生支持,使得在框架中使用WebSocket变得非常简单。具体来说,GoFrame提供了以下功能:

  1. WebSocket服务器:通过ghttp.Server对象的WebSocket方法,可以快速创建一个WebSocket服务器。
  2. WebSocket客户端:通过ghttp.WebSocket对象,可以方便地创建WebSocket客户端并与服务器建立连接。
  3. 消息发送与接收:GoFrame提供了直观的API用于在服务器和客户端之间发送和接收消息。

创建WebSocket服务器

在GoFrame中创建WebSocket服务器非常简单。以下是一个示例:

package main

import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
    "github.com/gogf/gf/v2/os/gctx"
)

func main() {
    ctx := gctx.New()
    s := g.Server()
    s.BindHandler("/ws", func(r *ghttp.Request) {
       ws, err := r.WebSocket()
       if err != nil {
          g.Log().Error(ctx, err)
          return
       }
       defer ws.Close()

       for {
          msgType, msg, err := ws.ReadMessage()
          if err != nil {
             return
          }
          if err = ws.WriteMessage(msgType, msg); err != nil {
             return
          }
       }
    })
    s.SetPort(8399)
    s.Run()
}

在上述代码中,我们创建了一个ghttp.Server对象,并通过BindHandler方法绑定了一个WebSocket处理函数。在处理函数中,我们首先通过r.WebSocket()获取了一个WebSocket连接对象。然后,我们在一个无限循环中不断读取客户端发送的消息,并将其原样发送回客户端。

创建WebSocket客户端

在客户端,我们可以使用JavaScript创建一个WebSocket对象,并与服务器建立连接。以下是一个示例:

<!DOCTYPE html>
<html>
<head>
    <title>WebSocket Client</title>
</head>
<body>
    <script>
        const socket = new WebSocket('ws://localhost:8199/ws');
        
        socket.onopen = function(e) {
            console.log('连接已建立');
            socket.send('Hello, server!');
        };
        
        socket.onmessage = function(event) {
            console.log('收到消息:', event.data);
        };
        
        socket.onclose = function(event) {
            console.log('连接已关闭');
        };
    </script>
</body>
</html>

在上述代码中,我们创建了一个WebSocket对象,并指定了服务器的地址。然后,我们监听了几个重要的事件:

  • onopen:连接建立时触发。我们可以在这里向服务器发送一条消息。

  • onmessage:收到服务器消息时触发。我们可以在这里处理收到的消息。

  • onclose:连接关闭时触发。我们可以在这里执行一些清理操作。

消息发送与接收

在服务器端,我们可以使用ws.ReadMessage()方法读取客户端发送的消息,使用ws.WriteMessage()方法向客户端发送消息。这两个方法的签名如下:

func (ws *WebSocket) ReadMessage() (messageType int, p []byte, err error)
func (ws *WebSocket) WriteMessage(messageType int, data []byte) error

其中,messageType表示消息的类型(文本消息或二进制消息),data表示消息的内容。

在客户端,我们可以使用socket.send()方法向服务器发送消息,使用onmessage事件处理函数接收服务器发送的消息。

连接的并发

在处理WebSocket连接的并发问题时,我们需要考虑以下几个方面:

Goroutine的使用

对于每个WebSocket连接,我们都应该启动一个独立的Goroutine来处理该连接的读写操作。这样可以确保每个连接都有自己的执行线程,不会相互阻塞。示例代码如下:

s.BindHandler("/ws", func(r *ghttp.Request) {
    ws, err := r.WebSocket()
    if err != nil {
        g.Log().Error(ctx, err)
        return
    }
    
    go func() {
        defer ws.Close()
        // 处理WebSocket连接的读写操作
        // ...
    }()
})

在上述代码中,我们在WebSocket处理函数中启动了一个新的Goroutine,并在其中处理WebSocket连接的读写操作。

同步和互斥

如果多个Goroutine需要访问共享的数据(如连接池、消息队列等),我们就需要使用同步原语(如互斥锁、读写锁等)来保证数据的一致性和线程安全。GoFrame提供了gmlock包,其中包含了常用的同步原语。示例代码如下:

import "github.com/gogf/gf/v2/os/gmlock"

var (
    connPool = make(map[string]*ghttp.WebSocket)
    mu       = gmlock.New()
)

func addConn(id string, ws *ghttp.WebSocket) {
    mu.Lock()
    connPool[id] = ws
    mu.Unlock()
}

func removeConn(id string) {
    mu.Lock()
    delete(connPool, id)
    mu.Unlock()
}

在上述代码中,我们使用了一个map来存储所有的WebSocket连接,并使用gmlock.Mutex来保护对map的并发访问。

消息的异步发送

在某些场景下,我们可能需要向多个WebSocket连接广播消息。如果使用同步的方式发送消息,可能会阻塞发送线程,影响性能。因此,我们可以考虑使用异步的方式发送消息。示例代码如下:

func broadcastMessage(ctx context.Context, id string, message []byte) {
    mu.RLock(id)
    defer mu.RUnlock(id)

    for _, ws := range connPool {
       go func(ws *ghttp.WebSocket) {
          if err := ws.WriteMessage(websocket.TextMessage, message); err != nil {
             g.Log().Error(ctx, err)
          }
       }(ws)
    }
}

在上述代码中,我们使用了一个读锁来保护对connPool的读取操作,然后遍历所有的WebSocket连接,并为每个连接启动一个Goroutine来异步地发送消息。

心跳机制

我们可以利用 GoFrame 提供的定时器功能来定期发送心跳消息。下面是一个完整的示例:

package main

import (
    "context"
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
    "github.com/gogf/gf/v2/os/gctx"
    "time"
)

func main() {
    ctx := gctx.New()
    s := g.Server()
    s.BindHandler("/ws", func(r *ghttp.Request) {
       ws, err := r.WebSocket()
       if err != nil {
          g.Log().Error(ctx, err)
          return
       }

       // 启动心跳goroutine
       go heartbeat(ctx, ws)

       for {
          msgType, msg, err := ws.ReadMessage()
          if err != nil {
             // 如果读取消息失败,可能是连接已经关闭,需要退出循环
             break
          }
          if msgType == ghttp.WsMsgPing {
             // 如果收到ping消息,则返回pong消息
             if err = ws.WriteMessage(ghttp.WsMsgPong, []byte{}); err != nil {
                break
             }
          } else {
             // 处理其他类型的消息
             g.Log().Info(ctx, string(msg))
          }
       }

       // 连接关闭时的清理工作
       ws.Close()
    })
    s.SetPort(8199)
    s.Run()
}

func heartbeat(ctx context.Context, ws *ghttp.WebSocket) {
    // 创建一个定时器,每隔10秒发送一次心跳
    ticker := time.NewTicker(10 * time.Second)
    defer ticker.Stop()

    for {
       select {
       case <-ticker.C:
          // 发送ping消息
          if err := ws.WriteMessage(ghttp.WsMsgPing, []byte("heartbeat")); err != nil {
             // 如果发送失败,可能连接已经关闭,需要退出心跳goroutine
             g.Log().Error(ctx, err)
             return
          }
       }
    }
}

在上述代码中,我们创建了一个 WebSocket 服务器,并为每个连接启动了一个心跳 Goroutine。在心跳 Goroutine 中,我们使用 time.NewTicker 创建了一个定时器,每隔 10 秒发送一次 ping 消息。如果发送失败,表明连接可能已经关闭,需要退出心跳 Goroutine。

同时,在主循环中,如果收到 ping 消息,我们需要返回一个 pong 消息,以完成一次心跳交互。如果读取消息失败,表明连接可能已经关闭,需要退出主循环。 在客户端,我们可以使用以下代码来配合服务器的心跳机制:

const socket = new WebSocket('ws://localhost:8199/ws');

socket.onopen = function(event) {
    console.log('WebSocket connected');
};

socket.onmessage = function(event) {
    if (event.data instanceof Blob) {
        // 处理二进制消息
        reader = new FileReader();
        reader.onload = function() {
            console.log('Received binary message: ' + this.result);
        };
        reader.readAsText(event.data);
    } else {
        // 处理文本消息
        console.log('Received message: ' + event.data);
    }
};

socket.onclose = function(event) {
    console.log('WebSocket closed');
};

// 当收到ping消息时,自动返回pong消息
socket.addEventListener('ping', function(event) {
    console.log('Received ping, sending pong');
    socket.pong();
});
在客户端代码中,我们监听了 WebSocket`ping` 事件。当收到 `ping` 消息时,我们自动返回一个 `pong` 消息,以完成一次心跳交互。

总结

GoFrame提供了对WebSocket的良好支持,使得在框架中使用WebSocket变得非常简单。通过几行代码,我们就可以创建一个WebSocket服务器,并与客户端建立实时通信。同时,GoFrame也提供了直观的API,用于在服务器和客户端之间发送和接收消息。