go+mysql+redis+vue3简单聊室,第4弹:gin的websocket通讯和多go程任务处理

建立websocket连接

前面我们使用gin的路由实现了常规的http请求,但是聊天是基于websocket的,我们需要建立一个新路由,用于接收websocket请求。

websocket的建立是基于http的,一开始需要通过http建立联系,所以我们是可以通过http路由监听到websocket请求的,然后判断请求类型,是websokcet的话,就建立真正的websocket服务

gin处理websocket就是用上述方式进行的。修改routes/route.go,如下

import(
	...
	"github.com/gorilla/websocket"
)
//过滤url,可以对请求来源做一些处理
func checkOrigin(r *http.Request) bool {
	return true
}
//初始化一个websockt升级程序
var upgrader = websocket.Upgrader{
	ReadBufferSize:    1024,
	WriteBufferSize:   1024,
	CheckOrigin:       checkOrigin,
}
func NewRouter() *gin.Engine{
	...
	//建立一个新的路由,用于监听websocket连接
	r.GET("/ws",func(c *gin.Context) {
		//判断是否是websokcet请求
		if websocket.IsWebSocketUpgrade(c.Request){
			//是websocket请求的话,需要把http连接升级为websocket连接			
			conn,err := upgrader.Upgrade(c.Writer,c.Request,c.Writer.Header())
	if err != nil {
				c.JSON(200,gin.H{"msg":"升级websocket失败"})
			}else{
				//接收和分发消息
				fmt.Println("do  work")
			}
			//注册一个监听客户端关闭socket的方法
			conn.SetCloseHandler(func(code int, text string) error {
				fmt.Println(code, text+"websocket closed")
				return nil
			})
			fmt.Println("websocket连接成功")
		}else{
			c.JSON(402,gin.H{"status":0,"msg":"非websocket连接"})
		}
	})
}

在go中,一个http请求就是一个协程,代码执行结束后该协程就结束了。但是当http升级为websocke连接后,会新建一个监听socket连接的go程,我们可以用conn向socket发送和读取数据,直到conn监听到客户端的关闭信号,socket连接的go程才会退出

注意:我们要在新的go程中执行conn接收和发送数据代码,否则conn在会执行一次后,随着http go程结束

服务端代码写好了,接下来我们在views/index.html中,添加如下代码

...
<script>
    var text = document.getElementById("send-text");
    var send = document.getElementById("send");

    var username = document.getElementById("username").value;
    var userid = document.getElementById("userid").value;
    var data = {username:username,userid:userid,msg:""}
	//声明一个socket变量
    var websocket;
    //链接socket
    websocket = new WebSocket("ws://127.0.0.1:8080/ws");
    //链接成功
    websocket.onopen=function(){
        console.log("connected");
        data.msg = username+":上线了";
        //发送消息,只接受字符串和二进制,一般使用字符串
        //把json对象转换为字符串后发送
        websocket.send(JSON.stringify(data));
        addContent(data.msg)
    };
    //socket监听接收消息
    websocket.onmessage = function(e){
        console.log(e.data);
        addContent(e.data)
    };
    //socket监听关闭信号
    websocket.onclose=function(e){
        console.log("closed",e);
    };
    //发送消息
    send.onclick = function(){
        let msg = text.value;
        data.msg = msg
        websocket.send(JSON.stringify(data));
        addContent(msg)
    };
    function addContent(msg) {
        let contentDiv = document.getElementById("content");
        let child = document.createElement('p');
        child.innerHTML = msg;
        contentDiv.appendChild(child)
    }

</script>

多go程任务处理

常规聊天系统中,接收和发送消息需要多个进程的配合,以免造成代码阻塞。
以swoole为例,work进程用来监听接收用户端消息,发送消息任务则投递给task进程处理,这样可以保证需要群发大量消息时,work不会阻塞

go则能通过协程和通道,不需要多进程就能实现非阻塞的消息分发
下面是基本原理

go语言中,当一个用户上线后,会把连接插入一个map中,然后分配3个专属客服(三个go程)

  • 一个负责监听客户端发来的消息,并把消息发送到一个全局通道中
  • 一个负责把全局通道中的消息一 一插入map中客户端连接的通道中
  • 一个负责从当前链接的通道读取消息,并发送给客户端

第一和第二go程个可以根据需要合并成一个

在根目录新建service文件,用于存放后续会用到的一些服务
在service中新建ws-chat.go文件,如下

package ws

import (
	"encoding/json"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
	"net/http"
	"project1/service"
	"strconv"
)


//socket的连接类型
type clientUser struct{
	id int64
	username string
	chanMsg chan []byte
	chanClose chan int
}
//全局消息通道
var globalMsgChan = make(chan []byte,1024)

//保存在线的socket链接的map
var clientUserMap = make(map[int64]clientUser)

//主方法
func Run(conn *Conn){
	//初始化一个当前socket链接的id,随后可把用户id赋值给它
	var clientID int64 = 0
	//初始化一个socket链接对象
	oneClient := clientUser{}
	//读取数据成功,解析数据
	//conn.ReadMessage 是从通道中读取的数据,当客户端没有发送信息,或通道中信息数量不够时,该方法会阻塞后面的程序
	//比如客户端发了一条消息,如果读取两次的话,就会阻塞,直到客户端发送了第二条信息
	t, m, _ := conn.ReadMessage()
	//等于-1 说明链接不存在或已经关闭
	if t != -1 {
		//初始一个接收客户端消息的map变量
		//变量的键和值的类型,必须和客户端发送的数据一致
		msgInfo := make(map[string]string)
		//客户端发送过来json字符串,被转换为了[]byte,需要转换回来
		if err := json.Unmarshal(m,&msgInfo);err != nil{
			//失败的话,记录下日志。会在文章最后附上日志代码
			service.Chatlog(err.Error(),"error")
			c.JSON(420,gin.H{"status":0,"msg":"解析连接数据失败"})
		}else{
			//给当前socket连接对象赋值
			clientID,_ = strconv.ParseInt(msgInfo["userid"], 10, 64)
			oneClient = clientUser{
				id:       clientID,
				username:  msgInfo["username"],
				chanMsg: make(chan []byte),
				chanClose:make(chan int),
			}
			//把当前socket连接加入到全局map中
			addClientMap(oneClient)
			//发送消息到全局通道
			globalMsgChan <- m
		}

	}
	//开启一个go程,循环读取和处理用户的发送的数据
	go func(){
		for {
			//客户端没有发送消息时,代码会阻塞在这里,直到有可读的消息
			t, c, _ := conn.ReadMessage()
			if t == -1 {
				fmt.Println(t, string(c)+":client closed")
				//发送关闭消息到客户端通道
				oneClient.chanClose <- -1
				//把当前socket链接对象从map中删除
				delClientMap(clientID)
				//当用户全部下线,向全局通道发送关闭通知
				if len(clientUserMap) == 0{
					closeGlobalChan := []byte("all-offline")
					globalMsgChan <- closeGlobalChan
				}
				fmt.Println(clientUserMap)
				return
			}else{
				//写入接收到的消息,到全局消息通道中
				globalMsgChan <- c
			}
		}
	}()
	//开启一个go程,把客户端对象消息通道中的消息,发送给客户端
	go func(){
		for {
			select {
			case v1 := <-oneClient.chanMsg:
				err := conn.WriteMessage(websocket.TextMessage,v1)
				if err != nil{
					service.Chatlog(err.Error(),"error")
				}
			case <- oneClient.chanClose:
				return
			}
		}
	}()
	//上面两个go程,是处理当前socket连接消息的专属go程,当前socket关闭的话,就没有存在的意义了,所以会随着socket的关闭而退出
	//本go程是处理全局通道消息的go程,不需要随当前socket关闭,会一直运行
	//但是每当有新用户上线时,会重复建立该go程,所以我们需要做个限制
	//即当在线用户是第一个用户时,才建立该go程
	//当用户全部下线时,关闭该go程
	if len(clientUserMap) < 2{
		go func(){
			for{
				msg := <-globalMsgChan
				//用户全部下线时,关闭该go程
				if (string(msg) == "all-offline"){
					return
				}
				//向客户端对象通道中插入消息
				for _,cln := range clientUserMap{
					cln.chanMsg <- msg
				}
			}
		}()
	}
	//注册监听关闭socket的方法
	conn.SetCloseHandler(func(code int, text string) error {
		fmt.Println(code, text+"fffkkk")
		return nil
	})

	//向socket链接的map中插入新链接对象
	func addClientMap(client clientUser){
		if _,ok := clientUserMap[client.id]; ok{
			//有老客户端存在,删除
			delete(clientUserMap,client.id)
		}
		//插入新的链接
		clientUserMap[userinfo.id] = client
	}
	//删除客户端对象
	func delClientMap(id int64){
		if _,ok := clientUserMap[id]; ok{
			delete(clientUserMap,id)
		}
	}
}

附上service/loger.go代码

package service

import (
	"log"
	"os"
	"path/filepath"
)
var objPath string

func init(){
	objPath,_ = filepath.Abs("")
}

//消息队列消息日志
func Mqlog(msg string,mType string){
	common( objPath+"/logs/mq1.log",msg,mType)
}
//聊天消息日志
func Chatlog(msg string,mType string){
	common( objPath+"/logs/main.log",msg,mType)
}


func common(filePath string,msg string,mType string){
	//打开或创建文件
	logFile,err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
	if err != nil{
		panic("创建日志文件异常:"+filePath)
	}
	log.SetOutput(logFile)
	log.SetFlags(log.Llongfile | log.Lmicroseconds | log.Ldate)
	log.SetPrefix("["+mType+"]")
	log.Println(msg)
}

把上线的用户客户端保存在一个map中,因为map是引用类型的,所以当其他go程修改map中的信息时,本go程就能获取到修改后的信息。所以当一个go程向所有map中的客户端的通道中发送数据时,接收客户端通道信息的go程就能马上读取到数据,并把数据发送到前端,实现联动

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值