Golang、WebSocket、Vue 搭建简易的多人聊天室

1,704 阅读2分钟

⒈ WebSocket 介绍

   往常的 web 应用中,要实现页面实时刷新,只能用 ajax 定期轮询。但毕竟 ajax 只是让页面看起来实时,实际上还是会有一些延迟。并且,ajax 需要频繁的向服务端发送 HTTP 请求,效率也不高。于是 WebSocket 便应运而生。

   WebSocket 可以理解为是一种升级之后的 HTTP 连接, 其生命周期直到服务端或客户端主动关闭连接为止。WebSocket 允许客户端与服务端之间在一个 TCP 长连接中进行双向通信,这大大减小了轮询的开销。

⒉ 数据结构定义

  • 首先需要记录用户与连接之间的映射
  • 然后需要有一个管道来发送消息
  • 还需要一个结构体来定义消息
  • 最后还需要一个 upgrader 将 HTTP 请求升级为 WebSocket 连接
import "github.com/gorilla/websocket"

type Message struct {
    User	string	`json:"user"`
    Info	string	`json:"message,omitempty"`
}

var Clients map[*websocket.Conn]string
var Broadcast chan Message
var upgrader websocket.Upgrader

func init() {
    upgrader = websocket.Upgrader{
		ReadBufferSize:    1024,
		WriteBufferSize:   1024,
		CheckOrigin: func(r *http.Request) bool {
			return true
		},
	}

    Clients = make(map[*websocket.Conn]string)
    Broadcast = make(chan Message)
}

⒊ 建立服务端

   建立服务端,首先需要定义路由。路由的定义在前述文章《golang 开发 REST 风格的 API》中已有提及,这里不再赘述,只记录连接的建立及响应过程。

import (
    "fmt"
    "log"
    "net/http"
)

func Chat(w http.ResponseWriter, r *http.Request) {
    ws, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Fatal(err)
    }
    defer ws.Close()

    var msg structure.Message

    for {
        err = ws.ReadJSON(&msg)
        if err != nil {
                log.Printf("error %v", err)
                delete(Clients, ws)
                break
        }
        if _, ok := Clients[ws]; !ok {
                Clients[ws] = msg.User
        }
        fmt.Printf("%+v", Clients)
        fmt.Println("服务端接收消息", msg)
        if msg.Info == "" {
                continue
        }

        Broadcast <- msg
    }
}
  • 在从客户端接受消息的过程当中,如果发生异常,则关闭连接并将该连接以及对应的用户从映射表中删除
  • 在客户端第一次建立连接时,此时映射表中并不存在该连接和用户的映射关系,所以需要添加映射关系
  • 客户端发送的空消息不进行分发

⒋ 消息分发

   聊天室中,每一个用户发送的消息都需要同步给其他用户。所以,这就需要一个单独的 goroutine 来监听消息管道,分发消息。

func DistributeMsg() {
    fmt.Println("distribute")
    for {
        msg := <- Broadcast
        fmt.Println("准备向客户端发送消息", msg)

        for ws, user := range Clients {
            fmt.Println(user)
            if user == msg.User {
                continue
            }
            err := ws.WriteJSON(msg)
            if err != nil {
                fmt.Printf("error %v", err)
                _ = ws.Close()
                fmt.Printf("user %s disconnected", Clients[ws])
                delete(Clients, ws)
            }
        }
    }
}

   在消息分发的过程中,同一个用户的消息不给自己分发;另外,在向客户端发送消息的过程中,如果发生异常,同样需要关闭连接并从映射表中删除响应的连接和用户的映射关系。

⒌ 建立客户端

   客户端使用 Vue 实现,同时借助了 ElementUi 组件。

    import {Notification} from 'element-ui';
    let base_url = 'ws://127.0.0.1:8081/ws';

    export default {
        name: 'ChatRoom',
        data(){
            return {
                nickname: '',
                message: '',
                messages: '',
                socket: null,
            }
        },
        methods: {
            // 发送消息
            send() {
                let info = this.nickname + ':' + this.message + '\n';
                let data = {
                    user: this.nickname,
                    message: this.message
                };
                this.socket.send(JSON.stringify(data));
                this.messages += info;
                this.message = '';
            },
            // 加入聊天室
            join() {
                this.$prompt('请输入昵称', '提示', {
                    confirmButtonText: '确定',
                    inputPlaceholder: '请输入昵称',
                    inputErrorMessage: '昵称不能为空',
                    inputValidator: function ($event) {
                    return $event.length > 0
                    }
                }).then(({ value }) => {
                    this.nickname = value;
                    // 发起 websocket 连接
                    this.createWebSocket();
                }).catch(() => {
                    console.log('取消输入')
                })
            },
            // 客户端关闭连接
            disconnect() {
                if (this.socket !== undefined && this.socket !== null) {
                    this.socket.close();
                }
                this.socket = null;
                this.nickname = '';
            },
            // 对浏览器刷新时间的响应
            leaving($event) {
                console.log($event);
                console.log(this.socket);
                if (this.socket !== undefined && this.socket !== null) {
                    this.socket.close();
                }
            },
            // 客户端建立 WebSocket 连接
            createWebSocket() {
                if (this.socket === null) {
                    this.socket = new WebSocket(base_url)
                }

                this.socket.onopen = (event) => {
                    console.log(event);
                    console.log('connected');
                    Notification.success('连接已建立');
                    // 初次建立连接,发送昵称
                    let data = {user: this.nickname};
                    this.socket.send(JSON.stringify(data));
                };

                this.socket.onmessage = (event) => {
                    console.log(event);
                    let data = JSON.parse(event.data)
                    let info = data.user + ':' + data.message + '\n';
                    Notification.info(info);
                    //    消息内容处理
                    this.messages += info
                };

                this.socket.onerror = (event) => {
                    console.log(event);
                    console.log('error');
                    Notification.error('服务端运行发生异常');
                };

                this.socket.onclose = (event) => {
                    console.log(event);
                    console.log('close');
                    Notification.info('服务端关闭连接');
                    this.nickname = '';
                    this.socket = null;
                };
            },
        },
        created() {
            // 监听浏览器刷新/关闭事件
            window.addEventListener('beforeunload', this.leaving);
        }
    }

   这里需要注意,浏览器刷新/关闭都会引起服务端异常,所以需要监听浏览器的刷新/关闭事件,在浏览器刷新/关闭时,关闭 WebSocket 连接。

   篇幅限制,这里只是部分代码,完整代码见 Github 仓库