不就是个聊天室,我从头撸给你看:1.实现一个最简单的Demo
前言
Hello,我是单木。接下来我将会开启一个新的博客系列,使用 GoLang 从 0 到 1 实现一个IM聊天室项目。在上一篇文章中,我们已经成功的搭建好了项目的整体框架,废话少说,接下来让我们正式开始聊天室的开发。在这篇文章中,我们将要完成聊天室最基本的需求:让一个用户发送的内容能够被其他用户接收。
需求分析
接手到一个需求,我们都应该首先进行一个简单的调研,看看有哪些方式可以实现我们的目的。
技术选型
让我们从最常用的 HTTP 请求开始,作为一个实时聊天室项目,第一个要求就是我们的用户发出的信息必须能够立刻到达接收方,很不幸,常见的 HTTP 通常以请求-响应为一次整体,必须要求客户端先向服务器发起请求,服务端才能将消息包装在响应中返回,对于发送方,服务器可以把信息包装在发送消息的响应中,但是对于接受方来说,服务器缺少直接推送消息的方式。要解决这个问题也很简单,只需要让接收方不断的请求服务器即可,如果有消息,就将消息使用响应返回。这个方式被称为短轮询。但是,这样存在一个显然的问题,通常来说,包含有消息的响应的比例将会是非常小的,有大量的 CPU 资源都被无效的空响应消耗了。并且随着客户端数量的增加,服务器所需要处理的请求也会随之增加,因此这种方式只适用于少量连接的情况。那么有什么方法能够解决这个问题呢,这个问题的实质是由于服务器需要处理大量无效的空请求,那么只要减少空请求的数量,就可以有效的缓解这个问题,为此,一种叫做长轮询的方式被提了出来。客户端依然对服务器进行轮询,但是如果当前没有新消息的到达,那么服务器不会立刻返回空响应,而是阻塞一段时间,在返回响应,这样子就可以减少单位时间内的响应数量。当然,这种方法终究是治标不治本,有没有什么更完美的解决方案呢?有!WebSocket协议就可以完美的解决这一个问题。WebSocket 协议是一种基于 TCP 协议的通信协议,它可以在客户端和服务器之间建立双向通信的连接,实现实时数据传输和交互操作。在Web应用程序中,WebSocket 协议可以替代 HTTP 协议的长轮询和短轮询技术,提供更高效和快速的通信方式。由于 DiTing 的目标是实现一个多人同时在线的聊天室项目,因此对于通信有着较高的要求,我决定使用 WebSocket 协议作为向客户端推送消息的基本协议。
需求分析
接下来,让我们来思考一下,我们需要如何实现**让一个用户发送的内容能够被其他用户接收。**在上一节中,已经确定了采用 WebSocket 与用户进行通讯,那么每一个用户应该都有一个对应的 WebSocket 连接,同时在我们需要维护一个包含所有用户的 WebSocket 列表,当有新消息到来的时候,就遍历这个列表,像所有人发送这一个消息。
具体实现
安装必要依赖
安装 gorilla 以获得 WebSocket 的支持
go get -u -v github.com/gorilla/websocket
模型层
<!-- model/user.go -->
// User 定义一个简单的用户结构体
type User struct {
Conn *websocket.Conn
Msg chan []byte
}
<!-- model/hub.go -->
package models
// 初始化处理中心,以便调用
var Users = &Hub{
// 所有用户的列表
userList: make(map[*User]bool),
// 用于注册新用户的chan
Register: make(chan *User),
// 用于下线用户的chan
Unregister: make(chan *User),
// 用来做消息广播的chan
Broadcast: make(chan []byte),
}
type Hub struct {
//用户列表,保存所有用户
userList map[*User]bool
//注册chan,用户注册时添加到chan中
Register chan *User
//注销chan,用户退出时添加到chan中,再从map中删除
Unregister chan *User
//广播消息,将消息广播给所有连接
Broadcast chan []byte
}
// 处理中心处理获取到的信息
func (h *Hub) Run() {
for {
select {
//从注册chan中取数据
case user := <-h.Register:
//取到数据后将数据添加到用户列表中
h.userList[user] = true
case user := <-h.Unregister:
//从注销列表中取数据,判断用户列表中是否存在这个用户,存在就删掉
if _, ok := h.userList[user]; ok {
delete(h.userList, user)
}
case data := <-h.Broadcast:
//从广播chan中取消息,然后遍历给每个用户,发送到用户的msg中
for u := range h.userList {
select {
case u.Msg <- data:
default:
delete(h.userList, u)
close(u.Msg)
}
}
}
}
}
路由层
在这里,我们实现业务的主要逻辑。现在,项目中包含有两个服务,一个是 Gin ,用于提供 HTTP 相关的功能,一个是 WebSocket 用于建立 WebSocket 连接,需要注意的是,由于 HTTP 和 WebSocket 底层都依赖于 TCP ,因此需要分别设置两个不同的端口。不同的服务需要调用不同的端口,并且这两个服务需要运行在不同的线程中,避免相互阻塞。对于 WebSocket ,还需要额外设置一个升级器,这是由于 WebSocke t建立连接时,会首先通过一个 HTTP 请求进行连接升级,我们可以通过升级器进行一些请求校验等操作。如果想要了解具体细节,可以看看这篇文章
<!-- router/init_router.go -->
package routes
import (
"DiTing-Go/models"
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"log"
"net/http"
)
// 定义一个升级器,将普通的http连接升级为websocket连接
var upgrader = &websocket.Upgrader{
//定义读写缓冲区大小
WriteBufferSize: 1024,
ReadBufferSize: 1024,
//校验请求
CheckOrigin: func(r *http.Request) bool {
//如果不是get请求,返回错误
if r.Method != "GET" {
fmt.Println("请求方式错误")
return false
}
//还可以根据其他需求定制校验规则
return true
},
}
// 处理websocket请求
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()
//连接成功后注册用户
user := &models.User{
Conn: conn,
Msg: make(chan []byte),
}
models.Users.Register <- user
//得到连接后,就可以开始读写数据了
go read(user)
write(user)
}
func read(user *models.User) {
//从连接中循环读取信息
for {
_, msg, err := user.Conn.ReadMessage()
if err != nil {
fmt.Println("用户退出:", user.Conn.RemoteAddr().String())
models.Users.Unregister <- user
break
}
//将读取到的信息传入websocket处理器中的broadcast中,
models.Users.Broadcast <- msg
}
}
func write(user *models.User) {
for data := range user.Msg {
err := user.Conn.WriteMessage(1, data)
if err != nil {
fmt.Println("写入错误")
break
}
}
}
// InitRouter 初始化路由
func InitRouter() {
// 在不同的线程中运行,否则将会被阻塞
go initWebSocket()
initGin()
}
// 初始化websocket
func initWebSocket() {
// 负责执行消息广播
go models.Users.Run()
http.HandleFunc("/socket", socketHandler)
log.Fatal(http.ListenAndServe("localhost:8080", nil))
}
// 初始化gin
func initGin() {
router := gin.Default()
router.GET("/", func(ctx *gin.Context) {
ctx.JSON(200, gin.H{
"code": 0,
"msg": "ok",
})
})
err := router.Run(":5000")
if err != nil {
return
}
}
主程序
<!-- main.go -->
package main
import "DiTing-Go/routes"
func main() {
routes.InitRouter()
}
测试
接下来,我们需要对程序进行一些简单的测试,由于需要建立 WebSocket 连接,这里采用 Postman 进行测试。如下图建立两个 WebSocket 连接,然后把 DiTing 运行起来
使用 Postman 分别发送两个消息,可以看到两个客户端可以分别接受到对方发送的消息,测试完成
总结
在这篇博客中,我们实现了一个最最基本的聊天室框架,当然,这个框架还有非常多不完善的地方,我们将在接下来逐步进行完善,敬请期待。
点关注,不迷路
好了,以上就是这篇文章的全部内容了,如果你能看到这里,非常感谢你的支持!如果你觉得这篇文章写的还不错, 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!!白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!如果本篇博客有任何错误,请批评指教,不胜感激 !本文的 Github,欢迎各位人才快快用Star砸倒我。如果想要加入这个项目或者有任何建议,欢迎联系
转载自:https://juejin.cn/post/7352555787300339746