掘金 后端 ( ) • 2024-04-01 13:50

theme: condensed-night-purple

前言

Hello,我是单木。接下来我将会开启一个新的博客系列,使用 GoLang 从 0 到 1 实现一个IM聊天室项目。在上一篇文章中,我们已经成功的搭建好了项目的整体框架,废话少说,接下来让我们正式开始聊天室的开发。在这篇文章中,我们将要完成聊天室最基本的需求:让一个用户发送的内容能够被其他用户接收。

需求分析

接手到一个需求,我们都应该首先进行一个简单的调研,看看有哪些方式可以实现我们的目的。

技术选型

让我们从最常用的 HTTP 请求开始,作为一个实时聊天室项目,第一个要求就是我们的用户发出的信息必须能够立刻到达接收方,很不幸,常见的 HTTP 通常以请求-响应为一次整体,必须要求客户端先向服务器发起请求,服务端才能将消息包装在响应中返回,对于发送方,服务器可以把信息包装在发送消息的响应中,但是对于接受方来说,服务器缺少直接推送消息的方式。要解决这个问题也很简单,只需要让接收方不断的请求服务器即可,如果有消息,就将消息使用响应返回。这个方式被称为短轮询
但是,这样存在一个显然的问题,通常来说,包含有消息的响应的比例将会是非常小的,有大量的 CPU 资源都被无效的空响应消耗了。并且随着客户端数量的增加,服务器所需要处理的请求也会随之增加,因此这种方式只适用于少量连接的情况。那么有什么方法能够解决这个问题呢,这个问题的实质是由于服务器需要处理大量无效的空请求,那么只要减少空请求的数量,就可以有效的缓解这个问题,为此,一种叫做长轮询的方式被提了出来。客户端依然对服务器进行轮询,但是如果当前没有新消息的到达,那么服务器不会立刻返回空响应,而是阻塞一段时间,在返回响应,这样子就可以减少单位时间内的响应数量。
当然,这种方法终究是治标不治本,有没有什么更完美的解决方案呢?有!WebSocket协议就可以完美的解决这一个问题。WebSocket 协议是一种基于 TCP 协议的通信协议,它可以在客户端和服务器之间建立双向通信的连接,实现实时数据传输和交互操作。在Web应用程序中,WebSocket 协议可以替代 HTTP 协议的长轮询和短轮询技术,提供更高效和快速的通信方式。
由于 DiTing 的目标是实现一个多人同时在线的聊天室项目,因此对于通信有着较高的要求,我决定使用 WebSocket 协议作为向客户端推送消息的基本协议。

image.png

需求分析

接下来,让我们来思考一下,我们需要如何实现**让一个用户发送的内容能够被其他用户接收。**在上一节中,已经确定了采用 WebSocket 与用户进行通讯,那么每一个用户应该都有一个对应的 WebSocket 连接,同时在我们需要维护一个包含所有用户的 WebSocket 列表,当有新消息到来的时候,就遍历这个列表,像所有人发送这一个消息。

image.png

具体实现

安装必要依赖

安装 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 运行起来
image.png image.png
使用 Postman 分别发送两个消息,可以看到两个客户端可以分别接受到对方发送的消息,测试完成

image.png image.png

总结

在这篇博客中,我们实现了一个最最基本的聊天室框架,当然,这个框架还有非常多不完善的地方,我们将在接下来逐步进行完善,敬请期待。

点关注,不迷路

好了,以上就是这篇文章的全部内容了,如果你能看到这里,非常感谢你的支持!
如果你觉得这篇文章写的还不错, 求点赞👍 求关注❤️ 求分享👥 对暖男我来说真的 非常有用!!!
白嫖不好,创作不易,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!
如果本篇博客有任何错误,请批评指教,不胜感激 !
本文的 Github,欢迎各位人才快快用Star砸倒我。如果想要加入这个项目或者有任何建议,欢迎联系