Go 语言打造高并发 web 即时聊天应用,主要实现功能:发送文字、表情包、图片、语音和群聊。
Go 后端逻辑
后端核心在于形成 用户id(userid) 和 Node 的映射关系。每个用户都有自己的 Node 节点。
Node :
Node 结构体,包含
- 此用户的长连接,Conn,类型为建立的 websocket 连接,我们通过这个连接跟用户进行信息的交互。
- 消息队列,DataQueue,类型为 channel ,我们将要发送的消息写入到此channel 中,后续监听此channel,如果有数据读取到,就通过上面的连接发送出去。
- 群聊集合,GroupSets,类型我们选择使用 Set ,用来存储用户所加入的群聊 id。
然后将所有用户通过 userid 和 Node 关联起来,后续我们可以通过 userid 来找到用户的 Node。
//本核心在于形成userid和Node的映射关系 type Node struct { Conn *websocket.Conn //并行转串行 DataQueue chan []byte GroupSets set.Interface } //映射关系表 var clientMap map[int64]*Node = make(map[int64]*Node, 0) 复制代码
Set 库
Set 是 Golang 中一个基本且简单的、基于散列的 Set 数据结构实现,提供通用集合数据结构的线程安全和非线程安全实现。此处我们不做详细讲解,详情可查看官方文档:
gorilla/websocket
我们使用 gorilla/websocket 来实现 websocket
- 文档
https://pkg.go.dev/github.com/gorilla/websocket
- 执行
go get github.com/gorilla/websocket
添加依赖
1. 升级请求
websocket 是由 http 升级而来,前端请求首先会发送附带 Upgrade 请求头的 Http 请求,所以我们需要在处理 Http 请求时拦截请求并判断其是否为 websocket 升级请求,如果是则调用Upgrader实例来升级请求。
var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, CheckOrigin: checkOrigin, } func checkOrigin(r *http.Request) bool { return true } 复制代码
然后调用 Upgrade 方法升级为websocket连接,并返回一个conn实例,之后的发送接收操作都是由 conn 完成,其类型为 websocket.Conn。
2. 向客户端发送消息
使用 WriteMessage(messageType int, data []byte),参数1为消息类型,参数2为消息内容。
示例:
func sendproc(node *Node) { for { select { //从 DataQueue 中读取消息,如果读到则将消息发送给客户端 case data := <-node.DataQueue: //向客户端发送消息WriteMessage(messageType int, data []byte),参数1为消息类型,参数2为消息内容 err := node.Conn.WriteMessage(websocket.TextMessage, data) if err != nil { log.Println(err.Error()) return } } } } 复制代码
3. 接受客户端消息
使用 ReadMessage(),该操作会阻塞线程,所以建议运行在其他协程上。 该函数有三个返回值分别是,接收消息类型、接收消息内容、发生的错误,正常执行时错误为 nil,一旦连接关闭返回值类型为-1可用来终止读操作。
示例:
func recvproc(node *Node) { for { //接受客户端消息ReadMessage(),作会阻塞线程所以建议运行在其他协程上。 //返回值:接收消息类型、接收消息内容、发生的错误。 _, data, err := node.Conn.ReadMessage() if err != nil { log.Println(err.Error()) return } log.Printf("recv[ws]<=%s\n", data) //调度消息给目标用户 dispatch(data) } } 复制代码
上面示例中,有一个 dispatch 函数,用来将消息转发给对应的目标用户。此函数接收一个参数 data,为前端发送过来的数据,数据包含 消息来源 userid ,目标用户 dstid,如:
{userid:2,dstid:3,cate:1,media:1,content:"hello"} 复制代码
我们只需要通过 dstid 找到对应的 Node,然后使用 Node 下的 Conn 链接进行发送,目标用户便可收到消息。
//调度逻辑处理 func dispatch(data []byte) { //解析data为message msg := Message{} err := json.Unmarshal(data, &msg) if err != nil { log.Println(err.Error()) return } _ = recordService.Record(msg.Userid, msg.Dstid, msg.Cate, data) //根据cate对逻辑进行处理 switch msg.Cate { case CMD_SINGLE_MSG: //单聊 sendMsg(msg.Dstid, data) case CMD_ROOM_MSG: //群聊 //群聊转发逻辑 for userId, v := range clientMap { if v.GroupSets.Has(msg.Dstid) { //自己排除,不发送 if msg.Userid != userId { v.DataQueue <- data } } } case CMD_HEART: //一般啥都不做 } } 复制代码
打包部署
1. 编译为 windows 程序
build.bat
rd /s/q release md release ::go build -ldflags "-H windowsgui" -o chat.exe go build -o chat.exe MOVE chat.exe release\ COPY favicon.ico release\favicon.ico XCOPY asset\*.* release\asset\ /s /e XCOPY view\*.* release\view\ /s /e XCOPY config\*.* release\config\ /s /e 复制代码
RD
: 删除文件夹(remove directory)的命令/S
: 表示除目录本身外,还将删除指定目录下的所有子目录和文件。用于删除目录树。/Q
: 安静模式,带/S
删除目录树时不要求确认。-ldflags "-H windowsgui"
: 将输出打印到命令窗口COPY
: 复制文件XCOPY
: 复制文件和目录树COPY
不能复制文件夹,当需要复制文件夹的时候使用XCOPY
。/S
复制目录和子目录,除了空的;/E
复制目录和子目录,包括空的。- 选用
/S
时对源目录下及其子目录下的所有文件进行COPY。除非指定/E
参数,否则/S
不会拷贝空目录。
2. 编译为 linux 程序
build.sh
#!/bin/sh rm -rf ./release mkdir release go build -o chat chmod +x ./chat cp chat ./release/ cp favicon.ico ./release/ cp -arf ./asset ./release/ cp -arf ./view ./release/ cp -arf ./config ./release/ 复制代码
cp
: 复制文件或目录-a
: 此选项通常在复制目录时使用,它保留链接、文件属性,并复制目录下的所有内容。其作用等于dpR参数组合。-r
: 若给出的源文件是一个目录文件,此时将复制该目录下所有的子目录和文件。-f
: 覆盖已经存在的目标文件而不给出提示。
3. 访问
http://127.0.0.1:8088/chat.html?id=1
id 为用户 userid 。
4. 源码
整套源码已上传到云仓库:
- github:github.com/wuchengshi/…
- gitee:gitee.com/wekenw/chat
5. 部分截图