571
社区成员




本项目主要使用Gin+Vue框架实现了一个多人在线聊天室,用户可以通过刷新、长轮询以及Websocket三种方式从聊天室获取消息。
Gin框架是Go世界里最流行的Web框架,Github上有32K+star。 基于httprouter开发的Web框架。 中文文档齐全,简单易用的轻量级框架。Gin是一个用Go语言编写的Web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍。
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
登陆页面,用户可以输入用户名,选择登陆方式。登陆方式主要分为Websocket、Long Polling以及Refresh,点击下方Login进行登录。
成功登录后进入聊天页面。页面左上角显示当前聊天室用户人数和登陆方式。
主体页面显示所有用户发送的信息以及输入框。输入消息按下回车即可发送消息。
// 聊天室
type Room struct {
users map[uid]chan Event // 当前房间订阅者
userCount int // 当前房间总人数
publishChn chan Event // 聊天室的消息推送入口
archive *list.List // 历史记录 todo 未持久化 重启失效
archiveChan chan chan []Event // 通过接受chan来同步聊天内容
joinChn chan chan Subscription // 接收订阅事件的通道 用户加入聊天室后要把历史事件推送给用户
leaveChn chan uid // 用户取消订阅通道 把通道中的历史事件释放并把用户从聊天室用户列表中删除
}
// 聊天室事件定义
type Event struct {
Type string `json:"type"` // 事件类型
User string `json:"user"` // 用户名
Timestamp int64 `json:"timestamp"` // 时间戳
Text string `json:"text"` // 事件内容
UserCount int `json:"userCount"` // 房间用户数
}
// 用户订阅
type Subscription struct {
Id string // 用户在聊天室中的ID
Username string // 用户名
Pipe <-chan Event // 事件接收通道 用户从这个通道接收消息
EmitCHn chan Event // 用户消息推送通道
LeaveChn chan uid // 用户离开事件推送
}
聊天室主要通过channel来同步信息。服务端维护一个通道来与用户进行交互,用户加入聊天室即订阅消息通道,聊天室需要把事件放入channel来让用户接收。
// 处理聊天室中的事件
func (r *Room) Serve() {
for {
select {
// 用户加入房间
case ch := <-r.joinChn:
chn := make(chan Event, chanSize)
r.userCount++
uid := uuid.New().String()
r.users[uid] = chn
ch <- Subscription{
Id: uid,
Pipe: chn,
EmitCHn: r.publishChn,
LeaveChn: r.leaveChn,
}
ev := NewEvent(EventTypeSystem, "", "")
ev.UserCount = r.userCount
for _, v := range r.users {
v <- ev
}
case arch := <-r.archiveChan:
var events []Event
//历史事件
for e := r.archive.Front(); e != nil; e = e.Next() {
events = append(events, e.Value.(Event))
}
arch <- events
// 有新的消息
case event := <-r.publishChn:
// 推送给所有用户
event.UserCount = r.userCount
for _, v := range r.users {
v <- event
}
// 推送消息后,限制本地只保存指定条历史消息
if r.archive.Len() >= archiveSize {
r.archive.Remove(r.archive.Front())
}
r.archive.PushBack(event)
// 用户退出房间
case k := <-r.leaveChn:
if _, ok := r.users[k]; ok {
delete(r.users, k)
r.userCount--
}
ev := NewEvent(EventTypeSystem, "", "")
ev.UserCount = r.userCount
for _, v := range r.users {
v <- ev
}
}
}
}
首先,ajax轮询的原理就是让浏览器每隔一定时间就向服务器发送一次请求,询问是否有新的信息。长轮询的原理类似,都是采用轮询的方式,不过采取了阻塞模型,也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回或超时,返回完之后,客户端再次建立连接,周而复始,基于事件的触发,一个事件接一个事件。
// 轮询获取指定时间戳之后的聊天记录
func (longPolling) Archive() gin.HandlerFunc {
return func(c *gin.Context) {
lastReceived, _ := strconv.ParseInt(c.Query("ts"), 10, 64)
var events []core.Event
// filter archive
for _, event := range Room.GetArchive() {
if event.Timestamp > lastReceived {
events = append(events, event)
}
}
c.JSON(http.StatusOK, events)
}
}
WebSocket是html5一种新的协议,实现了浏览器与服务器之间的全双工通信,能很好的节省服务器资源与带宽,并在服务器端与浏览器端实现实时通行,他建立在TCP之上, 同http一样,通过tcp来传输数据。只需要一次HTTP握手,所以说整个通讯过程是建立在一次连接/状态中,服务器端会知道连接的信息,知道客户端关闭请求,同时由服务器主动推送,当有信息需要发送时,直接发送。客户端的连接通过session对象存储,能够实现实时推送。
func (s *ws) Handle() gin.HandlerFunc {
return func(c *gin.Context) {
name := c.Query("name")
conn, err := s.upgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
panic(err)
}
// 加入房间
evs := Room.GetArchive()
Room.MsgJoin(name)
control := Room.Join(name)
defer control.Leave()
// 先把历史消息推送出去
for _, event := range evs {
if conn.WriteJSON(&event) != nil {
// 用户断开连接
return
}
}
// 开启通道监听用户事件然后发送给聊天室
newMessages := make(chan string)
go func() {
var res = struct {
Msg string `json:"msg"`
}{}
for {
err := conn.ReadJSON(&res)
if err != nil {
// 用户断开连接
close(newMessages)
return
}
newMessages <- res.Msg
}
}()
// 接收消息,在这里阻塞请求,循环退出就表示用户已经断开
for {
select {
case event := <-control.Pipe:
if conn.WriteJSON(&event) != nil {
// 用户断开连接
return
}
case msg, ok := <-newMessages:
// 断开连接
if !ok {
return
}
control.Say(msg)
}
}
}
}
methods: {
setUpSocket(socket) {
socket.onopen = () => {
this.$message({
type: "success",
message: "聊天室连接成功"
});
};
socket.onclose = () => {
this.$message({
type: "warning",
message: "连接断开"
});
this.socket = null;
};
socket.onmessage = event => {
let dt = JSON.parse(event.data);
switch (dt.type) {
case EventTypeMsg:
this.receiveMsg(dt);
this.userCount = dt.userCount;
console.log(this.userCount);
break;
case EventTypeSystem:
this.userCount = dt.userCount;
break;
}
};
},
onExit() {
window.location.href = "/";
},
clearInput() {
this.msg = "";
this.$refs.input.focus();
},
sendMessage() {
if (!this.msg) {
this.$refs.input.focus();
return;
}
const req = JSON.stringify({
msg: this.msg
});
this.socket &&
(this.socket.send(req),
(this.msg = ""),
this.$refs.input.focus());
},
receiveMsg(data) {
this.chatData.push(data);
}
}
作者:NP415
有源码吗大佬