這段時(shí)間項(xiàng)目不忙,想著搞點(diǎn)事情.于是花了大概一個(gè)月時(shí)間,寫了一套聊天系統(tǒng)。前端是用flutter寫的谅将,后臺(tái)服務(wù)用的go寫的。目前支持ios和安卓雙端運(yùn)行.前后端通訊采用的websocket.目前支持發(fā)送接收重慢。
服務(wù)端用到的技術(shù)
數(shù)據(jù)庫(kù):MySQL+Redis
通訊框架:GRPC
長(zhǎng)連接通訊協(xié)議:Protocol Buffers
日志框架:Zap
ORM框架:GORM
目前支持文字.語(yǔ)音.圖片.視頻消息.(語(yǔ)音圖片視頻存儲(chǔ)在阿里云oss服務(wù)器上)支持單聊饥臂。群聊以及上線拉取未讀離線消息。下面說說我整套設(shè)計(jì)思路(主要是服務(wù)端)以及其中遇到的難點(diǎn)似踱。
設(shè)計(jì)框架.png
已經(jīng)實(shí)現(xiàn)的功能
- 登陸
- 注冊(cè)
- 單聊
- 群聊
- 發(fā)送文字
- 發(fā)送語(yǔ)音
- 發(fā)送圖片
- 發(fā)送視頻
- 離線消息獲取
- 添加好友
- 刪除好友
- 加入群聊
- 語(yǔ)音實(shí)時(shí)通話
- 視頻實(shí)時(shí)通話
- 群拉人(后臺(tái)接口已經(jīng)做好隅熙,剩余前臺(tái))
- 群踢人(后臺(tái)接口已經(jīng)做好,剩余前臺(tái))
- 創(chuàng)建群
- 消息已讀未讀回執(zhí)
安卓端真機(jī)運(yùn)行效果
安卓端.gif
ios模擬器運(yùn)行效果
ios.gif
中間遇到的難點(diǎn)是如何獲取離線消息核芽,當(dāng)用戶端websocket處于離線狀態(tài)時(shí)囚戚,其他用戶發(fā)送的消息都不會(huì)收到,后來查閱資料轧简,目前的解決辦法是每次會(huì)話的message都增加自增seq的字段驰坊,客戶端上線后從本地?cái)?shù)據(jù)庫(kù)查詢每一條會(huì)話的最大的seq值上報(bào)給后端,后端查詢服務(wù)端數(shù)據(jù)哮独,將所有這個(gè)對(duì)象的每一個(gè)會(huì)話大于對(duì)于seq值的消息返回給客戶端拳芙。下面是服務(wù)端代碼
服務(wù)端處理離線消息的代碼
//接受客戶端最后一次的seq參數(shù)查詢離線消息
func (ctx *ConnContext) Sync(input defs.Input) {
var sync defs.SyncInput
err := json.Unmarshal([]byte(input.Data), &sync)
if err != nil {
log.Print(err)
ctx.Release()
return
}
seq, _ := strconv.ParseInt(sync.Seq, 10, 64)
messageList, err := service.MessageService.ListByUserIdAndSeq(ctx.AppId, ctx.UserId, seq)
var syncOutput defs.SyncOutput
if err == nil {
messageItems := make([]defs.MessageItem, 0, 5)
for _, v := range *messageList {
var messageItem defs.MessageItem
messageItem.SenderId = strconv.FormatInt(v.SenderId, 10)
messageItem.ReceiverId = strconv.FormatInt(v.ReceiverId, 10)
messageItem.SendTime = util.FormatDatetime(v.SendTime, util.YYYYMMDDHHMMSS)
messageItem.Type = defs.MessageType(v.Type)
messageItem.Content = v.Content
messageItem.Seq = strconv.FormatInt(v.Seq, 10)
messageItem.Avatar = v.Avatar
messageItems = append(messageItems, messageItem)
}
syncOutput = defs.SyncOutput{Messages: messageItems}
}
ctx.Output(defs.PackageType_SYNC, input.RequestId, err, &syncOutput)
}
func (ctx *ConnContext) Heartbeat(input defs.Input) {
ctx.Output(defs.PackageType_HEARTBEAT, input.RequestId, nil, "PONG")
log.Print("device_id:", ctx.DeviceId, " PING")
}
// 根據(jù)seq去查詢消息
func (*messageService) ListByUserIdAndSeq(appId, userId, seq int64) (*[]model.Message, error) {
var err error
if seq == 0 {
seq, err = DeviceAckService.GetMaxByUserId(appId, userId)
if err != nil {
return nil, err
}
}
messages, err := dao.MessageDao.ListBySeq(appId, model.MessageObjectTypeUser, userId, seq)
if err != nil {
return nil, err
}
return messages, nil
}
用戶端處理離線消息的代碼
//從服務(wù)端獲取離線消息
void getUnreadMessageFromServe(){
DBService().queryLastMessageSeq().then((value){
Map param = {
"seq":value == null?"0":value["Seq"]
};
Map sendParam = {
"type":2,
"requestId":0,
"data":convert.jsonEncode(param)
};
String sendParamString= convert.jsonEncode(sendParam);
WebSocketUtility().sendMessage(sendParamString);
});
}
//DBService
Future<Map> queryLastMessageSeq() async{
await dbUtil.open();
List<Map> data = await dbUtil.queryList("SELECT * FROM chat_flutter order by id desc");
print('數(shù)據(jù)庫(kù)查詢的data:$data');
await dbUtil.close();
return data.length == 0?null:data[0];
}