Golang :使用Gorilla Websockets包的方法

2,894 阅读5分钟

在本教程中,我们将研究如何在Golang中使用Gorilla Websocket包。

这个库为我们提供了在Go中编写websocket客户端/服务器的便利。它有一个websocket协议的工作生产质量Go实现,使我们能够使用websocket处理有状态的http连接。

主要的仓库代码位于Github上。

现在让我们了解一下,我们如何使用Gorilla快速建立一个可测试的websocket应用程序。


安装Gorilla Websocket Go包

除了工作中的Go编译器外,没有其他依赖性,所以你只需要使用go get!

go get github.com/gorilla/websocket


Websocket应用程序的设计

在继续讨论任何实例之前,我们先来设计一下需要做的事情的大致布局。

任何使用websocket协议的应用程序一般都需要一个客户端和一个服务器

服务器程序绑定到服务器上的一个端口,并开始监听任何websocket连接。连接相关的细节由websocket协议定义,它在原始HTTP连接上发挥作用。

客户端程序尝试使用websocket URL与服务器建立连接。请注意,客户端程序不需要用Golang实现,尽管Gorilla为我们提供了编写客户端的API。

如果你有一个使用独立前端的网络应用,一般来说,websocket客户端将用该语言实现(Javascript等)。

然而,为了说明问题,我们将用Go编写客户端和服务器程序。

现在让我们开始运行我们的客户端-服务器架构吧

我们将有一个用于服务器的程序server.go ,以及用于客户端的client.go


使用Gorilla Websockets - 创建我们的服务器

Websocket服务器将在普通的http服务器上实现。我们将使用net/http ,为原始HTTP连接提供服务。

现在,在server.go ,让我们编写我们的常规HTTP服务器,并添加一个socketHandler() 函数来处理websocket逻辑。

// server.go
package main

import (
    "log"
    "net/http"
    "time"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{} // use default options

func socketHandler(w http.ResponseWriter, r *http.Request) {
    // Upgrade our raw HTTP connection to a websocket based one
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Print("Error during connection upgradation:", err)
        return
    }
    defer conn.Close()

    // The event loop
    for {
        messageType, message, err := conn.ReadMessage()
        if err != nil {
            log.Println("Error during message reading:", err)
            break
        }
        log.Printf("Received: %s", message)
        err = conn.WriteMessage(messageType, message)
        if err != nil {
            log.Println("Error during message writing:", err)
            break
        }
    }
}

func home(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Index Page")
}

func main() {
    http.HandleFunc("/socket", socketHandler)
    http.HandleFunc("/", home)
    log.Fatal(http.ListenAndServe("localhost:8080", nil))
}

gorilla的神奇之处在于,通过连接升级,将这些原始HTTP连接转换成有状态的websocket连接。这就是为什么该库使用一个名为Upgraderstruct 来帮助我们完成这一工作。

我们使用一个全局升级器变量来帮助我们将任何传入的HTTP连接转换成websocket协议,通过upgrader.Upgrade() 。这将返回给我们一个*websocket.Connection ,我们现在可以用它来处理websocket连接。

服务器使用conn.ReadMessage() 读取信息,并使用 conn.WriteMessage()写回信息。

该服务器只需将任何传入的websocket消息回传给客户端,因此这表明websockets可用于全双工通信。

现在让我们来看看客户端的实现:client.go


创建我们的客户端程序

我们将使用Gorilla来编写客户端。这个简单的客户端将在每隔1秒后不断发射消息。如果我们的整个系统按计划工作,服务器将以1秒的间隔接收数据包,并回复同样的消息。

客户端也将具有接收传入的websocket数据包的功能。在我们的程序中,我们将有一个单独的goroutine处理程序receiveHandler ,该处理程序监听这些传入的数据包。

// client.go
package main

import (
    "log"
    "os"
    "os/signal"
    "time"

    "github.com/gorilla/websocket"
)

var done chan interface{}
var interrupt chan os.Signal

func receiveHandler(connection *websocket.Conn) {
    defer close(done)
    for {
        _, msg, err := connection.ReadMessage()
        if err != nil {
            log.Println("Error in receive:", err)
            return
        }
        log.Printf("Received: %s\n", msg)
    }
}

func main() {
    done = make(chan interface{}) // Channel to indicate that the receiverHandler is done
    interrupt = make(chan os.Signal) // Channel to listen for interrupt signal to terminate gracefully

    signal.Notify(interrupt, os.Interrupt) // Notify the interrupt channel for SIGINT

    socketUrl := "ws://localhost:8080" + "/socket"
    conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil)
    if err != nil {
        log.Fatal("Error connecting to Websocket Server:", err)
    }
    defer conn.Close()
    go receiveHandler(conn)

    // Our main loop for the client
    // We send our relevant packets here
    for {
        select {
        case <-time.After(time.Duration(1) * time.Millisecond * 1000):
            // Send an echo packet every second
            err := conn.WriteMessage(websocket.TextMessage, []byte("Hello from GolangDocs!"))
            if err != nil {
                log.Println("Error during writing to websocket:", err)
                return
            }

        case <-interrupt:
            // We received a SIGINT (Ctrl + C). Terminate gracefully...
            log.Println("Received SIGINT interrupt signal. Closing all pending connections")

            // Close our websocket connection
            err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
            if err != nil {
                log.Println("Error during closing websocket:", err)
                return
            }

            select {
            case <-done:
                log.Println("Receiver Channel Closed! Exiting....")
            case <-time.After(time.Duration(1) * time.Second):
                log.Println("Timeout in closing receiving channel. Exiting....")
            }
            return
        }

如果你观察代码,你会发现我创建了两个通道 doneinterrupt ,用于receiveHandler()main() 之间的通信。

我们使用一个无限循环,通过通道使用select来监听事件。我们每秒钟使用conn.WriteMessage() 写一条消息。如果中断信号被激活,任何悬而未决的连接就会被关闭,我们就会优雅地退出!

嵌套的select ,是为了确保两件事:

  • 如果receiveHandler 通道退出,通道'done' 将被关闭。这就是第一个case <-done 条件
  • 如果'done' 通道没有关闭,1秒后会有一个超时,所以程序会在1秒的超时后退出。

通过仔细处理所有使用通道的情况,select ,你可以有一个最小的架构,可以很容易地扩展。

希望你已经对如何在Golang中开始编写自己的WebSocket客户端/服务器程序有了一个很好的概念!这种风格在许多应用程序中都有使用。

Github中的许多开源项目也采用了同样的风格,因此你也可以在不同的软件库中探索,以了解这种架构在实际工作中是如何运作的。直到下一次!