建立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程就能马上读取到数据,并把数据发送到前端,实现联动