Go: IM系统接入ws进行消息发送以及群聊功能 (5)

概述

  • 在即时通讯(IM)系统中,实现多媒体消息(如文本、表情包、拍照、图片、音频、视频)的实时传输是一项核心功能
  • 随着HTML5和WebSocket技术的发展,现代Web应用能够支持更高效、更实时的通信方式
  • 本文将详细探讨如何使用Go语言结合WebSocket技术,在IM系统中实现多媒体消息的发送和接收

基于MVC的目录设计

im-project
├── go.mod
├── main.go          主程序
├── ctrl             控制器层
│     └── chat.go
├── views            模板层
│     └── chat
│           ├── foot.html
│           └── x.html

主程序

main.go 核心代码

package main

import (
	"net/http"
	"im-project/ctrl"
)

func main() {
	// 1. 绑定请求和处理函数
	http.HandleFunc("/chat", ctrl.Chat)
	http.HandleFunc("/attach/upload", ctrl.Upload)
	// 2. 指定目录的静态文件
	http.Handle("/asset/",http.FileServer(http.Dir(".")))
	http.Handle("/mnt/",http.FileServer(http.Dir(".")))
	// 3. 启动
	http.ListenAndServe(":80",nil)
}

控制器

ctrl/attach.go

package ctrl

import (
	"net/http"
	"im-project/util"
	"os"
	"strings"
	"fmt"
	"time"
	"math/rand"
	"io"
	"github.com/aliyun/aliyun-oss-go-sdk/oss"
)

func init(){
	os.MkdirAll("./mnt",os.ModePerm)
}

func Upload(w http.ResponseWriter, r *http.Request){
	//UploadLocal(w,r)
	UploadOss(w,r)
}

// 1.存储位置 ./mnt,需要确保已经创建好
// 2.url格式 /mnt/xxxx.png  需要确保网络能访问/mnt/
func UploadLocal(writer http.ResponseWriter, request * http.Request) {
	// 获得上传的源文件
    srcfile,head,err:=request.FormFile("file")
    if err!=nil{
    	util.RespFail(writer,err.Error())
	}
	// 创建一个新文件
	suffix := ".png"
	// 如果前端文件名称包含后缀 xx.xx.png
	ofilename := head.Filename
	tmp := strings.Split(ofilename,".")
	if len(tmp)>1 {
		suffix = "."+tmp[len(tmp)-1]
	}
	// 如果前端指定filetype
	// formdata.append("filetype",".png")
	filetype := request.FormValue("filetype")
	if len(filetype)>0 {
		suffix = filetype
	}
	// time.Now().Unix()
    filename := fmt.Sprintf("%d%04d%s", time.Now().Unix(), rand.Int31(), suffix)
    dstfile,err:= os.Create("./mnt/"+filename)
    if err!=nil {
    	util.RespFail(writer,err.Error())
    	return
	}
	// todo 将源文件内容copy到新文件
	_,err = io.Copy(dstfile,srcfile)
	if err!=nil{
		util.RespFail(writer,err.Error())
		return
	}
	// 将新文件路径转换成url地址
	url := "/mnt/"+filename
	// 响应到前端
	util.RespOk(writer,url,"")
}

// 即将删掉,定期更新
const (
	AccessKeyId="5p2RZ******nMuQw9" // 填入自己的 key
	AccessKeySecret="bsNmjU8Au08*****S5XIFAkK" // 填入自己的secret
	EndPoint="oss-cn-shenzhen.aliyuncs.com"
	Bucket="winliondev"
)

// 权限设置为公共读状态
// 需要安装
func UploadOss(writer http.ResponseWriter, request * http.Request) {
	// 获得上传的文件
	srcfile,head,err := request.FormFile("file")
	if err!=nil {
		util.RespFail(writer,err.Error())
		return
	}
	// 获得文件后缀.png/.mp3
	suffix := ".png"
	//如果前端文件名称包含后缀 xx.xx.png
	ofilename := head.Filename
	tmp := strings.Split(ofilename,".")
	if len(tmp)>1 {
		suffix = "."+tmp[len(tmp)-1]
	}
	// 如果前端指定filetype
	// formdata.append("filetype",".png")
	filetype := request.FormValue("filetype")
	if len(filetype)>0{
		suffix = filetype
	}
	// 初始化ossclient
	client,err:=oss.New(EndPoint,AccessKeyId,AccessKeySecret)
	if err!=nil{
		util.RespFail(writer,err.Error())
		return
	}
	// todo 获得bucket
	bucket,err := client.Bucket(Bucket)
	if err!=nil{
		util.RespFail(writer,err.Error())
		return
	}
	// 设置文件名称
	// time.Now().Unix()
	filename := fmt.Sprintf("mnt/%d%04d%s", time.Now().Unix(), rand.Int31(), suffix)
	// 通过bucket上传
	err = bucket.PutObject(filename, srcfile)
	if err!=nil {
		util.RespFail(writer,err.Error())
		return
	}
	// 获得url地址
	url := "http://"+Bucket+"."+EndPoint+"/"+filename
	// 响应到前端
	util.RespOk(writer,url,"")
}

ctrl/chat.go

package ctrl

import (
	"net/http"
	"github.com/gorilla/websocket"
	"gopkg.in/fatih/set.v0"
	"sync"
	"strconv"
	"log"
	"fmt"
	"encoding/json"
)

const (
	CMD_SINGLE_MSG = 10
	CMD_ROOM_MSG   = 11
	CMD_HEART      = 0
)

type Message struct {
	Id      int64  `json:"id,omitempty" form:"id"` //消息ID
	Userid  int64  `json:"userid,omitempty" form:"userid"` //谁发的
	Cmd     int    `json:"cmd,omitempty" form:"cmd"` //群聊还是私聊
	Dstid   int64  `json:"dstid,omitempty" form:"dstid"`//对端用户ID/群ID
	Media   int    `json:"media,omitempty" form:"media"` //消息按照什么样式展示
	Content string `json:"content,omitempty" form:"content"` //消息的内容
	Pic     string `json:"pic,omitempty" form:"pic"` //预览图片
	Url     string `json:"url,omitempty" form:"url"` //服务的URL
	Memo    string `json:"memo,omitempty" form:"memo"` //简单描述
	Amount  int    `json:"amount,omitempty" form:"amount"` //其他和数字相关的
}
/**
消息发送结构体
1、MEDIA_TYPE_TEXT
{id:1,userid:2,dstid:3,cmd:10,media:1,content:"hello"}
2、MEDIA_TYPE_News
{id:1,userid:2,dstid:3,cmd:10,media:2,content:"标题",pic:"http://www.baidu.com/a/log,jpg",url:"http://www.a,com/dsturl","memo":"这是描述"}
3、MEDIA_TYPE_VOICE,amount单位秒
{id:1,userid:2,dstid:3,cmd:10,media:3,url:"http://www.a,com/dsturl.mp3",anount:40}
4、MEDIA_TYPE_IMG
{id:1,userid:2,dstid:3,cmd:10,media:4,url:"http://www.baidu.com/a/log,jpg"}
5、MEDIA_TYPE_REDPACKAGR //红包amount 单位分
{id:1,userid:2,dstid:3,cmd:10,media:5,url:"http://www.baidu.com/a/b/c/redpackageaddress?id=100000","amount":300,"memo":"恭喜发财"}
6、MEDIA_TYPE_EMOJ 6
{id:1,userid:2,dstid:3,cmd:10,media:6,"content":"cry"}
7、MEDIA_TYPE_Link 6
{id:1,userid:2,dstid:3,cmd:10,media:7,"url":"http://www.a,com/dsturl.html"}

7、MEDIA_TYPE_Link 6
{id:1,userid:2,dstid:3,cmd:10,media:7,"url":"http://www.a,com/dsturl.html"}

8、MEDIA_TYPE_VIDEO 8
{id:1,userid:2,dstid:3,cmd:10,media:8,pic:"http://www.baidu.com/a/log,jpg",url:"http://www.a,com/a.mp4"}

9、MEDIA_TYPE_CONTACT 9
{id:1,userid:2,dstid:3,cmd:10,media:9,"content":"10086","pic":"http://www.baidu.com/a/avatar,jpg","memo":"胡大力"}

*/

// 本核心在于形成userid和Node的映射关系
type Node struct {
	Conn *websocket.Conn
	//并行转串行,
	DataQueue chan []byte
	GroupSets set.Interface
}
// 映射关系表
var clientMap map[int64]*Node = make(map[int64]*Node,0)
// 读写锁
var rwlocker sync.RWMutex

// ws://127.0.0.1/chat?id=1&token=xxxx
func Chat(writer http.ResponseWriter, request *http.Request) {

	// 检验接入是否合法
    // checkToken(userId int64, token string)
    query := request.URL.Query()
    id := query.Get("id")
    token := query.Get("token")
    userId ,_ := strconv.ParseInt(id,10,64)
	isvalida := checkToken(userId,token)
	// 如果 isvalida = true
	// isvalida = false
	conn,err :=(&websocket.Upgrader {
		CheckOrigin: func(r *http.Request) bool {
			return isvalida
		},
	}).Upgrade(writer,request,nil)
	
	if err!=nil {
		log.Println(err.Error())
		return
	}
	//  获得conn
	node := &Node {
		Conn:conn,
		DataQueue:make(chan []byte,50),
		GroupSets:set.New(set.ThreadSafe),
	}
	// 获取用户全部群Id
	comIds := contactService.SearchComunityIds(userId)
	for _,v:=range comIds {
		node.GroupSets.Add(v)
	}
	// userid和node形成绑定关系
	rwlocker.Lock()
	clientMap[userId] = node
	rwlocker.Unlock()
	// 完成发送逻辑, con
	go sendproc(node)
	// 完成接收逻辑
	go recvproc(node)
	sendMsg(userId,[]byte("hello,world!"))
}

//发送协程
func sendproc(node *Node) {
	for {
		select {
			case data:= <-node.DataQueue:
				err := node.Conn.WriteMessage(websocket.TextMessage,data)
			    if err!=nil{
			    	log.Println(err.Error())
			    	return
				}
		}
	}
}
// 添加新的群ID到用户的groupset中
func AddGroupId(userId,gid int64) {
	// 取得node
	rwlocker.Lock()
	node,ok := clientMap[userId]
	if ok {
		node.GroupSets.Add(gid)
	}
	// clientMap[userId] = node
	rwlocker.Unlock()
	// 添加gid到set
}
// 接收协程
func recvproc(node *Node) {
	for {
		_,data,err := node.Conn.ReadMessage()
		if err!=nil {
			log.Println(err.Error())
			return
		}
		// 对data进一步处理
		dispatch(data)
		fmt.Printf("recv<=%s",data)
	}
}
//后端调度逻辑处理
func dispatch(data[]byte) {
	// 解析data为message
	msg := Message{}
	err := json.Unmarshal(data, &msg)
	if err!=nil {
		log.Println(err.Error())
		return
	}
	// 根据cmd对逻辑进行处理
	switch msg.Cmd {
		case CMD_SINGLE_MSG:
			sendMsg(msg.Dstid, data)
		case CMD_ROOM_MSG:
			// 群聊转发逻辑
			for _,v:= range clientMap {
				if v.GroupSets.Has(msg.Dstid){
					v.DataQueue <- data
				}
			}
		case CMD_HEART:
			// 一般啥都不做
		}
}

// 发送消息
func sendMsg(userId int64,msg []byte) {
	rwlocker.RLock()
	node,ok := clientMap[userId]
	rwlocker.RUnlock()
	if ok {
		node.DataQueue<- msg
	}
}

// 检测是否有效
func checkToken(userId int64,token string) bool {
	// 从数据库里面查询并比对
	user := userService.Find(userId)
	return user.Token == token
}

视图层


这里,视图层要着重说明下

1 )发送文本

sendtxtmsg:function(txt) {
    //{id:1,userid:2,dstid:3,cmd:10,media:1,content:"hello"}
    var msg =this.createmsgcontext();
    msg.media=1;
    msg.content=txt;
    this.showmsg(userInfo(),msg);
    this.webSocket.send(JSON.stringify(msg))
}
  • 前端user1拼接好数据对象Message msg={id:1,userid:2,dstid:3,cmd:10,media:1,content:txt}
  • 转化成json字符串jsonstr : jsonstr = JSON.stringify(msg)
  • 通过websocket.send(jsonstr)发送, 后端S在recvproc中接收收数据data
  • 并做相应的逻辑处理dispatch(data)-转发给user2
  • user2通过websocket.onmessage收到消息后做解析并显示

2 )发送表情包

{
	loaddoutures:function(){
      var res=[];
      var config = this.doutu.config;
      for(var i in config.pkgids){
          res[config.pkgids[i]]= (config.baseurl+"/"+config.pkgids[i]+"/info.json")
      }
      var that = this;
      for(var id in res){
          //console.log("res[i]",id,res[id])
          post(res[id],{},function(pkginfo){
              //console.log("post res[i]",id,res[id],pkginfo)
              var baseurl= config.baseurl+"/"+pkginfo.id+"/"
              for(var j in pkginfo.assets){
                  pkginfo.assets[j] = baseurl+pkginfo.assets[j];
              }
              pkginfo.icon = baseurl + pkginfo.icon;
              that.doutu.packages.push(pkginfo)
              if(that.doutu.choosed.pkgid==pkginfo.id){
                  that.doutu.choosed.assets=pkginfo.assets;
              }

          })
      }
  },
}
  • 表情包简单逻辑:弹出一个窗口, 选择图片获得一个连接地址
  • 调用sendpicmsg方法开始发送流程

3 ) 拍照

3.1 照片

<input accept=\"image/gif,image/jpeg,,image/png\" type=\"file\" οnchange=\"upload(this)\" class='upload' />

3.2 拍照

<input accept=\"image/*\" capture=\"camera\" type=\"file\" οnchange=\"upload(this)\" class='upload' />
function upload(dom){
    uploadfile("attach/upload",dom,function(res){
        if(res.code==0){
            app.sendpicmsg(res.data)
        }
    })
}

function uploadfile(uri,dom,fn){
    var xhr = new XMLHttpRequest();
    xhr.open("POST","//"+location.host+"/"+uri, true);
    // 添加http头,发送信息至服务器时内容编码类型
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) {
            fn.call(this, JSON.parse(xhr.responseText));
        }
    };
    var _data=[];
    var formdata = new FormData();
    if(!! userId()){
        formdata.append("userid",userId());
    }
    formdata.append("file",dom.files[0])
    xhr.send(formdata);
}

// vue methods 中的发送图片方法
{
	sendpicmsg:function(picurl){
		// {id:1,userid:2,dstid:3,cmd:10,media:4,url:"http://www.baidu.com/a/log,jpg"}
        var msg =this.createmsgcontext();
        msg.media=4;
        msg.url=picurl;
        this.showmsg(userInfo(),msg)
        this.webSocket.send(JSON.stringify(msg))
    },
}
  • 发送图片/拍照, 弹出一个窗口,
  • 选择图片,上传到服务器, 获得一个链接地址
  • 调用sendpicmsg方法开始发送流程

4 )音视频

// 上传语音
function uploadblob(uri,blob,filetype,fn){
    var xhr = new XMLHttpRequest();
    xhr.open("POST","//"+location.host+"/"+uri, true);
    // 添加http头,发送信息至服务器时内容编码类型
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 304)) {
            fn.call(this, JSON.parse(xhr.responseText));
        }
    };
    var _data=[];
    var formdata = new FormData();
    formdata.append("filetype",filetype);
    formdata.append("file",blob)
    xhr.send(formdata);
}

// vue methods 中的方法
{
	playaudio:function(url) {
      document.getElementById('audio4play').src = url;
      document.getElementById('audio4play').play();
    },
    startrecorder:function(){
      let audioTarget = document.getElementById('audio');
      var types = ["video/webm",
          "audio/webm",
          "video/webm\;codecs=vp8",
          "video/webm\;codecs=daala",
          "video/webm\;codecs=h264",
          "audio/webm\;codecs=opus",
          "video/mpeg"];
      var suporttype ="";
      for (var i in types) {
          if(MediaRecorder.isTypeSupported(types[i])){
              suporttype = types[i];
          }
      }
      if(!suporttype){
          mui.toast("编码不支持")
          return ;
      }
      this.duration = new Date().getTime();
      navigator.mediaDevices.getUserMedia({audio: true, video: false})
      .then(function(stream){
          this.showprocess = true
          this.recorder = new MediaRecorder(stream);
          audioTarget.srcObject = stream;

          this.recorder.ondataavailable = (event) => {
              console.log("ondataavailable");
              uploadblob("attach/upload",event.data,".mp3",res=>{
                  var duration = Math.ceil((new Date().getTime()-this.duration)/1000);
                  this.sendaudiomsg(res.data,duration);
              })
              stream.getTracks().forEach(function (track) {
                  track.stop();
              });
              this.showprocess = false
          }
          this.recorder.start();
      }.bind(this))
       .catch(function(err){
          console.log(err)
          mui.toast(err)
          this.showprocess = false
      }.bind(this));
  },
  stoprecorder :function() {
      if(typeof this.recorder.stop=="function"){
          this.recorder.stop();
      }
      this.showprocess = false
      console.log("stoprecorder")

  },
  sendaudiomsg:function(url,num) {
  	//{id:1,userid:2,dstid:3,cmd:10,media:3,url:"http://www.a,com/dsturl.mp3",anount:40}
    var msg = this.createmsgcontext();
    msg.media = 3;
    msg.url = url;
    msg.amount = num;
    this.showmsg(userInfo(), msg)
    // console.log("sendaudiomsg",this.msglist);
    this.webSocket.send(JSON.stringify(msg))
  },
}

5 )单聊

{
	singlemsg:function(user){
	    //console.log(user)
	    this.win = "single";
	    this.title = "和"+user.nickname+"聊天中";
	    this.msgcontext.dstid = parseInt(user.id);
	    this.msgcontext.cmd = 10;
	},
}

6 )群聊

// vue methods 中的方法
{
	groupmsg: function(group){
        this.win = "group";
        this.title=group.name;
        this.msgcontext.dstid = parseInt(group.id);
        this.msgcontext.cmd = 11;
    },
}
  • 群聊原理: 分析群id,找到加了这个群的用户,把消息发送过去
  • 方案一:map<qunid1,qunid2,qunid3>
    • 优势是锁的频次低
    • 劣势是要轮训全部map
      type Node struct {
      	Conn *websocket.Conn
      	//并行转串行,
      	DataQueue chan []byte
      	GroupSets set.Interface
      }
      // 映射关系表
      var clientMap map[int64]*Node = make(map[int64]*Node,0)
      
  • 方案二: map<群id><userid1,userid2,userid3>
    • 优势是找用户ID非常快
    • 劣势是发送信息时需要根据userid获取node,锁的频次太高
      type Node struct {
      	Conn *websocket.Conn
      	//并行转串行,
      	DataQueue chan []byte
      }
      // 映射关系表
      var clientMap map[int64]*Node = make(map[int64]*Node,0)
      var comMap map[int64]set.Interface= make(map[int64]set.Interface,0)
      
  • 需要处理的问题
    • 当用户接入的时候初始化groupset
    • 当用户加入群的时候刷新groupset
    • 完成信息分发
计算机硬件系统: 计算机硬件系统是构成计算机物理实体的所有部件的集合,包括核心组件以及外设。其主要组成部分包括: 中央处理单元 (CPU):作为计算机的大脑,负责执行指令、进行逻辑运算和数据处理。 内存:包括随机访问内存 (RAM) 和只读存储器 (ROM),用于临时或永久地存储程序和数据供CPU快速访问。 存储设备:如硬盘、固态硬盘 (SSD)、光盘驱动器等,用于长期保存大量的程序和数据。 输入/输出设备:如键盘、鼠标、显示器、打印机、扫描仪、摄像头等,实现人与计算机之间的交互以及数据的输入和输出。 主板:连接和协调各硬件组件工作,包含芯片组、扩展插槽、接口等。 其他外设:如声卡、网卡、显卡等,提供特定功能支持。 计算机软件系统: 软件系统是指在硬件之上运行的各种程序和数据的集合,分为两大类: 系统软件: 操作系统 (OS):如Windows、macOS、Linux、Unix等,是管理和控制计算机硬件与软件资源、提供公共服务、协调计算机各部分工作的基础平台,是用户与计算机硬件之间的桥梁。 驱动程序:为特定硬件设备提供接口,使操作系统能够识别和控制这些设备。 实用工具:如编译器、链接器、调试器、文件管理器等,协助开发、维护和管理计算机系统。 应用软件: 办公套件:如Microsoft Office、LibreOffice,包括文字处理、电子表格、演示文稿等工具。 专业软件:如AutoCAD(工程制图)、Adobe Creative Suite(图形设计与多媒体编辑)、MATLAB(数值计算与数据分析)等,针对特定行业或任务的专业应用。 互联网应用:如浏览器、电子邮件客户端、即时通讯软件、社交媒体平台等。 游戏:休闲游戏、网络游戏、模拟游戏等各类娱乐软件。 信息系统: 在企业、机构或组织中,信息系统是指由硬件、软件、人员、数据资源、通信网络等组成的,用于收集、处理、存储、分发和管理信息,以支持决策制定、业务运营和战略规划的系统。这类系统包括: 数据库管理系统 (DBMS):如Oracle、MySQL、SQL Server,用于创建、维护和查询结构化数据。 企业资源计划 (ERP):整合企业的财务、供应链、人力资源、生产等多方面管理功能的综合性信息系统。 客户关系管理 (CRM):用于管理与客户互动的全过程,提升销售、营销和服务效率。 供应链管理 (SCM):优化供应链流程,包括采购、库存、物流、分销等环节。 决策支持系统 (DSS):辅助决策者分析复杂问题,提供数据驱动的决策建议。 网络系统: 包括局域网 (LAN)、广域网 (WAN)、互联网 (Internet) 等,通过路由器、交换机、调制解调器等网络设备,以及通信协议(如TCP/IP),实现计算机之间的数据传输和资源共享。 分布式系统: 由多台计算机通过网络互相协作,共同完成一项任务的系统。分布式系统可以提供高可用性、可扩展性、负载均衡等优点,如云计算平台、分布式数据库、区块链系统等。 安全系统: 旨在保护计算机系统免受恶意攻击、未经授权访问、数据泄露等安全威胁的措施和工具,包括防火墙、入侵检测系统、防病毒软件、身份认证与访问控制机制、数据加密技术等。 综上所述,计算机领域的“系统”概念广泛涉及硬件架构、软件层次、信息管理、网络通信、分布式计算以及安全保障等多个方面,它们相互交织,共同构成了现代计算机技术的复杂生态系统
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值