feat: incr sync version.

This commit is contained in:
Gordon
2024-06-24 17:48:33 +08:00
parent e8ccae6349
commit 88b8043224
308 changed files with 55952 additions and 59 deletions

View File

@@ -0,0 +1,61 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"bytes"
"compress/gzip"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"io"
)
type Compressor interface {
Compress(rawData []byte) ([]byte, error)
DeCompress(compressedData []byte) ([]byte, error)
}
type GzipCompressor struct {
compressProtocol string
}
func NewGzipCompressor() *GzipCompressor {
return &GzipCompressor{compressProtocol: "gzip"}
}
func (g *GzipCompressor) Compress(rawData []byte) ([]byte, error) {
gzipBuffer := bytes.Buffer{}
gz := gzip.NewWriter(&gzipBuffer)
if _, err := gz.Write(rawData); err != nil {
return nil, utils.Wrap(err, "")
}
if err := gz.Close(); err != nil {
return nil, utils.Wrap(err, "")
}
return gzipBuffer.Bytes(), nil
}
func (g *GzipCompressor) DeCompress(compressedData []byte) ([]byte, error) {
buff := bytes.NewBuffer(compressedData)
reader, err := gzip.NewReader(buff)
if err != nil {
return nil, utils.Wrap(err, "NewReader failed")
}
compressedData, err = io.ReadAll(reader)
if err != nil {
return nil, utils.Wrap(err, "ReadAll failed")
}
_ = reader.Close()
return compressedData, nil
}

View File

@@ -0,0 +1,39 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
const (
WebSocket = iota
Tcp
)
const (
// MessageText is for UTF-8 encoded text messages like JSON.
MessageText = iota + 1
// MessageBinary is for binary messages like protobufs.
MessageBinary
// CloseMessage denotes a close control message. The optional message
// payload contains a numeric code and text. Use the FormatCloseMessage
// function to format a close message payload.
CloseMessage = 8
// PingMessage denotes a ping control message. The optional message payload
// is UTF-8 encoded text.
PingMessage = 9
// PongMessage denotes a pong control message. The optional message payload
// is UTF-8 encoded text.
PongMessage = 10
)

View File

@@ -0,0 +1,52 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"time"
"github.com/openimsdk/protocol/constant"
)
type ConnContext struct {
RemoteAddr string
}
func (c *ConnContext) Deadline() (deadline time.Time, ok bool) {
return
}
func (c *ConnContext) Done() <-chan struct{} {
return nil
}
func (c *ConnContext) Err() error {
return nil
}
func (c *ConnContext) Value(key any) any {
switch key {
case constant.RemoteAddr:
return c.RemoteAddr
default:
return ""
}
}
func newContext(remoteAddr string) *ConnContext {
return &ConnContext{
RemoteAddr: remoteAddr,
}
}

View File

@@ -0,0 +1,51 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"bytes"
"encoding/gob"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
)
type Encoder interface {
Encode(data interface{}) ([]byte, error)
Decode(encodeData []byte, decodeData interface{}) error
}
type GobEncoder struct {
}
func NewGobEncoder() *GobEncoder {
return &GobEncoder{}
}
func (g *GobEncoder) Encode(data interface{}) ([]byte, error) {
buff := bytes.Buffer{}
enc := gob.NewEncoder(&buff)
err := enc.Encode(data)
if err != nil {
return nil, err
}
return buff.Bytes(), nil
}
func (g *GobEncoder) Decode(encodeData []byte, decodeData interface{}) error {
buff := bytes.NewBuffer(encodeData)
dec := gob.NewDecoder(buff)
err := dec.Decode(decodeData)
if err != nil {
return utils.Wrap(err, "")
}
return nil
}

View File

@@ -0,0 +1,579 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/openimsdk/openim-sdk-core/v3/open_im_sdk_callback"
"github.com/openimsdk/openim-sdk-core/v3/pkg/ccontext"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/sdkerrs"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"io"
"runtime"
"strconv"
"strings"
"sync"
"time"
"github.com/golang/protobuf/proto"
"github.com/gorilla/websocket"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/errs"
"github.com/openimsdk/tools/log"
)
const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer.
pongWait = 30 * time.Second
// Send pings to peer with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
// Maximum message size allowed from peer.
maxMessageSize = 1024 * 1024
//Maximum number of reconnection attempts
maxReconnectAttempts = 300
)
const (
DefaultNotConnect = iota
Closed = iota + 1
Connecting
Connected
)
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var (
ErrChanClosed = errors.New("send channel closed")
ErrConnClosed = errors.New("conn has closed")
ErrNotSupportMessageProtocol = errors.New("not support message protocol")
ErrClientClosed = errors.New("client actively close the connection")
ErrPanic = errors.New("panic error")
)
type LongConnMgr struct {
//conn status mutex
w sync.Mutex
connStatus int
// The long connection,can be set tcp or websocket.
conn LongConn
listener open_im_sdk_callback.OnConnListener
// Buffered channel of outbound messages.
send chan Message
pushMsgAndMaxSeqCh chan common.Cmd2Value
conversationCh chan common.Cmd2Value
loginMgrCh chan common.Cmd2Value
heartbeatCh chan common.Cmd2Value
closedErr error
ctx context.Context
IsCompression bool
Syncer *WsRespAsyn
encoder Encoder
compressor Compressor
reconnectStrategy ReconnectStrategy
mutex sync.Mutex
IsBackground bool
// write conn lock
connWrite *sync.Mutex
}
type Message struct {
Message GeneralWsReq
Resp chan *GeneralWsResp
}
func NewLongConnMgr(ctx context.Context, listener open_im_sdk_callback.OnConnListener, heartbeatCmdCh, pushMsgAndMaxSeqCh, loginMgrCh chan common.Cmd2Value) *LongConnMgr {
l := &LongConnMgr{listener: listener, pushMsgAndMaxSeqCh: pushMsgAndMaxSeqCh,
loginMgrCh: loginMgrCh, IsCompression: true,
Syncer: NewWsRespAsyn(), encoder: NewGobEncoder(), compressor: NewGzipCompressor(),
reconnectStrategy: NewExponentialRetry()}
l.send = make(chan Message, 10)
l.conn = NewWebSocket(WebSocket)
l.connWrite = new(sync.Mutex)
l.ctx = ctx
l.heartbeatCh = heartbeatCmdCh
return l
}
func (c *LongConnMgr) Run(ctx context.Context) {
//fmt.Println(mcontext.GetOperationID(ctx), "login run", string(debug.Stack()))
go c.readPump(ctx)
go c.writePump(ctx)
go c.heartbeat(ctx)
}
func (c *LongConnMgr) SendReqWaitResp(ctx context.Context, m proto.Message, reqIdentifier int, resp proto.Message) error {
data, err := proto.Marshal(m)
if err != nil {
return sdkerrs.ErrArgs
}
msg := Message{
Message: GeneralWsReq{
ReqIdentifier: reqIdentifier,
SendID: ccontext.Info(ctx).UserID(),
OperationID: ccontext.Info(ctx).OperationID(),
Data: data,
},
Resp: make(chan *GeneralWsResp, 1),
}
c.send <- msg
log.ZDebug(ctx, "send message to send channel success", "msg", m, "reqIdentifier", reqIdentifier)
select {
case <-ctx.Done():
return sdkerrs.ErrCtxDeadline
case v, ok := <-msg.Resp:
if !ok {
return errors.New("response channel closed")
}
if v.ErrCode != 0 {
return errs.NewCodeError(v.ErrCode, v.ErrMsg)
}
if err := proto.Unmarshal(v.Data, resp); err != nil {
return sdkerrs.ErrArgs
}
return nil
}
}
// readPump pumps messages from the websocket connection to the hub.
//
// The application runs readPump in a per-connection goroutine. The application
// ensures that there is at most one reader on a connection by executing all
// reads from this goroutine.
func (c *LongConnMgr) readPump(ctx context.Context) {
log.ZDebug(ctx, "readPump start", "goroutine ID:", getGoroutineID())
defer func() {
_ = c.close()
log.ZWarn(c.ctx, "readPump closed", c.closedErr)
}()
connNum := 0
//c.conn.SetPongHandler(function(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil })
for {
ctx = ccontext.WithOperationID(ctx, utils.OperationIDGenerator())
needRecon, err := c.reConn(ctx, &connNum)
if !needRecon {
c.closedErr = err
return
}
if err != nil {
log.ZWarn(c.ctx, "reConn", err)
time.Sleep(c.reconnectStrategy.GetSleepInterval())
continue
}
c.conn.SetReadLimit(maxMessageSize)
_ = c.conn.SetReadDeadline(pongWait)
messageType, message, err := c.conn.ReadMessage()
if err != nil {
//if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
// log.Printf("error: %v", err)
//}
//break
//c.closedErr = err
log.ZError(c.ctx, "readMessage err", err, "goroutine ID:", getGoroutineID())
_ = c.close()
continue
}
switch messageType {
case MessageBinary:
err := c.handleMessage(message)
if err != nil {
c.closedErr = err
return
}
case MessageText:
c.closedErr = ErrNotSupportMessageProtocol
return
//case PingMessage:
// err := c.writePongMsg()
// log.ZError(c.ctx, "writePongMsg", err)
case CloseMessage:
c.closedErr = ErrClientClosed
return
default:
}
}
}
// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func (c *LongConnMgr) writePump(ctx context.Context) {
log.ZDebug(ctx, "writePump start", "goroutine ID:", getGoroutineID())
defer func() {
c.close()
close(c.send)
}()
for {
select {
case <-ctx.Done():
c.closedErr = ctx.Err()
return
case message, ok := <-c.send:
if !ok {
// The hub closed the channel.
_ = c.conn.SetWriteDeadline(writeWait)
err := c.conn.WriteMessage(websocket.CloseMessage, []byte{})
if err != nil {
log.ZError(c.ctx, "send close message error", err)
}
c.closedErr = ErrChanClosed
return
}
log.ZDebug(c.ctx, "writePump recv message", "reqIdentifier", message.Message.ReqIdentifier,
"operationID", message.Message.OperationID, "sendID", message.Message.SendID)
resp, err := c.sendAndWaitResp(&message.Message)
if err != nil {
resp = &GeneralWsResp{
ReqIdentifier: message.Message.ReqIdentifier,
OperationID: message.Message.OperationID,
Data: nil,
}
if code, ok := errs.Unwrap(err).(errs.CodeError); ok {
resp.ErrCode = code.Code()
resp.ErrMsg = code.Msg()
} else {
log.ZError(c.ctx, "writeBinaryMsgAndRetry failed", err, "wsReq", message.Message)
}
}
nErr := c.Syncer.notifyCh(message.Resp, resp, 1)
if nErr != nil {
log.ZError(c.ctx, "TriggerCmdNewMsgCome failed", nErr, "wsResp", resp)
}
}
}
}
func (c *LongConnMgr) heartbeat(ctx context.Context) {
log.ZDebug(ctx, "heartbeat start", "goroutine ID:", getGoroutineID())
ticker := time.NewTicker(pingPeriod)
defer func() {
ticker.Stop()
log.ZWarn(c.ctx, "heartbeat closed", nil, "heartbeat", "heartbeat done sdk logout.....")
}()
for {
select {
case <-ctx.Done():
log.ZInfo(ctx, "heartbeat done sdk logout.....")
return
case <-c.heartbeatCh:
c.sendPingToServer(ctx)
case <-ticker.C:
c.sendPingToServer(ctx)
}
}
}
func getGoroutineID() int64 {
buf := make([]byte, 64)
buf = buf[:runtime.Stack(buf, false)]
idField := strings.Fields(strings.TrimPrefix(string(buf), "goroutine "))[0]
id, err := strconv.ParseInt(idField, 10, 64)
if err != nil {
panic(fmt.Sprintf("cannot get goroutine id: %v", err))
}
return id
}
func (c *LongConnMgr) sendPingToServer(ctx context.Context) {
if c.conn == nil {
return
}
var m sdkws.GetMaxSeqReq
m.UserID = ccontext.Info(ctx).UserID()
opID := utils.OperationIDGenerator()
sCtx := ccontext.WithOperationID(c.ctx, opID)
log.ZInfo(sCtx, "ping and getMaxSeq start", "goroutine ID:", getGoroutineID())
data, err := proto.Marshal(&m)
if err != nil {
log.ZError(sCtx, "proto.Marshal", err)
return
}
req := &GeneralWsReq{
ReqIdentifier: constant.GetNewestSeq,
SendID: m.UserID,
OperationID: opID,
Data: data,
}
resp, err := c.sendAndWaitResp(req)
if err != nil {
log.ZError(sCtx, "sendAndWaitResp", err)
_ = c.close()
time.Sleep(time.Second * 1)
return
} else {
if resp.ErrCode != 0 {
log.ZError(sCtx, "getMaxSeq failed", nil, "errCode:", resp.ErrCode, "errMsg:", resp.ErrMsg)
}
var wsSeqResp sdkws.GetMaxSeqResp
err = proto.Unmarshal(resp.Data, &wsSeqResp)
if err != nil {
log.ZError(sCtx, "proto.Unmarshal", err)
}
var cmd sdk_struct.CmdMaxSeqToMsgSync
cmd.ConversationMaxSeqOnSvr = wsSeqResp.MaxSeqs
err := common.TriggerCmdMaxSeq(sCtx, &cmd, c.pushMsgAndMaxSeqCh)
if err != nil {
log.ZError(sCtx, "TriggerCmdMaxSeq failed", err)
}
}
}
func (c *LongConnMgr) sendAndWaitResp(msg *GeneralWsReq) (*GeneralWsResp, error) {
tempChan, err := c.writeBinaryMsgAndRetry(msg)
defer c.Syncer.DelCh(msg.MsgIncr)
if err != nil {
return nil, err
} else {
select {
case resp := <-tempChan:
return resp, nil
case <-time.After(time.Second * 5):
return nil, sdkerrs.ErrNetworkTimeOut
}
}
}
func (c *LongConnMgr) writeBinaryMsgAndRetry(msg *GeneralWsReq) (chan *GeneralWsResp, error) {
msgIncr, tempChan := c.Syncer.AddCh(msg.SendID)
msg.MsgIncr = msgIncr
if c.GetConnectionStatus() != Connected && msg.ReqIdentifier == constant.GetNewestSeq {
return tempChan, sdkerrs.ErrNetwork.WrapMsg("connection closed,conning...")
}
for i := 0; i < maxReconnectAttempts; i++ {
err := c.writeBinaryMsg(*msg)
if err != nil {
log.ZError(c.ctx, "send binary message error", err, "message", msg)
c.closedErr = err
_ = c.close()
time.Sleep(time.Second * 1)
continue
} else {
return tempChan, nil
}
}
return nil, sdkerrs.ErrNetwork.WrapMsg("send binary message error")
}
func (c *LongConnMgr) writeBinaryMsg(req GeneralWsReq) error {
c.connWrite.Lock()
defer c.connWrite.Unlock()
encodeBuf, err := c.encoder.Encode(req)
if err != nil {
return err
}
if c.GetConnectionStatus() != Connected {
return sdkerrs.ErrNetwork.WrapMsg("connection closed,re conning...")
}
_ = c.conn.SetWriteDeadline(writeWait)
if c.IsCompression {
resultBuf, compressErr := c.compressor.Compress(encodeBuf)
if compressErr != nil {
return compressErr
}
return c.conn.WriteMessage(MessageBinary, resultBuf)
} else {
return c.conn.WriteMessage(MessageBinary, encodeBuf)
}
}
func (c *LongConnMgr) close() error {
c.w.Lock()
defer c.w.Unlock()
if c.connStatus == Closed || c.connStatus == Connecting || c.connStatus == DefaultNotConnect {
return nil
}
c.connStatus = Closed
log.ZWarn(c.ctx, "conn closed", c.closedErr)
return c.conn.Close()
}
func (c *LongConnMgr) handleMessage(message []byte) error {
if c.IsCompression {
var decompressErr error
message, decompressErr = c.compressor.DeCompress(message)
if decompressErr != nil {
log.ZError(c.ctx, "DeCompress failed", decompressErr, message)
return sdkerrs.ErrMsgDeCompression
}
}
var wsResp GeneralWsResp
err := c.encoder.Decode(message, &wsResp)
if err != nil {
log.ZError(c.ctx, "decodeBinaryWs err", err, "message", message)
return sdkerrs.ErrMsgDecodeBinaryWs
}
ctx := context.WithValue(c.ctx, "operationID", wsResp.OperationID)
log.ZInfo(ctx, "recv msg", "errCode", wsResp.ErrCode, "errMsg", wsResp.ErrMsg,
"reqIdentifier", wsResp.ReqIdentifier)
switch wsResp.ReqIdentifier {
case constant.PushMsg:
if err = c.doPushMsg(ctx, wsResp); err != nil {
log.ZError(ctx, "doWSPushMsg failed", err, "wsResp", wsResp)
}
case constant.LogoutMsg:
if err := c.Syncer.NotifyResp(ctx, wsResp); err != nil {
log.ZError(ctx, "notifyResp failed", err, "wsResp", wsResp)
}
return sdkerrs.ErrLoginOut
case constant.KickOnlineMsg:
log.ZDebug(ctx, "client kicked offline")
c.listener.OnKickedOffline()
_ = common.TriggerCmdLogOut(ctx, c.loginMgrCh)
return errors.New("client kicked offline")
case constant.GetNewestSeq:
fallthrough
case constant.PullMsgBySeqList:
fallthrough
case constant.SendMsg:
fallthrough
case constant.SendSignalMsg:
fallthrough
case constant.SetBackgroundStatus:
if err := c.Syncer.NotifyResp(ctx, wsResp); err != nil {
log.ZError(ctx, "notifyResp failed", err, "reqIdentifier", wsResp.ReqIdentifier, "errCode",
wsResp.ErrCode, "errMsg", wsResp.ErrMsg, "msgIncr", wsResp.MsgIncr, "operationID", wsResp.OperationID)
}
default:
// log.Error(wsResp.OperationID, "type failed, ", wsResp.ReqIdentifier)
return sdkerrs.ErrMsgBinaryTypeNotSupport
}
return nil
}
func (c *LongConnMgr) IsConnected() bool {
c.w.Lock()
defer c.w.Unlock()
if c.connStatus == Connected {
return true
}
return false
}
func (c *LongConnMgr) GetConnectionStatus() int {
c.w.Lock()
defer c.w.Unlock()
return c.connStatus
}
func (c *LongConnMgr) SetConnectionStatus(status int) {
c.w.Lock()
defer c.w.Unlock()
c.connStatus = status
}
func (c *LongConnMgr) reConn(ctx context.Context, num *int) (needRecon bool, err error) {
if c.IsConnected() {
return true, nil
}
c.connWrite.Lock()
defer c.connWrite.Unlock()
log.ZDebug(ctx, "conn start")
c.listener.OnConnecting()
c.SetConnectionStatus(Connecting)
url := fmt.Sprintf("%s?sendID=%s&token=%s&platformID=%d&operationID=%s&isBackground=%t",
ccontext.Info(ctx).WsAddr(), ccontext.Info(ctx).UserID(), ccontext.Info(ctx).Token(),
ccontext.Info(ctx).PlatformID(), ccontext.Info(ctx).OperationID(), c.GetBackground())
if c.IsCompression {
url += fmt.Sprintf("&compression=%s", "gzip")
}
resp, err := c.conn.Dial(url, nil)
if err != nil {
c.SetConnectionStatus(Closed)
if resp != nil {
body, err := io.ReadAll(resp.Body)
if err != nil {
return true, err
}
log.ZInfo(ctx, "reConn resp", "body", string(body))
var apiResp struct {
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
ErrDlt string `json:"errDlt"`
}
if err := json.Unmarshal(body, &apiResp); err != nil {
return true, err
}
err = errs.NewCodeError(apiResp.ErrCode, apiResp.ErrMsg).WithDetail(apiResp.ErrDlt).Wrap()
ccontext.GetApiErrCodeCallback(ctx).OnError(ctx, err)
switch apiResp.ErrCode {
case
errs.TokenExpiredError,
errs.TokenMalformedError,
errs.TokenNotValidYetError,
errs.TokenUnknownError:
return false, err
default:
return true, err
}
}
c.listener.OnConnectFailed(sdkerrs.NetworkError, err.Error())
return true, err
}
c.listener.OnConnectSuccess()
c.ctx = newContext(c.conn.LocalAddr())
c.ctx = context.WithValue(ctx, "ConnContext", c.ctx)
c.SetConnectionStatus(Connected)
*num++
log.ZInfo(c.ctx, "long conn establish success", "localAddr", c.conn.LocalAddr(), "connNum", *num)
c.reconnectStrategy.Reset()
_ = common.TriggerCmdConnected(ctx, c.pushMsgAndMaxSeqCh)
return true, nil
}
func (c *LongConnMgr) doPushMsg(ctx context.Context, wsResp GeneralWsResp) error {
var msg sdkws.PushMessages
err := proto.Unmarshal(wsResp.Data, &msg)
if err != nil {
return err
}
return common.TriggerCmdPushMsg(ctx, &msg, c.pushMsgAndMaxSeqCh)
}
func (c *LongConnMgr) Close(ctx context.Context) {
if c.GetConnectionStatus() == Connected {
log.ZInfo(ctx, "network change conn close")
c.closedErr = errors.New("closed by client network change")
_ = c.close()
} else {
log.ZInfo(ctx, "conn already closed")
}
}
func (c *LongConnMgr) GetBackground() bool {
c.mutex.Lock()
defer c.mutex.Unlock()
return c.IsBackground
}
func (c *LongConnMgr) SetBackground(isBackground bool) {
c.mutex.Lock()
defer c.mutex.Unlock()
c.IsBackground = isBackground
}

View File

@@ -0,0 +1,44 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"net/http"
"time"
)
type PongHandler func(string) error
type LongConn interface {
//Close this connection
Close() error
// WriteMessage Write message to connection,messageType means data type,can be set binary(2) and text(1).
WriteMessage(messageType int, message []byte) error
// ReadMessage Read message from connection.
ReadMessage() (int, []byte, error)
// SetReadDeadline sets the read deadline on the underlying network connection,
//after a read has timed out, will return an error.
SetReadDeadline(timeout time.Duration) error
// SetWriteDeadline sets to write deadline when send message,when read has timed out,will return error.
SetWriteDeadline(timeout time.Duration) error
// Dial Try to dial a connection,url must set auth args,header can control compress data
Dial(urlStr string, requestHeader http.Header) (*http.Response, error)
// IsNil Whether the connection of the current long connection is nil
IsNil() bool
// SetReadLimit sets the maximum size for a message read from the peer.bytes
SetReadLimit(limit int64)
SetPongHandler(handler PongHandler)
// LocalAddr returns the local network address.
LocalAddr() string
}

View File

@@ -0,0 +1,387 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"context"
"strings"
"github.com/openimsdk/openim-sdk-core/v3/pkg/common"
"github.com/openimsdk/openim-sdk-core/v3/pkg/constant"
"github.com/openimsdk/openim-sdk-core/v3/pkg/db/db_interface"
"github.com/openimsdk/openim-sdk-core/v3/sdk_struct"
"github.com/openimsdk/protocol/sdkws"
"github.com/openimsdk/tools/log"
)
const (
connectPullNums = 1
defaultPullNums = 10
SplitPullMsgNum = 100
)
// The callback synchronization starts. The reconnection ends
type MsgSyncer struct {
loginUserID string // login user ID
longConnMgr *LongConnMgr // long connection manager
PushMsgAndMaxSeqCh chan common.Cmd2Value // channel for receiving push messages and the maximum SEQ number
conversationCh chan common.Cmd2Value // storage and session triggering
syncedMaxSeqs map[string]int64 // map of the maximum synced SEQ numbers for all group IDs
db db_interface.DataBase // data store
syncTimes int // times of sync
ctx context.Context // context
reinstalled bool //true if the app was uninstalled and reinstalled
}
// NewMsgSyncer creates a new instance of the message synchronizer.
func NewMsgSyncer(ctx context.Context, conversationCh, PushMsgAndMaxSeqCh chan common.Cmd2Value,
loginUserID string, longConnMgr *LongConnMgr, db db_interface.DataBase, syncTimes int) (*MsgSyncer, error) {
m := &MsgSyncer{
loginUserID: loginUserID,
longConnMgr: longConnMgr,
PushMsgAndMaxSeqCh: PushMsgAndMaxSeqCh,
conversationCh: conversationCh,
ctx: ctx,
syncedMaxSeqs: make(map[string]int64),
db: db,
syncTimes: syncTimes,
}
if err := m.loadSeq(ctx); err != nil {
log.ZError(ctx, "loadSeq err", err)
return nil, err
}
return m, nil
}
// seq The db reads the data to the memory,set syncedMaxSeqs
func (m *MsgSyncer) loadSeq(ctx context.Context) error {
conversationIDList, err := m.db.GetAllConversationIDList(ctx)
if err != nil {
log.ZError(ctx, "get conversation id list failed", err)
return err
}
if len(conversationIDList) == 0 {
m.reinstalled = true
}
//TODO With a large number of sessions, this could potentially cause blocking and needs optimization.
for _, v := range conversationIDList {
maxSyncedSeq, err := m.db.GetConversationNormalMsgSeq(ctx, v)
if err != nil {
log.ZError(ctx, "get group normal seq failed", err, "conversationID", v)
} else {
m.syncedMaxSeqs[v] = maxSyncedSeq
}
}
notificationSeqs, err := m.db.GetNotificationAllSeqs(ctx)
if err != nil {
log.ZError(ctx, "get notification seq failed", err)
return err
}
for _, notificationSeq := range notificationSeqs {
m.syncedMaxSeqs[notificationSeq.ConversationID] = notificationSeq.Seq
}
log.ZDebug(ctx, "loadSeq", "syncedMaxSeqs", m.syncedMaxSeqs)
return nil
}
// DoListener Listen to the message pipe of the message synchronizer
// and process received and pushed messages
func (m *MsgSyncer) DoListener(ctx context.Context) {
for {
select {
case cmd := <-m.PushMsgAndMaxSeqCh:
m.handlePushMsgAndEvent(cmd)
case <-ctx.Done():
log.ZInfo(m.ctx, "msg syncer done, sdk logout.....")
return
}
}
}
// get seqs need sync interval
func (m *MsgSyncer) getSeqsNeedSync(syncedMaxSeq, maxSeq int64) []int64 {
var seqs []int64
for i := syncedMaxSeq + 1; i <= maxSeq; i++ {
seqs = append(seqs, i)
}
return seqs
}
// recv msg from
func (m *MsgSyncer) handlePushMsgAndEvent(cmd common.Cmd2Value) {
switch cmd.Cmd {
case constant.CmdConnSuccesss:
log.ZInfo(cmd.Ctx, "recv long conn mgr connected", "cmd", cmd.Cmd, "value", cmd.Value)
m.doConnected(cmd.Ctx)
case constant.CmdMaxSeq:
log.ZInfo(cmd.Ctx, "recv max seqs from long conn mgr, start sync msgs", "cmd", cmd.Cmd, "value", cmd.Value)
m.compareSeqsAndBatchSync(cmd.Ctx, cmd.Value.(*sdk_struct.CmdMaxSeqToMsgSync).ConversationMaxSeqOnSvr, defaultPullNums)
case constant.CmdPushMsg:
m.doPushMsg(cmd.Ctx, cmd.Value.(*sdkws.PushMessages))
}
}
func (m *MsgSyncer) compareSeqsAndBatchSync(ctx context.Context, maxSeqToSync map[string]int64, pullNums int64) {
needSyncSeqMap := make(map[string][2]int64)
//when app reinstalled do not pull notifications messages.
if m.reinstalled {
notificationsSeqMap := make(map[string]int64)
messagesSeqMap := make(map[string]int64)
for conversationID, seq := range maxSeqToSync {
if IsNotification(conversationID) {
notificationsSeqMap[conversationID] = seq
} else {
messagesSeqMap[conversationID] = seq
}
}
for conversationID, seq := range notificationsSeqMap {
err := m.db.SetNotificationSeq(ctx, conversationID, seq)
if err != nil {
log.ZWarn(ctx, "SetNotificationSeq err", err, "conversationID", conversationID, "seq", seq)
continue
} else {
m.syncedMaxSeqs[conversationID] = seq
}
}
for conversationID, maxSeq := range messagesSeqMap {
if syncedMaxSeq, ok := m.syncedMaxSeqs[conversationID]; ok {
if maxSeq > syncedMaxSeq {
needSyncSeqMap[conversationID] = [2]int64{syncedMaxSeq + 1, maxSeq}
}
} else {
needSyncSeqMap[conversationID] = [2]int64{0, maxSeq}
}
}
m.reinstalled = false
} else {
for conversationID, maxSeq := range maxSeqToSync {
if syncedMaxSeq, ok := m.syncedMaxSeqs[conversationID]; ok {
if maxSeq > syncedMaxSeq {
needSyncSeqMap[conversationID] = [2]int64{syncedMaxSeq + 1, maxSeq}
}
} else {
needSyncSeqMap[conversationID] = [2]int64{0, maxSeq}
}
}
}
_ = m.syncAndTriggerMsgs(m.ctx, needSyncSeqMap, pullNums)
}
func (m *MsgSyncer) doPushMsg(ctx context.Context, push *sdkws.PushMessages) {
log.ZDebug(ctx, "push msgs", "push", push, "syncedMaxSeqs", m.syncedMaxSeqs)
m.pushTriggerAndSync(ctx, push.Msgs, m.triggerConversation)
m.pushTriggerAndSync(ctx, push.NotificationMsgs, m.triggerNotification)
}
func (m *MsgSyncer) pushTriggerAndSync(ctx context.Context, pullMsgs map[string]*sdkws.PullMsgs, triggerFunc func(ctx context.Context, msgs map[string]*sdkws.PullMsgs) error) {
if len(pullMsgs) == 0 {
return
}
needSyncSeqMap := make(map[string][2]int64)
var lastSeq int64
var storageMsgs []*sdkws.MsgData
for conversationID, msgs := range pullMsgs {
for _, msg := range msgs.Msgs {
if msg.Seq == 0 {
_ = triggerFunc(ctx, map[string]*sdkws.PullMsgs{conversationID: {Msgs: []*sdkws.MsgData{msg}}})
continue
}
lastSeq = msg.Seq
storageMsgs = append(storageMsgs, msg)
}
if lastSeq == m.syncedMaxSeqs[conversationID]+int64(len(storageMsgs)) && lastSeq != 0 {
log.ZDebug(ctx, "trigger msgs", "msgs", storageMsgs)
_ = triggerFunc(ctx, map[string]*sdkws.PullMsgs{conversationID: {Msgs: storageMsgs}})
m.syncedMaxSeqs[conversationID] = lastSeq
} else if lastSeq != 0 && lastSeq > m.syncedMaxSeqs[conversationID] {
//must pull message when message type is notification
needSyncSeqMap[conversationID] = [2]int64{m.syncedMaxSeqs[conversationID] + 1, lastSeq}
}
}
m.syncAndTriggerMsgs(ctx, needSyncSeqMap, defaultPullNums)
}
// Called after successful reconnection to synchronize the latest message
func (m *MsgSyncer) doConnected(ctx context.Context) {
common.TriggerCmdNotification(m.ctx, sdk_struct.CmdNewMsgComeToConversation{SyncFlag: constant.MsgSyncBegin}, m.conversationCh)
var resp sdkws.GetMaxSeqResp
if err := m.longConnMgr.SendReqWaitResp(m.ctx, &sdkws.GetMaxSeqReq{UserID: m.loginUserID}, constant.GetNewestSeq, &resp); err != nil {
log.ZError(m.ctx, "get max seq error", err)
common.TriggerCmdNotification(m.ctx, sdk_struct.CmdNewMsgComeToConversation{SyncFlag: constant.MsgSyncFailed}, m.conversationCh)
return
} else {
log.ZDebug(m.ctx, "get max seq success", "resp", resp)
}
m.compareSeqsAndBatchSync(ctx, resp.MaxSeqs, connectPullNums)
common.TriggerCmdNotification(m.ctx, sdk_struct.CmdNewMsgComeToConversation{SyncFlag: constant.MsgSyncEnd}, m.conversationCh)
}
func IsNotification(conversationID string) bool {
return strings.HasPrefix(conversationID, "n_")
}
// Fragment synchronization message, seq refresh after successful trigger
func (m *MsgSyncer) syncAndTriggerMsgs(ctx context.Context, seqMap map[string][2]int64, syncMsgNum int64) error {
if len(seqMap) > 0 {
log.ZDebug(ctx, "current sync seqMap", "seqMap", seqMap)
tempSeqMap := make(map[string][2]int64, 50)
msgNum := 0
for k, v := range seqMap {
oneConversationSyncNum := v[1] - v[0] + 1
if (oneConversationSyncNum/SplitPullMsgNum) > 1 && IsNotification(k) {
nSeqMap := make(map[string][2]int64, 1)
count := int(oneConversationSyncNum / SplitPullMsgNum)
startSeq := v[0]
var end int64
for i := 0; i <= count; i++ {
if i == count {
nSeqMap[k] = [2]int64{startSeq, v[1]}
} else {
end = startSeq + int64(SplitPullMsgNum)
if end > v[1] {
end = v[1]
i = count
}
nSeqMap[k] = [2]int64{startSeq, end}
}
resp, err := m.pullMsgBySeqRange(ctx, nSeqMap, syncMsgNum)
if err != nil {
log.ZError(ctx, "syncMsgFromSvr err", err, "nSeqMap", nSeqMap)
return err
}
_ = m.triggerConversation(ctx, resp.Msgs)
_ = m.triggerNotification(ctx, resp.NotificationMsgs)
for conversationID, seqs := range nSeqMap {
m.syncedMaxSeqs[conversationID] = seqs[1]
}
startSeq = end + 1
}
continue
}
tempSeqMap[k] = v
if oneConversationSyncNum > 0 {
msgNum += int(oneConversationSyncNum)
}
if msgNum >= SplitPullMsgNum {
resp, err := m.pullMsgBySeqRange(ctx, tempSeqMap, syncMsgNum)
if err != nil {
log.ZError(ctx, "syncMsgFromSvr err", err, "tempSeqMap", tempSeqMap)
return err
}
_ = m.triggerConversation(ctx, resp.Msgs)
_ = m.triggerNotification(ctx, resp.NotificationMsgs)
for conversationID, seqs := range tempSeqMap {
m.syncedMaxSeqs[conversationID] = seqs[1]
}
tempSeqMap = make(map[string][2]int64, 50)
msgNum = 0
}
}
resp, err := m.pullMsgBySeqRange(ctx, tempSeqMap, syncMsgNum)
if err != nil {
log.ZError(ctx, "syncMsgFromSvr err", err, "seqMap", seqMap)
return err
}
_ = m.triggerConversation(ctx, resp.Msgs)
_ = m.triggerNotification(ctx, resp.NotificationMsgs)
for conversationID, seqs := range seqMap {
m.syncedMaxSeqs[conversationID] = seqs[1]
}
} else {
log.ZDebug(ctx, "noting conversation to sync", "syncMsgNum", syncMsgNum)
}
return nil
}
func (m *MsgSyncer) splitSeqs(split int, seqsNeedSync []int64) (splitSeqs [][]int64) {
if len(seqsNeedSync) <= split {
splitSeqs = append(splitSeqs, seqsNeedSync)
return
}
for i := 0; i < len(seqsNeedSync); i += split {
end := i + split
if end > len(seqsNeedSync) {
end = len(seqsNeedSync)
}
splitSeqs = append(splitSeqs, seqsNeedSync[i:end])
}
return
}
func (m *MsgSyncer) pullMsgBySeqRange(ctx context.Context, seqMap map[string][2]int64, syncMsgNum int64) (resp *sdkws.PullMessageBySeqsResp, err error) {
log.ZDebug(ctx, "pullMsgBySeqRange", "seqMap", seqMap, "syncMsgNum", syncMsgNum)
req := sdkws.PullMessageBySeqsReq{UserID: m.loginUserID}
for conversationID, seqs := range seqMap {
req.SeqRanges = append(req.SeqRanges, &sdkws.SeqRange{
ConversationID: conversationID,
Begin: seqs[0],
End: seqs[1],
Num: syncMsgNum,
})
}
resp = &sdkws.PullMessageBySeqsResp{}
if err := m.longConnMgr.SendReqWaitResp(ctx, &req, constant.PullMsgBySeqList, resp); err != nil {
return nil, err
}
return resp, nil
}
// synchronizes messages by SEQs.
func (m *MsgSyncer) syncMsgBySeqs(ctx context.Context, conversationID string, seqsNeedSync []int64) (allMsgs []*sdkws.MsgData, err error) {
pullMsgReq := sdkws.PullMessageBySeqsReq{}
pullMsgReq.UserID = m.loginUserID
split := constant.SplitPullMsgNum
seqsList := m.splitSeqs(split, seqsNeedSync)
for i := 0; i < len(seqsList); {
var pullMsgResp sdkws.PullMessageBySeqsResp
err := m.longConnMgr.SendReqWaitResp(ctx, &pullMsgReq, constant.PullMsgBySeqList, &pullMsgResp)
if err != nil {
log.ZError(ctx, "syncMsgFromSvrSplit err", err, "pullMsgReq", pullMsgReq)
continue
}
i++
allMsgs = append(allMsgs, pullMsgResp.Msgs[conversationID].Msgs...)
}
return allMsgs, nil
}
// triggers a conversation with a new message.
func (m *MsgSyncer) triggerConversation(ctx context.Context, msgs map[string]*sdkws.PullMsgs) error {
if len(msgs) >= 0 {
err := common.TriggerCmdNewMsgCome(ctx, sdk_struct.CmdNewMsgComeToConversation{Msgs: msgs}, m.conversationCh)
if err != nil {
log.ZError(ctx, "triggerCmdNewMsgCome err", err, "msgs", msgs)
}
log.ZDebug(ctx, "triggerConversation", "msgs", msgs)
return err
}
return nil
}
func (m *MsgSyncer) triggerNotification(ctx context.Context, msgs map[string]*sdkws.PullMsgs) error {
if len(msgs) >= 0 {
err := common.TriggerCmdNotification(ctx, sdk_struct.CmdNewMsgComeToConversation{Msgs: msgs}, m.conversationCh)
if err != nil {
log.ZError(ctx, "triggerCmdNewMsgCome err", err, "msgs", msgs)
}
return err
}
return nil
}

View File

@@ -0,0 +1,32 @@
package interaction
import (
"time"
)
type ReconnectStrategy interface {
GetSleepInterval() time.Duration
Reset()
}
type ExponentialRetry struct {
attempts []int
index int
}
func NewExponentialRetry() *ExponentialRetry {
return &ExponentialRetry{
attempts: []int{1, 2, 4, 8, 16},
index: -1,
}
}
func (rs *ExponentialRetry) GetSleepInterval() time.Duration {
rs.index++
interval := rs.index % len(rs.attempts)
return time.Second * time.Duration(rs.attempts[interval])
}
func (rs *ExponentialRetry) Reset() {
rs.index = -1
}

View File

@@ -0,0 +1,87 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !js
package interaction
import (
"net/http"
"time"
"github.com/gorilla/websocket"
)
type Default struct {
ConnType int
conn *websocket.Conn
isSetConf bool
}
func (d *Default) SetReadDeadline(timeout time.Duration) error {
return d.conn.SetReadDeadline(time.Now().Add(timeout))
}
func (d *Default) SetWriteDeadline(timeout time.Duration) error {
return d.conn.SetWriteDeadline(time.Now().Add(timeout))
}
func (d *Default) SetReadLimit(limit int64) {
if !d.isSetConf {
d.conn.SetReadLimit(limit)
}
}
func (d *Default) SetPongHandler(handler PongHandler) {
if !d.isSetConf {
d.conn.SetPongHandler(handler)
d.isSetConf = true
}
}
func (d *Default) LocalAddr() string {
return d.conn.LocalAddr().String()
}
func NewWebSocket(connType int) *Default {
return &Default{ConnType: connType}
}
func (d *Default) Close() error {
return d.conn.Close()
}
func (d *Default) WriteMessage(messageType int, message []byte) error {
return d.conn.WriteMessage(messageType, message)
}
func (d *Default) ReadMessage() (int, []byte, error) {
return d.conn.ReadMessage()
}
func (d *Default) Dial(urlStr string, requestHeader http.Header) (*http.Response, error) {
conn, httpResp, err := websocket.DefaultDialer.Dial(urlStr, requestHeader)
if err == nil {
d.conn = conn
}
return httpResp, err
}
func (d *Default) IsNil() bool {
if d.conn != nil {
return false
}
return true
}

View File

@@ -0,0 +1,140 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build js && wasm
// +build js,wasm
package interaction
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/openimsdk/tools/log"
"io"
"net/http"
"net/url"
"nhooyr.io/websocket"
"time"
)
type JSWebSocket struct {
ConnType int
conn *websocket.Conn
sendConn *websocket.Conn
}
func (w *JSWebSocket) SetReadDeadline(timeout time.Duration) error {
return nil
}
func (w *JSWebSocket) SetWriteDeadline(timeout time.Duration) error {
return nil
}
func (w *JSWebSocket) SetReadLimit(limit int64) {
w.conn.SetReadLimit(limit)
}
func (w *JSWebSocket) SetPongHandler(handler PongHandler) {
}
func (w *JSWebSocket) LocalAddr() string {
return ""
}
func NewWebSocket(connType int) *JSWebSocket {
return &JSWebSocket{ConnType: connType}
}
func (w *JSWebSocket) Close() error {
return w.conn.Close(websocket.StatusGoingAway, "Actively close the conn have old conn")
}
func (w *JSWebSocket) WriteMessage(messageType int, message []byte) error {
return w.conn.Write(context.Background(), websocket.MessageType(messageType), message)
}
func (w *JSWebSocket) ReadMessage() (int, []byte, error) {
messageType, b, err := w.conn.Read(context.Background())
return int(messageType), b, err
}
func (w *JSWebSocket) dial(ctx context.Context, urlStr string) (*websocket.Conn, *http.Response, error) {
u, err := url.Parse(urlStr)
if err != nil {
return nil, nil, err
}
query := u.Query()
query.Set("isMsgResp", "true")
u.RawQuery = query.Encode()
conn, httpResp, err := websocket.Dial(ctx, u.String(), nil)
if err != nil {
return nil, nil, err
}
if httpResp == nil {
httpResp = &http.Response{
StatusCode: http.StatusSwitchingProtocols,
}
}
_, data, err := conn.Read(ctx)
if err != nil {
_ = conn.CloseNow()
return nil, nil, fmt.Errorf("read response error %w", err)
}
var apiResp struct {
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
ErrDlt string `json:"errDlt"`
}
if err := json.Unmarshal(data, &apiResp); err != nil {
return nil, nil, fmt.Errorf("unmarshal response error %w", err)
}
if apiResp.ErrCode == 0 {
return conn, httpResp, nil
}
log.ZDebug(ctx, "ws msg read resp", "data", string(data))
httpResp.Body = io.NopCloser(bytes.NewReader(data))
return conn, httpResp, fmt.Errorf("read response error %d %s %s",
apiResp.ErrCode, apiResp.ErrMsg, apiResp.ErrDlt)
}
func (w *JSWebSocket) Dial(urlStr string, _ http.Header) (*http.Response, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
conn, httpResp, err := w.dial(ctx, urlStr)
if err == nil {
w.conn = conn
}
return httpResp, err
}
//func (w *JSWebSocket) Dial(urlStr string, _ http.Header) (*http.Response, error) {
// ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
// defer cancel()
// conn, httpResp, err := websocket.Dial(ctx, urlStr, nil)
// if err == nil {
// w.conn = conn
// }
// return httpResp, err
//}
func (w *JSWebSocket) IsNil() bool {
if w.conn != nil {
return false
}
return true
}

View File

@@ -0,0 +1,168 @@
// Copyright © 2023 OpenIM SDK. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package interaction
import (
"context"
"errors"
"github.com/openimsdk/openim-sdk-core/v3/pkg/utils"
"sync"
"time"
"github.com/openimsdk/tools/log"
)
type GeneralWsResp struct {
ReqIdentifier int `json:"reqIdentifier"`
ErrCode int `json:"errCode"`
ErrMsg string `json:"errMsg"`
MsgIncr string `json:"msgIncr"`
OperationID string `json:"operationID"`
Data []byte `json:"data"`
}
type GeneralWsReq struct {
ReqIdentifier int `json:"reqIdentifier"`
Token string `json:"token"`
SendID string `json:"sendID"`
OperationID string `json:"operationID"`
MsgIncr string `json:"msgIncr"`
Data []byte `json:"data"`
}
type WsRespAsyn struct {
wsNotification map[string]chan *GeneralWsResp
wsMutex sync.RWMutex
}
func NewWsRespAsyn() *WsRespAsyn {
return &WsRespAsyn{wsNotification: make(map[string]chan *GeneralWsResp, 10)}
}
func GenMsgIncr(userID string) string {
return userID + "_" + utils.OperationIDGenerator()
}
func (u *WsRespAsyn) AddCh(userID string) (string, chan *GeneralWsResp) {
u.wsMutex.Lock()
defer u.wsMutex.Unlock()
msgIncr := GenMsgIncr(userID)
ch := make(chan *GeneralWsResp, 1)
_, ok := u.wsNotification[msgIncr]
if ok {
}
u.wsNotification[msgIncr] = ch
return msgIncr, ch
}
func (u *WsRespAsyn) AddChByIncr(msgIncr string) chan *GeneralWsResp {
u.wsMutex.Lock()
defer u.wsMutex.Unlock()
ch := make(chan *GeneralWsResp, 1)
_, ok := u.wsNotification[msgIncr]
if ok {
log.ZError(context.Background(), "Repeat failed", nil, msgIncr)
}
u.wsNotification[msgIncr] = ch
return ch
}
func (u *WsRespAsyn) GetCh(msgIncr string) chan *GeneralWsResp {
ch, ok := u.wsNotification[msgIncr]
if ok {
return ch
}
return nil
}
func (u *WsRespAsyn) DelCh(msgIncr string) {
u.wsMutex.Lock()
defer u.wsMutex.Unlock()
ch, ok := u.wsNotification[msgIncr]
if ok {
close(ch)
delete(u.wsNotification, msgIncr)
}
}
func (u *WsRespAsyn) notifyCh(ch chan *GeneralWsResp, value *GeneralWsResp, timeout int64) error {
var flag = 0
select {
case ch <- value:
flag = 1
case <-time.After(time.Second * time.Duration(timeout)):
flag = 2
}
if flag == 1 {
return nil
} else {
return errors.New("send cmd timeout")
}
}
// write a unit test for this function
func (u *WsRespAsyn) NotifyResp(ctx context.Context, wsResp GeneralWsResp) error {
u.wsMutex.Lock()
defer u.wsMutex.Unlock()
ch := u.GetCh(wsResp.MsgIncr)
if ch == nil {
return utils.Wrap(errors.New("no ch"), "GetCh failed "+wsResp.MsgIncr)
}
for {
err := u.notifyCh(ch, &wsResp, 1)
if err != nil {
log.ZWarn(ctx, "TriggerCmdNewMsgCome failed ", err, "ch", ch, "wsResp", wsResp)
continue
}
return nil
}
}
func (u *WsRespAsyn) WaitResp(ctx context.Context, ch chan *GeneralWsResp, timeout int) (*GeneralWsResp, error) {
select {
case r, ok := <-ch:
if !ok { //ch has been closed
//log.Debug(operationID, "ws network has been changed ")
return nil, nil
}
//log.Debug(operationID, "ws ch recvMsg success, code ", r.ErrCode)
if r.ErrCode != 0 {
//log.Error(operationID, "ws ch recvMsg failed, code, err msg: ", r.ErrCode, r.ErrMsg)
//switch r.ErrCode {
//case int(constant.ErrInBlackList.ErrCode):
// return nil, &constant.ErrInBlackList
//case int(constant.ErrNotFriend.ErrCode):
// return nil, &constant.ErrNotFriend
//}
//return nil, errors.New(utils.IntToString(r.ErrCode) + ":" + r.ErrMsg)
} else {
return r, nil
}
case <-time.After(time.Second * time.Duration(timeout)):
//log.Error(operationID, "ws ch recvMsg err, timeout")
//if w.conn.IsNil() {
// return nil, errors.New("ws ch recvMsg err, timeout,conn is nil")
//}
//if w.conn.CheckSendConnDiffNow() {
// return nil, constant.WsRecvConnDiff
//} else {
// return nil, constant.WsRecvConnSame
//}
}
return nil, nil
}