美文网首页unity优化程序员Unity技术分享
谈谈Unity游戏TCP连接和网络重连

谈谈Unity游戏TCP连接和网络重连

作者: Aodota | 来源:发表于2017-06-04 12:20 被阅读554次

    谈谈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;
    }
    

    相关文章

      网友评论

      • alongffff123:看了这篇文章,基于tolua写的非常好!
        我有一个疑问,这个在ANDROID下一点问题没有!
        但是ios 64位下,如果手动切断网络,发送一条消息并不及时报异常信息!貌似发送几条数据才报异常,时间很长!
        android却可以及时报异常,麻烦加我qq:1357098586

      本文标题:谈谈Unity游戏TCP连接和网络重连

      本文链接:https://www.haomeiwen.com/subject/mspvfxtx.html