談談Unity游戲TCP連接和網絡重連

談談Unity游戲TCP連接和網絡重連

Unity中通常使用TcpClient來進行Tcp連接,TcpClient支持異步讀寫,避免了我們需要另外開辟線程管理網絡數據發送。
當異步讀寫經常會讓人摸不著頭腦,比較困惑。

1. 建立連接

/// <summary>
/// 連接服務器
/// </summary>
public void ConnectServer (string host, int port)
{
    Log.Instance.infoFormat ("start connect server host:{0}, port:{1}", host, port);
    lock (lockObj) {
        // 關閉老的連接
        if (null != client) {
            Close ();
        }
        // 建立新的連接
        client = new TcpClient ();
        client.SendTimeout = 1000;
        client.ReceiveTimeout = 1000;
        client.NoDelay = true;
        IsConnected = false;
        connectingFlag = true;
        try {
            client.BeginConnect (host, port, new AsyncCallback (OnConnect), client);
            
            // 這里是一個任務管理器,可以用來執行定時任務。連接時候添加一個超時檢查的定時任務。
            TimerManager timer = AppFacade.Instance.GetManager<TimerManager> (ManagerName.Timer);
            timer.AddTask (OnConnectTimeout, CONN_TIMEOUT);
        } catch (Exception e) {
            Log.Instance.error ("connect server error", e);
            // 通知連接失敗
            NetworkManager.AddEvent (Protocal.ConnectFail, null);
        }
    }
}

2. 異步處理連接結果

/// <summary>
/// 連接上服務器
/// </summary>
void OnConnect (IAsyncResult asr)
{
    lock (lockObj) {
        TcpClient client = (TcpClient)asr.AsyncState;
        bool validConn = (client == this.client);
        connectingFlag = false;
        try {
            // 結束異步連接
            client.EndConnect (asr);

            // 非當前連接
            if (!validConn) {
                client.Close ();
            }

            if (client.Connected) {
                Log.Instance.info ("connect server succ");

                // 異步讀socket數據
                socketStream = client.GetStream ();
                socketStream.BeginRead (byteBuffer, 0, MAX_READ, new AsyncCallback (OnRead), new SocketState (client, socketStream));

                // 通知連接成功
                IsConnected = true;
                NetworkManager.AddEvent (Protocal.Connect, null);
            } else {
                // 通知連接失敗
                Log.Instance.info ("connect server failed");

                NetworkManager.AddEvent (Protocal.ConnectFail, null);
            }
        } catch (SocketException e) {
            Log.Instance.error ("connect error", e);

            if (validConn) {
                // 通知連接失敗
                NetworkManager.AddEvent (Protocal.ConnectFail, null);
            } else {
                client.Close ();
            }
        }
    }
}

3. 處理連接超時

/// <summary>
/// 連接超時
/// </summary>
void OnConnectTimeout ()
{
    lock (lockObj) {
        if (connectingFlag) {
            Log.Instance.error ("connect server timeout");

            // 通知連接失敗
            NetworkManager.AddEvent (Protocal.ConnectFail, null);
        }
    }
}

4. 異步讀取數據

/// <summary>
/// 讀取消息
/// </summary>
void OnRead (IAsyncResult asr)
{
    int bytesRead = 0; // 讀取到的字節
    bool validConn = false; // 是否是合法的連接

    SocketState socketState = (SocketState)asr.AsyncState;
    TcpClient client = socketState.client;
    if (client == null || !client.Connected) {
        return;
    }

    lock (lockObj) {
        try {
            validConn = (client == this.client);
            NetworkStream socketStream = socketState.socketStream;

            // 讀取字節流到緩沖區
            bytesRead = socketStream.EndRead (asr);

            if (bytesRead < 1) { 
                if (!validConn) {
                    // 已經重新連接過了
                    socketStream.Close ();
                    client.Close ();
                } else {
                    // 被動斷開時
                    // 通知連接被斷開
                    OnDisconnected (DisType.Disconnect, "bytesRead < 1");
                }
                return;
            }

            // 接受數據包,寫入緩沖區
            OnReceive (byteBuffer, bytesRead); 

            // 再次監聽服務器發過來的新消息
            Array.Clear (byteBuffer, 0, byteBuffer.Length);   //清空數組
            socketStream.BeginRead (byteBuffer, 0, MAX_READ, new AsyncCallback (OnRead), socketState);
        } catch (Exception e) {
            Log.Instance.errorFormat ("read data error, connect valid:{0}", e, validConn);

            if (validConn) {
                // 通知連接被斷開
                OnDisconnected (DisType.Exception, e);
            } else {
                socketStream.Close ();
                client.Close ();
            }
        }
    }

    // 對消息進行解碼
    if (bytesRead > 0) {
        OnDecodeMessage ();
    }
}

對于數據的解包和封包,推薦MiscUtil這個庫十分好用,大端小端模式都能很好處理。

5. 發送消息

/// <summary>
/// 發送消息
/// </summary>
public bool SendMessage (Request request)
{
    try {
        bool ret = WriteMessage (request.ToBytes ());
        request.Clear ();
        return ret;
    } catch (Exception e) {
        Log.Instance.errorFormat ("write message error, requestId:{0}", e, request.GetRequestId ());
    }
    return false;
}
/// <summary>
/// 寫數據
/// </summary>
bool WriteMessage (byte[] message)
{
    bool ret = true;
    using (MemoryStream ms = new MemoryStream ()) {
        ms.Position = 0;
        EndianBinaryWriter writer = new EndianBinaryWriter (EndianBitConverter.Big, ms);
        int msglen = message.Length;
        writer.Write (msglen);
        writer.Write (message);
        writer.Flush ();

        lock (lockObj) {
            if (null != socketStream) {
                byte[] bytes = ms.ToArray ();
                socketStream.BeginWrite (bytes, 0, bytes.Length, new AsyncCallback (OnWrite), socketStream);
                
                ret = true;
            } else {
                Log.Instance.warn ("write data, but socket not connected");
                ret = false;
            }
        }
    }
    return ret;
}

/// <summary>
/// 向鏈接寫入數據流
/// </summary>
void OnWrite (IAsyncResult r)
{
    lock (lockObj) {
        try {
            NetworkStream socketStream = (NetworkStream)r.AsyncState;
            socketStream.EndWrite (r);
        } catch (Exception e) {
            Log.Instance.error ("write data error", e);
            if ((e is IOException) && socketStream == this.socketStream) {
                // IO 異常并且還是當前連接
                OnDisconnected (DisType.Exception, e);
            }
        }
    }
}

6. 總結

為了防止并發,這里使用lock對于共享變量clientsocketStream是使用都加了鎖。
在出現異常,連接斷開的時候都通過事件機制拋給上層使用者,由上層使用者決定如何
處理這個異常。

7. 斷線重連處理

斷線重連第一步監聽TcpClient使用的過程中,對于異常發生之后觸發重連邏輯。
但在移動端比較重要的一點還要做好從后臺切回前臺過程中及時檢查網絡連接狀態
及時重連。

Android后臺切回前臺的事件流

onPause(切回后臺之前) -> onResume -> focusChanged(false) -> focusChanged(true) (后面3個都是要在前臺才能收到)
不切出游戲暫停游戲 focusChanged(false) -> focusChanged(true) // 如呼出鍵盤,或者下拉通知欄

IOS后臺切回前臺的事件流

IOS的消息順序  resignActive(切回后臺之前) -> enterBackground -> enterForeground -> becomeActive (后面3個都是要在前臺才能收到)
不切出游戲暫停游戲 resignctive -> becomeActive

由上不難看出:

  • Android可以監聽focusChanged(false) -> focusChanged(true) ,注意onPause要當做一次focusChanged(false)。記錄兩次事件的間隔,比如間隔時間過長直接重新建立連接,比較短的話立即做一次
    網絡檢查。
  • IOS可以監聽resignctive -> becomeActive

TcpClient做網絡檢查可以發送一個0字節的包,代碼如下:

/// <summary>
/// 檢查socket狀態
/// </summary>
/// <returns><c>true</c>, if socket was checked, <c>false</c> otherwise.</returns>
public bool CheckSocketState ()
{
    Log.Instance.info ("check socket state start");

    // socket流為空
    if (client == null) {
        return true;
    }

    // 不在連接狀態
    if (!client.Connected) {
        Log.Instance.info ("check socket state end, socket is not connected");
        return false;
    }

    // 判斷連接狀態
    bool connectState = true;
    Socket socket = client.Client;
    bool blockingState = socket.Blocking;
    try {
        byte[] tmp = new byte[1];

        socket.Blocking = false;
        socket.Send (tmp, 0, 0);
        connectState = true; // 若Send錯誤會跳去執行catch體,而不會執行其try體里其之后的代碼

        Log.Instance.info("check socket state succ");
    } catch (SocketException e) {
        Log.Instance.warnFormat ("check socket error, errorCode:{0}", e.NativeErrorCode);

        // 10035 == WSAEWOULDBLOCK
        if (e.NativeErrorCode.Equals (10035)) {
            // Still Connected, but the Send would block
            connectState = true;
        } else {
            // Disconnected
            connectState = false;
        }
    } finally {
        socket.Blocking = blockingState;
    }

    return connectState;
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • http://www.lxweimin.com/p/d610d352e1f0
    GameMobile閱讀 990評論 0 1
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,845評論 25 708
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 寫在最前面: 關于這篇散文式的小說,寫在09年,當時青春懵懂,以為愛情就是一切,但有誰說的“世事一場冰雪”...
    死在水里的魚閱讀 287評論 1 2
  • 問:你能想象到的最好的朋友如何相處? 答:在異性朋友在追她一籌莫展的時候,我可以神氣地告訴他,我睡過你的心上人(哈...
    呀_肉串閱讀 479評論 0 1