im实时通讯项目的逻辑实现
使用websocket技术来实现;
websocket介绍
WebSocket 是一种网络通信协议,提供了一种在单个TCP连接上进行全双工通信的方式。这意味着客户端和服务器可以同时发送和接收数据,而不需要等待对方的响应,这与传统的HTTP请求-响应模式不同。
WebSocket 的主要特点:
- 全双工通信:与传统的HTTP请求-响应模式不同,WebSocket 允许服务器和客户端在同一个连接上同时发送和接收数据。
- 持久连接:一旦WebSocket连接建立,它会保持打开状态,直到客户端或服务器端明确关闭连接。
- 减少通信开销:由于不需要为每次通信重新建立连接,WebSocket 减少了HTTP请求和响应头的开销。
- 实时性能:WebSocket 连接的建立后,可以立即进行数据交换,减少了通信的延迟。
- 应用层协议:WebSocket 是建立在TCP之上的应用层协议,它使用不同的端口和协议名。
- 安全性:存在
ws://
(非加密)和wss://
(加密,使用TLS)两种形式,后者提供了数据传输的加密。
WebSocket 的工作流程:
- 握手过程:客户端通过发送一个HTTP请求到服务器来发起WebSocket连接,这个请求包含特定的头部信息,如
Upgrade: websocket
和Connection: Upgrade
。 - 连接建立:如果服务器支持WebSocket,它会响应客户端的请求,并完成握手过程,将连接升级为WebSocket连接。
- 数据传输:连接建立后,客户端和服务器就可以通过这个连接发送和接收数据帧。
- 连接关闭:任何一方都可以发送一个关闭帧来关闭连接,或者在出现错误时自动关闭。
websocket是一种通信协议,用来做一些http做起来很复杂的业务;
先看一个具体的流程图
在这个图中,其实websocket只在接近用户的web层发挥了作用,即用户发送消息时,需要放入websocket传入,到后端后进行一系列操作,然后再用websocket通信将消息发送给指定的用户;
他优化的是数据传输过程中的时间和性能损耗;
由于websocket的使用,用户只需要在登陆时建立自己的客户端和后台的服务端连接,之后发送消息和接收消息将不再使用http,直到用户断开连接;
为了详细说明该过程,需要引入生产者-消费者模式;
生产者-消费者模式
消费者-生产者模式(Consumer-Producer Pattern)是一种并发编程中常用的设计模式,用于解决多线程环境下的资源共享问题,特别是当一个或多个线程(生产者)生成数据,而另一个或多个线程(消费者)使用这些数据时。
主要概念:
- 生产者(Producer):负责生成数据的线程或进程。生产者在数据准备好后将其放入一个共享资源(通常是队列)中。
- 消费者(Consumer):负责处理或消费数据的线程或进程。消费者从共享资源中取出数据并进行处理。
- 共享资源:生产者和消费者之间共享的数据结构,通常是队列。共享资源作为缓冲区,平衡生产者的生产速度和消费者的消费速度。
模式特点:
- 解耦生产者和消费者:生产者和消费者之间不需要直接通信,它们通过共享资源交换数据。
- 同步:共享资源需要同步机制来保证数据的正确性和线程的安全,例如使用互斥锁(Mutex)或信号量(Semaphore)。
- 避免资源浪费:通过控制共享资源的大小,可以避免生产者生产过多数据而占用过多内存,同时也可以避免消费者因数据不足而空闲。
- 提高效率:生产者和消费者可以并行工作,提高整体的处理效率。
应用场景:
- 多线程数据处理:在多线程应用程序中,生产者线程生成数据,消费者线程处理数据。
- 消息队列:在消息队列系统中,消息生产者发送消息到队列,消息消费者从队列中取出并处理消息。
- 缓存系统:生产者生成缓存数据,消费者从缓存中读取数据。
在这个场景下,即用户在登录时直接创建自己的client,在后端留档(后端会保存很多用户的client,且能唯一确定一个用户),当用户发送消息时,用户会作为生产者,先将消息通过websocket传入后端,后端会监听每一个用户的消息传入,传入后查看具体消息内容,将消息放入到指定的通道(技术实现可以是消息队列或管道,以go中管道为例)(放入哪个通道根据消息体中的内容来判别),该过程就实现了生产者的职责;
发送消息(通过websocket),解析消息,存入指定通道
同时,对方用户也会一直监听自己的通道中是否有消息,当有消息时会解析其中的内容,并将内容转换成websocket格式发送到用户client;
接收消息,解析消息,发送消息到前端(通过websocket);
过程其实并不是很复杂,只要理清消息传送到过程,还是很容易理解的;
核心代码:看懂这个其实,im实时通讯中最重要的一部分就掌握了,其他的只是crud相关;
// 映射关系
var clientMap map[int64]*Node = make(map[int64]*Node, 0)
// 读写锁
var rwLocker sync.RWMutex
// Chat 需要 :发送者ID ,接受者ID ,消息类型,发送的内容,发送类型
func Chat(writer http.ResponseWriter, request *http.Request) {
//1. 获取参数 并 检验 token 等合法性
//token := query.Get("token")
query := request.URL.Query()
Id := query.Get("userId")
userId, _ := strconv.ParseInt(Id, 10, 64)
isvalida := true //checkToke() 待以后添加.........
conn, err := (&websocket.Upgrader{
//token 校验
CheckOrigin: func(r *http.Request) bool {
return isvalida
},
}).Upgrade(writer, request, nil)
if err != nil {
fmt.Println(err)
return
}
//2.获取conn
currentTime := uint64(time.Now().Unix())
node := &Node{
Conn: conn,
Addr: conn.RemoteAddr().String(), //客户端地址
HeartbeatTime: currentTime, //心跳时间
LoginTime: currentTime, //登录时间
DataQueue: make(chan []byte, 50),
GroupSets: set.New(set.ThreadSafe),
}
//3. 用户关系
//4. userid 跟 node绑定 并加锁
rwLocker.Lock()
clientMap[userId] = node
rwLocker.Unlock()
//5.其实逻辑是接收其他用户给自己发送的信息(放到管道里了),然后逐条拿出,通过里边调用的websocket直接发给自己
go sendProc(node) //协程,会一直运行下去
//6.其实逻辑是接收该用户从websocket发出去的消息,选择具体的方式发送出去(发到其他用户指定的管道中)
go recvProc(node) //协程,会一直运行下去
//7.加入在线用户到缓存
SetUserOnlineInfo("online_"+Id, []byte(node.Addr), time.Duration(viper.GetInt("timeout.RedisOnlineTime"))*time.Hour)
}
// 其实逻辑是接收其他用户给自己发送的信息,通过websocket发给自己
func sendProc(node *Node) { //传入node表示唯一标识每个用户
for {
select { //管道的方法,表示从管道中接收其他人发给自己的信息存到data中
case data := <-node.DataQueue:
fmt.Println("[ws]sendProc >>>> msg :", string(data))
err := node.Conn.WriteMessage(websocket.TextMessage, data)
//自带的方法:写消息:即,将data消息发送给客户端,客户端通过其他方法接收,然后传给用户;
if err != nil {
fmt.Println(err)
return
}
}
}
}
func recvProc(node *Node) { //传入node表示唯一标识每个用户:
for {
_, data, err := node.Conn.ReadMessage() //循环从websocket中读msg中的具体信息是什么,根据信息的不同放入不同的管道中;
if err != nil {
fmt.Println(err)
return
}
msg := Message{}
err = json.Unmarshal(data, &msg)
if err != nil {
fmt.Println(err)
}
//心跳检测 msg.Media == -1 || msg.Type == 3
if msg.Type == 3 {
currentTime := uint64(time.Now().Unix())
node.Heartbeat(currentTime) //心跳时间设置为当前时间;表示用户还存在:
// 这种是官方发送的,表示想知道用户是否还在线
} else {
//用户发送给自己的消息
dispatch(data)
broadMsg(data) //todo 将消息广播到局域网
fmt.Println("[ws] recvProc <<<<< ", string(data))
}
}
}
// 后端调度逻辑处理
func dispatch(data []byte) {
msg := Message{}
msg.CreateTime = uint64(time.Now().Unix())
err := json.Unmarshal(data, &msg)
if err != nil {
fmt.Println(err)
return
}
switch msg.Type {
case 1: //私信
fmt.Println("dispatch data :", string(data))
sendMsg(msg.TargetId, data)
case 2: //群发
sendGroupMsg(msg.TargetId, data) //发送的群ID ,消息内容
//心跳检测写出去了,在前一步已经进行过判断;
}
}
func sendGroupMsg(targetId int64, msg []byte) { //发送群聊消息:其实就是执行了多个私发消息;
fmt.Println("开始群发消息")
userIds := SearchUserByGroupId(uint(targetId)) //这个是群聊的id
for i := 0; i < len(userIds); i++ {
//排除给群聊id自己的,因为群聊id并不是用户,而是一个索引一样,肯定不能给群聊id发信息;
if targetId != int64(userIds[i]) { //
sendMsg(int64(userIds[i]), msg) //userid是接收者
}
}
}
func sendMsg(userId int64, msg []byte) { //发送消息:存入到redis中;
rwLocker.RLock()
node, ok := clientMap[userId] //实例化指定用户的client
rwLocker.RUnlock()
jsonMsg := Message{}
json.Unmarshal(msg, &jsonMsg)
ctx := context.Background()
targetIdStr := strconv.Itoa(int(userId))
userIdStr := strconv.Itoa(int(jsonMsg.UserId))
jsonMsg.CreateTime = uint64(time.Now().Unix())
r, err := utils.Red.Get(ctx, "online_"+userIdStr).Result()
if err != nil {
fmt.Println(err)
}
if r != "" {
if ok {
fmt.Println("sendMsg >>> userID: ", userId, " msg:", string(msg))
node.DataQueue <- msg //将消息存到该用户的消息管道中
}
}
var key string
if userId > jsonMsg.UserId {
key = "msg_" + userIdStr + "_" + targetIdStr
} else {
key = "msg_" + targetIdStr + "_" + userIdStr
}
res, err := utils.Red.ZRevRange(ctx, key, 0, -1).Result()
if err != nil {
fmt.Println(err)
}
score := float64(cap(res)) + 1
ress, e := utils.Red.ZAdd(ctx, key, &redis.Z{score, msg}).Result() //jsonMsg
//res, e := utils.Red.Do(ctx, "zadd", key, 1, jsonMsg).Result() //备用 后续拓展 记录完整msg
if e != nil {
fmt.Println(e)
}
fmt.Println(ress)
}
该代码包含了整个项目的核心实现
具体代码分析
实现这个项目的具体代码分析
就以这个代码为例
1