在第一天进行了登录之后,要开始进行信息的交互了
对于信息的交互,最重要的是选择一定的数据结构,我们同样在model包中编写如下数据结构
//message.go
package model
import (
"gorm.io/plugin/soft_delete"
"time"
)
type Message struct {
ID int32 `json:"id" gorm:"primarykey"`
CreatedAt time.Time `json:"createAt"`
UpdatedAt time.Time `json:"updatedAt"`
DeletedAt soft_delete.DeletedAt `json:"deletedAt"`
FromUserId int32 `json:"fromUserId" gorm:"index"`
ToUserId int32 `json:"toUserId" gorm:"index;comment:'发送给端的id,可为用户id或者群id'"`
Content string `json:"content" gorm:"type:varchar(2500)"`
MessageType int16 `json:"messageType" gorm:"comment:'消息类型:1单聊,2群聊'"`
ContentType int16 `json:"contentType" gorm:"comment:'消息内容类型:1文字 2.普通文件 3.图片 4.音频 5.视频 6.语音聊天 7.视频聊天'"`
Pic string `json:"pic" gorm:"type:text;comment:'缩略图"`
Url string `json:"url" gorm:"type:varchar(350);comment:'文件或者图片地址'"`
}
这个Message结构体同样作为gorm的必须,进行与数据库的交互,每次进行一次信息的发送,就会保存在对应的数据库中。
程序的开始?
有了对应的数据结构,那程序的主体是什么呢?因为是利用golang来编写,可以选择用一些golang 的特性进行编写,比如利用协程,可以很方便的实现高度的并发,在每一个客户端进行连接的同时,为每一个客户端开启一个协程,所以就有了如下逻辑
// cmd/main.go
package main
import (
"gochat/internal/route"
"gochat/internal/server"
"time"
"net/http"
)
func main(){
newRouter := route.NewRoute()
go server.MyServer.Start()
s := &http.Server{
Addr: ":3000",
Handler: newRouter,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
查看代码的11行,在昨天编写了route的部分之后,今天需要添加一些
group.POST("/friend", ChatApi.AddFriend)
group.GET("/user", ChatApi.GetUserList)
group.GET("/user/:uuid", ChatApi.GetUserDetails)
group.GET("/user/name", ChatApi.GetUserOrGroupByName)
group.PUT("/user", ChatApi.ModifyUserInfo)
group.GET("/message", ChatApi.GetMessage)
group.GET("/file/:fileName", ChatApi.GetFile)
group.POST("/file", ChatApi.SaveFile)
group.GET("/group/:uuid", ChatApi.GetGroup)
group.POST("/group/:uuid", ChatApi.SaveGroup)
group.POST("/group/join/:userUuid/:groupUuid", ChatApi.JoinGroup)
group.GET("/group/user/:uuid", ChatApi.GetGroupUsers)
这些与今天主体通信无关系,但是也是很重要的,其实最主要的是编写对应的api,这些单独来说,今天主要解决最主要的通信部分
通信部分最主要的是使用websocket,也就是下面这句代码
group.GET("/socket.io", socket)
对于通信,我们为什么使用使用websocket呢?<br />websocket和socket有什么关系呢,就和周杰伦和周杰,卡巴斯基和巴基斯坦,java和javascript一样,没啥关系。对于即时通讯,按照以前的技术就是采用轮询,Comet来实现的,当需要即时通讯时,就在特定的时间间隔发送一个request,然后返回最新的数据,这样的缺点就是每次都要发一个http的request,他的头很长,这样代价就太高了,就很浪费,然后就有了websocket<br />WebSocket同HTTP一样也是应用层的协议,但是它是一种双向通信协议,是建立在TCP之上的。
连接过程 —— 握手过程
1. 浏览器、服务器建立TCP连接,三次握手。这是通信的基础,传输控制层,若失败后续都不执行。
2. TCP连接成功后,浏览器通过HTTP协议向服务器传送WebSocket支持的版本号等信息。(开始前的HTTP握手)
3. 服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据。
4. 当收到了连接成功的消息后,通过TCP通道进行传输通信。
这样传输的时候,并不需要http协议,对于传统 HTTP 每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket 是类似 Socket 的 TCP 长连接的通讯模式,一旦 WebSocket 连接建立后,后续数据都以帧序列的形式传输。在客户端断开 WebSocket 连接或 Server 端断掉连接前,不需要客户端和服务端重新发起连接请求。在海量并发及客户端与服务器交互负载流量大的情况下,极大的节省了网络带宽资源的消耗,有明显的性能优势,且客户端发送和接受消息是在同一个持久连接上发起,实时性优势明显。
[
](https://blog.csdn.net/wwd0501/article/details/54582912)
下面是一个简单的demo
http://www.jsons.cn/websocket/,用这个当作客户端就ok
package main
import (
"github.com/gorilla/websocket"
"net/http"
)
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func test(c http.ResponseWriter, r* http.Request){
conn ,_ := upGrader.Upgrade(c,r ,nil)
defer conn.Close()
for {
i,msg,_ := conn.ReadMessage()
conn.WriteMessage(i,msg)
}
}
func main(){
http.HandleFunc("/",test)
http.ListenAndServe(":8888",nil)
}
https://www.cnblogs.com/barrywxx/p/7412808.html
https://blog.csdn.net/chenyu201003/article/details/81449762
https://blog.csdn.net/whynottrythis/article/details/109119328 这些是websocket的一些参考资料
怎么实现socket.io?
还是同样的,在/internal/route 中编写socket.go
package route
import (
"gochat/internal/server"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
//需要使用websocket
var upGrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { //允许跨域
return true
},
}
func RunSocekt(c *gin.Context) {
user := c.Query("user")
if user == "" {
return
}
ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
}
client := &server.Client{
Name: user,
Conn: ws,
Send: make(chan []byte),
}
server.MyServer.Register <- client
go client.Read()
go client.Write()
}
所对应的数据结构client和其方法read,write在server中实现,先暂时搁置,你只需要大概知道一个是启动协程进行读写的就ok了
这段代码的意思就是当启动了一个socket的时候,将传入的user等信息新建一个client客户端传入server这个主体代码逻辑中进行新建一个客户端,当然现在看着云里雾里的,下面开始解读server中的内容
server?client?
// internal/server/server.go
package server
import (
"github.com/gorilla/websocket"
"github.com/gogo/protobuf/proto"
"github.com/google/uuid"
"gochat/pkg/protocol"
"gochat/pkg/common/util"
"gochat/pkg/common/constant"
"gochat/config"
"gochat/internal/service"
"encoding/base64"
"io/ioutil"
"strings"
"sync"
)
type Client struct {
Conn *websocket.Conn
Name string
Send chan []byte
}
type Server struct {
Clients map[string]*Client
mutex *sync.Mutex
Broadcast chan []byte
Register chan *Client
Ungister chan *Client
}
var MyServer = NewServer()
func NewServer() *Server {
return &Server{
mutex: &sync.Mutex{},
Clients: make(map[string]*Client),
Broadcast: make(chan []byte),
Register: make(chan *Client),
Ungister: make(chan *Client),
}
}
//启动服务
func (s *Server) Start() {
for {
select {
case conn := <-s.Register://登录
s.Clients[conn.Name] = conn
msg := &protocol.Message{
From: "System",
To: conn.Name,
Content: "welcome!",
}
protoMsg, _ := proto.Marshal(msg)
conn.Send <- protoMsg
case conn := <-s.Ungister://退出
if _, ok := s.Clients[conn.Name]; ok {
close(conn.Send)
delete(s.Clients, conn.Name)
}
case message := <-s.Broadcast://发消息
msg := &protocol.Message{}
proto.Unmarshal(message, msg)
if msg.To != "" {
// 一般消息,比如文本消息,视频文件消息等
if msg.ContentType >= constant.TEXT && msg.ContentType <= constant.VIDEO {
// 保存消息只会在存在socket的一个端上进行保存,防止分布式部署后,消息重复问题
_, exits := s.Clients[msg.From]
if exits {
saveMessage(msg)
}
if msg.MessageType == constant.MESSAGE_TYPE_USER {
client, ok := s.Clients[msg.To]
if ok {
msgByte, err := proto.Marshal(msg)
if err == nil {
client.Send <- msgByte
}
}
} else if msg.MessageType == constant.MESSAGE_TYPE_GROUP {
sendGroupMessage(msg, s)
}
} else {
// 语音电话,视频电话等,仅支持单人聊天,不支持群聊
// 不保存文件,直接进行转发
client, ok := s.Clients[msg.To]
if ok {
client.Send <- message
}
}
} else {
// 无对应接受人员进行广播
for _, conn := range s.Clients {
select {
case conn.Send <- message:
default:
close(conn.Send)
delete(s.Clients, conn.Name)
}
}
}
}
}
}// 发送给群组消息,需要查询该群所有人员依次发送
func sendGroupMessage(msg *protocol.Message, s *Server) {
// 发送给群组的消息,查找该群所有的用户进行发送
users := service.GroupService.GetUserIdByGroupUuid(msg.To)
for _, user := range users {
if user.Uuid == msg.From {
continue
}
client, ok := s.Clients[user.Uuid]
if !ok {
continue
}
fromUserDetails := service.UserService.GetUserDetails(msg.From)
// 由于发送群聊时,from是个人,to是群聊uuid。所以在返回消息时,将form修改为群聊uuid,和单聊进行统一
msgSend := protocol.Message{
Avatar: fromUserDetails.Avatar,
FromUsername: msg.FromUsername,
From: msg.To,
To: msg.From,
Content: msg.Content,
ContentType: msg.ContentType,
Type: msg.Type,
MessageType: msg.MessageType,
Url: msg.Url,
}
msgByte, err := proto.Marshal(&msgSend)
if err == nil {
client.Send <- msgByte
}
}
}
// 保存消息,如果是文本消息直接保存,如果是文件,语音等消息,保存文件后,保存对应的文件路径
func saveMessage(message *protocol.Message) {
// 如果上传的是base64字符串文件,解析文件保存
if message.ContentType == 2 {
url := uuid.New().String() + ".png"
index := strings.Index(message.Content, "base64")
index += 7
content := message.Content
content = content[index:]
dataBuffer, dataErr := base64.StdEncoding.DecodeString(content)
if dataErr != nil {
return
}
err := ioutil.WriteFile(config.GetConfig().StaticPath.FilePath+url, dataBuffer, 0666)
if err != nil {
return
}
message.Url = url
message.Content = ""
} else if message.ContentType == 3 {
// 普通的文件二进制上传
fileSuffix := util.GetFileType(message.File)
nullStr := ""
if nullStr == fileSuffix {
fileSuffix = strings.ToLower(message.FileSuffix)
}
contentType := util.GetContentTypeBySuffix(fileSuffix)
url := uuid.New().String() + "." + fileSuffix
err := ioutil.WriteFile(config.GetConfig().StaticPath.FilePath+url, message.File, 0666)
if err != nil {
return
}
message.Url = url
message.File = nil
message.ContentType = contentType
}
service.MessageService.SaveMessage(*message)
}
func (c *Client) Read() {
defer func() {
MyServer.Ungister <- c
c.Conn.Close()
}()
for {
c.Conn.PongHandler()
_, message, err := c.Conn.ReadMessage()
if err != nil {
MyServer.Ungister <- c
c.Conn.Close()
break
}
msg := &protocol.Message{}
proto.Unmarshal(message, msg)
// pong WebSocket 协议定义了三种类型的控制消息:
//close、ping 和 pong。调用连接 WriteControl、WriteMessage 或 NextWriter 方法
//向对端发送控制消息。
if msg.Type == constant.HEAT_BEAT {
pong := &protocol.Message{
Content: constant.PONG,
Type: constant.HEAT_BEAT,
}
pongByte, err2 := proto.Marshal(pong)
if nil != err2 {
}
c.Conn.WriteMessage(websocket.BinaryMessage, pongByte)
} else {
MyServer.Broadcast <- message
}
}
}
func (c *Client) Write() {
defer func() {
c.Conn.Close()
}()
for message := range c.Send {
c.Conn.WriteMessage(websocket.BinaryMessage, message)
}
}
protobuf
这里涉及到了一个东西,就是protobuf,什么是protobuf
protobuf也叫protocol buffer是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python,每一种实现都包含了相应语言的编译器以及库文件。由于它是一种二进制的格式,比使用 xml 、json进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。要使用protobuf,得先创建.proto文件才行。 .proto文件中的定义很简单:为要序列化的每个数据结构定义消息,然后为消息中的每个字段指定名称和类型。
/**********pkg/protocol/message.proto*******************/
syntax = "proto3";
package protocol;
message Message {
string avatar = 1; //头像
string fromUsername = 2; // 发送消息用户的用户名
string from = 3; // 发送消息用户uuid
string to = 4; // 发送给对端用户的uuid
string content = 5; // 文本消息内容
int32 contentType = 6; // 消息内容类型:1.文字 2.图片 3.普通文件 4.音频 5.视频 6.语音聊天 7.视频聊天
string type = 7; // 消息传输类型:如果是心跳消息,该内容为heatbeat,在线视频或者音频为webrtc
int32 messageType = 8; // 消息类型,1.单聊 2.群聊
string url = 9; // 图片,视频,语音的路径
string fileSuffix = 10; // 文件后缀,如果通过二进制头不能解析文件后缀,使用该后缀
bytes file = 11; // 如果是图片,文件,视频等的二进制
}
然后有命令,生成了message.pb.go,你就可以用里面的函数了,但是最基本用的就是Marshal与unMarshal进行序列化和反序列化。
proto的部分大约就是这些,下面来进行server.go的解读
可以这么理解
一个server服务端由一个map[string]Client结构的map,一个互斥锁mutex,一个用于登录的channel Register与一个用于退出的channel ungister,一个用于发送消息的broadcast。对应的服务端拥有一个websocket的长连接Conn websocket.Conn,名字 Name string,一个发送消息的channel Send chan []byte。
在启动服务的时候,利用一个for循环进行循环处理,同时利用channel的特性,select ,用来监听几个channel是否有消息,这里分为三个部分,分别为
- 登录
- 登出
- 发消息
对于登录a来说,登录后直接新增加一个map,然后向用户发送一个message用来欢迎,其实我觉得可有可无吧,最后调用conn.send发送消息
对于登出b来说同理,首先关闭client的send channel,然后从map中delete这个client
最后是最重要的发消息c了
case message := <-s.Broadcast://发消息
msg := &protocol.Message{}
proto.Unmarshal(message, msg)
if msg.To != "" {
// 一般消息,比如文本消息,视频文件消息等
if msg.ContentType >= constant.TEXT && msg.ContentType <= constant.VIDEO {
// 保存消息只会在存在socket的一个端上进行保存,防止分布式部署后,消息重复问题
_, exits := s.Clients[msg.From]
if exits {
saveMessage(msg)
}
if msg.MessageType == constant.MESSAGE_TYPE_USER {//发给个人
client, ok := s.Clients[msg.To]
if ok {
msgByte, err := proto.Marshal(msg)
if err == nil {
client.Send <- msgByte
}
}
} else if msg.MessageType == constant.MESSAGE_TYPE_GROUP {//发送给群组
sendGroupMessage(msg, s)
}
} else {
// 语音电话,视频电话等,仅支持单人聊天,不支持群聊
// 不保存文件,直接进行转发
client, ok := s.Clients[msg.To]
if ok {
client.Send <- message
}
}
} else {
// 无对应接受人员进行广播
for _, conn := range s.Clients {
select {
case conn.Send <- message:
default:
close(conn.Send)
delete(s.Clients, conn.Name)
}
}
}
代码逻辑清晰,利用反序列化的message结构体中的“to”字段来判断这段消息是广播还是单发,然后在判断是文字消息还是视频等信息,在分为群组和私聊,下面解读saveMessage(msg)与sendGroupMessage(msg, s)这两个函数
//saveMessage(msg)
// 消息内容类型:1.文字 2.图片 3.普通文件 4.音频 5.视频 6.语音聊天 7.视频聊天
func saveMessage(message *protocol.Message) {
// 如果上传的是base64字符串文件,解析文件保存
if message.ContentType == 2 {
url := uuid.New().String() + ".png"
index := strings.Index(message.Content, "base64")
index += 7
content := message.Content
content = content[index:]
dataBuffer, dataErr := base64.StdEncoding.DecodeString(content)
if dataErr != nil {
return
}
err := ioutil.WriteFile(config.GetConfig().StaticPath.FilePath+url, dataBuffer, 0666)
if err != nil {
return
}
message.Url = url
message.Content = ""
} else if message.ContentType == 3 {
// 普通的文件二进制上传
fileSuffix := util.GetFileType(message.File)
nullStr := ""
if nullStr == fileSuffix {
fileSuffix = strings.ToLower(message.FileSuffix)
}
contentType := util.GetContentTypeBySuffix(fileSuffix)
url := uuid.New().String() + "." + fileSuffix
err := ioutil.WriteFile(config.GetConfig().StaticPath.FilePath+url, message.File, 0666)
if err != nil {
return
}
message.Url = url
message.File = nil
message.ContentType = contentType
}
service.MessageService.SaveMessage(*message) //这句话就是写入数据库的意思,在service包中有详细解释
}
util
在获取类型的时候,遇到了util这个东西,这个是自己编写的,目的就是为了判断文件类型,打开
/pkg/common/file_suffix.go查看
package util
import (
"bytes"
"gochat/pkg/common/constant"
"encoding/hex"
"strconv"
"strings"
"sync"
"github.com/wxnacy/wgo/arrays"
)
var fileTypeMap sync.Map
func init() {
fileTypeMap.Store("ffd8ffe000104a464946", "jpg") //JPEG (jpg)
fileTypeMap.Store("89504e470d0a1a0a0000", "png") //PNG (png)
fileTypeMap.Store("47494638396126026f01", "gif") //GIF (gif)
fileTypeMap.Store("49492a00227105008037", "tif") //TIFF (tif)
fileTypeMap.Store("424d228c010000000000", "bmp") //16色位图(bmp)
fileTypeMap.Store("424d8240090000000000", "bmp") //24位位图(bmp)
fileTypeMap.Store("424d8e1b030000000000", "bmp") //256色位图(bmp)
fileTypeMap.Store("41433130313500000000", "dwg") //CAD (dwg)
fileTypeMap.Store("3c21444f435459504520", "html") //HTML (html) 3c68746d6c3e0 3c68746d6c3e0
fileTypeMap.Store("3c68746d6c3e0", "html") //HTML (html) 3c68746d6c3e0 3c68746d6c3e0
fileTypeMap.Store("3c21646f637479706520", "htm") //HTM (htm)
fileTypeMap.Store("48544d4c207b0d0a0942", "css") //css
fileTypeMap.Store("696b2e71623d696b2e71", "js") //js
fileTypeMap.Store("7b5c727466315c616e73", "rtf") //Rich Text Format (rtf)
fileTypeMap.Store("38425053000100000000", "psd") //Photoshop (psd)
fileTypeMap.Store("46726f6d3a203d3f6762", "eml") //Email [Outlook Express 6] (eml)
fileTypeMap.Store("d0cf11e0a1b11ae10000", "vsd") //Visio 绘图
fileTypeMap.Store("5374616E64617264204A", "mdb") //MS Access (mdb)
fileTypeMap.Store("252150532D41646F6265", "ps")
fileTypeMap.Store("255044462d312e350d0a", "pdf") //Adobe Acrobat (pdf)
fileTypeMap.Store("D0CF11E0", "xls") //xls
fileTypeMap.Store("504B030414000600080000002100", "xlsx") //xls
fileTypeMap.Store("d0cf11e0a1b11ae10000", "doc") //MS Excel 注意:word、msi 和 excel的文件头一样
fileTypeMap.Store("504b0304140006000800", "docx") //docx文件
fileTypeMap.Store("d0cf11e0a1b11ae10000", "wps") //WPS文字wps、表格et、演示dps都是一样的
fileTypeMap.Store("2e524d46000000120001", "rmvb") //rmvb/rm相同
fileTypeMap.Store("464c5601050000000900", "flv") //flv与f4v相同
fileTypeMap.Store("00000020667479706d70", "mp4")
fileTypeMap.Store("49443303000000002176", "mp3")
fileTypeMap.Store("000001ba210001000180", "mpg") //
fileTypeMap.Store("3026b2758e66cf11a6d9", "wmv") //wmv与asf相同
fileTypeMap.Store("52494646e27807005741", "wav") //Wave (wav)
fileTypeMap.Store("52494646246009005741", "wav") //Wave (wav)
fileTypeMap.Store("52494646", "wav") //Wave (wav)
fileTypeMap.Store("52494646d07d60074156", "avi")
fileTypeMap.Store("1a45dfa3a34286810142", "webm")
fileTypeMap.Store("4d546864000000060001", "mid") //MIDI (mid)
fileTypeMap.Store("504b0304140000000800", "zip")
fileTypeMap.Store("526172211a0700cf9073", "rar")
fileTypeMap.Store("235468697320636f6e66", "ini")
fileTypeMap.Store("504b03040a0000000000", "jar")
fileTypeMap.Store("4d5a9000030000000400", "exe") //可执行文件
fileTypeMap.Store("3c25402070616765206c", "jsp") //jsp文件
fileTypeMap.Store("4d616e69666573742d56", "mf") //MF文件
fileTypeMap.Store("3c3f786d6c2076657273", "xml") //xml文件
fileTypeMap.Store("494e5345525420494e54", "sql") //xml文件
fileTypeMap.Store("7061636b616765207765", "java") //java文件
fileTypeMap.Store("406563686f206f66660d", "bat") //bat文件
fileTypeMap.Store("1f8b0800000000000000", "gz") //gz文件
fileTypeMap.Store("6c6f67346a2e726f6f74", "properties") //bat文件
fileTypeMap.Store("cafebabe0000002e0041", "class") //bat文件
fileTypeMap.Store("49545346030000006000", "chm") //bat文件
fileTypeMap.Store("04000000010000001300", "mxp") //bat文件
fileTypeMap.Store("6431303a637265617465", "torrent")
fileTypeMap.Store("6D6F6F76", "mov") //Quicktime (mov)
fileTypeMap.Store("FF575043", "wpd") //WordPerfect (wpd)
fileTypeMap.Store("CFAD12FEC5FD746F", "dbx") //Outlook Express (dbx)
fileTypeMap.Store("2142444E", "pst") //Outlook (pst)
fileTypeMap.Store("AC9EBD8F", "qdf") //Quicken (qdf)
fileTypeMap.Store("E3828596", "pwl") //Windows Password (pwl)
fileTypeMap.Store("2E7261FD", "ram") //Real Audio (ram)
}
// 获取前面结果字节的二进制
func bytesToHexString(src []byte) string {
res := bytes.Buffer{}
if src == nil || len(src) <= 0 {
return ""
}
temp := make([]byte, 0)
for _, v := range src {
sub := v & 0xFF
hv := hex.EncodeToString(append(temp, sub))
if len(hv) < 2 {
res.WriteString(strconv.FormatInt(int64(0), 10))
}
res.WriteString(hv)
}
return res.String()
}
// 用文件前面几个字节来判断
// fSrc: 文件字节流(就用前面几个字节)
func GetFileType(fSrc []byte) string {
var fileType string
fileCode := bytesToHexString(fSrc)
fileTypeMap.Range(func(key, value interface{}) bool {
k := key.(string)
v := value.(string)
if strings.HasPrefix(fileCode, strings.ToLower(k)) ||
strings.HasPrefix(k, strings.ToLower(fileCode)) {
fileType = v
return false
}
return true
})
return fileType
}
func GetContentTypeBySuffix(suffix string) int32 {
imgList := []string{"jpeg", "jpg", "png", "gif", "tif", "bmp", "dwg"}
exists := arrays.Contains(imgList, suffix)
if exists >= 0 {
return constant.IMAGE
}
audioList := []string{"mp3", "wma", "wav", "mid", "ape", "flac"}
existAudio := arrays.Contains(audioList, suffix)
if existAudio >= 0 {
return constant.AUDIO
}
videoList := []string{"rmvb", "flv", "mp4", "mpg", "mpeg", "avi", "rm", "mov", "wmv", "webm"}
existVideo := arrays.Contains(videoList, suffix)
if existVideo >= 0 {
return constant.VIDEO
}
return constant.FILE
}
里面使用了一个sync.Map,这个是线程安全的,防止使用的时候遇到冲突,然后在根据map与读取到的头进行对比,返回相对应的名称
接下来是群组消息的发送
func sendGroupMessage(msg *protocol.Message, s *Server) {
// 发送给群组的消息,查找该群所有的用户进行发送
users := service.GroupService.GetUserIdByGroupUuid(msg.To)
for _, user := range users {
if user.Uuid == msg.From {
continue
}
client, ok := s.Clients[user.Uuid]
if !ok {
continue
}
fromUserDetails := service.UserService.GetUserDetails(msg.From)
// 由于发送群聊时,from是个人,to是群聊uuid。所以在返回消息时,将form修改为群聊uuid,和单聊进行统一
msgSend := protocol.Message{
Avatar: fromUserDetails.Avatar,
FromUsername: msg.FromUsername,
From: msg.To,
To: msg.From,
Content: msg.Content,
ContentType: msg.ContentType,
Type: msg.Type,
MessageType: msg.MessageType,
Url: msg.Url,
}
msgByte, err := proto.Marshal(&msgSend)
if err == nil {
client.Send <- msgByte
}
}
}
read and write
这俩可以说是比较重要的了,作为client的两个方法,提供着对于message逻辑的最主要的功能
其中Read()方法
func (c *Client) Read() {
defer func() {
MyServer.Ungister <- c
c.Conn.Close()
}()
for {
c.Conn.PongHandler()
_, message, err := c.Conn.ReadMessage()
if err != nil {
MyServer.Ungister <- c
c.Conn.Close()
break
}
msg := &protocol.Message{}
proto.Unmarshal(message, msg)
// pong WebSocket 协议定义了三种类型的控制消息:
//close、ping 和 pong。调用连接 WriteControl、WriteMessage 或 NextWriter 方法
//向对端发送控制消息。
if msg.Type == constant.HEAT_BEAT {
pong := &protocol.Message{
Content: constant.PONG,
Type: constant.HEAT_BEAT,
}
pongByte, err2 := proto.Marshal(pong)
if nil != err2 {
}
c.Conn.WriteMessage(websocket.BinaryMessage, pongByte)
} else {
MyServer.Broadcast <- message
}
}
}
func (c *Client) Write() {
defer func() {
c.Conn.Close()
}()
for message := range c.Send {
c.Conn.WriteMessage(websocket.BinaryMessage, message)
}
}
在之前已经对每一个连接启动了两个协程read和write进行读取,代码逻辑很简单,反正就是读取信息和发送消息。<br />那今天就结束了,明天详解api中的代码逻辑