⒈ 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 仓库。