该项目的客户端为安卓客户端,服务端语言为Go,数据库用的是mongodb。本人在此项目中负责的是服务端的开发。最后完成的主要功能有:
由于服务端用到了Go和mongodb,因此首先要安装Go和mongodb,无论在windows系统还是在linux系统,二者的安装都较为容易,为了管理方便,还安装了MongoDB Compass。Go所用的WEB框架为gin,在安装gin的过程中会下载许多相应的依赖,由于网络原因,最好需要Go的版本大于1.13并在安装的之前开启代理
Linux下:
$ export GO111MODULE=on
$ export GOPROXY=https://goproxy.cn
Windows下
C:\> $env:GO111MODULE = "on"
C:\> $env:GOPROXY = "https://goproxy.cn"
helper中存放的是一些帮助函数,models中包含了各个结构体的定义,controllers主要是具体函数功能的实现。
models:
controllers:
用到了第三方包
"gopkg.in/mgo.v2"
再进行一些简单的封装
const (
host = "127.0.0.1:27017"
source = "admin"
//user = "user"
//pass = "123456"
)
var globalS *mgo.Session
func init() {
dialInfo := &mgo.DialInfo{
Addrs: []string{host},
Source: source,
//Username: user,
//Password: pass,
}
s, err := mgo.DialWithInfo(dialInfo)
if err != nil {
log.Fatalln("create session error ", err)
}
globalS = s
}
func connect(db, collection string) (*mgo.Session, *mgo.Collection) {
s := globalS.Copy()
c := s.DB(db).C(collection)
return s, c
}
func Insert(db, collection string, docs ...interface{}) error {
ms, c := connect(db, collection)
defer ms.Close()
return c.Insert(docs...)
}
func IsExist(db, collection string, query interface{}) bool {
ms, c := connect(db, collection)
defer ms.Close()
count, _ := c.Find(query).Count()
return count > 0
}
func FindOne(db, collection string, query, selector, result interface{}) error {
ms, c := connect(db, collection)
defer ms.Close()
return c.Find(query).Select(selector).One(result)
}
func FindAll(db, collection string, query, selector, result interface{}) error {
ms, c := connect(db, collection)
defer ms.Close()
return c.Find(query).Select(selector).All(result)
}
func Update(db, collection string, query, update interface{}) error {
ms, c := connect(db, collection)
defer ms.Close()
return c.Update(query, update)
}
func UpdateAll(db, collection string, query, update interface{}) (*mgo.ChangeInfo, error) {
ms, c := connect(db, collection)
defer ms.Close()
return c.UpdateAll(query, update)
}
func Remove(db, collection string, query interface{}) error {
ms, c := connect(db, collection)
defer ms.Close()
return c.Remove(query)
}
由于整个IM即时通信分为几个部分,需要对每个部分的结构体进行定义。
用户信息:
消息结构体定义:
Type用来区分群聊还是私聊
群组的定义
评论:
点赞:
朋友圈:
当然这只是存放在数据库中的格式,还需要定义许多在此基础上扩展或压缩的结构体用来给客户端返回或接收客户端传来的请求。
具体的实现细节需要查看代码,这里主要挑选比较关键的部分进行讲解。
注册需要用户提供邮箱、用户名和密码,需要保证邮箱不能重复,其中密码在存储进数据库之前会进行哈希。
var users []User
FindAll(db, collection, bson.M{"email": email}, nil, &users)
if len(users) != 0 {
return errors.New("email has exits")
}
var data []byte = []byte(password)
hashCode := helper.GetSHA256HashCode(data) //使用SHA256进行加密
登录主要检测用户的邮箱和密码是否匹配
func Login(name, password string) error {
var users []User
FindAll(db, collection, bson.M{"email": name}, nil, &users)
if len(users) == 0 {
return errors.New("user not exits")
}
var data []byte = []byte(password)
hashCode := helper.GetSHA256HashCode(data)
if hashCode != users[0].Password {
return errors.New("password error")
}
return nil
}
由于用户登录后,需要识别该用户,客户端在接下来所进行的任何操作例如:修改个人信息、获取个人信息、发布朋友圈、聊天等等,都应该不需要再向服务端标识用户是谁,这需要用到session,通过将用户的ID加密后保存在cookie中,发送给客户端,客户端获取到cookie后存放在请求头上,以后的每个请求都携带该cookie,服务端获取后进行解密就获得了该用户的id,通过这种方式即可识别出用户是谁。
首先设置cookie生效的时间、范围:
func Login(c *gin.Context) {
session := sessions.Default(c)
option := sessions.Options{MaxAge: 3600 * 24, Path: "/"}
session.Options(option)
然后在登录成功后将该用户的ID保存
var u models.User
models.FindOne(db, collection, bson.M{"email": user.Name}, nil, &u)
fmt.Println(u.ID.Hex())
session.Set("sessionid", u.ID.Hex())
session.Save()
为了验证cookie,需要设计一个中间件,该中间件写得十分简单,作用只是查看用户有无携带上cookie
func Authorize() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
v := session.Get("sessionid")
fmt.Println("cookie: ", v)
if v != nil {
// 验证通过,会继续访问下一个中间件
c.Next()
} else {
// 验证不通过,不再调用后续的函数处理
c.Abort()
c.JSON(http.StatusUnauthorized, gin.H{"message": "访问未授权"})
return
}
}
}
该中间件应该作用在所有需要知道用户id的接口,基本上,除了登录和注册,其它接口函数都需要经过该中间件。
由于是即时通信,因此聊天功能要区别于其它功能的实现,其它功能的实现大多数都是客户端向服务端发起请求,然后服务端根据相应的请求对数据库进行操作,或向客户端返回对应的数据。而由于聊天功能是实时的,服务端收到用户A发给用户B的消息后就应该将消息立刻转发给用户B,因此每个用户在上线后就应该与服务端建立一个长连接,使得服务端能够在收到消息的第一时间将消息转发给该用户。若用户不在线,则服务端需要将消息保存在数据库中,待用户上线再转发消息。
由于传统的http连接是单向连接,只能是客户端向服务器发出请求,服务器返回查询结果,如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询":每隔一段时候,就发出一个询问,了解服务器有没有新的信息,最典型的场景就是聊天室。因此为了实现即时通信功能采用了websocket,用户在上线时便与服务端建立一个websocket连接,该连接是双向的、持久的、全双工的通信连接。
在Go语言的实现中,需要导入相应的websocket第三方包
"github.com/gorilla/websocket"
由于需要将http连接转换成websocket连接,因此需要将请求协议升级。
func ConnectWeb(c *gin.Context) {
session := sessions.Default(c)
v := session.Get("sessionid")
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
连接成功后,需要在服务端维护一个当前在线的用户表,为了索引快速,采用了map。
type ClientManger struct {
clientsLock sync.RWMutex
clients map[*Client]string
singleClient map[string]*Client
}
在用户连接成功时,将该用户的id加入到map中
func (manager *ClientManger) AddClients(client *Client, uuid string) {
manager.clientsLock.Lock()
defer manager.clientsLock.Unlock()
manager.clients[client] = uuid
manager.singleClient[uuid] = client
fmt.Println("add: ", uuid)
fmt.Println("client: ", client)
}
为了检测客户端是否和服务端断开,加入了心跳机制,每隔60s给客户端发送一个心跳。
go func() {
var err error
for {
// 每隔一秒发送一次心跳
//client.mutex.Lock()
if err = client.socket.WriteMessage(websocket.TextMessage, []byte("heartbeat")); err != nil {
return
}
//client.mutex.Unlock()
time.Sleep(60 * time.Second)
}
}()
开启一个goroutine,用来检测客户端的输入
go client.read()
在read()函数中,需要读取客户端发送的数据,由于客户端传来的数据中包含了Type、Sendid、Name等之前Message结构体中定义的许多内容,因此需要对其进行解码,解成json格式。
var receiveMsg models.Message
...
_, msg, err := c.socket.ReadMessage() //读取客户端传来的数据
...
json.Unmarshal(msg, &receiveMsg) //进行解码
根据解码得到的type可以确定是群聊还是私聊,若是私聊则根据接收者的id,将消息发给对应的用户,若是群聊则根据群号,将消息发给对应的群。由于群里保存着对应的群成员,只需将消息挨个转发给群成员即可。
在消息的数据结构中定义了image和content,前者用来保存图片后者用来保存文字内容,图片由前端编码后转成string类型保存,传回去时再由前端解码。其它地方涉及到图片的例如朋友圈配图、用户头像等都是采取这种方式存储在数据库中。
用户可以创建群聊、通过群号来加群、群主可以发布群公告、将特定的群成员踢出群聊、删除群聊。
用户可以发布朋友圈,只有好友才能看到自己发布的朋友圈,好友看到后可以点赞、评论。用户可以看到所有好友的朋友圈,也能专门选择某一个好友的相册,点进去则看到的都是该好友的朋友圈。由于朋友圈仅限好友可见,因此当发布朋友圈时,将好友的列表保存在朋友圈中,有新朋友添加时,再将新的好友更新进朋友圈里。这样当想查看全部好友的朋友圈时,只需查找数据库中每一个moment的friends列表中有无自己,若有,则表示这是好友所发的朋友圈,自己能够查看。同样,每一条moment里都包含了点赞和评论的列表,当有新用户点赞和评论时都会更新该列表。用户获取朋友圈时也能够看到对应的点赞和评论。
一条朋友圈在数据库中是这样存储的
还有一些接口主要是个人信息等,主要是对数据库进行操作,这里就不赘述了。
本次项目主要担任了服务端方面的工作,利用Go加mongodb搭建起了一个小小服务器,项目过程中遇到了许多问题,例如Gin框架由于网络问题无法下载依赖,如何进行即时通信等等,开始时也没什么头绪,IOS期中项目所用到的后端coffee代码给了我许多启发。
当然,珠玉在前,最终完成的服务端与coffee还是存在着不小的差距,还是存在一些问题,虽然大致的功能都实现了,但是在实现的方式上存在着很多可以优化的地方,例如数据库表设计是否合理;是否存在着许多冗余的关系;在对数据库进行增删改查时是否可以用更优的设计来减少查找的次数;返回的错误信息是否全面;整个项目结构,分层做得不是太好等等问题都是值得我进一步思考和总结学习的。总之,这次项目收获颇丰。