使用 Go 和 Vue.js 构建一个简单的 WebSocket 聊天应用程序
在本教程中,我们将构建一个功能齐全的聊天应用程序,服务器部分将使用 Go 中的 WebSockets 构建,在前端我们将利用 Vue.js 创建一个简单的界面。
我们从简单的开始,享受构建一个功能齐全的聊天应用程序的乐趣。我计划很快发布一些后续文章,解释如何添加更多功能。
先决条件
确保你的 Go 环境已设置好。如果没有,请参考此处的官方文档。本教程要求你具备 Go 语法和 JavaScript/Vue.js 的一些基础知识。
步骤 1:设置 WebSocket 服务器
第一步,我们将启动 WebSocket 服务器。为了实现 WebSocket,我们将使用 gorilla/WebSocket 包。要安装此包,请在项目文件夹中的控制台中粘贴以下内容:
go get github.com/gorilla/websocket
客户
好了,现在让我们添加 client.go,这个文件将代表服务器端的 WebSocket 客户端。
我们从最基本的开始,定义一个 Client 类型来保存连接。然后暴露一个 ServeWs() 函数来创建 Websocket 连接,并使用 newClient() 创建 Client 结构体。
//client.go
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 4096,
WriteBufferSize: 4096,
}
// Client represents the websocket client at the server
type Client struct {
// The actual websocket connection.
conn *websocket.Conn
}
func newClient(conn *websocket.Conn) *Client {
return &Client{
conn: conn,
}
}
// ServeWs handles websocket requests from clients requests.
func ServeWs(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := newClient(conn)
fmt.Println("New Client joined the hub!")
fmt.Println(client)
}
升级器用于将HTTP服务器连接升级到WebSocket协议。该函数的返回值是一个WebSocket连接。
主要
好了,现在我们需要一个简单的 HTTP 服务器来处理来自客户端的请求并将它们传递给 ServeWs 函数。
首先创建一个 HTTP 服务器,监听通过参数指定的端口,或者默认的 :8080。对端点“/ws”的请求将由我们的 ServeWs 函数处理。
//main.go
package main
import (
"flag"
"log"
"net/http"
)
var addr = flag.String("addr", ":8080", "http server address")
func main() {
flag.Parse()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
ServeWs(w, r)
})
log.Fatal(http.ListenAndServe(*addr, nil))
}
这就是服务器部分,至少目前是这样...接下来,我们将尝试使用客户端连接到 WebSocket 服务器。
步骤 2:创建前端
前端将保持简单,首先,我们将添加一个带有一些外部依赖项的 index.html。
<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Chat</title>
<!-- Load required Bootstrap and BootstrapVue CSS -->
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap/dist/css/bootstrap.min.css" />
<link type="text/css" rel="stylesheet" href="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.css" />
<!-- Load polyfills to support older browsers -->
<script src="//polyfill.io/v3/polyfill.min.js?features=es2015%2CIntersectionObserver" crossorigin="anonymous"></script>
<!-- Load Vue followed by BootstrapVue -->
<script src="https://unpkg.com/vue"></script>
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue.min.js"></script>
<!-- Load the following for BootstrapVueIcons support -->
<script src="//unpkg.com/bootstrap-vue@latest/dist/bootstrap-vue-icons.min.js"></script>
</head>
<body>
<div id="app">
</div>
</body>
<script src="assets/app.js"></script>
</html>
然后在 /public/assets 文件夹中创建一个 app.js 文件。在这个文件中,我们暂时做三件事:
- 使用 new Vue() 创建 Vue.js 应用程序
- 创建与服务器的 WebSocket 连接
- 监听 WebSocket 打开事件,这表明我们已建立连接。
// public/assets/app.js
var app = new Vue({
el: '#app',
data: {
ws: null,
serverUrl: "ws://localhost:8080/ws"
},
mounted: function() {
this.connectToWebsocket()
},
methods: {
connectToWebsocket() {
this.ws = new WebSocket( this.serverUrl );
this.ws.addEventListener('open', (event) => { this.onWebsocketOpen(event) });
},
onWebsocketOpen() {
console.log("connected to WS!");
}
}
})
好了,现在让我们确保 Go 提供我们的静态文件,打开 main.go 文件并在 listenAndServe 调用之前添加以下几行:
...
fs := http.FileServer(http.Dir("./public"))
http.Handle("/", fs)
log.Fatal(http.ListenAndServe(*addr, nil))
测试连接
现在你应该能够从浏览器与 Go 服务器建立 WebSocket 连接了。使用以下命令从终端启动服务器:
go run ./
打开浏览器,访问http://localhost:8080。如果一切正常,你应该会看到一条 console.log 消息,告诉你已连接到 WebSocket!你还可以查看控制台的“网络”选项卡,查看待处理的 WebSocket 连接:
同时,在您启动 Go 程序的终端中,您应该会看到一条带有消息“新客户端加入了中心!”的日志。
步骤 3:发送和接收消息
好的,连接已建立……让我们确保能够与连接的客户端发送和接收消息。为了在服务器上跟踪已连接的客户端,我们添加了一个名为 chatServer.go 的新文件。
该文件包含一个 WsServer 类型,该类型包含一个用于服务器中注册的客户端的映射。它还包含两个通道,一个用于注册请求,一个用于注销请求。
package main
type WsServer struct {
clients map[*Client]bool
register chan *Client
unregister chan *Client
}
// NewWebsocketServer creates a new WsServer type
func NewWebsocketServer() *WsServer {
return &WsServer{
clients: make(map[*Client]bool),
register: make(chan *Client),
unregister: make(chan *Client),
}
}
// Run our websocket server, accepting various requests
func (server *WsServer) Run() {
for {
select {
case client := <-server.register:
server.registerClient(client)
case client := <-server.unregister:
server.unregisterClient(client)
}
}
}
func (server *WsServer) registerClient(client *Client) {
server.clients[client] = true
}
func (server *WsServer) unregisterClient(client *Client) {
if _, ok := server.clients[client]; ok {
delete(server.clients, client)
}
}
Run() 函数将无限运行并监听通道。新的请求会自动出现,并由专门的函数处理。目前,这只是将客户端添加到映射中或将其移除。
主要
此后我们必须更新 main.go 文件并:
- 创建一个新的 WsServer
- 在 Go 例程中运行
- 将服务器传递给 ServeWs 函数
wsServer := NewWebsocketServer()
go wsServer.Run()
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
ServeWs(wsServer, w, r)
})
Client.go
下一步涉及客户端文件。首先,我们修改类型结构体,希望为每个客户端保留对 WsServer 的引用。我们也可以通过将新创建的客户端推送到注册通道来在服务器中注册客户端。在注册服务器之前,我们启动了两个 goroutine,我们将在下面定义它们。
// Client represents the websocket client at the server
type Client struct {
// The actual websocket connection.
conn *websocket.Conn
wsServer *WsServer
}
func newClient(conn *websocket.Conn, wsServer *WsServer) *Client {
return &Client{
conn: conn,
wsServer: wsServer,
}
}
// ServeWs handles websocket requests from clients requests.
func ServeWs(wsServer *WsServer, w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
return
}
client := newClient(conn, wsServer)
go client.writePump()
go client.readPump()
wsServer.register <- client
}
修改类型声明、newClient() 和 ServeWs() 函数。
readPump
在 readPump Goroutine 中,客户端将读取通过 WebSocket 连接发送的新消息。它将无限循环执行此操作,直到客户端断开连接。当连接关闭时,客户端将调用其自身的 disconnect 方法进行清理。
//import statements
const (
// Max wait time when writing message to peer
writeWait = 10 * time.Second
// Max time till next pong from peer
pongWait = 60 * time.Second
// Send ping interval, must be less then pong wait time
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 10000
)
....
func (client *Client) readPump() {
defer func() {
client.disconnect()
}()
client.conn.SetReadLimit(maxMessageSize)
client.conn.SetReadDeadline(time.Now().Add(pongWait))
client.conn.SetPongHandler(func(string) error { client.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
// Start endless read loop, waiting for messages from client
for {
_, jsonMessage, err := client.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
log.Printf("unexpected close error: %v", err)
}
break
}
client.wsServer.broadcast <- jsonMessage
}
}
收到新消息后,客户端会将其推送到 WsServer 广播通道。我们将在下面创建此通道。首先,我们通过添加 WritePump 方法来完成客户端。
WritePump
writePump goroutine 负责将消息发送到已连接的客户端。它会无限循环地等待 client.send 通道中的新消息。收到新消息后,它会将消息写入客户端。如果有多条可用消息,则会将它们合并到一次写入操作中。
...
var (
newline = []byte{'\n'}
space = []byte{' '}
)
...
func (client *Client) writePump() {
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
client.conn.Close()
}()
for {
select {
case message, ok := <-client.send:
client.conn.SetWriteDeadline(time.Now().Add(writeWait))
if !ok {
// The WsServer closed the channel.
client.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
w, err := client.conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(message)
// Attach queued chat messages to the current websocket message.
n := len(client.send)
for i := 0; i < n; i++ {
w.Write(newline)
w.Write(<-client.send)
}
if err := w.Close(); err != nil {
return
}
case <-ticker.C:
client.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
writePump 还负责通过按照 pingPeriod 指定的间隔向客户端发送 ping 消息来保持连接处于活动状态。如果客户端没有响应 pong 消息,则连接将被关闭。
连接 WsServer
我们在 Go 应用程序中需要做的最后一件事是在 WsServer 中创建广播频道。
type WsServer struct {
...
broadcast chan []byte
}
func NewWebsocketServer() *WsServer {
return &WsServer{
...
broadcast: make(chan []byte),
}
}
func (server *WsServer) Run() {
for {
select {
...
case message := <-server.broadcast:
server.broadcastToClients(message)
}
}
}
func (server *WsServer) broadcastToClients(message []byte) {
for client := range server.clients {
client.send <- message
}
}
广播通道监听客户端 readPump 发送的消息,并把这些消息推送到所有已注册客户端的发送通道中。
步骤 4:创建聊天窗口
在最后一步中,我们将创建聊天窗口来发送和显示消息!
首先更新你的 index.html,在 之间添加以下内容<div id=”app”></div>
。这将显示每条消息,并提供一个文本区域来提交新消息。
<div class="container-fluid h-100">
<div class="row justify-content-center h-100">
<div class="col-md-8 col-xl-6 chat">
<div class="card">
<div class="card-header msg_head">
<div class="d-flex bd-highlight justify-content-center">
Chat
</div>
</div>
<div class="card-body msg_card_body">
<div
v-for="(message, key) in messages"
:key="key"
class="d-flex justify-content-start mb-4"
>
<div class="msg_cotainer">
{{message.message}}
<span class="msg_time"></span>
</div>
</div>
</div>
<div class="card-footer">
<div class="input-group">
<textarea
v-model="newMessage"
name=""
class="form-control type_msg"
placeholder="Type your message..."
@keyup.enter.exact="sendMessage"
></textarea>
<div class="input-group-append">
<span class="input-group-text send_btn" @click="sendMessage"
>></span
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
要获得一些基本样式,您可以在 public/assets/ 中添加 style.css 文件。本例中使用以下样式表:https://github.com/jeroendk/...。
App.js
现在我们完成 VueJs 组件,确保聊天窗口正常工作。首先,添加两个新的数据属性(messages 和 newMessage)。
然后为 WebSocket 连接上的 message 事件添加一个事件监听器。最后添加两个函数,一个用于处理通过连接接收到的新消息(记住一次可以接收多条消息),另一个用于从文本区域发送消息。
data: {
...
messages: [],
newMessage: ""
},
connectToWebsocket() {
...
this.ws.addEventListener('message', (event) => { this.handleNewMessage(event) });
},
handleNewMessage(event) {
let data = event.data;
data = data.split(/\r?\n/);
for (let i = 0; i < data.length; i++) {
let msg = JSON.parse(data[i]);
this.messages.push(msg);
}
}
sendMessage() {
if(this.newMessage !== "") {
this.ws.send(JSON.stringify({message: this.newMessage}));
this.newMessage = "";
}
}
就这样!现在,当您在浏览器中访问http://localhost:8080时,您应该能够发送和接收聊天消息了。
下一步是什么?
您可以在发送消息之前询问用户的姓名,然后在消息中显示他/她的姓名。您可以将任何信息添加到传递给 WebSocket 的消息对象中。
另外,请继续关注计划中的后续帖子:
- 多房间和一对一聊天。
- 使用 Redis Pub/Sub 实现可扩展性。
- 添加身份验证并允许用户登录。
该部分的最终源代码可以在这里找到:
https://github.com/jeroendk/go-vuejs-chat/tree/v1.0