简单的IM入门讲解

im实时通讯项目的逻辑实现

使用websocket技术来实现;

websocket介绍

WebSocket 是一种网络通信协议,提供了一种在单个TCP连接上进行全双工通信的方式。这意味着客户端和服务器可以同时发送和接收数据,而不需要等待对方的响应,这与传统的HTTP请求-响应模式不同。

WebSocket 的主要特点:

  1. 全双工通信:与传统的HTTP请求-响应模式不同,WebSocket 允许服务器和客户端在同一个连接上同时发送和接收数据。
  2. 持久连接:一旦WebSocket连接建立,它会保持打开状态,直到客户端或服务器端明确关闭连接。
  3. 减少通信开销:由于不需要为每次通信重新建立连接,WebSocket 减少了HTTP请求和响应头的开销。
  4. 实时性能:WebSocket 连接的建立后,可以立即进行数据交换,减少了通信的延迟。
  5. 应用层协议:WebSocket 是建立在TCP之上的应用层协议,它使用不同的端口和协议名。
  6. 安全性:存在 ws://(非加密)和 wss://(加密,使用TLS)两种形式,后者提供了数据传输的加密。

WebSocket 的工作流程:

  1. 握手过程:客户端通过发送一个HTTP请求到服务器来发起WebSocket连接,这个请求包含特定的头部信息,如 Upgrade: websocketConnection: Upgrade
  2. 连接建立:如果服务器支持WebSocket,它会响应客户端的请求,并完成握手过程,将连接升级为WebSocket连接。
  3. 数据传输:连接建立后,客户端和服务器就可以通过这个连接发送和接收数据帧。
  4. 连接关闭:任何一方都可以发送一个关闭帧来关闭连接,或者在出现错误时自动关闭。

websocket是一种通信协议,用来做一些http做起来很复杂的业务;

先看一个具体的流程图

image-20240518114051075

在这个图中,其实websocket只在接近用户的web层发挥了作用,即用户发送消息时,需要放入websocket传入,到后端后进行一系列操作,然后再用websocket通信将消息发送给指定的用户;

他优化的是数据传输过程中的时间和性能损耗;

由于websocket的使用,用户只需要在登陆时建立自己的客户端和后台的服务端连接,之后发送消息和接收消息将不再使用http,直到用户断开连接;

为了详细说明该过程,需要引入生产者-消费者模式;

生产者-消费者模式

消费者-生产者模式(Consumer-Producer Pattern)是一种并发编程中常用的设计模式,用于解决多线程环境下的资源共享问题,特别是当一个或多个线程(生产者)生成数据,而另一个或多个线程(消费者)使用这些数据时。

主要概念:

  1. 生产者(Producer):负责生成数据的线程或进程。生产者在数据准备好后将其放入一个共享资源(通常是队列)中。
  2. 消费者(Consumer):负责处理或消费数据的线程或进程。消费者从共享资源中取出数据并进行处理。
  3. 共享资源:生产者和消费者之间共享的数据结构,通常是队列。共享资源作为缓冲区,平衡生产者的生产速度和消费者的消费速度。

模式特点:

  • 解耦生产者和消费者:生产者和消费者之间不需要直接通信,它们通过共享资源交换数据。
  • 同步:共享资源需要同步机制来保证数据的正确性和线程的安全,例如使用互斥锁(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)
}

im实时通讯项目的逻辑实现

使用websocket技术来实现;

websocket介绍

WebSocket 是一种网络通信协议,提供了一种在单个TCP连接上进行全双工通信的方式。这意味着客户端和服务器可以同时发送和接收数据,而不需要等待对方的响应,这与传统的HTTP请求-响应模式不同。

WebSocket 的主要特点:

  1. 全双工通信:与传统的HTTP请求-响应模式不同,WebSocket 允许服务器和客户端在同一个连接上同时发送和接收数据。
  2. 持久连接:一旦WebSocket连接建立,它会保持打开状态,直到客户端或服务器端明确关闭连接。
  3. 减少通信开销:由于不需要为每次通信重新建立连接,WebSocket 减少了HTTP请求和响应头的开销。
  4. 实时性能:WebSocket 连接的建立后,可以立即进行数据交换,减少了通信的延迟。
  5. 应用层协议:WebSocket 是建立在TCP之上的应用层协议,它使用不同的端口和协议名。
  6. 安全性:存在 ws://(非加密)和 wss://(加密,使用TLS)两种形式,后者提供了数据传输的加密。

WebSocket 的工作流程:

  1. 握手过程:客户端通过发送一个HTTP请求到服务器来发起WebSocket连接,这个请求包含特定的头部信息,如 Upgrade: websocketConnection: Upgrade
  2. 连接建立:如果服务器支持WebSocket,它会响应客户端的请求,并完成握手过程,将连接升级为WebSocket连接。
  3. 数据传输:连接建立后,客户端和服务器就可以通过这个连接发送和接收数据帧。
  4. 连接关闭:任何一方都可以发送一个关闭帧来关闭连接,或者在出现错误时自动关闭。

websocket是一种通信协议,用来做一些http做起来很复杂的业务;

先看一个具体的流程图

image-20240518114051075

在这个图中,其实websocket只在接近用户的web层发挥了作用,即用户发送消息时,需要放入websocket传入,到后端后进行一系列操作,然后再用websocket通信将消息发送给指定的用户;

他优化的是数据传输过程中的时间和性能损耗;

由于websocket的使用,用户只需要在登陆时建立自己的客户端和后台的服务端连接,之后发送消息和接收消息将不再使用http,直到用户断开连接;

为了详细说明该过程,需要引入生产者-消费者模式;

生产者-消费者模式

消费者-生产者模式(Consumer-Producer Pattern)是一种并发编程中常用的设计模式,用于解决多线程环境下的资源共享问题,特别是当一个或多个线程(生产者)生成数据,而另一个或多个线程(消费者)使用这些数据时。

主要概念:

  1. 生产者(Producer):负责生成数据的线程或进程。生产者在数据准备好后将其放入一个共享资源(通常是队列)中。
  2. 消费者(Consumer):负责处理或消费数据的线程或进程。消费者从共享资源中取出数据并进行处理。
  3. 共享资源:生产者和消费者之间共享的数据结构,通常是队列。共享资源作为缓冲区,平衡生产者的生产速度和消费者的消费速度。

模式特点:

  • 解耦生产者和消费者:生产者和消费者之间不需要直接通信,它们通过共享资源交换数据。
  • 同步:共享资源需要同步机制来保证数据的正确性和线程的安全,例如使用互斥锁(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)
}

  • 29
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值